diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index b3b79a89..f0b29816 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -16,22 +16,6 @@ limitations under the License. import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue"; -type SegmentType = { - "login": true; - "session": string; - "sso": string; - "logout": true; - "room": string; - "rooms": string[]; - "settings": true; - "create-room": true; - "empty-grid-tile": number; - "lightbox": string; - "right-panel": boolean; - "details": true; - "members": true; - "member": string; -}; type AllowsChild = (parent: Segment | undefined, child: Segment) => boolean; @@ -55,7 +39,7 @@ export class Navigation { return this._path; } - push(type: K, ...value: T[K] extends true? [undefined?]: [T[K]]): void { + push(type: K, ...value: T[K] extends true? [(undefined | true)?]: [T[K]]): void { const newPath = this.path.with(new Segment(type, ...value)); if (newPath) { this.applyPath(newPath); @@ -107,7 +91,7 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type: K, ...value: T[K] extends true? [undefined?]: [T[K]]): Segment { + segment(type: K, ...value: T[K] extends true? [(undefined | true)?]: [T[K]]): Segment { return new Segment(type, ...value); } } @@ -133,7 +117,7 @@ function segmentValueEqual(a?: T[keyof T], b?: T[keyof T]): boolean { export class Segment { public value: T[K]; - constructor(public type: K, ...value: T[K] extends true? [undefined?]: [T[K]]) { + constructor(public type: K, ...value: T[K] extends true? [(undefined | true)?]: [T[K]]) { this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K]; } } @@ -231,6 +215,8 @@ class SegmentObservable extends BaseObservableValue { } } +export type { Path }; + export function tests() { function createMockNavigation() { diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.ts similarity index 75% rename from src/domain/navigation/index.js rename to src/domain/navigation/index.ts index 68fbe993..92cf41a1 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.ts @@ -16,6 +16,24 @@ limitations under the License. import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter.js"; +import type { Path } from "./Navigation"; + +type SegmentType = { + login: true; + session: string; + 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() { return new Navigation(allowsChild); @@ -25,7 +43,7 @@ export function createRouter({history, navigation}) { return new URLRouter({history, navigation, stringifyPath, parseUrlPath}); } -function allowsChild(parent, child) { +function allowsChild(parent: {type: string, value: any} | undefined, child: {type: string, value: any}): 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 | null { + let newPath: Path | null = 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,29 @@ function roomsSegmentWithRoom(rooms, roomId, path) { } } -function pushRightPanelSegment(array, segment, value = true) { +// todo-self: verify code change here is okay +function pushRightPanelSegment(array: Segment[], segment: T, ...value: SegmentType[T] extends true? [(undefined | true)?]: [SegmentType[T]]) { 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) { +export function parseUrlPath(urlPath: string, currentNavPath: Path, defaultSessionId: string): Segment[] { // substr(1) to take of initial / const parts = urlPath.substr(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 +190,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 +225,16 @@ 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 +244,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 +254,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 +266,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 +284,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 +295,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 +306,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,13 +317,13 @@ 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"]), new Segment("room", "b") ]); - const segments = parseUrlPath("/session/1/open-room/d", path); + const segments = parseUrlPath("/session/1/open-room/d", path, ""); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -301,13 +333,13 @@ 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"]), new Segment("room", "b") ]); - const segments = parseUrlPath("/session/1/open-room/a", path); + const segments = parseUrlPath("/session/1/open-room/a", path, ""); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -317,7 +349,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"]), @@ -325,7 +357,7 @@ export function tests() { new Segment("right-panel", true), new Segment("details", true) ]); - const segments = parseUrlPath("/session/1/open-room/a", path); + const segments = parseUrlPath("/session/1/open-room/a", path, ""); assert.equal(segments.length, 5); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -339,7 +371,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"]), @@ -347,7 +379,7 @@ export function tests() { new Segment("right-panel", true), new Segment("members", true) ]); - const segments = parseUrlPath("/session/1/open-room/a/member/foo", path); + const segments = parseUrlPath("/session/1/open-room/a/member/foo", path, ""); assert.equal(segments.length, 5); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -361,13 +393,13 @@ 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"]), new Segment("empty-grid-tile", 4) ]); - const segments = parseUrlPath("/session/1/open-room/d", path); + const segments = parseUrlPath("/session/1/open-room/d", path, ""); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -377,82 +409,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