Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lightbox support and Hydrogen URL hashes relative to the room #12

Merged
merged 11 commits into from
Jun 8, 2022
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"node": true
},
"parserOptions": {
"ecmaVersion": 2018,
"ecmaVersion": 2022,
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
"sourceType": "script"
},
"plugins": ["node"],
Expand Down
12 changes: 11 additions & 1 deletion shared/ArchiveView.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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)
),
]
);
}
Expand Down
151 changes: 90 additions & 61 deletions shared/hydrogen-vm-render-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -73,6 +73,45 @@ function makeEventEntryFromEventJson(eventJson, memberEvent) {
return eventEntry;
}

// For any `<a href="">` (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 `<a href="">` (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');
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -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();
50 changes: 50 additions & 0 deletions shared/lib/archive-history.js
Original file line number Diff line number Diff line change
@@ -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 `<a href="">` (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;