Move Hydrogen `timelineViewModel` creation to our main view model (#118)

Move Hydrogen `timelineViewModel` creation to our main view model.

Was it better to keep this Hydrogen boilerplate outside? Maybe 🤷. It's possible that all of this will go away with something like https://github.com/MadLittleMods/hydrogen-static-archives-prototype/pull/2

Should we also move the [`room` VM creation](8275f120a6/shared/hydrogen-vm-render-script.js (L88-L111))? Maybe but it's not as much noise as all of this tile/timeline stuff.
This commit is contained in:
Eric Eastwood 2022-11-01 08:54:43 -05:00 committed by GitHub
parent 718f01e5a4
commit dc7017ae4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 195 additions and 160 deletions

View File

@ -14,22 +14,12 @@ const {
RetainedObservableValue, RetainedObservableValue,
PowerLevels, PowerLevels,
TilesCollection,
FragmentIdComparer,
EventEntry,
encodeKey,
encodeEventIdKey,
Timeline,
} = require('hydrogen-view-sdk'); } = require('hydrogen-view-sdk');
const ArchiveRoomView = require('matrix-public-archive-shared/views/ArchiveRoomView'); const ArchiveRoomView = require('matrix-public-archive-shared/views/ArchiveRoomView');
const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history'); const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history');
const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page'); const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page');
const ArchiveRoomViewModel = require('matrix-public-archive-shared/viewmodels/ArchiveRoomViewModel'); const ArchiveRoomViewModel = require('matrix-public-archive-shared/viewmodels/ArchiveRoomViewModel');
const {
customTileClassForEntry,
} = require('matrix-public-archive-shared/lib/custom-tile-utilities');
const fromTimestamp = window.matrixPublicArchiveContext.fromTimestamp; const fromTimestamp = window.matrixPublicArchiveContext.fromTimestamp;
assert(fromTimestamp); assert(fromTimestamp);
@ -48,12 +38,6 @@ assert(config);
assert(config.matrixServerUrl); assert(config.matrixServerUrl);
assert(config.basePath); assert(config.basePath);
let txnCount = 0;
function getFakeEventId() {
txnCount++;
return `fake-event-id-${new Date().getTime()}--${txnCount}`;
}
function addSupportClasses() { function addSupportClasses() {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'month'; input.type = 'month';
@ -63,31 +47,6 @@ function addSupportClasses() {
document.body.classList.toggle('fallback-input-month', !isMonthTypeSupported); document.body.classList.toggle('fallback-input-month', !isMonthTypeSupported);
} }
let eventIndexCounter = 0;
const fragmentIdComparer = new FragmentIdComparer([]);
function makeEventEntryFromEventJson(eventJson, memberEvent) {
assert(eventJson);
const eventIndex = eventIndexCounter;
const eventEntry = new EventEntry(
{
fragmentId: 0,
eventIndex: eventIndex, // TODO: What should this be?
roomId: roomData.id,
event: eventJson,
displayName: memberEvent && memberEvent.content && memberEvent.content.displayname,
avatarUrl: memberEvent && memberEvent.content && memberEvent.content.avatar_url,
key: encodeKey(roomData.id, 0, eventIndex),
eventIdKey: encodeEventIdKey(roomData.id, eventJson.event_id),
},
fragmentIdComparer
);
eventIndexCounter++;
return eventEntry;
}
supressBlankAnchorsReloadingThePage(); supressBlankAnchorsReloadingThePage();
// eslint-disable-next-line max-statements // eslint-disable-next-line max-statements
@ -122,14 +81,6 @@ async function mountHydrogen() {
// page don't say `undefined`. // page don't say `undefined`.
urlRouter.attach(); urlRouter.attach();
// We use the timeline to setup the relations between entries
const timeline = new Timeline({
roomId: roomData.id,
fragmentIdComparer: fragmentIdComparer,
clock: platform.clock,
logger: platform.logger,
});
const mediaRepository = new MediaRepository({ const mediaRepository = new MediaRepository({
homeserver: config.matrixServerUrl, homeserver: config.matrixServerUrl,
}); });
@ -140,6 +91,7 @@ async function mountHydrogen() {
canonicalAlias: roomData.canonicalAlias, canonicalAlias: roomData.canonicalAlias,
avatarUrl: roomData.avatarUrl, avatarUrl: roomData.avatarUrl,
avatarColorId: roomData.id, avatarColorId: roomData.id,
// Hydrogen options used by the event TilesCollection (roomVM)
mediaRepository: mediaRepository, mediaRepository: mediaRepository,
// Based on https://github.com/vector-im/hydrogen-web/blob/5f9cfffa3b547991b665f57a8bf715270a1b2ef1/src/matrix/room/BaseRoom.js#L480 // Based on https://github.com/vector-im/hydrogen-web/blob/5f9cfffa3b547991b665f57a8bf715270a1b2ef1/src/matrix/room/BaseRoom.js#L480
observePowerLevels: async function () { observePowerLevels: async function () {
@ -159,111 +111,20 @@ async function mountHydrogen() {
}, },
}; };
// Something we can modify with new state updates as we see them
const workingStateEventMap = {
...stateEventMap,
};
// Add a summary item to the bottom of the timeline that explains if we found
// events on the day requested.
const hasEventsFromGivenDay = events[events.length - 1]?.origin_server_ts >= fromTimestamp;
let daySummaryKind;
if (events.length === 0) {
daySummaryKind = 'no-events-at-all';
} else if (hasEventsFromGivenDay) {
daySummaryKind = 'some-events-in-day';
} else if (!hasEventsFromGivenDay) {
daySummaryKind = 'no-events-in-day';
}
events.push({
event_id: getFakeEventId(),
type: 'org.matrix.archive.not_enough_events_from_day_summary',
room_id: roomData.id,
// Even though this isn't used for sort, just using the time where the event
// would logically be.
//
// -1 so we're not at 00:00:00 of the next day
origin_server_ts: toTimestamp - 1,
content: {
canonicalAlias: roomData.canonicalAlias,
daySummaryKind,
// The timestamp from the URL that was originally visited
dayTimestamp: fromTimestamp,
// The end of the range to use as a jumping off point to the next activity
rangeEndTimestamp: toTimestamp,
// This is a bit cheating but I don't know how else to pass this kind of
// info to the Tile viewmodel
basePath: config.basePath,
},
});
const eventEntries = events.map((event) => {
if (event.type === 'm.room.member') {
workingStateEventMap[event.state_key] = event;
}
const memberEvent = workingStateEventMap[event.user_id];
return makeEventEntryFromEventJson(event, memberEvent);
});
//console.log('eventEntries', eventEntries.length);
// Map of `event_id` to `EventEntry`
const eventEntriesByEventId = eventEntries.reduce((currentMap, eventEntry) => {
currentMap[eventEntry.id] = eventEntry;
return currentMap;
}, {});
// We have to use `timeline._setupEntries([])` because it sets
// `this._allEntries` in `Timeline` and we don't want to use `timeline.load()`
// to request remote things.
timeline._setupEntries([]);
// Make it safe to iterate a derived observable collection
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
// We use the timeline to setup the relations between entries
timeline.addEntries(eventEntries);
//console.log('timeline.entries', timeline.entries.length, timeline.entries);
const tiles = new TilesCollection(timeline.entries, {
tileClassForEntry: customTileClassForEntry,
platform,
navigation,
urlCreator: urlRouter,
timeline,
roomVM: {
room,
},
});
// Trigger `onSubscribeFirst` -> `tiles._populateTiles()` so it creates a tile
// for each entry to display. This way we can also call `tile.notifyVisible()`
// on each tile so that the tile creation doesn't happen later when the
// `TilesListView` is mounted and subscribes which is a bit out of our
// control.
tiles.subscribe({ onAdd: () => null, onUpdate: () => null });
// Make the lazy-load images appear
for (const tile of tiles) {
tile.notifyVisible();
}
const timelineViewModel = {
showJumpDown: false,
setVisibleTileRange: () => {},
tiles,
};
const archiveRoomViewModel = new ArchiveRoomViewModel({ const archiveRoomViewModel = new ArchiveRoomViewModel({
// Hydrogen options // Hydrogen options
platform: platform,
navigation: navigation, navigation: navigation,
urlCreator: urlRouter, urlCreator: urlRouter,
history: archiveHistory, history: archiveHistory,
// Our options // Our options
homeserverUrl: config.matrixServerUrl, homeserverUrl: config.matrixServerUrl,
timelineViewModel,
room, room,
// The timestamp from the URL that was originally visited // The timestamp from the URL that was originally visited
dayTimestamp: fromTimestamp, dayTimestampFrom: fromTimestamp,
eventEntriesByEventId, dayTimestampTo: toTimestamp,
events,
stateEventMap,
shouldIndex, shouldIndex,
basePath: config.basePath, basePath: config.basePath,
}); });

View File

@ -1,6 +1,15 @@
'use strict'; 'use strict';
const { ViewModel, setupLightboxNavigation } = require('hydrogen-view-sdk'); const {
ViewModel,
setupLightboxNavigation,
TilesCollection,
Timeline,
FragmentIdComparer,
EventEntry,
encodeKey,
encodeEventIdKey,
} = require('hydrogen-view-sdk');
const assert = require('matrix-public-archive-shared/lib/assert'); const assert = require('matrix-public-archive-shared/lib/assert');
@ -10,35 +19,83 @@ const CalendarViewModel = require('matrix-public-archive-shared/viewmodels/Calen
const DeveloperOptionsContentViewModel = require('matrix-public-archive-shared/viewmodels/DeveloperOptionsContentViewModel'); const DeveloperOptionsContentViewModel = require('matrix-public-archive-shared/viewmodels/DeveloperOptionsContentViewModel');
const RightPanelContentView = require('matrix-public-archive-shared/views/RightPanelContentView'); const RightPanelContentView = require('matrix-public-archive-shared/views/RightPanelContentView');
const AvatarViewModel = require('matrix-public-archive-shared/viewmodels/AvatarViewModel'); const AvatarViewModel = require('matrix-public-archive-shared/viewmodels/AvatarViewModel');
const {
customTileClassForEntry,
} = require('matrix-public-archive-shared/lib/custom-tile-utilities');
let txnCount = 0;
function getFakeEventId() {
txnCount++;
return `fake-event-id-${new Date().getTime()}--${txnCount}`;
}
let eventIndexCounter = 0;
const fragmentIdComparer = new FragmentIdComparer([]);
function makeEventEntryFromEventJson(eventJson, memberEvent) {
assert(eventJson);
const roomId = eventJson.roomId;
const eventIndex = eventIndexCounter;
const eventEntry = new EventEntry(
{
fragmentId: 0,
eventIndex: eventIndex, // TODO: What should this be?
roomId,
event: eventJson,
displayName: memberEvent && memberEvent.content && memberEvent.content.displayname,
avatarUrl: memberEvent && memberEvent.content && memberEvent.content.avatar_url,
key: encodeKey(roomId, 0, eventIndex),
eventIdKey: encodeEventIdKey(roomId, eventJson.event_id),
},
fragmentIdComparer
);
eventIndexCounter++;
return eventEntry;
}
class ArchiveRoomViewModel extends ViewModel { class ArchiveRoomViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const { const {
homeserverUrl, homeserverUrl,
timelineViewModel,
room, room,
dayTimestamp, dayTimestampFrom,
eventEntriesByEventId, dayTimestampTo,
events,
stateEventMap,
shouldIndex, shouldIndex,
basePath, basePath,
} = options; } = options;
assert(homeserverUrl); assert(homeserverUrl);
assert(timelineViewModel);
assert(room); assert(room);
assert(dayTimestamp); assert(dayTimestampFrom);
assert(dayTimestampTo);
assert(events);
assert(stateEventMap);
assert(shouldIndex !== undefined); assert(shouldIndex !== undefined);
assert(eventEntriesByEventId); assert(events);
this._room = room; this._room = room;
this._dayTimestamp = dayTimestamp; this._dayTimestampFrom = dayTimestampFrom;
this._eventEntriesByEventId = eventEntriesByEventId; this._dayTimestampTo = dayTimestampTo;
this._currentTopPositionEventEntry = null; this._currentTopPositionEventEntry = null;
this._matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(basePath); this._matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(basePath);
this._basePath = basePath;
const navigation = this.navigation; const navigation = this.navigation;
const urlCreator = this.urlCreator; const urlCreator = this.urlCreator;
// Setup events and tiles necessary to render
const eventsToDisplay = this._addJumpSummaryEvents(events);
const { eventEntriesByEventId, tiles } = this._createHydrogenTilesFromEvents({
room: this._room,
events: eventsToDisplay,
stateEventMap,
});
this._eventEntriesByEventId = eventEntriesByEventId;
this._roomAvatarViewModel = new AvatarViewModel({ this._roomAvatarViewModel = new AvatarViewModel({
homeserverUrlToPullMediaFrom: homeserverUrl, homeserverUrlToPullMediaFrom: homeserverUrl,
avatarUrl: this._room.avatarUrl, avatarUrl: this._room.avatarUrl,
@ -52,7 +109,7 @@ class ArchiveRoomViewModel extends ViewModel {
entityId: this._room.id, entityId: this._room.id,
}); });
const initialDate = new Date(dayTimestamp); const initialDate = new Date(dayTimestampFrom);
this._calendarViewModel = new CalendarViewModel({ this._calendarViewModel = new CalendarViewModel({
// The day being shown in the archive // The day being shown in the archive
activeDate: initialDate, activeDate: initialDate,
@ -80,7 +137,12 @@ class ArchiveRoomViewModel extends ViewModel {
}) })
); );
this._timelineViewModel = timelineViewModel; this._timelineViewModel = {
showJumpDown: false,
setVisibleTileRange: () => {},
tiles,
};
// FIXME: Do we have to fake this? // FIXME: Do we have to fake this?
this.rightPanelModel = { this.rightPanelModel = {
navigation, navigation,
@ -205,8 +267,8 @@ class ArchiveRoomViewModel extends ViewModel {
); );
} }
get dayTimestamp() { get dayTimestampFrom() {
return this._dayTimestamp; return this._dayTimestampFrom;
} }
get roomDirectoryUrl() { get roomDirectoryUrl() {
@ -231,6 +293,118 @@ class ArchiveRoomViewModel extends ViewModel {
path = path.with(this.navigation.segment('change-dates', true)); path = path.with(this.navigation.segment('change-dates', true));
this.navigation.applyPath(path); this.navigation.applyPath(path);
} }
// Add the placeholder events which render the "Jump to previous/next activity" links
// in the timeline
_addJumpSummaryEvents(inputEventList) {
const events = [...inputEventList];
// Add a summary item to the bottom of the timeline that explains if we found
// events on the day requested.
const hasEventsFromGivenDay =
events[events.length - 1]?.origin_server_ts >= this._dayTimestampFrom;
let daySummaryKind;
if (events.length === 0) {
daySummaryKind = 'no-events-at-all';
} else if (hasEventsFromGivenDay) {
daySummaryKind = 'some-events-in-day';
} else if (!hasEventsFromGivenDay) {
daySummaryKind = 'no-events-in-day';
}
events.push({
event_id: getFakeEventId(),
type: 'org.matrix.archive.not_enough_events_from_day_summary',
room_id: this._room.id,
// Even though this isn't used for sort, just using the time where the event
// would logically be.
//
// -1 so we're not at 00:00:00 of the next day
origin_server_ts: this._dayTimestampTo - 1,
content: {
canonicalAlias: this._room.canonicalAlias,
daySummaryKind,
// The timestamp from the URL that was originally visited
dayTimestamp: this._dayTimestampFrom,
// The end of the range to use as a jumping off point to the next activity
rangeEndTimestamp: this._dayTimestampTo,
// This is a bit cheating but I don't know how else to pass this kind of
// info to the Tile viewmodel
basePath: this._basePath,
},
});
return events;
}
// A bunch of Hydrogen boilerplate to convert the events JSON into some `tiles` we can
// use with the `TimelineView`
_createHydrogenTilesFromEvents({ room, events, stateEventMap }) {
// We use the timeline to setup the relations between entries
const timeline = new Timeline({
roomId: room.id,
fragmentIdComparer: fragmentIdComparer,
clock: this.platform.clock,
logger: this.platform.logger,
});
// Something we can modify with new state updates as we see them
const workingStateEventMap = {
...stateEventMap,
};
const eventEntries = events.map((event) => {
if (event.type === 'm.room.member') {
workingStateEventMap[event.state_key] = event;
}
const memberEvent = workingStateEventMap[event.user_id];
return makeEventEntryFromEventJson(event, memberEvent);
});
//console.log('eventEntries', eventEntries.length);
// Map of `event_id` to `EventEntry`
const eventEntriesByEventId = eventEntries.reduce((currentMap, eventEntry) => {
currentMap[eventEntry.id] = eventEntry;
return currentMap;
}, {});
// We have to use `timeline._setupEntries([])` because it sets
// `this._allEntries` in `Timeline` and we don't want to use `timeline.load()`
// to request remote things.
timeline._setupEntries([]);
// Make it safe to iterate a derived observable collection
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
// We use the timeline to setup the relations between entries
timeline.addEntries(eventEntries);
//console.log('timeline.entries', timeline.entries.length, timeline.entries);
const tiles = new TilesCollection(timeline.entries, {
tileClassForEntry: customTileClassForEntry,
platform: this.platform,
navigation: this.navigation,
urlCreator: this.urlCreator,
timeline,
roomVM: {
room,
},
});
// Trigger `onSubscribeFirst` -> `tiles._populateTiles()` so it creates a tile
// for each entry to display. This way we can also call `tile.notifyVisible()`
// on each tile so that the tile creation doesn't happen later when the
// `TilesListView` is mounted and subscribes which is a bit out of our
// control.
tiles.subscribe({ onAdd: () => null, onUpdate: () => null });
// Make the lazy-load images appear
for (const tile of tiles) {
tile.notifyVisible();
}
return {
tiles,
eventEntriesByEventId,
};
}
} }
module.exports = ArchiveRoomViewModel; module.exports = ArchiveRoomViewModel;

View File

@ -92,8 +92,8 @@ class DisabledComposerView extends TemplateView {
const activeDate = new Date( const activeDate = new Date(
// If the date from our `archiveRoomViewModel` is available, use that // If the date from our `archiveRoomViewModel` is available, use that
vm?.currentTopPositionEventEntry?.timestamp || vm?.currentTopPositionEventEntry?.timestamp ||
// Otherwise, use our initial `dayTimestamp` // Otherwise, use our initial `dayTimestampFrom`
vm.dayTimestamp vm.dayTimestampFrom
); );
const dateString = activeDate.toISOString().split('T')[0]; const dateString = activeDate.toISOString().split('T')[0];
return t.span(`You're viewing an archive of events from ${dateString}. Use a `); return t.span(`You're viewing an archive of events from ${dateString}. Use a `);