diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index b0409edd..b7aecc2c 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -16,10 +16,11 @@ limitations under the License. import {Options as BaseOptions, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; +import {SegmentType} from "./navigation/index"; type Options = { sessionId: string; } & BaseOptions; -export class LogoutViewModel extends ViewModel { +export class LogoutViewModel extends ViewModel { private _sessionId: string; private _busy: boolean; private _showConfirm: boolean; @@ -41,7 +42,7 @@ export class LogoutViewModel extends ViewModel { return this._busy; } - get cancelUrl(): string { + get cancelUrl(): string | undefined { return this.urlCreator.urlForSegment("session", true); } diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 00ae847b..62da159f 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -27,17 +27,19 @@ import type {Platform} from "../platform/web/Platform"; 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 {SegmentType} from "./navigation/index"; +import type {IURLRouter} from "./navigation/URLRouter"; -export type Options = { - platform: Platform - logger: ILogger - urlCreator: URLRouter - navigation: Navigation - emitChange?: (params: any) => void +export type Options = { + platform: Platform; + logger: ILogger; + urlCreator: IURLRouter; + navigation: Navigation; + emitChange?: (params: any) => void; } -export class ViewModel extends EventEmitter<{change: never}> { + +export class ViewModel = Options> extends EventEmitter<{change: never}> { private disposables?: Disposables; private _isDisposed = false; private _options: Readonly; @@ -47,7 +49,7 @@ export class ViewModel extends EventEmitter<{change this._options = options; } - childOptions(explicitOptions: T): T & Options { + childOptions(explicitOptions: T): T & Options { return Object.assign({}, this._options, explicitOptions); } @@ -58,9 +60,9 @@ export class ViewModel extends EventEmitter<{change return this._options[name]; } - observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void): void { + observeNavigation(type: T, onChange: (value: N[T], type: T) => void): void { const segmentObservable = this.navigation.observe(type); - const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { + const unsubscribe = segmentObservable.subscribe((value: N[T]) => { onChange(value, type); }); this.track(unsubscribe); @@ -135,11 +137,12 @@ export class ViewModel extends EventEmitter<{change return this.platform.logger; } - get urlCreator(): URLRouter { + get urlCreator(): IURLRouter { return this._options.urlCreator; } - get navigation(): Navigation { - return this._options.navigation; + get navigation(): Navigation { + // typescript needs a little help here + return this._options.navigation as unknown as Navigation; } } diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index aaeca54f..8eb11a9e 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -21,6 +21,8 @@ import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; +import {SegmentType} from "../navigation/index"; + import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login"; type Options = { @@ -29,7 +31,7 @@ type Options = { loginToken?: string; } & BaseOptions; -export class LoginViewModel extends ViewModel { +export class LoginViewModel extends ViewModel { private _ready: ReadyFn; private _loginToken?: string; private _client: Client; diff --git a/src/domain/navigation/Navigation.js b/src/domain/navigation/Navigation.ts similarity index 65% rename from src/domain/navigation/Navigation.js rename to src/domain/navigation/Navigation.ts index 340ae0d5..f5039732 100644 --- a/src/domain/navigation/Navigation.js +++ b/src/domain/navigation/Navigation.ts @@ -16,27 +16,49 @@ limitations under the License. import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue"; -export class Navigation { - constructor(allowsChild) { + +type AllowsChild = (parent: Segment | undefined, child: Segment) => boolean; + +/** + * OptionalValue is basically stating that if SegmentType[type] = true: + * - Allow this type to be optional + * - Give it a default value of undefined + * - Also allow it to be true + * This lets us do: + * const s: Segment = new Segment("create-room"); + * instead of + * const s: Segment = new Segment("create-room", undefined); + */ +export type OptionalValue = T extends true? [(undefined | true)?]: [T]; + +export class Navigation { + private readonly _allowsChild: AllowsChild; + private _path: Path; + private readonly _observables: Map> = new Map(); + private readonly _pathObservable: ObservableValue>; + + constructor(allowsChild: AllowsChild) { this._allowsChild = allowsChild; this._path = new Path([], allowsChild); - this._observables = new Map(); this._pathObservable = new ObservableValue(this._path); } - get pathObservable() { + get pathObservable(): ObservableValue> { return this._pathObservable; } - get path() { + get path(): Path { return this._path; } - push(type, value = undefined) { - return this.applyPath(this.path.with(new Segment(type, value))); + push(type: K, ...value: OptionalValue): void { + const newPath = this.path.with(new Segment(type, ...value)); + if (newPath) { + this.applyPath(newPath); + } } - applyPath(path) { + applyPath(path: Path): void { // Path is not exported, so you can only create a Path through Navigation, // so we assume it respects the allowsChild rules const oldPath = this._path; @@ -60,7 +82,7 @@ export class Navigation { this._pathObservable.set(this._path); } - observe(type) { + observe(type: keyof T): SegmentObservable { let observable = this._observables.get(type); if (!observable) { observable = new SegmentObservable(this, type); @@ -69,9 +91,9 @@ export class Navigation { return observable; } - pathFrom(segments) { - let parent; - let i; + pathFrom(segments: Segment[]): Path { + let parent: Segment | undefined; + let i: number; for (i = 0; i < segments.length; i += 1) { if (!this._allowsChild(parent, segments[i])) { return new Path(segments.slice(0, i), this._allowsChild); @@ -81,12 +103,12 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type, value) { - return new Segment(type, value); + segment(type: K, ...value: OptionalValue): Segment { + return new Segment(type, ...value); } } -function segmentValueEqual(a, b) { +function segmentValueEqual(a?: T[keyof T], b?: T[keyof T]): boolean { if (a === b) { return true; } @@ -103,24 +125,29 @@ function segmentValueEqual(a, b) { return false; } -export class Segment { - constructor(type, value) { - this.type = type; - this.value = value === undefined ? true : value; + +export class Segment { + public value: T[K]; + + constructor(public type: K, ...value: OptionalValue) { + this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K]; } } -class Path { - constructor(segments = [], allowsChild) { +class Path { + private readonly _segments: Segment[]; + private readonly _allowsChild: AllowsChild; + + constructor(segments: Segment[] = [], allowsChild: AllowsChild) { this._segments = segments; this._allowsChild = allowsChild; } - clone() { + clone(): Path { return new Path(this._segments.slice(), this._allowsChild); } - with(segment) { + with(segment: Segment): Path | undefined { let index = this._segments.length - 1; do { if (this._allowsChild(this._segments[index], segment)) { @@ -132,10 +159,10 @@ class Path { index -= 1; } while(index >= -1); // allow -1 as well so we check if the segment is allowed as root - return null; + return undefined; } - until(type) { + until(type: keyof T): Path { const index = this._segments.findIndex(s => s.type === type); if (index !== -1) { return new Path(this._segments.slice(0, index + 1), this._allowsChild) @@ -143,11 +170,11 @@ class Path { return new Path([], this._allowsChild); } - get(type) { + get(type: keyof T): Segment | undefined { return this._segments.find(s => s.type === type); } - replace(segment) { + replace(segment: Segment): Path | undefined { const index = this._segments.findIndex(s => s.type === segment.type); if (index !== -1) { const parent = this._segments[index - 1]; @@ -160,10 +187,10 @@ class Path { } } } - return null; + return undefined; } - get segments() { + get segments(): Segment[] { return this._segments; } } @@ -172,43 +199,49 @@ class Path { * custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet. * This ensures that observers of a segment can also read the most recent value of other segments. */ -class SegmentObservable extends BaseObservableValue { - constructor(navigation, type) { +class SegmentObservable extends BaseObservableValue { + private readonly _navigation: Navigation; + private _type: keyof T; + private _lastSetValue?: T[keyof T]; + + constructor(navigation: Navigation, type: keyof T) { super(); this._navigation = navigation; this._type = type; this._lastSetValue = navigation.path.get(type)?.value; } - get() { + get(): T[keyof T] | undefined { const path = this._navigation.path; const segment = path.get(this._type); const value = segment?.value; return value; } - emitIfChanged() { + emitIfChanged(): void { const newValue = this.get(); - if (!segmentValueEqual(newValue, this._lastSetValue)) { + if (!segmentValueEqual(newValue, this._lastSetValue)) { this._lastSetValue = newValue; this.emit(newValue); } } } +export type {Path}; + export function tests() { function createMockNavigation() { return new Navigation((parent, {type}) => { switch (parent?.type) { case undefined: - return type === "1" || "2"; + return type === "1" || type === "2"; case "1": return type === "1.1"; case "1.1": return type === "1.1.1"; case "2": - return type === "2.1" || "2.2"; + return type === "2.1" || type === "2.2"; default: return false; } @@ -216,7 +249,7 @@ export function tests() { } function observeTypes(nav, types) { - const changes = []; + const changes: {type:string, value:any}[] = []; for (const type of types) { nav.observe(type).subscribe(value => { changes.push({type, value}); @@ -225,6 +258,12 @@ export function tests() { return changes; } + type SegmentType = { + "foo": number; + "bar": number; + "baz": number; + } + return { "applying a path emits an event on the observable": assert => { const nav = createMockNavigation(); @@ -242,18 +281,18 @@ export function tests() { assert.equal(changes[1].value, 8); }, "path.get": assert => { - const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); - assert.equal(path.get("foo").value, 5); - assert.equal(path.get("bar").value, 6); + const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); + assert.equal(path.get("foo")!.value, 5); + assert.equal(path.get("bar")!.value, 6); }, "path.replace success": assert => { - const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); + const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); const newPath = path.replace(new Segment("foo", 1)); - assert.equal(newPath.get("foo").value, 1); - assert.equal(newPath.get("bar").value, 6); + assert.equal(newPath!.get("foo")!.value, 1); + assert.equal(newPath!.get("bar")!.value, 6); }, "path.replace not found": assert => { - const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); + const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); const newPath = path.replace(new Segment("baz", 1)); assert.equal(newPath, null); } diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.ts similarity index 58% rename from src/domain/navigation/URLRouter.js rename to src/domain/navigation/URLRouter.ts index ab5dc5ce..bf1c218d 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.ts @@ -14,28 +14,55 @@ See the License for the specific language governing permissions and limitations under the License. */ -export class URLRouter { - constructor({history, navigation, parseUrlPath, stringifyPath}) { +import type {History} from "../../platform/web/dom/History.js"; +import type {Navigation, Segment, Path, OptionalValue} from "./Navigation"; +import type {SubscriptionHandle} from "../../observable/BaseObservable"; + +type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId?: string) => Segment[]; +type StringifyPath = (path: Path) => string; + +export interface IURLRouter { + attach(): void; + dispose(): void; + pushUrl(url: string): void; + tryRestoreLastUrl(): boolean; + urlForSegments(segments: Segment[]): string | undefined; + urlForSegment(type: K, ...value: OptionalValue): string | undefined; + urlUntilSegment(type: keyof T): string; + urlForPath(path: Path): string; + openRoomActionUrl(roomId: string): string; + createSSOCallbackURL(): string; + normalizeUrl(): void; +} + +export class URLRouter implements IURLRouter { + private readonly _history: History; + private readonly _navigation: Navigation; + private readonly _parseUrlPath: ParseURLPath; + private readonly _stringifyPath: StringifyPath; + private _subscription?: SubscriptionHandle; + private _pathSubscription?: SubscriptionHandle; + private _isApplyingUrl: boolean = false; + private _defaultSessionId?: string; + + constructor(history: History, navigation: Navigation, parseUrlPath: ParseURLPath, stringifyPath: StringifyPath) { this._history = history; this._navigation = navigation; this._parseUrlPath = parseUrlPath; this._stringifyPath = stringifyPath; - this._subscription = null; - this._pathSubscription = null; - this._isApplyingUrl = false; this._defaultSessionId = this._getLastSessionId(); } - _getLastSessionId() { + private _getLastSessionId(): string | undefined { const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || ""); const sessionId = navPath.get("session")?.value; if (typeof sessionId === "string") { return sessionId; } - return null; + return undefined; } - attach() { + attach(): void { this._subscription = this._history.subscribe(url => this._applyUrl(url)); // subscribe to path before applying initial url // so redirects in _applyNavPathToHistory are reflected in url bar @@ -43,12 +70,12 @@ export class URLRouter { this._applyUrl(this._history.get()); } - dispose() { - this._subscription = this._subscription(); - this._pathSubscription = this._pathSubscription(); + dispose(): void { + if (this._subscription) { this._subscription = this._subscription(); } + if (this._pathSubscription) { this._pathSubscription = this._pathSubscription(); } } - _applyNavPathToHistory(path) { + private _applyNavPathToHistory(path: Path): void { const url = this.urlForPath(path); if (url !== this._history.get()) { if (this._isApplyingUrl) { @@ -60,7 +87,7 @@ export class URLRouter { } } - _applyNavPathToNavigation(navPath) { + private _applyNavPathToNavigation(navPath: Path): void { // this will cause _applyNavPathToHistory to be called, // so set a flag whether this request came from ourselves // (in which case it is a redirect if the url does not match the current one) @@ -69,21 +96,21 @@ export class URLRouter { this._isApplyingUrl = false; } - _urlAsNavPath(url) { + private _urlAsNavPath(url: string): Path { const urlPath = this._history.urlAsPath(url); return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId)); } - _applyUrl(url) { + private _applyUrl(url: string): void { const navPath = this._urlAsNavPath(url); this._applyNavPathToNavigation(navPath); } - pushUrl(url) { + pushUrl(url: string): void { this._history.pushUrl(url); } - tryRestoreLastUrl() { + tryRestoreLastUrl(): boolean { const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || ""); if (lastNavPath.segments.length !== 0) { this._applyNavPathToNavigation(lastNavPath); @@ -92,8 +119,8 @@ export class URLRouter { return false; } - urlForSegments(segments) { - let path = this._navigation.path; + urlForSegments(segments: Segment[]): string | undefined { + let path: Path | undefined = this._navigation.path; for (const segment of segments) { path = path.with(segment); if (!path) { @@ -103,29 +130,29 @@ export class URLRouter { return this.urlForPath(path); } - urlForSegment(type, value) { - return this.urlForSegments([this._navigation.segment(type, value)]); + urlForSegment(type: K, ...value: OptionalValue): string | undefined { + return this.urlForSegments([this._navigation.segment(type, ...value)]); } - urlUntilSegment(type) { + urlUntilSegment(type: keyof T): string { return this.urlForPath(this._navigation.path.until(type)); } - urlForPath(path) { + urlForPath(path: Path): string { return this._history.pathAsUrl(this._stringifyPath(path)); } - openRoomActionUrl(roomId) { + openRoomActionUrl(roomId: string): string { // not a segment to navigation knowns about, so append it manually const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; return this._history.pathAsUrl(urlPath); } - createSSOCallbackURL() { + createSSOCallbackURL(): string { return window.location.origin; } - normalizeUrl() { + normalizeUrl(): void { // Remove any queryParameters from the URL // Gets rid of the loginToken after SSO this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`); diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.ts similarity index 73% rename from src/domain/navigation/index.js rename to src/domain/navigation/index.ts index 086367ce..afba0d86 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.ts @@ -14,18 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Navigation, Segment} from "./Navigation.js"; -import {URLRouter} from "./URLRouter.js"; +import {Navigation, Segment} from "./Navigation"; +import {URLRouter} from "./URLRouter"; +import type {Path, OptionalValue} from "./Navigation"; -export function createNavigation() { +export type SegmentType = { + "login": true; + "session": string | boolean; + "sso": string; + "logout": true; + "room": string; + "rooms": string[]; + "settings": true; + "create-room": true; + "empty-grid-tile": number; + "lightbox": string; + "right-panel": true; + "details": true; + "members": true; + "member": string; +}; + +export function createNavigation(): Navigation { return new Navigation(allowsChild); } -export function createRouter({history, navigation}) { - return new URLRouter({history, navigation, stringifyPath, parseUrlPath}); +export function createRouter({history, navigation}: {history: History, navigation: Navigation}): URLRouter { + return new URLRouter(history, navigation, parseUrlPath, stringifyPath); } -function allowsChild(parent, child) { +function allowsChild(parent: Segment | undefined, child: Segment): boolean { const {type} = child; switch (parent?.type) { case undefined: @@ -45,8 +63,9 @@ function allowsChild(parent, child) { } } -export function removeRoomFromPath(path, roomId) { - const rooms = path.get("rooms"); +export function removeRoomFromPath(path: Path, roomId: string): Path | undefined { + let newPath: Path | undefined = path; + const rooms = newPath.get("rooms"); let roomIdGridIndex = -1; // first delete from rooms segment if (rooms) { @@ -54,22 +73,22 @@ export function removeRoomFromPath(path, roomId) { if (roomIdGridIndex !== -1) { const idsWithoutRoom = rooms.value.slice(); idsWithoutRoom[roomIdGridIndex] = ""; - path = path.replace(new Segment("rooms", idsWithoutRoom)); + newPath = newPath.replace(new Segment("rooms", idsWithoutRoom)); } } - const room = path.get("room"); + const room = newPath!.get("room"); // then from room (which occurs with or without rooms) if (room && room.value === roomId) { if (roomIdGridIndex !== -1) { - path = path.with(new Segment("empty-grid-tile", roomIdGridIndex)); + newPath = newPath!.with(new Segment("empty-grid-tile", roomIdGridIndex)); } else { - path = path.until("session"); + newPath = newPath!.until("session"); } } - return path; + return newPath; } -function roomsSegmentWithRoom(rooms, roomId, path) { +function roomsSegmentWithRoom(rooms: Segment, roomId: string, path: Path): Segment { if(!rooms.value.includes(roomId)) { const emptyGridTile = path.get("empty-grid-tile"); const oldRoom = path.get("room"); @@ -87,28 +106,28 @@ function roomsSegmentWithRoom(rooms, roomId, path) { } } -function pushRightPanelSegment(array, segment, value = true) { +function pushRightPanelSegment(array: Segment[], segment: T, ...value: OptionalValue): void { array.push(new Segment("right-panel")); - array.push(new Segment(segment, value)); + array.push(new Segment(segment, ...value)); } -export function addPanelIfNeeded(navigation, path) { +export function addPanelIfNeeded(navigation: Navigation, path: Path): Path { const segments = navigation.path.segments; const i = segments.findIndex(segment => segment.type === "right-panel"); let _path = path; if (i !== -1) { _path = path.until("room"); - _path = _path.with(segments[i]); - _path = _path.with(segments[i + 1]); + _path = _path.with(segments[i])!; + _path = _path.with(segments[i + 1])!; } return _path; } -export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { - // substr(1) to take of initial / - const parts = urlPath.substr(1).split("/"); +export function parseUrlPath(urlPath: string, currentNavPath: Path, defaultSessionId?: string): Segment[] { + // substring(1) to take of initial / + const parts = urlPath.substring(1).split("/"); const iterator = parts[Symbol.iterator](); - const segments = []; + const segments: Segment[] = []; let next; while (!(next = iterator.next()).done) { const type = next.value; @@ -170,9 +189,9 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { return segments; } -export function stringifyPath(path) { +export function stringifyPath(path: Path): string { let urlPath = ""; - let prevSegment; + let prevSegment: Segment | undefined; for (const segment of path.segments) { switch (segment.type) { case "rooms": @@ -205,9 +224,15 @@ export function stringifyPath(path) { } export function tests() { + function createEmptyPath() { + const nav: Navigation = new Navigation(allowsChild); + const path = nav.pathFrom([]); + return path; + } + return { "stringify grid url with focused empty tile": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -217,7 +242,7 @@ export function tests() { assert.equal(urlPath, "/session/1/rooms/a,b,c/3"); }, "stringify grid url with focused room": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -227,7 +252,7 @@ export function tests() { assert.equal(urlPath, "/session/1/rooms/a,b,c/1"); }, "stringify url with right-panel and details segment": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -239,13 +264,15 @@ export function tests() { assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); }, "Parse loginToken query parameter into SSO segment": assert => { - const segments = parseUrlPath("?loginToken=a1232aSD123"); + const path = createEmptyPath(); + const segments = parseUrlPath("?loginToken=a1232aSD123", path); assert.equal(segments.length, 1); assert.equal(segments[0].type, "sso"); assert.equal(segments[0].value, "a1232aSD123"); }, "parse grid url path with focused empty tile": assert => { - const segments = parseUrlPath("/session/1/rooms/a,b,c/3"); + const path = createEmptyPath(); + const segments = parseUrlPath("/session/1/rooms/a,b,c/3", path); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -255,7 +282,8 @@ export function tests() { assert.equal(segments[2].value, 3); }, "parse grid url path with focused room": assert => { - const segments = parseUrlPath("/session/1/rooms/a,b,c/1"); + const path = createEmptyPath(); + const segments = parseUrlPath("/session/1/rooms/a,b,c/1", path); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -265,7 +293,8 @@ export function tests() { assert.equal(segments[2].value, "b"); }, "parse empty grid url": assert => { - const segments = parseUrlPath("/session/1/rooms/"); + const path = createEmptyPath(); + const segments = parseUrlPath("/session/1/rooms/", path); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -275,7 +304,8 @@ export function tests() { assert.equal(segments[2].value, 0); }, "parse empty grid url with focus": assert => { - const segments = parseUrlPath("/session/1/rooms//1"); + const path = createEmptyPath(); + const segments = parseUrlPath("/session/1/rooms//1", path); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -285,7 +315,7 @@ export function tests() { assert.equal(segments[2].value, 1); }, "parse open-room action replacing the current focused room": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -301,7 +331,7 @@ export function tests() { assert.equal(segments[2].value, "d"); }, "parse open-room action changing focus to an existing room": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -317,7 +347,7 @@ export function tests() { assert.equal(segments[2].value, "a"); }, "parse open-room action changing focus to an existing room with details open": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -339,7 +369,7 @@ export function tests() { assert.equal(segments[4].value, true); }, "open-room action should only copy over previous segments if there are no parts after open-room": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -361,7 +391,7 @@ export function tests() { assert.equal(segments[4].value, "foo"); }, "parse open-room action setting a room in an empty tile": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), @@ -377,82 +407,83 @@ export function tests() { assert.equal(segments[2].value, "d"); }, "parse session url path without id": assert => { - const segments = parseUrlPath("/session"); + const path = createEmptyPath(); + const segments = parseUrlPath("/session", path); assert.equal(segments.length, 1); assert.equal(segments[0].type, "session"); assert.strictEqual(segments[0].value, true); }, "remove active room from grid path turns it into empty tile": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), new Segment("room", "b") ]); const newPath = removeRoomFromPath(path, "b"); - assert.equal(newPath.segments.length, 3); - assert.equal(newPath.segments[0].type, "session"); - assert.equal(newPath.segments[0].value, 1); - assert.equal(newPath.segments[1].type, "rooms"); - assert.deepEqual(newPath.segments[1].value, ["a", "", "c"]); - assert.equal(newPath.segments[2].type, "empty-grid-tile"); - assert.equal(newPath.segments[2].value, 1); + assert.equal(newPath?.segments.length, 3); + assert.equal(newPath?.segments[0].type, "session"); + assert.equal(newPath?.segments[0].value, 1); + assert.equal(newPath?.segments[1].type, "rooms"); + assert.deepEqual(newPath?.segments[1].value, ["a", "", "c"]); + assert.equal(newPath?.segments[2].type, "empty-grid-tile"); + assert.equal(newPath?.segments[2].value, 1); }, "remove inactive room from grid path": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), new Segment("room", "b") ]); const newPath = removeRoomFromPath(path, "a"); - assert.equal(newPath.segments.length, 3); - assert.equal(newPath.segments[0].type, "session"); - assert.equal(newPath.segments[0].value, 1); - assert.equal(newPath.segments[1].type, "rooms"); - assert.deepEqual(newPath.segments[1].value, ["", "b", "c"]); - assert.equal(newPath.segments[2].type, "room"); - assert.equal(newPath.segments[2].value, "b"); + assert.equal(newPath?.segments.length, 3); + assert.equal(newPath?.segments[0].type, "session"); + assert.equal(newPath?.segments[0].value, 1); + assert.equal(newPath?.segments[1].type, "rooms"); + assert.deepEqual(newPath?.segments[1].value, ["", "b", "c"]); + assert.equal(newPath?.segments[2].type, "room"); + assert.equal(newPath?.segments[2].value, "b"); }, "remove inactive room from grid path with empty tile": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", ""]), new Segment("empty-grid-tile", 3) ]); const newPath = removeRoomFromPath(path, "b"); - assert.equal(newPath.segments.length, 3); - assert.equal(newPath.segments[0].type, "session"); - assert.equal(newPath.segments[0].value, 1); - assert.equal(newPath.segments[1].type, "rooms"); - assert.deepEqual(newPath.segments[1].value, ["a", "", ""]); - assert.equal(newPath.segments[2].type, "empty-grid-tile"); - assert.equal(newPath.segments[2].value, 3); + assert.equal(newPath?.segments.length, 3); + assert.equal(newPath?.segments[0].type, "session"); + assert.equal(newPath?.segments[0].value, 1); + assert.equal(newPath?.segments[1].type, "rooms"); + assert.deepEqual(newPath?.segments[1].value, ["a", "", ""]); + assert.equal(newPath?.segments[2].type, "empty-grid-tile"); + assert.equal(newPath?.segments[2].value, 3); }, "remove active room": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("room", "b") ]); const newPath = removeRoomFromPath(path, "b"); - assert.equal(newPath.segments.length, 1); - assert.equal(newPath.segments[0].type, "session"); - assert.equal(newPath.segments[0].value, 1); + assert.equal(newPath?.segments.length, 1); + assert.equal(newPath?.segments[0].type, "session"); + assert.equal(newPath?.segments[0].value, 1); }, "remove inactive room doesn't do anything": assert => { - const nav = new Navigation(allowsChild); + const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("room", "b") ]); const newPath = removeRoomFromPath(path, "a"); - assert.equal(newPath.segments.length, 2); - assert.equal(newPath.segments[0].type, "session"); - assert.equal(newPath.segments[0].value, 1); - assert.equal(newPath.segments[1].type, "room"); - assert.equal(newPath.segments[1].value, "b"); + assert.equal(newPath?.segments.length, 2); + assert.equal(newPath?.segments[0].type, "session"); + assert.equal(newPath?.segments[0].value, 1); + assert.equal(newPath?.segments[1].type, "room"); + assert.equal(newPath?.segments[1].value, "b"); }, } diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index a7d19054..8e443e2d 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../ViewModel"; -import {addPanelIfNeeded} from "../navigation/index.js"; +import {addPanelIfNeeded} from "../navigation/index"; function dedupeSparse(roomIds) { return roomIds.map((id, idx) => { @@ -185,7 +185,7 @@ export class RoomGridViewModel extends ViewModel { } } -import {createNavigation} from "../navigation/index.js"; +import {createNavigation} from "../navigation/index"; import {ObservableValue} from "../../observable/ObservableValue"; export function tests() { diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 2fd3ca7e..8c8d71a2 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -21,7 +21,7 @@ import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; -import {addPanelIfNeeded} from "../../navigation/index.js"; +import {addPanelIfNeeded} from "../../navigation/index"; export class LeftPanelViewModel extends ViewModel { constructor(options) { diff --git a/src/lib.ts b/src/lib.ts index 90bf597c..4d1f906f 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -18,7 +18,7 @@ export {Platform} from "./platform/web/Platform.js"; export {Client, LoadStatus} from "./matrix/Client.js"; export {RoomStatus} from "./matrix/room/common"; // export main view & view models -export {createNavigation, createRouter} from "./domain/navigation/index.js"; +export {createNavigation, createRouter} from "./domain/navigation/index"; export {RootViewModel} from "./domain/RootViewModel.js"; export {RootView} from "./platform/web/ui/RootView.js"; export {SessionViewModel} from "./domain/session/SessionViewModel.js"; diff --git a/src/platform/web/main.js b/src/platform/web/main.js index edc2cf14..83644456 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -17,7 +17,7 @@ limitations under the License. // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay"; import {RootViewModel} from "../../domain/RootViewModel.js"; -import {createNavigation, createRouter} from "../../domain/navigation/index.js"; +import {createNavigation, createRouter} from "../../domain/navigation/index"; // Don't use a default export here, as we use multiple entries during legacy build, // which does not support default exports, // see https://github.com/rollup/plugins/tree/master/packages/multi-entry