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) {