diff --git a/.eslintrc.json b/.eslintrc.json
index 70f28763..d7dcb5d9 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -5,7 +5,7 @@
"node": true
},
"parserOptions": {
- "ecmaVersion": 2018,
+ "ecmaVersion": 2022,
"sourceType": "script"
},
"plugins": ["node"],
diff --git a/shared/ArchiveView.js b/shared/ArchiveView.js
index 84adc4b0..343cafaa 100644
--- a/shared/ArchiveView.js
+++ b/shared/ArchiveView.js
@@ -1,6 +1,12 @@
'use strict';
-const { TemplateView, RoomView, RightPanelView, viewClassForTile } = require('hydrogen-view-sdk');
+const {
+ TemplateView,
+ RoomView,
+ RightPanelView,
+ LightboxView,
+ viewClassForTile,
+} = require('hydrogen-view-sdk');
class ArchiveView extends TemplateView {
render(t, vm) {
@@ -13,6 +19,10 @@ class ArchiveView extends TemplateView {
[
t.view(new RoomView(vm.roomViewModel, viewClassForTile)),
t.view(new RightPanelView(vm.rightPanelModel)),
+ t.mapView(
+ (vm) => vm.lightboxViewModel,
+ (lightboxViewModel) => (lightboxViewModel ? new LightboxView(lightboxViewModel) : null)
+ ),
]
);
}
diff --git a/shared/hydrogen-vm-render-script.js b/shared/hydrogen-vm-render-script.js
index d47b4c50..33e5b833 100644
--- a/shared/hydrogen-vm-render-script.js
+++ b/shared/hydrogen-vm-render-script.js
@@ -14,15 +14,15 @@ const {
encodeKey,
encodeEventIdKey,
Timeline,
- // TimelineView,
- // RoomView,
RoomViewModel,
ViewModel,
+ setupLightboxNavigation,
} = require('hydrogen-view-sdk');
const ArchiveView = require('matrix-public-archive-shared/ArchiveView');
const RightPanelContentView = require('matrix-public-archive-shared/RightPanelContentView');
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
+const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history');
const fromTimestamp = window.matrixPublicArchiveContext.fromTimestamp;
assert(fromTimestamp);
@@ -73,6 +73,45 @@ function makeEventEntryFromEventJson(eventJson, memberEvent) {
return eventEntry;
}
+// For any `` (anchor with a blank href), instead of reloading the
+// page just remove the hash. Also cleanup whenever the hash changes for
+// whatever reason.
+//
+// For example, when closing the lightbox by clicking the close "x" icon, it
+// would reload the page instead of SPA because `href=""` will cause a page
+// navigation if we didn't have this code. Also cleanup whenever the hash is
+// emptied out (like when pressing escape in the lightbox).
+function supressBlankAnchorsReloadingThePage() {
+ const eventHandler = {
+ clearHash() {
+ // Cause a `hashchange` event to be fired
+ document.location.hash = '';
+ // Cleanup the leftover `#` left on the URL
+ window.history.replaceState(null, null, window.location.pathname);
+ },
+ handleEvent(e) {
+ // For any `` (anchor with a blank href), instead of reloading
+ // the page just remove the hash.
+ if (
+ e.type === 'click' &&
+ e.target.tagName?.toLowerCase() === 'a' &&
+ e.target?.getAttribute('href') === ''
+ ) {
+ this.clearHash();
+ // Prevent the page navigation (reload)
+ e.preventDefault();
+ }
+ // Also cleanup whenever the hash is emptied out (like when pressing escape in the lightbox)
+ else if (e.type === 'hashchange' && document.location.hash === '') {
+ this.clearHash();
+ }
+ },
+ };
+
+ document.addEventListener('click', eventHandler);
+ window.addEventListener('hashchange', eventHandler);
+}
+
// eslint-disable-next-line max-statements
async function mountHydrogen() {
const app = document.querySelector('#app');
@@ -88,54 +127,33 @@ async function mountHydrogen() {
const navigation = createNavigation();
platform.setNavigation(navigation);
+
+ const archiveHistory = new ArchiveHistory(roomData.id);
const urlRouter = createRouter({
navigation: navigation,
- history: platform.history,
+ // We use our own history because we want the hash to be relative to the
+ // room and not include the session/room.
+ //
+ // Normally, people use `history: platform.history,`
+ history: archiveHistory,
});
+ // Make it listen to changes from the history instance. And populate the
+ // `Navigation` with path segments to work from so `href`'s rendered on the
+ // page don't say `undefined`.
+ urlRouter.attach();
// We use the timeline to setup the relations between entries
const timeline = new Timeline({
roomId: roomData.id,
- //storage: this._storage,
fragmentIdComparer: fragmentIdComparer,
clock: platform.clock,
logger: platform.logger,
- //hsApi: this._hsApi
});
const mediaRepository = new MediaRepository({
homeserver: config.matrixServerUrl,
});
- // const urlRouter = {
- // urlUntilSegment: () => {
- // return 'todo';
- // },
- // urlForSegments: (segments) => {
- // const isLightBox = segments.find((segment) => {
- // return segment.type === 'lightbox';
- // console.log('segment', segment);
- // });
-
- // if (isLightBox) {
- // return '#';
- // }
-
- // return 'todo';
- // },
- // };
-
- // const navigation = {
- // segment: (type, value) => {
- // return new Segment(type, value);
- // },
- // };
-
- const lightbox = navigation.observe('lightbox');
- lightbox.subscribe((eventId) => {
- this._updateLightbox(eventId);
- });
-
const room = {
name: roomData.name,
id: roomData.id,
@@ -156,9 +174,14 @@ async function mountHydrogen() {
const memberEvent = workingStateEventMap[event.user_id];
return makeEventEntryFromEventJson(event, memberEvent);
});
- //console.log('eventEntries', eventEntries);
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.
@@ -198,23 +221,6 @@ async function mountHydrogen() {
tiles,
};
- // const view = new TimelineView(timelineViewModel);
-
- // const roomViewModel = {
- // kind: 'room',
- // timelineViewModel,
- // composerViewModel: {
- // kind: 'none',
- // },
- // i18n: RoomViewModel.prototype.i18n,
-
- // id: roomData.id,
- // name: roomData.name,
- // avatarUrl(size) {
- // return getAvatarHttpUrl(roomData.avatarUrl, size, platform, mediaRepository);
- // },
- // };
-
const roomViewModel = new RoomViewModel({
room,
ownUserId: 'xxx',
@@ -223,6 +229,7 @@ async function mountHydrogen() {
navigation,
});
+ // FIXME: We shouldn't have to dive into the internal fields to make this work
roomViewModel._timelineVM = timelineViewModel;
roomViewModel._composerVM = {
kind: 'none',
@@ -277,9 +284,9 @@ async function mountHydrogen() {
}
const fromDate = new Date(fromTimestamp);
- const archiveViewModel = {
- roomViewModel,
- rightPanelModel: {
+ class ArchiveViewModel extends ViewModel {
+ roomViewModel = roomViewModel;
+ rightPanelModel = {
activeViewModel: {
type: 'custom',
customView: RightPanelContentView,
@@ -290,18 +297,40 @@ async function mountHydrogen() {
calendarDate: fromDate,
}),
},
- },
- };
+ };
+
+ constructor(options) {
+ super(options);
+
+ this.#setupNavigation();
+ }
+
+ #setupNavigation() {
+ setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => {
+ return {
+ room,
+ eventEntry: eventEntriesByEventId[eventId],
+ };
+ });
+ }
+ }
+
+ const archiveViewModel = new ArchiveViewModel({
+ navigation: navigation,
+ urlCreator: urlRouter,
+ history: archiveHistory,
+ });
const view = new ArchiveView(archiveViewModel);
- //console.log('view.mount()', view.mount());
app.replaceChildren(view.mount());
addSupportClasses();
+
+ supressBlankAnchorsReloadingThePage();
}
-// N.B.: When we run this in a `vm`, it will return the last statement. It's
-// important to leave this as the last statement so we can await the promise it
-// returns and signal that all of the async tasks completed.
+// N.B.: When we run this in a virtual machine (`vm`), it will return the last
+// statement. It's important to leave this as the last statement so we can await
+// the promise it returns and signal that all of the async tasks completed.
mountHydrogen();
diff --git a/shared/lib/archive-history.js b/shared/lib/archive-history.js
new file mode 100644
index 00000000..35d0a304
--- /dev/null
+++ b/shared/lib/archive-history.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const { History } = require('hydrogen-view-sdk');
+const assert = require('./assert');
+
+// Mock a full hash whenever someone asks via `history.get()` but when
+// constructing URL's for use `href` etc, they should relative to the room
+// (remove session and room from the hash).
+class ArchiveHistory extends History {
+ constructor(roomId) {
+ super();
+
+ assert(roomId);
+ this._baseHash = `#/session/123/room/${roomId}`;
+ }
+
+ // Even though the page hash is relative to the room, we still expose the full
+ // hash for Hydrogen to route things internally as expected.
+ get() {
+ const hash = super.get()?.replace(/^#/, '') ?? '';
+ return this._baseHash + hash;
+ }
+
+ replaceUrlSilently(url) {
+ // We don't need to do this when server-side rendering in Node.js because
+ // the #hash is not available to servers. This will be called as a
+ // downstream call of `urlRouter.attach()` which we do when bootstraping
+ // everything.
+ if (window.history) {
+ super.replaceUrlSilently(url);
+ }
+ }
+
+ // Make the URLs we use in the UI of the app relative to the room:
+ // Before: #/session/123/room/!HBehERstyQBxyJDLfR:my.synapse.server/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk
+ // After: #/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk
+ pathAsUrl(path) {
+ const leftoverPath = super.pathAsUrl(path).replace(this._baseHash, '');
+ // Only add back the hash when there is hash content beyond the base so we
+ // don't end up with an extraneous `#` on the end of the URL. This will end
+ // up creating some `` (anchors with a blank href) but we have
+ // some code to clean this up, see `supressBlankAnchorsReloadingThePage`.
+ if (leftoverPath.length) {
+ return `#${leftoverPath}`;
+ }
+ return leftoverPath;
+ }
+}
+
+module.exports = ArchiveHistory;