diff --git a/shared/hydrogen-vm-render-script.js b/shared/hydrogen-vm-render-script.js index 2048431..22a07c3 100644 --- a/shared/hydrogen-vm-render-script.js +++ b/shared/hydrogen-vm-render-script.js @@ -14,22 +14,12 @@ const { RetainedObservableValue, PowerLevels, - - TilesCollection, - FragmentIdComparer, - EventEntry, - encodeKey, - encodeEventIdKey, - Timeline, } = require('hydrogen-view-sdk'); const ArchiveRoomView = require('matrix-public-archive-shared/views/ArchiveRoomView'); 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 ArchiveRoomViewModel = require('matrix-public-archive-shared/viewmodels/ArchiveRoomViewModel'); -const { - customTileClassForEntry, -} = require('matrix-public-archive-shared/lib/custom-tile-utilities'); const fromTimestamp = window.matrixPublicArchiveContext.fromTimestamp; assert(fromTimestamp); @@ -48,12 +38,6 @@ assert(config); assert(config.matrixServerUrl); assert(config.basePath); -let txnCount = 0; -function getFakeEventId() { - txnCount++; - return `fake-event-id-${new Date().getTime()}--${txnCount}`; -} - function addSupportClasses() { const input = document.createElement('input'); input.type = 'month'; @@ -63,31 +47,6 @@ function addSupportClasses() { 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(); // eslint-disable-next-line max-statements @@ -122,14 +81,6 @@ async function mountHydrogen() { // page don't say `undefined`. 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({ homeserver: config.matrixServerUrl, }); @@ -140,6 +91,7 @@ async function mountHydrogen() { canonicalAlias: roomData.canonicalAlias, avatarUrl: roomData.avatarUrl, avatarColorId: roomData.id, + // Hydrogen options used by the event TilesCollection (roomVM) mediaRepository: mediaRepository, // Based on https://github.com/vector-im/hydrogen-web/blob/5f9cfffa3b547991b665f57a8bf715270a1b2ef1/src/matrix/room/BaseRoom.js#L480 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({ // Hydrogen options + platform: platform, navigation: navigation, urlCreator: urlRouter, history: archiveHistory, // Our options homeserverUrl: config.matrixServerUrl, - timelineViewModel, room, // The timestamp from the URL that was originally visited - dayTimestamp: fromTimestamp, - eventEntriesByEventId, + dayTimestampFrom: fromTimestamp, + dayTimestampTo: toTimestamp, + events, + stateEventMap, shouldIndex, basePath: config.basePath, }); diff --git a/shared/viewmodels/ArchiveRoomViewModel.js b/shared/viewmodels/ArchiveRoomViewModel.js index d2db2e5..b2c1bad 100644 --- a/shared/viewmodels/ArchiveRoomViewModel.js +++ b/shared/viewmodels/ArchiveRoomViewModel.js @@ -1,6 +1,15 @@ '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'); @@ -10,35 +19,83 @@ const CalendarViewModel = require('matrix-public-archive-shared/viewmodels/Calen const DeveloperOptionsContentViewModel = require('matrix-public-archive-shared/viewmodels/DeveloperOptionsContentViewModel'); const RightPanelContentView = require('matrix-public-archive-shared/views/RightPanelContentView'); 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 { constructor(options) { super(options); const { homeserverUrl, - timelineViewModel, room, - dayTimestamp, - eventEntriesByEventId, + dayTimestampFrom, + dayTimestampTo, + events, + stateEventMap, shouldIndex, basePath, } = options; assert(homeserverUrl); - assert(timelineViewModel); assert(room); - assert(dayTimestamp); + assert(dayTimestampFrom); + assert(dayTimestampTo); + assert(events); + assert(stateEventMap); assert(shouldIndex !== undefined); - assert(eventEntriesByEventId); + assert(events); this._room = room; - this._dayTimestamp = dayTimestamp; - this._eventEntriesByEventId = eventEntriesByEventId; + this._dayTimestampFrom = dayTimestampFrom; + this._dayTimestampTo = dayTimestampTo; this._currentTopPositionEventEntry = null; this._matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(basePath); + this._basePath = basePath; const navigation = this.navigation; 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({ homeserverUrlToPullMediaFrom: homeserverUrl, avatarUrl: this._room.avatarUrl, @@ -52,7 +109,7 @@ class ArchiveRoomViewModel extends ViewModel { entityId: this._room.id, }); - const initialDate = new Date(dayTimestamp); + const initialDate = new Date(dayTimestampFrom); this._calendarViewModel = new CalendarViewModel({ // The day being shown in the archive 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? this.rightPanelModel = { navigation, @@ -205,8 +267,8 @@ class ArchiveRoomViewModel extends ViewModel { ); } - get dayTimestamp() { - return this._dayTimestamp; + get dayTimestampFrom() { + return this._dayTimestampFrom; } get roomDirectoryUrl() { @@ -231,6 +293,118 @@ class ArchiveRoomViewModel extends ViewModel { path = path.with(this.navigation.segment('change-dates', true)); 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; diff --git a/shared/views/ArchiveRoomView.js b/shared/views/ArchiveRoomView.js index 016eef2..3a4c0cb 100644 --- a/shared/views/ArchiveRoomView.js +++ b/shared/views/ArchiveRoomView.js @@ -92,8 +92,8 @@ class DisabledComposerView extends TemplateView { const activeDate = new Date( // If the date from our `archiveRoomViewModel` is available, use that vm?.currentTopPositionEventEntry?.timestamp || - // Otherwise, use our initial `dayTimestamp` - vm.dayTimestamp + // Otherwise, use our initial `dayTimestampFrom` + vm.dayTimestampFrom ); const dateString = activeDate.toISOString().split('T')[0]; return t.span(`You're viewing an archive of events from ${dateString}. Use a `);