Add lightbox support and Hydrogen URL hashes relative to the room (#12)
Add image lightbox support and Hydrogen URL hashes relative to the room Related to https://github.com/vector-im/hydrogen-web/issues/677 Requires the changes from https://github.com/vector-im/hydrogen-web/pull/749 (split out from https://github.com/vector-im/hydrogen-web/pull/653) ![](https://user-images.githubusercontent.com/558581/172526457-38c108e8-8c46-4e0c-9979-734348ec67fc.gif) ### Hydrogen routing relative to the room (remove session and room from the URL hash) Before: Page URL: doesn't work ```html <div class="Timeline_messageBody"> <div class="media" style="max-width: 400px"> <div class="spacer" style="padding-top: 48.75%;"></div> <a href="undefined"> <img src="http://192.168.1.182:8008//_matrix/media/r0/thumbnail/my.synapse.server/RxfuMxEgYcXHKYWERkKVUkqO?width=400&height=195&method=scale" alt="Friction_between_surfaces.jpeg" title="Friction_between_surfaces.jpeg" style="max-width: 400px; max-height: 195px;"> </a> <time>2/24 6:20 PM</time> </div> <!--node binding placeholder--> </div> ``` Before (not relative): Page URL: `http://localhost:3050/!HBehERstyQBxyJDLfR:my.synapse.server/date/2022/02/24#/session/123/room/!HBehERstyQBxyJDLfR:my.synapse.server/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk` ```html <div class="Timeline_messageBody"> <div class="media" style="max-width: 400px"> <div class="spacer" style="padding-top: 48.75%;"></div> <a href="#/session/123/room/!HBehERstyQBxyJDLfR:my.synapse.server/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk"> <img src="http://192.168.1.182:8008//_matrix/media/r0/thumbnail/my.synapse.server/RxfuMxEgYcXHKYWERkKVUkqO?width=400&height=195&method=scale" alt="Friction_between_surfaces.jpeg" title="Friction_between_surfaces.jpeg" style="max-width: 400px; max-height: 195px;"> </a> <time>2/24 6:20 PM</time> </div> <!--node binding placeholder--> </div> ``` After (nice relative links): Page URL: `http://localhost:3050/!HBehERstyQBxyJDLfR:my.synapse.server/date/2022/02/24#/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk` ```html <div class="Timeline_messageBody"> <div class="media" style="max-width: 400px"> <div class="spacer" style="padding-top: 48.75%;"></div> <a href="#/lightbox/$17cgP6YBP9ny9xuU1vBmpOYFhRG4zpOe9SOgWi2Wxsk"> <img src="http://192.168.1.182:8008//_matrix/media/r0/thumbnail/my.synapse.server/RxfuMxEgYcXHKYWERkKVUkqO?width=400&height=195&method=scale" alt="Friction_between_surfaces.jpeg" title="Friction_between_surfaces.jpeg" style="max-width: 400px; max-height: 195px;"> </a> <time>2/24 6:20 PM</time> </div> <!--node binding placeholder--> </div> ```
This commit is contained in:
parent
940c73868f
commit
7dfe8cabc9
|
@ -5,7 +5,7 @@
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018,
|
"ecmaVersion": 2022,
|
||||||
"sourceType": "script"
|
"sourceType": "script"
|
||||||
},
|
},
|
||||||
"plugins": ["node"],
|
"plugins": ["node"],
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
'use strict';
|
'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 {
|
class ArchiveView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
|
@ -13,6 +19,10 @@ class ArchiveView extends TemplateView {
|
||||||
[
|
[
|
||||||
t.view(new RoomView(vm.roomViewModel, viewClassForTile)),
|
t.view(new RoomView(vm.roomViewModel, viewClassForTile)),
|
||||||
t.view(new RightPanelView(vm.rightPanelModel)),
|
t.view(new RightPanelView(vm.rightPanelModel)),
|
||||||
|
t.mapView(
|
||||||
|
(vm) => vm.lightboxViewModel,
|
||||||
|
(lightboxViewModel) => (lightboxViewModel ? new LightboxView(lightboxViewModel) : null)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,15 @@ const {
|
||||||
encodeKey,
|
encodeKey,
|
||||||
encodeEventIdKey,
|
encodeEventIdKey,
|
||||||
Timeline,
|
Timeline,
|
||||||
// TimelineView,
|
|
||||||
// RoomView,
|
|
||||||
RoomViewModel,
|
RoomViewModel,
|
||||||
ViewModel,
|
ViewModel,
|
||||||
|
setupLightboxNavigation,
|
||||||
} = require('hydrogen-view-sdk');
|
} = require('hydrogen-view-sdk');
|
||||||
|
|
||||||
const ArchiveView = require('matrix-public-archive-shared/ArchiveView');
|
const ArchiveView = require('matrix-public-archive-shared/ArchiveView');
|
||||||
const RightPanelContentView = require('matrix-public-archive-shared/RightPanelContentView');
|
const RightPanelContentView = require('matrix-public-archive-shared/RightPanelContentView');
|
||||||
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
|
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;
|
const fromTimestamp = window.matrixPublicArchiveContext.fromTimestamp;
|
||||||
assert(fromTimestamp);
|
assert(fromTimestamp);
|
||||||
|
@ -73,6 +73,45 @@ function makeEventEntryFromEventJson(eventJson, memberEvent) {
|
||||||
return eventEntry;
|
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
|
// eslint-disable-next-line max-statements
|
||||||
async function mountHydrogen() {
|
async function mountHydrogen() {
|
||||||
const app = document.querySelector('#app');
|
const app = document.querySelector('#app');
|
||||||
|
@ -88,54 +127,33 @@ async function mountHydrogen() {
|
||||||
|
|
||||||
const navigation = createNavigation();
|
const navigation = createNavigation();
|
||||||
platform.setNavigation(navigation);
|
platform.setNavigation(navigation);
|
||||||
|
|
||||||
|
const archiveHistory = new ArchiveHistory(roomData.id);
|
||||||
const urlRouter = createRouter({
|
const urlRouter = createRouter({
|
||||||
navigation: navigation,
|
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
|
// We use the timeline to setup the relations between entries
|
||||||
const timeline = new Timeline({
|
const timeline = new Timeline({
|
||||||
roomId: roomData.id,
|
roomId: roomData.id,
|
||||||
//storage: this._storage,
|
|
||||||
fragmentIdComparer: fragmentIdComparer,
|
fragmentIdComparer: fragmentIdComparer,
|
||||||
clock: platform.clock,
|
clock: platform.clock,
|
||||||
logger: platform.logger,
|
logger: platform.logger,
|
||||||
//hsApi: this._hsApi
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mediaRepository = new MediaRepository({
|
const mediaRepository = new MediaRepository({
|
||||||
homeserver: config.matrixServerUrl,
|
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 = {
|
const room = {
|
||||||
name: roomData.name,
|
name: roomData.name,
|
||||||
id: roomData.id,
|
id: roomData.id,
|
||||||
|
@ -156,9 +174,14 @@ async function mountHydrogen() {
|
||||||
const memberEvent = workingStateEventMap[event.user_id];
|
const memberEvent = workingStateEventMap[event.user_id];
|
||||||
return makeEventEntryFromEventJson(event, memberEvent);
|
return makeEventEntryFromEventJson(event, memberEvent);
|
||||||
});
|
});
|
||||||
//console.log('eventEntries', eventEntries);
|
|
||||||
console.log('eventEntries', eventEntries.length);
|
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
|
// We have to use `timeline._setupEntries([])` because it sets
|
||||||
// `this._allEntries` in `Timeline` and we don't want to use `timeline.load()`
|
// `this._allEntries` in `Timeline` and we don't want to use `timeline.load()`
|
||||||
// to request remote things.
|
// to request remote things.
|
||||||
|
@ -198,23 +221,6 @@ async function mountHydrogen() {
|
||||||
tiles,
|
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({
|
const roomViewModel = new RoomViewModel({
|
||||||
room,
|
room,
|
||||||
ownUserId: 'xxx',
|
ownUserId: 'xxx',
|
||||||
|
@ -223,6 +229,7 @@ async function mountHydrogen() {
|
||||||
navigation,
|
navigation,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: We shouldn't have to dive into the internal fields to make this work
|
||||||
roomViewModel._timelineVM = timelineViewModel;
|
roomViewModel._timelineVM = timelineViewModel;
|
||||||
roomViewModel._composerVM = {
|
roomViewModel._composerVM = {
|
||||||
kind: 'none',
|
kind: 'none',
|
||||||
|
@ -277,9 +284,9 @@ async function mountHydrogen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromDate = new Date(fromTimestamp);
|
const fromDate = new Date(fromTimestamp);
|
||||||
const archiveViewModel = {
|
class ArchiveViewModel extends ViewModel {
|
||||||
roomViewModel,
|
roomViewModel = roomViewModel;
|
||||||
rightPanelModel: {
|
rightPanelModel = {
|
||||||
activeViewModel: {
|
activeViewModel: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
customView: RightPanelContentView,
|
customView: RightPanelContentView,
|
||||||
|
@ -290,18 +297,40 @@ async function mountHydrogen() {
|
||||||
calendarDate: fromDate,
|
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);
|
const view = new ArchiveView(archiveViewModel);
|
||||||
|
|
||||||
//console.log('view.mount()', view.mount());
|
|
||||||
app.replaceChildren(view.mount());
|
app.replaceChildren(view.mount());
|
||||||
|
|
||||||
addSupportClasses();
|
addSupportClasses();
|
||||||
|
|
||||||
|
supressBlankAnchorsReloadingThePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// N.B.: When we run this in a `vm`, it will return the last statement. It's
|
// N.B.: When we run this in a virtual machine (`vm`), it will return the last
|
||||||
// important to leave this as the last statement so we can await the promise it
|
// statement. It's important to leave this as the last statement so we can await
|
||||||
// returns and signal that all of the async tasks completed.
|
// the promise it returns and signal that all of the async tasks completed.
|
||||||
mountHydrogen();
|
mountHydrogen();
|
||||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue