From 788bce7904bfc54f42fbf9b16e5373e9549c8d1b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 16 Oct 2020 12:46:14 +0200 Subject: [PATCH] reduce navigation boilerplate this makes the url router adjust the url when the navigation path is changed, instead of doing urlRouter.applyUrl() and urlRouter.history.pushUrl(). This history field and applyUrl method on URLRouter are now private, as the URLRouter should only be used to generate urls you want to put in an , anything else should use navigator.push() --- src/domain/RootViewModel.js | 36 +++++-------- src/domain/navigation/Navigation.js | 15 +++++- src/domain/navigation/URLRouter.js | 52 +++++++------------ src/domain/session/RoomGridViewModel.js | 8 +-- .../session/leftpanel/LeftPanelViewModel.js | 21 ++++++-- src/main.js | 5 +- src/ui/web/dom/History.js | 26 +++------- 7 files changed, 74 insertions(+), 89 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 24c810c7..b8ef2015 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -35,13 +35,13 @@ export class RootViewModel extends ViewModel { this._sessionViewModel = null; } - async load(lastUrlHash) { + async load() { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); - this._applyNavigation(lastUrlHash); + this._applyNavigation(this.urlRouter.getLastUrl()); } - async _applyNavigation(restoreHashIfAtDefault) { + async _applyNavigation(restoreUrlIfAtDefault) { const isLogin = this.navigation.observe("login").get(); const sessionId = this.navigation.observe("session").get(); if (isLogin) { @@ -58,30 +58,24 @@ export class RootViewModel extends ViewModel { } } else { try { - let url = restoreHashIfAtDefault; - if (!url) { - // redirect depending on what sessions are already present + if (restoreUrlIfAtDefault) { + this.urlRouter.pushUrl(restoreUrlIfAtDefault); + } else { const sessionInfos = await this._sessionInfoStorage.getAll(); - url = this._urlForSessionInfos(sessionInfos); + if (sessionInfos.length === 0) { + this.navigation.push("login"); + } else if (sessionInfos.length === 1) { + this.navigation.push("session", sessionInfos[0].id); + } else { + this.navigation.push("session"); + } } - this.urlRouter.history.replaceUrl(url); - this.urlRouter.applyUrl(url); } catch (err) { this._setSection(() => this._error = err); } } } - _urlForSessionInfos(sessionInfos) { - if (sessionInfos.length === 0) { - return this.urlRouter.urlForSegment("login"); - } else if (sessionInfos.length === 1) { - return this.urlRouter.urlForSegment("session", sessionInfos[0].id); - } else { - return this.urlRouter.urlForSegment("session"); - } - } - async _showPicker() { this._setSection(() => { this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({ @@ -102,10 +96,8 @@ export class RootViewModel extends ViewModel { defaultHomeServer: "https://matrix.org", createSessionContainer: this._createSessionContainer, ready: sessionContainer => { - const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId); - this.urlRouter.applyUrl(url); - this.urlRouter.history.replaceUrl(url); this._showSession(sessionContainer); + this.navigation.push("session", sessionContainer.sessionId); }, })); }); diff --git a/src/domain/navigation/Navigation.js b/src/domain/navigation/Navigation.js index f7222ec2..fa1c7142 100644 --- a/src/domain/navigation/Navigation.js +++ b/src/domain/navigation/Navigation.js @@ -14,19 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableValue} from "../../observable/ObservableValue.js"; +import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue.js"; export class Navigation { constructor(allowsChild) { this._allowsChild = allowsChild; this._path = new Path([], allowsChild); this._observables = new Map(); + this._pathObservable = new ObservableValue(this._path); + } + + get pathObservable() { + return this._pathObservable; } get path() { return this._path; } + push(type, value = undefined) { + return this.applyPath(this.path.with(new Segment(type, value))); + } + applyPath(path) { // Path is not exported, so you can only create a Path through Navigation, // so we assume it respects the allowsChild rules @@ -45,6 +54,10 @@ export class Navigation { const observable = this._observables.get(segment.type); observable?.emitIfChanged(); } + // to observe the whole path having changed + // Since paths are immutable, + // we can just use set here which will compare the references + this._pathObservable.set(this._path); } observe(type) { diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index ac3f968c..082be2b9 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -14,40 +14,50 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Segment} from "./Navigation.js"; - export class URLRouter { constructor({history, navigation, parseUrlPath, stringifyPath}) { - this._subscription = null; this._history = history; this._navigation = navigation; this._parseUrlPath = parseUrlPath; this._stringifyPath = stringifyPath; + this._subscription = null; + this._pathSubscription = null; } attach() { this._subscription = this._history.subscribe(url => { - const redirectedUrl = this.applyUrl(url); + const redirectedUrl = this._applyUrl(url); if (redirectedUrl !== url) { - this._history.replaceUrl(redirectedUrl); + this._history.replaceUrlSilently(redirectedUrl); + } + }); + this._applyUrl(this._history.get()); + this._pathSubscription = this._navigation.pathObservable.subscribe(path => { + const url = this.urlForPath(path); + if (url !== this._history.get()) { + this._history.pushUrlSilently(url); } }); - this.applyUrl(this._history.get()); } dispose() { this._subscription = this._subscription(); + this._pathSubscription = this._pathSubscription(); } - applyUrl(url) { + _applyUrl(url) { const urlPath = this._history.urlAsPath(url) const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path)); this._navigation.applyPath(navPath); return this._history.pathAsUrl(this._stringifyPath(navPath)); } - get history() { - return this._history; + pushUrl(url) { + this._history.pushUrl(url); + } + + getLastUrl() { + return this._history.getLastUrl(); } urlForSegments(segments) { @@ -70,7 +80,7 @@ export class URLRouter { } urlForPath(path) { - return this.history.pathAsUrl(this._stringifyPath(path)); + return this._history.pathAsUrl(this._stringifyPath(path)); } openRoomActionUrl(roomId) { @@ -78,26 +88,4 @@ export class URLRouter { const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; return this._history.pathAsUrl(urlPath); } - - disableGridUrl() { - let path = this._navigation.path.until("session"); - const room = this._navigation.path.get("room"); - if (room) { - path = path.with(room); - } - return this.urlForPath(path); - } - - enableGridUrl() { - let path = this._navigation.path.until("session"); - const room = this._navigation.path.get("room"); - if (room) { - path = path.with(this._navigation.segment("rooms", [room.value])); - path = path.with(room); - } else { - path = path.with(this._navigation.segment("rooms", [])); - path = path.with(this._navigation.segment("empty-grid-tile", 0)); - } - return this.urlForPath(path); - } } diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index 04e810fb..b9b62153 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -83,16 +83,12 @@ export class RoomGridViewModel extends ViewModel { if (index === this._selectedIndex) { return; } - let path = this.navigation.path; const vm = this._viewModels[index]; if (vm) { - path = path.with(this.navigation.segment("room", vm.id)); + this.navigation.push("room", vm.id); } else { - path = path.with(this.navigation.segment("empty-grid-tile", index)); + this.navigation.push("empty-grid-tile", index); } - let url = this.urlRouter.urlForPath(path); - url = this.urlRouter.applyUrl(url); - this.urlRouter.history.pushUrl(url); } /** called from SessionViewModel */ diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index b5b652ab..cca0ae06 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -76,14 +76,25 @@ export class LeftPanelViewModel extends ViewModel { } toggleGrid() { - let url; if (this.gridEnabled) { - url = this.urlRouter.disableGridUrl(); + let path = this.navigation.path.until("session"); + const room = this.navigation.path.get("room"); + if (room) { + path = path.with(room); + } + this.navigation.applyPath(path); } else { - url = this.urlRouter.enableGridUrl(); + let path = this.navigation.path.until("session"); + const room = this.navigation.path.get("room"); + if (room) { + path = path.with(this.navigation.segment("rooms", [room.value])); + path = path.with(room); + } else { + path = path.with(this.navigation.segment("rooms", [])); + path = path.with(this.navigation.segment("empty-grid-tile", 0)); + } + this.navigation.applyPath(path); } - url = this.urlRouter.applyUrl(url); - this.urlRouter.history.pushUrl(url); } get roomList() { diff --git a/src/main.js b/src/main.js index 6e9571f4..d98448dd 100644 --- a/src/main.js +++ b/src/main.js @@ -118,8 +118,7 @@ export async function main(container, paths, legacyExtras) { } const navigation = createNavigation(); - const history = new History(); - const urlRouter = createRouter({navigation, history}); + const urlRouter = createRouter({navigation, history: new History()}); urlRouter.attach(); const vm = new RootViewModel({ @@ -143,7 +142,7 @@ export async function main(container, paths, legacyExtras) { navigation }); window.__brawlViewModel = vm; - await vm.load(history.getLastUrl()); + await vm.load(); // TODO: replace with platform.createAndMountRootView(vm, container); const view = new RootView(vm); container.appendChild(view.mount()); diff --git a/src/ui/web/dom/History.js b/src/ui/web/dom/History.js index 8bc433cd..61f04444 100644 --- a/src/ui/web/dom/History.js +++ b/src/ui/web/dom/History.js @@ -20,14 +20,9 @@ export class History extends BaseObservableValue { constructor() { super(); this._boundOnHashChange = null; - this._expectSetEcho = false; } _onHashChange() { - if (this._expectSetEcho) { - this._expectSetEcho = false; - return; - } this.emit(this.get()); this._storeHash(this.get()); } @@ -37,28 +32,19 @@ export class History extends BaseObservableValue { } /** does not emit */ - replaceUrl(url) { + replaceUrlSilently(url) { window.history.replaceState(null, null, url); this._storeHash(url); } /** does not emit */ - pushUrl(url) { + pushUrlSilently(url) { window.history.pushState(null, null, url); this._storeHash(url); - // const hash = this.urlAsPath(url); - // // important to check before we expect an echo - // // as setting the hash to it's current value doesn't - // // trigger onhashchange - // if (hash === document.location.hash) { - // return; - // } - // // this operation is silent, - // // so avoid emitting on echo hashchange event - // if (this._boundOnHashChange) { - // this._expectSetEcho = true; - // } - // document.location.hash = hash; + } + + pushUrl(url) { + document.location.hash = url; } urlAsPath(url) {