From ad56f4db4f13698a6cec757a25aaadeb6a8ac92e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jun 2022 23:02:13 -0500 Subject: [PATCH] More simple lightbox for SDK and compatible with custom History Changes to go along with https://github.com/matrix-org/matrix-public-archive/pull/12 Split out from https://github.com/vector-im/hydrogen-web/pull/653 --- src/domain/ViewModel.ts | 6 ++ src/domain/session/SessionViewModel.js | 29 ++------ src/domain/session/room/LightboxViewModel.js | 20 +++--- .../session/room/lightbox-navigation.js | 69 +++++++++++++++++++ src/lib.ts | 3 + src/platform/web/dom/History.js | 4 +- src/platform/web/ui/general/Link.js | 30 ++++++++ 7 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 src/domain/session/room/lightbox-navigation.js create mode 100644 src/platform/web/ui/general/Link.js diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 0bc52f6e..6e3bfd77 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -28,12 +28,14 @@ import type {Clock} from "../platform/web/dom/Clock"; import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {URLRouter} from "./navigation/URLRouter"; +import type {History} from "../platform/web/dom/History"; export type Options = { platform: Platform logger: ILogger urlCreator: URLRouter navigation: Navigation + history: History emitChange?: (params: any) => void } @@ -142,4 +144,8 @@ export class ViewModel extends EventEmitter<{change get navigation(): Navigation { return this._options.navigation; } + + get history(): History { + return this._options.history; + } } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index a67df3a7..3b0c63d1 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -20,7 +20,7 @@ import {RoomViewModel} from "./room/RoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js"; import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js"; -import {LightboxViewModel} from "./room/LightboxViewModel.js"; +import {setupLightboxNavigation} from "./room/lightbox-navigation.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; @@ -81,12 +81,12 @@ export class SessionViewModel extends ViewModel { })); this._updateCreateRoom(createRoom.get()); - const lightbox = this.navigation.observe("lightbox"); - this.track(lightbox.subscribe(eventId => { - this._updateLightbox(eventId); - })); - this._updateLightbox(lightbox.get()); - + setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => { + return { + room, + eventId, + }; + }); const rightpanel = this.navigation.observe("right-panel"); this.track(rightpanel.subscribe(() => this._updateRightPanel())); @@ -267,21 +267,6 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } - _updateLightbox(eventId) { - if (this._lightboxViewModel) { - this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); - } - if (eventId) { - const room = this._roomFromNavigation(); - this._lightboxViewModel = this.track(new LightboxViewModel(this.childOptions({eventId, room}))); - } - this.emitChange("lightboxViewModel"); - } - - get lightboxViewModel() { - return this._lightboxViewModel; - } - _roomFromNavigation() { const roomId = this.navigation.path.get("room")?.value; const room = this._client.session.rooms.get(roomId); diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 8ce8757a..b22ba1c4 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -19,21 +19,25 @@ import {ViewModel} from "../../ViewModel"; export class LightboxViewModel extends ViewModel { constructor(options) { super(options); - this._eventId = options.eventId; + this._eventEntry = options.eventEntry; + this._eventId = options.eventId || options.eventEntry.id; this._unencryptedImageUrl = null; this._decryptedImage = null; this._closeUrl = this.urlCreator.urlUntilSegment("room"); - this._eventEntry = null; this._date = null; this._subscribeToEvent(options.room, options.eventId); } _subscribeToEvent(room, eventId) { - const eventObservable = room.observeEvent(eventId); - this.track(eventObservable.subscribe(eventEntry => { - this._loadEvent(room, eventEntry); - })); - this._loadEvent(room, eventObservable.get()); + let event = this._eventEntry; + if (!this._eventEntry) { + const eventObservable = room.observeEvent(eventId); + this.track(eventObservable.subscribe(eventEntry => { + this._loadEvent(room, eventEntry); + })); + event = eventObservable.get(); + } + this._loadEvent(room, event); } async _loadEvent(room, eventEntry) { @@ -92,6 +96,6 @@ export class LightboxViewModel extends ViewModel { } close() { - this.platform.history.pushUrl(this.closeUrl); + this.history.pushUrl(this.closeUrl); } } diff --git a/src/domain/session/room/lightbox-navigation.js b/src/domain/session/room/lightbox-navigation.js new file mode 100644 index 00000000..1cc2886c --- /dev/null +++ b/src/domain/session/room/lightbox-navigation.js @@ -0,0 +1,69 @@ +/* +Copyright 2022 Bruno Windels +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {LightboxViewModel} from "./LightboxViewModel.js"; + +// Store the `LightboxViewModel` under a symbol so no one else can tamper with +// it. This acts like a private field on the class since no one else has the +// symbol to look it up. +let lightboxViewModelSymbol = Symbol('lightboxViewModel'); + +/** + * Destroys and creates a new the `LightboxViewModel` depending if + * `lightboxChildOptions.eventEntry` or `lightboxChildOptions.eventId` are + * provided. + */ +function updateLightboxViewModel(vm, fieldName, lightboxChildOptions) { + // Remove any existing `LightboxViewModel` before we assemble the new one below + if (vm[lightboxViewModelSymbol]) { + vm[lightboxViewModelSymbol] = vm.disposeTracked(vm[lightboxViewModelSymbol]); + // Let the `LightboxView` know that the `LightboxViewModel` has changed + vm.emitChange(fieldName); + } + // Create the new `LightboxViewModel` if the `eventEntry` exists directly or + // `eventId` which we can load from the store + if (lightboxChildOptions.eventId || lightboxChildOptions.eventEntry) { + vm[lightboxViewModelSymbol] = vm.track(new LightboxViewModel(vm.childOptions(lightboxChildOptions))); + // Let the `LightboxView` know that the `LightboxViewModel` has changed + vm.emitChange(fieldName); + } +} + +/** + * Handles updating the `LightboxViewModel` whenever the page URL changes and + * emits changes which the `LightboxView` will use to re-render. This is a + * composable piece of logic to call in an existing `ViewModel`'s constructor. + */ +export function setupLightboxNavigation(vm, fieldName = 'lightboxViewModel', lightboxChildOptionsFunction) { + // On the given `vm`, create a getter at `fieldName` that the + // `LightboxViewModel` is exposed at for usage in the view. + Object.defineProperty(vm, fieldName, { + get: function() { + return vm[lightboxViewModelSymbol]; + } + }); + + // Whenever the page navigates somewhere, keep the `lightboxViewModel` up to date + const lightbox = vm.navigation.observe("lightbox"); + vm.track(lightbox.subscribe(eventId => { + updateLightboxViewModel(vm, fieldName, lightboxChildOptionsFunction(eventId)); + })); + // Also handle the case where the URL already includes `/lightbox/$eventId` (like + // from page-load) + const initialLightBoxEventId = lightbox.get(); + updateLightboxViewModel(vm, fieldName, lightboxChildOptionsFunction(initialLightBoxEventId)); +} diff --git a/src/lib.ts b/src/lib.ts index 90bf597c..8688b2a2 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -25,6 +25,8 @@ export {SessionViewModel} from "./domain/session/SessionViewModel.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; +export {LightboxView} from "./platform/web/ui/session/room/LightboxView.js"; +export {setupLightboxNavigation} from "./domain/session/room/lightbox-navigation.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index"; export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index"; @@ -62,6 +64,7 @@ export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessa export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js"; export {Navigation} from "./domain/navigation/Navigation.js"; +export {History} from "./platform/web/dom/History.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; export {TemplateView} from "./platform/web/ui/general/TemplateView"; diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index d51974bb..7a989879 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -30,10 +30,10 @@ export class History extends BaseObservableValue { But for SSO, we need to handle /?loginToken= Handle that as a special case for now. */ - if (document.location.search.includes("loginToken")) { + if (document?.location?.search.includes("loginToken")) { return document.location.search; } - return document.location.hash; + return document?.location?.hash; } /** does not emit */ diff --git a/src/platform/web/ui/general/Link.js b/src/platform/web/ui/general/Link.js new file mode 100644 index 00000000..2e809f90 --- /dev/null +++ b/src/platform/web/ui/general/Link.js @@ -0,0 +1,30 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "./general/TemplateView"; + +export class LinkView extends TemplateView { + render(t, vm) { + return t.a({ + ...vm, + onClick: (e) => { + // Allow the `urlRouter` to cancel the URL navigation and any upstream + // consumer that added their own `onClick` handler. + return vm.urlCreator.linkClick(this, e) || vm.onClick?.(e); + } + }); + } +}