From 66f6c4aba15c555cf0e77b1671e5c3dc92f92df7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 23:37:10 +0530 Subject: [PATCH 001/354] WIP --- .../{Navigation.js => Navigation.ts} | 91 +++++++++++++------ src/domain/navigation/index.js | 2 +- 2 files changed, 63 insertions(+), 30 deletions(-) rename src/domain/navigation/{Navigation.js => Navigation.ts} (76%) diff --git a/src/domain/navigation/Navigation.js b/src/domain/navigation/Navigation.ts similarity index 76% rename from src/domain/navigation/Navigation.js rename to src/domain/navigation/Navigation.ts index 340ae0d5..da6841f8 100644 --- a/src/domain/navigation/Navigation.js +++ b/src/domain/navigation/Navigation.ts @@ -15,28 +15,53 @@ limitations under the License. */ import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue"; +import type {allowsChild as AllowsChild} from "./index.js"; + +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; +}; export class Navigation { - constructor(allowsChild) { + 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, value = undefined): 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 +85,7 @@ export class Navigation { this._pathObservable.set(this._path); } - observe(type) { + observe(type: keyof SegmentType): SegmentObservable { let observable = this._observables.get(type); if (!observable) { observable = new SegmentObservable(this, type); @@ -69,9 +94,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 +106,12 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type, value) { + segment(type: T, value: SegmentType[T]): Segment { return new Segment(type, value); } } -function segmentValueEqual(a, b) { +function segmentValueEqual(a?: SegmentType[keyof SegmentType], b?: SegmentType[keyof SegmentType]): boolean { if (a === b) { return true; } @@ -103,24 +128,28 @@ function segmentValueEqual(a, b) { return false; } -export class Segment { - constructor(type, value) { - this.type = type; - this.value = value === undefined ? true : value; - } + +export class Segment { + constructor( + public type: T, + public value: SegmentType[T] | true = value === undefined ? true : value + ) {} } class Path { - constructor(segments = [], allowsChild) { + 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 | null { let index = this._segments.length - 1; do { if (this._allowsChild(this._segments[index], segment)) { @@ -135,7 +164,7 @@ class Path { return null; } - until(type) { + until(type: keyof SegmentType): 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 +172,11 @@ class Path { return new Path([], this._allowsChild); } - get(type) { + get(type: keyof SegmentType): Segment | undefined { return this._segments.find(s => s.type === type); } - replace(segment) { + replace(segment: Segment): Path | null { const index = this._segments.findIndex(s => s.type === segment.type); if (index !== -1) { const parent = this._segments[index - 1]; @@ -163,7 +192,7 @@ class Path { return null; } - get segments() { + get segments(): Segment[] { return this._segments; } } @@ -172,22 +201,26 @@ 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 SegmentType; + private _lastSetValue?: SegmentType[keyof SegmentType]; + + constructor(navigation: Navigation, type: keyof SegmentType) { super(); this._navigation = navigation; this._type = type; this._lastSetValue = navigation.path.get(type)?.value; } - get() { + get(): SegmentType[keyof SegmentType] | 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)) { this._lastSetValue = newValue; diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 086367ce..68fbe993 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Navigation, Segment} from "./Navigation.js"; +import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter.js"; export function createNavigation() { From 04d5b9bfda15b085a7aea8377ec9fb0a62681b06 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Feb 2022 16:07:18 +0530 Subject: [PATCH 002/354] WIP - 2 --- src/domain/navigation/Navigation.ts | 82 ++++++++++++++++------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index da6841f8..cb6637a9 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -34,11 +34,11 @@ type SegmentType = { "member": string; }; -export class Navigation { +export class Navigation { private readonly _allowsChild: AllowsChild; - private _path: Path; - private readonly _observables: Map = new Map(); - private readonly _pathObservable: ObservableValue; + private _path: Path; + private readonly _observables: Map> = new Map(); + private readonly _pathObservable: ObservableValue>; constructor(allowsChild: AllowsChild) { this._allowsChild = allowsChild; @@ -46,11 +46,11 @@ export class Navigation { this._pathObservable = new ObservableValue(this._path); } - get pathObservable(): ObservableValue { + get pathObservable(): ObservableValue> { return this._pathObservable; } - get path(): Path { + get path(): Path { return this._path; } @@ -61,7 +61,7 @@ export class Navigation { } } - applyPath(path: Path): void { + 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; @@ -85,7 +85,7 @@ export class Navigation { this._pathObservable.set(this._path); } - observe(type: keyof SegmentType): SegmentObservable { + observe(type: keyof T): SegmentObservable { let observable = this._observables.get(type); if (!observable) { observable = new SegmentObservable(this, type); @@ -94,7 +94,7 @@ export class Navigation { return observable; } - pathFrom(segments: Segment[]): Path { + pathFrom(segments: Segment[]): Path { let parent: Segment | undefined; let i: number; for (i = 0; i < segments.length; i += 1) { @@ -106,12 +106,12 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type: T, value: SegmentType[T]): Segment { + segment(type: K, value: T[K]): Segment { return new Segment(type, value); } } -function segmentValueEqual(a?: SegmentType[keyof SegmentType], b?: SegmentType[keyof SegmentType]): boolean { +function segmentValueEqual(a?: T[keyof T], b?: T[keyof T]): boolean { if (a === b) { return true; } @@ -129,27 +129,27 @@ function segmentValueEqual(a?: SegmentType[keyof SegmentType], b?: SegmentType[k } -export class Segment { +export class Segment { constructor( - public type: T, - public value: SegmentType[T] | true = value === undefined ? true : value + public type: K, + public value: T[K] = (value === undefined ? true : value) as T[K] ) {} } -class Path { - private readonly _segments: Segment[]; +class Path { + private readonly _segments: Segment[]; private readonly _allowsChild: AllowsChild; - constructor(segments: Segment[] = [], allowsChild: AllowsChild) { + constructor(segments: Segment[] = [], allowsChild: AllowsChild) { this._segments = segments; this._allowsChild = allowsChild; } - clone(): Path { + clone(): Path { return new Path(this._segments.slice(), this._allowsChild); } - with(segment: Segment): Path | null { + with(segment: Segment): Path | null { let index = this._segments.length - 1; do { if (this._allowsChild(this._segments[index], segment)) { @@ -164,7 +164,7 @@ class Path { return null; } - until(type: keyof SegmentType): Path { + 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) @@ -172,11 +172,11 @@ class Path { return new Path([], this._allowsChild); } - get(type: keyof SegmentType): Segment | undefined { + get(type: keyof T): Segment | undefined { return this._segments.find(s => s.type === type); } - replace(segment: Segment): Path | null { + replace(segment: Segment): Path | null { const index = this._segments.findIndex(s => s.type === segment.type); if (index !== -1) { const parent = this._segments[index - 1]; @@ -192,7 +192,7 @@ class Path { return null; } - get segments(): Segment[] { + get segments(): Segment[] { return this._segments; } } @@ -201,19 +201,19 @@ 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 { - private readonly _navigation: Navigation; - private _type: keyof SegmentType; - private _lastSetValue?: SegmentType[keyof SegmentType]; +class SegmentObservable extends BaseObservableValue { + private readonly _navigation: Navigation; + private _type: keyof T; + private _lastSetValue?: T[keyof T]; - constructor(navigation: Navigation, type: keyof SegmentType) { + constructor(navigation: Navigation, type: keyof T) { super(); this._navigation = navigation; this._type = type; this._lastSetValue = navigation.path.get(type)?.value; } - get(): SegmentType[keyof SegmentType] | undefined { + get(): T[keyof T] | undefined { const path = this._navigation.path; const segment = path.get(this._type); const value = segment?.value; @@ -222,7 +222,7 @@ class SegmentObservable extends BaseObservableValue(newValue, this._lastSetValue)) { this._lastSetValue = newValue; this.emit(newValue); } @@ -249,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}); @@ -258,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(); @@ -275,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); } From 3efc426fedf596697f12c357b5bcca7483871ae0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 21 Feb 2022 17:30:46 +0530 Subject: [PATCH 003/354] Complete converting Navigation.js to ts --- src/domain/navigation/Navigation.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index cb6637a9..dfd69bd1 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -34,6 +34,7 @@ type SegmentType = { "member": string; }; + export class Navigation { private readonly _allowsChild: AllowsChild; private _path: Path; @@ -54,8 +55,8 @@ export class Navigation { return this._path; } - push(type, value = undefined): void { - const newPath = this.path.with(new Segment(type, value)); + push(type: K, ...value: T[K] extends true? [undefined?]: [T[K]]): void { + const newPath = this.path.with(new Segment(type, ...value)); if (newPath) { this.applyPath(newPath); } @@ -106,8 +107,8 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type: K, value: T[K]): Segment { - return new Segment(type, value); + segment(type: K, ...value: T[K] extends true? [undefined?]: [T[K]]): Segment { + return new Segment(type, ...value); } } @@ -130,10 +131,11 @@ function segmentValueEqual(a?: T[keyof T], b?: T[keyof T]): boolean { export class Segment { - constructor( - public type: K, - public value: T[K] = (value === undefined ? true : value) as T[K] - ) {} + public value: T[K]; + + constructor(public type: K, ...value: T[K] extends true? [undefined?]: [T[K]]) { + this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K]; + } } class Path { From 55229252d750a6a00a26bade28b802f1f02e786b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 21 Feb 2022 17:37:30 +0530 Subject: [PATCH 004/354] Type allowsChild --- src/domain/navigation/Navigation.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index dfd69bd1..b3b79a89 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -15,7 +15,6 @@ limitations under the License. */ import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue"; -import type {allowsChild as AllowsChild} from "./index.js"; type SegmentType = { "login": true; @@ -34,14 +33,15 @@ type SegmentType = { "member": string; }; +type AllowsChild = (parent: Segment | undefined, child: Segment) => boolean; export class Navigation { - private readonly _allowsChild: AllowsChild; + private readonly _allowsChild: AllowsChild; private _path: Path; private readonly _observables: Map> = new Map(); private readonly _pathObservable: ObservableValue>; - constructor(allowsChild: AllowsChild) { + constructor(allowsChild: AllowsChild) { this._allowsChild = allowsChild; this._path = new Path([], allowsChild); this._pathObservable = new ObservableValue(this._path); @@ -140,9 +140,9 @@ export class Segment { class Path { private readonly _segments: Segment[]; - private readonly _allowsChild: AllowsChild; + private readonly _allowsChild: AllowsChild; - constructor(segments: Segment[] = [], allowsChild: AllowsChild) { + constructor(segments: Segment[] = [], allowsChild: AllowsChild) { this._segments = segments; this._allowsChild = allowsChild; } @@ -237,13 +237,13 @@ export function tests() { 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; } From 92c79c853d2442da004f91744617deb2120e59ef Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 11:41:36 +0530 Subject: [PATCH 005/354] Convert index.js to typescript --- src/domain/navigation/Navigation.ts | 24 +-- src/domain/navigation/{index.js => index.ts} | 175 +++++++++++------- src/domain/session/RoomGridViewModel.js | 4 +- .../session/leftpanel/LeftPanelViewModel.js | 2 +- src/lib.ts | 2 +- src/platform/web/main.js | 2 +- 6 files changed, 114 insertions(+), 95 deletions(-) rename src/domain/navigation/{index.js => index.ts} (75%) 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 From 92e8fc8ad3641ed1d3a955de604059259ae065b0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 11:43:35 +0530 Subject: [PATCH 006/354] Remove deprecated method --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 92cf41a1..143ae734 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -126,7 +126,7 @@ export function addPanelIfNeeded(navigation: Navigation, defaultSessionId: string): Segment[] { // substr(1) to take of initial / - const parts = urlPath.substr(1).split("/"); + const parts = urlPath.substring(1).split("/"); const iterator = parts[Symbol.iterator](); const segments: Segment[] = []; let next; From 646cbe0fff7bbfc9568fef5945e61b818c8193fe Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 11:48:02 +0530 Subject: [PATCH 007/354] Make all keys string --- src/domain/navigation/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 143ae734..4e840bf4 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -19,20 +19,20 @@ 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; + "login": true; + "session": string; + "sso": string; + "logout": true; + "room": string; + "rooms": string[]; + "settings": true; "create-room": true; "empty-grid-tile": number; - lightbox: string; + "lightbox": string; "right-panel": true; - details: true; - members: true; - member: string; + "details": true; + "members": true; + "member": string; }; export function createNavigation() { From bf2fb52691a47dad0c4a2ffd414cd5b5979a3646 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 11:48:20 +0530 Subject: [PATCH 008/354] Fix formatting --- src/domain/navigation/Navigation.ts | 2 +- src/domain/navigation/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index f0b29816..4a0e4969 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -215,7 +215,7 @@ class SegmentObservable extends BaseObservableValue { } } -export type { Path }; +export type {Path}; export function tests() { diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 4e840bf4..ac856f86 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -16,7 +16,7 @@ limitations under the License. import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter.js"; -import type { Path } from "./Navigation"; +import type {Path} from "./Navigation"; type SegmentType = { "login": true; From d9bfca10e1085b7f8a8bab0ba6527273b201fc01 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 11:51:30 +0530 Subject: [PATCH 009/354] Type function --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index ac856f86..fb75b04d 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -35,7 +35,7 @@ type SegmentType = { "member": string; }; -export function createNavigation() { +export function createNavigation(): Navigation { return new Navigation(allowsChild); } From 4c3e0a6ff042b8298751f59a287a7efe8fdbf352 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:14:27 +0530 Subject: [PATCH 010/354] Convert URLRouter.js to typescript --- src/domain/navigation/Navigation.ts | 2 + .../navigation/{URLRouter.js => URLRouter.ts} | 64 +++++++++++-------- src/domain/navigation/index.ts | 6 +- 3 files changed, 44 insertions(+), 28 deletions(-) rename src/domain/navigation/{URLRouter.js => URLRouter.ts} (65%) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index 4a0e4969..5157f86c 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -19,6 +19,8 @@ import {BaseObservableValue, ObservableValue} from "../../observable/ObservableV type AllowsChild = (parent: Segment | undefined, child: Segment) => boolean; +export type OptionalValue = T extends true? [(undefined | true)?]: [T]; + export class Navigation { private readonly _allowsChild: AllowsChild; private _path: Path; diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.ts similarity index 65% rename from src/domain/navigation/URLRouter.js rename to src/domain/navigation/URLRouter.ts index 586eec8a..f865a1de 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.ts @@ -14,19 +14,33 @@ 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"; +import type {SegmentType} from "./index"; + +type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId: string | null) => Segment[]; +type StringifyPath = (path: Path) => string; + +export class URLRouter { + 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 | null; + + 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() { + _getLastSessionId(): string | null { const navPath = this._urlAsNavPath(this._history.getLastUrl() || ""); const sessionId = navPath.get("session")?.value; if (typeof sessionId === "string") { @@ -35,7 +49,7 @@ export class URLRouter { return null; } - 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 +57,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) { + _applyNavPathToHistory(path: Path): void { const url = this.urlForPath(path); if (url !== this._history.get()) { if (this._isApplyingUrl) { @@ -60,7 +74,7 @@ export class URLRouter { } } - _applyNavPathToNavigation(navPath) { + _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 +83,21 @@ export class URLRouter { this._isApplyingUrl = false; } - _urlAsNavPath(url) { + _urlAsNavPath(url: string): Path { const urlPath = this._history.urlAsPath(url); return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId)); } - _applyUrl(url) { + _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.getLastUrl() || ""); if (lastNavPath.segments.length !== 0) { this._applyNavPathToNavigation(lastNavPath); @@ -92,8 +106,8 @@ export class URLRouter { return false; } - urlForSegments(segments) { - let path = this._navigation.path; + urlForSegments(segments: Segment[]): string | undefined { + let path: Path | null = this._navigation.path; for (const segment of segments) { path = path.with(segment); if (!path) { @@ -103,29 +117,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) { // 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.ts b/src/domain/navigation/index.ts index fb75b04d..386eac53 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -15,10 +15,10 @@ limitations under the License. */ import {Navigation, Segment} from "./Navigation"; -import {URLRouter} from "./URLRouter.js"; +import {URLRouter} from "./URLRouter"; import type {Path} from "./Navigation"; -type SegmentType = { +export type SegmentType = { "login": true; "session": string; "sso": string; @@ -124,7 +124,7 @@ export function addPanelIfNeeded(navigation: Navigation, defaultSessionId: string): Segment[] { +export function parseUrlPath(urlPath: string, currentNavPath: Path, defaultSessionId: string | null): Segment[] { // substr(1) to take of initial / const parts = urlPath.substring(1).split("/"); const iterator = parts[Symbol.iterator](); From 5d42f372f6b2d902332f5df54ec75d3dffafeccf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:15:30 +0530 Subject: [PATCH 011/354] Pass as separate arguments to constructor --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 386eac53..13d2df84 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -40,7 +40,7 @@ export function createNavigation(): Navigation { } export function createRouter({history, navigation}) { - return new URLRouter({history, navigation, stringifyPath, parseUrlPath}); + return new URLRouter(history, navigation, parseUrlPath, stringifyPath); } function allowsChild(parent: {type: string, value: any} | undefined, child: {type: string, value: any}): boolean { From c14e4f3eed5ff4738f1376ed12a161b37d13f426 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:16:54 +0530 Subject: [PATCH 012/354] Use segment type --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 13d2df84..c90926cf 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -43,7 +43,7 @@ export function createRouter({history, navigation}) { return new URLRouter(history, navigation, parseUrlPath, stringifyPath); } -function allowsChild(parent: {type: string, value: any} | undefined, child: {type: string, value: any}): boolean { +function allowsChild(parent: Segment | undefined, child: Segment): boolean { const {type} = child; switch (parent?.type) { case undefined: From f28dfc6964e29c8e5b6db5cd7f47dc0135ac67cf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:22:37 +0530 Subject: [PATCH 013/354] Type createRouter function --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index c90926cf..740ebf9d 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -39,7 +39,7 @@ export function createNavigation(): Navigation { return new Navigation(allowsChild); } -export function createRouter({history, navigation}) { +export function createRouter({history, navigation}: {history: History, navigation: Navigation}): URLRouter { return new URLRouter(history, navigation, parseUrlPath, stringifyPath); } From 76d04ee27783ee3316cdd6727d865fbcd5e8e340 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:26:59 +0530 Subject: [PATCH 014/354] Make defaultSessionId optional --- src/domain/navigation/URLRouter.ts | 8 ++++---- src/domain/navigation/index.ts | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index f865a1de..06a67c25 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -19,7 +19,7 @@ import type {Navigation, Segment, Path, OptionalValue} from "./Navigation"; import type {SubscriptionHandle} from "../../observable/BaseObservable"; import type {SegmentType} from "./index"; -type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId: string | null) => Segment[]; +type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId?: string) => Segment[]; type StringifyPath = (path: Path) => string; export class URLRouter { @@ -30,7 +30,7 @@ export class URLRouter { private _subscription?: SubscriptionHandle; private _pathSubscription?: SubscriptionHandle; private _isApplyingUrl: boolean = false; - private _defaultSessionId: string | null; + private _defaultSessionId?: string; constructor(history: History, navigation: Navigation, parseUrlPath: ParseURLPath, stringifyPath: StringifyPath) { this._history = history; @@ -40,13 +40,13 @@ export class URLRouter { this._defaultSessionId = this._getLastSessionId(); } - _getLastSessionId(): string | null { + _getLastSessionId(): string | undefined { const navPath = this._urlAsNavPath(this._history.getLastUrl() || ""); const sessionId = navPath.get("session")?.value; if (typeof sessionId === "string") { return sessionId; } - return null; + return undefined; } attach(): void { diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 740ebf9d..bcce8936 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -124,7 +124,7 @@ export function addPanelIfNeeded(navigation: Navigation, defaultSessionId: string | null): Segment[] { +export function parseUrlPath(urlPath: string, currentNavPath: Path, defaultSessionId?: string): Segment[] { // substr(1) to take of initial / const parts = urlPath.substring(1).split("/"); const iterator = parts[Symbol.iterator](); @@ -267,14 +267,14 @@ export function tests() { }, "Parse loginToken query parameter into SSO segment": assert => { const path = createEmptyPath(); - const segments = parseUrlPath("?loginToken=a1232aSD123", path, ""); + 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 path = createEmptyPath(); - const segments = parseUrlPath("/session/1/rooms/a,b,c/3", path, ""); + 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"); @@ -285,7 +285,7 @@ export function tests() { }, "parse grid url path with focused room": assert => { const path = createEmptyPath(); - const segments = parseUrlPath("/session/1/rooms/a,b,c/1", path, ""); + 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"); @@ -296,7 +296,7 @@ export function tests() { }, "parse empty grid url": assert => { const path = createEmptyPath(); - const segments = parseUrlPath("/session/1/rooms/", path, ""); + const segments = parseUrlPath("/session/1/rooms/", path); assert.equal(segments.length, 3); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); @@ -307,7 +307,7 @@ export function tests() { }, "parse empty grid url with focus": assert => { const path = createEmptyPath(); - const segments = parseUrlPath("/session/1/rooms//1", path, ""); + 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"); @@ -323,7 +323,7 @@ export function tests() { 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"); @@ -339,7 +339,7 @@ export function tests() { 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"); @@ -357,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"); @@ -379,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"); @@ -399,7 +399,7 @@ export function tests() { 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"); @@ -410,7 +410,7 @@ export function tests() { }, "parse session url path without id": assert => { const path = createEmptyPath(); - const segments = parseUrlPath("/session", path, ""); + const segments = parseUrlPath("/session", path); assert.equal(segments.length, 1); assert.equal(segments[0].type, "session"); assert.strictEqual(segments[0].value, true); From 09bc0f1b603323f8dbfce2a7c832657842e5acb1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 13:30:37 +0530 Subject: [PATCH 015/354] Extract complex type as type alias --- src/domain/navigation/Navigation.ts | 6 +++--- src/domain/navigation/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index 5157f86c..6cde56c2 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -41,7 +41,7 @@ export class Navigation { return this._path; } - push(type: K, ...value: T[K] extends true? [(undefined | true)?]: [T[K]]): void { + push(type: K, ...value: OptionalValue): void { const newPath = this.path.with(new Segment(type, ...value)); if (newPath) { this.applyPath(newPath); @@ -93,7 +93,7 @@ export class Navigation { return new Path(segments, this._allowsChild); } - segment(type: K, ...value: T[K] extends true? [(undefined | true)?]: [T[K]]): Segment { + segment(type: K, ...value: OptionalValue): Segment { return new Segment(type, ...value); } } @@ -119,7 +119,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 | true)?]: [T[K]]) { + constructor(public type: K, ...value: OptionalValue) { this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K]; } } diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index bcce8936..06920e61 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -16,7 +16,7 @@ limitations under the License. import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter"; -import type {Path} from "./Navigation"; +import type {Path, OptionalValue} from "./Navigation"; export type SegmentType = { "login": true; @@ -107,7 +107,7 @@ function roomsSegmentWithRoom(rooms: Segment, roomId: stri } // todo-self: verify code change here is okay -function pushRightPanelSegment(array: Segment[], segment: T, ...value: SegmentType[T] extends true? [(undefined | true)?]: [SegmentType[T]]) { +function pushRightPanelSegment(array: Segment[], segment: T, ...value: OptionalValue) { array.push(new Segment("right-panel")); array.push(new Segment(segment, ...value)); } From e7f4ce61751769f91ff388dae7f962d779c398cb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:24:12 +0530 Subject: [PATCH 016/354] Mark methods as private --- src/domain/navigation/URLRouter.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index 06a67c25..f250ab74 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -40,7 +40,7 @@ export class URLRouter { this._defaultSessionId = this._getLastSessionId(); } - _getLastSessionId(): string | undefined { + private _getLastSessionId(): string | undefined { const navPath = this._urlAsNavPath(this._history.getLastUrl() || ""); const sessionId = navPath.get("session")?.value; if (typeof sessionId === "string") { @@ -62,7 +62,7 @@ export class URLRouter { if (this._pathSubscription) { this._pathSubscription = this._pathSubscription(); } } - _applyNavPathToHistory(path: Path): void { + private _applyNavPathToHistory(path: Path): void { const url = this.urlForPath(path); if (url !== this._history.get()) { if (this._isApplyingUrl) { @@ -74,7 +74,7 @@ export class URLRouter { } } - _applyNavPathToNavigation(navPath: Path): void { + 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) @@ -83,12 +83,12 @@ export class URLRouter { this._isApplyingUrl = false; } - _urlAsNavPath(url: string): Path { + 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: string): void { + private _applyUrl(url: string): void { const navPath = this._urlAsNavPath(url); this._applyNavPathToNavigation(navPath); } From 5be00f051f566686b7117c6e184157947b08f3f5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:39:33 +0530 Subject: [PATCH 017/354] Use subtype instead of whole SegmentType --- src/domain/navigation/URLRouter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index f250ab74..2d06ca5f 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -17,12 +17,11 @@ limitations under the License. import type {History} from "../../platform/web/dom/History.js"; import type {Navigation, Segment, Path, OptionalValue} from "./Navigation"; import type {SubscriptionHandle} from "../../observable/BaseObservable"; -import type {SegmentType} from "./index"; type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId?: string) => Segment[]; type StringifyPath = (path: Path) => string; -export class URLRouter { +export class URLRouter { private readonly _history: History; private readonly _navigation: Navigation; private readonly _parseUrlPath: ParseURLPath; From 4ae3a5bf7a05e8805de85100d8e2fb86fb6ec696 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:44:27 +0530 Subject: [PATCH 018/354] Use undefined instead of null --- src/domain/navigation/Navigation.ts | 8 ++++---- src/domain/navigation/URLRouter.ts | 2 +- src/domain/navigation/index.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index 6cde56c2..d6ffa3c5 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -137,7 +137,7 @@ class Path { return new Path(this._segments.slice(), this._allowsChild); } - with(segment: Segment): Path | null { + with(segment: Segment): Path | undefined { let index = this._segments.length - 1; do { if (this._allowsChild(this._segments[index], segment)) { @@ -149,7 +149,7 @@ 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: keyof T): Path { @@ -164,7 +164,7 @@ class Path { return this._segments.find(s => s.type === type); } - replace(segment: Segment): Path | null { + replace(segment: Segment): Path | undefined { const index = this._segments.findIndex(s => s.type === segment.type); if (index !== -1) { const parent = this._segments[index - 1]; @@ -177,7 +177,7 @@ class Path { } } } - return null; + return undefined; } get segments(): Segment[] { diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index 2d06ca5f..31dffe58 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -106,7 +106,7 @@ export class URLRouter { } urlForSegments(segments: Segment[]): string | undefined { - let path: Path | null = this._navigation.path; + let path: Path | undefined = this._navigation.path; for (const segment of segments) { path = path.with(segment); if (!path) { diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 06920e61..f6533580 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -63,8 +63,8 @@ function allowsChild(parent: Segment | undefined, child: Segment, roomId: string): Path | null { - let newPath: Path | null = path; +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 From 4fd1918202845a9e38d443d48c98ff0306778679 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:47:48 +0530 Subject: [PATCH 019/354] Remove comment --- src/domain/navigation/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index f6533580..3715d817 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -106,7 +106,6 @@ function roomsSegmentWithRoom(rooms: Segment, roomId: stri } } -// todo-self: verify code change here is okay function pushRightPanelSegment(array: Segment[], segment: T, ...value: OptionalValue) { array.push(new Segment("right-panel")); array.push(new Segment(segment, ...value)); From 7a24059337056da7a7aa2c5ac21ead6ed52a7e23 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:48:15 +0530 Subject: [PATCH 020/354] Remove empty line --- src/domain/navigation/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 3715d817..5f084d0f 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -224,7 +224,6 @@ export function stringifyPath(path: Path): string { } export function tests() { - function createEmptyPath() { const nav: Navigation = new Navigation(allowsChild); const path = nav.pathFrom([]); From 52f0690c702f51de30559bbb59c22c30b5a20dc3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:53:52 +0530 Subject: [PATCH 021/354] Add return type --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 5f084d0f..0c36e21b 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -106,7 +106,7 @@ function roomsSegmentWithRoom(rooms: Segment, roomId: stri } } -function pushRightPanelSegment(array: Segment[], segment: T, ...value: OptionalValue) { +function pushRightPanelSegment(array: Segment[], segment: T, ...value: OptionalValue): void { array.push(new Segment("right-panel")); array.push(new Segment(segment, ...value)); } From 263948faa384d132637d567d4c659f5ae1009f1a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 16:58:13 +0530 Subject: [PATCH 022/354] Remove unwanted export --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 0c36e21b..b8652f18 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -18,7 +18,7 @@ import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter"; import type {Path, OptionalValue} from "./Navigation"; -export type SegmentType = { +type SegmentType = { "login": true; "session": string; "sso": string; From f49d580d49afbbbb60e6d6186b3b1728902b175b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 20:17:51 +0530 Subject: [PATCH 023/354] WIP --- src/domain/ViewModel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 0bc52f6e..743815ee 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -29,15 +29,15 @@ import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {URLRouter} from "./navigation/URLRouter"; -export type Options = { +type Options = { platform: Platform logger: ILogger - urlCreator: URLRouter - navigation: Navigation + urlCreator: URLRouter + 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 +47,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); } @@ -135,11 +135,11 @@ export class ViewModel extends EventEmitter<{change return this.platform.logger; } - get urlCreator(): URLRouter { + get urlCreator(): URLRouter { return this._options.urlCreator; } - get navigation(): Navigation { + get navigation(): Navigation { return this._options.navigation; } } From 9300347e9bd2b73dd667d9ff42a6ff32b423761c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Feb 2022 22:23:52 +0530 Subject: [PATCH 024/354] Give defaultt type --- src/domain/ViewModel.ts | 3 ++- src/domain/navigation/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 743815ee..5bec39e8 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -27,6 +27,7 @@ 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 {SegmentType} from "./navigation/index"; import type {URLRouter} from "./navigation/URLRouter"; type Options = { @@ -37,7 +38,7 @@ type Options = { emitChange?: (params: any) => void } -export class ViewModel = Options> extends EventEmitter<{change: never}> { +export class ViewModel = Options> extends EventEmitter<{change: never}> { private disposables?: Disposables; private _isDisposed = false; private _options: Readonly; diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index b8652f18..f739b668 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -18,7 +18,7 @@ import {Navigation, Segment} from "./Navigation"; import {URLRouter} from "./URLRouter"; import type {Path, OptionalValue} from "./Navigation"; -type SegmentType = { +export type SegmentType = { "login": true; "session": string; "sso": string; @@ -124,7 +124,7 @@ export function addPanelIfNeeded(navigation: Navigation, defaultSessionId?: string): Segment[] { - // substr(1) to take of initial / + // substring(1) to take of initial / const parts = urlPath.substring(1).split("/"); const iterator = parts[Symbol.iterator](); const segments: Segment[] = []; From a336623f3ac8cbf8bcf0bb86df8f6e1a563571ea Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Mar 2022 17:57:03 +0530 Subject: [PATCH 025/354] Generic parameter should extend object --- src/domain/navigation/Navigation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index d6ffa3c5..c786d7a7 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -21,7 +21,7 @@ type AllowsChild = (parent: Segment | undefined, child: Segment) => boo export type OptionalValue = T extends true? [(undefined | true)?]: [T]; -export class Navigation { +export class Navigation { private readonly _allowsChild: AllowsChild; private _path: Path; private readonly _observables: Map> = new Map(); @@ -189,7 +189,7 @@ 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 { +class SegmentObservable extends BaseObservableValue { private readonly _navigation: Navigation; private _type: keyof T; private _lastSetValue?: T[keyof T]; From ec1cc89cf9555c6f18bee31d1b084266869ee6eb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Mar 2022 20:17:15 +0530 Subject: [PATCH 026/354] Make URLRouter in options conditional on generic URLRouter can be passed in option to vm only if the SegmentType used contains session. ViewModel.urlCreator returns undefined when used with a SegmentType that lacks session. --- src/domain/ViewModel.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 5bec39e8..7442d921 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -30,15 +30,17 @@ import type {Navigation} from "./navigation/Navigation"; import type {SegmentType} from "./navigation/index"; import type {URLRouter} from "./navigation/URLRouter"; -type Options = { +type OptionsWithoutUrlCreator = { platform: Platform logger: ILogger - urlCreator: URLRouter navigation: Navigation emitChange?: (params: any) => void } +type OptionsWithUrlCreator = OptionsWithoutUrlCreator & {urlCreator: URLRouter}; -export class ViewModel = Options> extends EventEmitter<{change: never}> { +type Options = N extends { session: string } ? OptionsWithUrlCreator : OptionsWithoutUrlCreator; + +export class ViewModel = Options> extends EventEmitter<{change: never}> { private disposables?: Disposables; private _isDisposed = false; private _options: Readonly; @@ -136,11 +138,13 @@ export class ViewModel { - return this._options.urlCreator; + get urlCreator(): N extends { session: string }? URLRouter: undefined { + // typescript needs a little help here + return (this._options as unknown as {urlCreator: any}).urlCreator; } get navigation(): Navigation { - return this._options.navigation; + // typescript needs a little help here + return this._options.navigation as unknown as Navigation; } } From fc873757d80c264b9f01995029bfcda093f69ed7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 27 May 2022 22:38:53 +0530 Subject: [PATCH 027/354] WIP --- src/domain/LogoutViewModel.ts | 5 +++-- src/domain/ViewModel.ts | 20 +++++++++----------- src/domain/navigation/URLRouter.ts | 18 ++++++++++++++++-- src/domain/navigation/index.ts | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index 3edfcad5..9a39f601 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -16,10 +16,11 @@ limitations under the License. import {Options, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; +import {SegmentType} from "./navigation/index"; type LogoutOptions = { sessionId: string; } & Options; -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 7442d921..64db4266 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -28,17 +28,16 @@ import type {Clock} from "../platform/web/dom/Clock"; import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {SegmentType} from "./navigation/index"; -import type {URLRouter} from "./navigation/URLRouter"; +import type {IURLRouter} from "./navigation/URLRouter"; -type OptionsWithoutUrlCreator = { - platform: Platform - logger: ILogger - navigation: Navigation - emitChange?: (params: any) => void +export type Options = { + platform: Platform; + logger: ILogger; + urlCreator: IURLRouter; + navigation: Navigation; + emitChange?: (params: any) => void; } -type OptionsWithUrlCreator = OptionsWithoutUrlCreator & {urlCreator: URLRouter}; -type Options = N extends { session: string } ? OptionsWithUrlCreator : OptionsWithoutUrlCreator; export class ViewModel = Options> extends EventEmitter<{change: never}> { private disposables?: Disposables; @@ -138,9 +137,8 @@ export class ViewModel = Op return this.platform.logger; } - get urlCreator(): N extends { session: string }? URLRouter: undefined { - // typescript needs a little help here - return (this._options as unknown as {urlCreator: any}).urlCreator; + get urlCreator(): IURLRouter { + return this._options.urlCreator; } get navigation(): Navigation { diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index 31dffe58..923c9b43 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -21,7 +21,21 @@ import type {SubscriptionHandle} from "../../observable/BaseObservable"; type ParseURLPath = (urlPath: string, currentNavPath: Path, defaultSessionId?: string) => Segment[]; type StringifyPath = (path: Path) => string; -export class URLRouter { +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; @@ -128,7 +142,7 @@ export class URLRouter { return this._history.pathAsUrl(this._stringifyPath(path)); } - openRoomActionUrl(roomId: string) { + 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); diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index f739b668..afba0d86 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -20,7 +20,7 @@ import type {Path, OptionalValue} from "./Navigation"; export type SegmentType = { "login": true; - "session": string; + "session": string | boolean; "sso": string; "logout": true; "room": string; From ba647d012dc0c778df81e4e152e2be8736297c04 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 29 May 2022 20:38:14 +0530 Subject: [PATCH 028/354] Fix type in observeNavigation --- src/domain/ViewModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 64db4266..f63f569d 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -60,9 +60,9 @@ export class ViewModel = Op return this._options[name]; } - observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) { + observeNavigation(type: T, onChange: (value: N[T], type: T) => 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); From d31f127982a60ac2bd2df611524d7caca8629664 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Jun 2022 13:28:56 +0530 Subject: [PATCH 029/354] Add explaining comment --- src/domain/navigation/Navigation.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/domain/navigation/Navigation.ts b/src/domain/navigation/Navigation.ts index c786d7a7..f5039732 100644 --- a/src/domain/navigation/Navigation.ts +++ b/src/domain/navigation/Navigation.ts @@ -19,6 +19,16 @@ import {BaseObservableValue, ObservableValue} from "../../observable/ObservableV 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 { From e3372f0f2bfe991156cd1dce4872610ba2076977 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 12:54:18 +0530 Subject: [PATCH 030/354] Don't use theme-name in manifest file names --- scripts/build-plugins/rollup-plugin-build-themes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 43a21623..e74d3b85 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -257,8 +257,10 @@ module.exports = function buildThemes(options) { const derivedVariables = compiledVariables["derived-variables"]; const icon = compiledVariables["icon"]; const builtAssets = {}; + let themeKey; for (const chunk of chunkArray) { const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/); + themeKey = name; builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName; } manifest.source = { @@ -267,7 +269,7 @@ module.exports = function buildThemes(options) { "derived-variables": derivedVariables, "icon": icon }; - const name = `theme-${manifest.name}.json`; + const name = `theme-${themeKey}.json`; manifestLocations.push(`assets/${name}`); this.emitFile({ type: "asset", From 93165cb947ee0ad895e24d15c0ce5410afde3668 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 13:46:14 +0530 Subject: [PATCH 031/354] runtime theme chunks should also be stored in map There will be more than one runtime theme file when multiple theme collections exist. --- .../build-plugins/rollup-plugin-build-themes.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index e74d3b85..41e78044 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -46,7 +46,7 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { function parseBundle(bundle) { const chunkMap = new Map(); const assetMap = new Map(); - let runtimeThemeChunk; + let runtimeThemeChunkMap = new Map(); for (const [fileName, info] of Object.entries(bundle)) { if (!fileName.endsWith(".css")) { continue; @@ -60,18 +60,18 @@ function parseBundle(bundle) { assetMap.set(info.name, info); continue; } + const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; + if (!location) { + throw new Error("Cannot find location of css chunk!"); + } if (info.facadeModuleId?.includes("type=runtime")) { /** * We have a separate field in manifest.source just for the runtime theme, * so store this separately. */ - runtimeThemeChunk = info; + runtimeThemeChunkMap.set(location, info); continue; } - const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; - if (!location) { - throw new Error("Cannot find location of css chunk!"); - } const array = chunkMap.get(location); if (!array) { chunkMap.set(location, [info]); @@ -80,7 +80,7 @@ function parseBundle(bundle) { array.push(info); } } - return { chunkMap, assetMap, runtimeThemeChunk }; + return { chunkMap, assetMap, runtimeThemeChunkMap }; } module.exports = function buildThemes(options) { @@ -249,7 +249,7 @@ module.exports = function buildThemes(options) { // assetMap: Mapping from asset-name (eg: element-dark.css) to AssetInfo // chunkMap: Mapping from theme-location (eg: hydrogen-web/src/.../css/themes/element) to a list of ChunkInfo // types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle - const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); + const { assetMap, chunkMap, runtimeThemeChunkMap } = parseBundle(bundle); const manifestLocations = []; for (const [location, chunkArray] of chunkMap) { const manifest = require(`${location}/manifest.json`); @@ -263,6 +263,7 @@ module.exports = function buildThemes(options) { themeKey = name; builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName; } + const runtimeThemeChunk = runtimeThemeChunkMap.get(location); manifest.source = { "built-assets": builtAssets, "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, From 5eec72471217f0770101598659bf4f1e603d7005 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 20:35:06 +0530 Subject: [PATCH 032/354] Locations must be relative to manifest --- scripts/build-plugins/rollup-plugin-build-themes.js | 11 ++++++++--- src/platform/web/Platform.js | 2 +- src/platform/web/ThemeLoader.ts | 11 ++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 41e78044..89eaeec1 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -251,6 +251,8 @@ module.exports = function buildThemes(options) { // types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle const { assetMap, chunkMap, runtimeThemeChunkMap } = parseBundle(bundle); const manifestLocations = []; + // Location of the directory containing manifest relative to the root of the build output + const manifestLocation = "assets"; for (const [location, chunkArray] of chunkMap) { const manifest = require(`${location}/manifest.json`); const compiledVariables = options.compiledVariables.get(location); @@ -261,17 +263,20 @@ module.exports = function buildThemes(options) { for (const chunk of chunkArray) { const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/); themeKey = name; - builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName; + const locationRelativeToBuildRoot = assetMap.get(chunk.fileName).fileName; + const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot); + builtAssets[`${name}-${variant}`] = locationRelativeToManifest; } const runtimeThemeChunk = runtimeThemeChunkMap.get(location); + const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName); manifest.source = { "built-assets": builtAssets, - "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + "runtime-asset": runtimeAssetLocation, "derived-variables": derivedVariables, "icon": icon }; const name = `theme-${themeKey}.json`; - manifestLocations.push(`assets/${name}`); + manifestLocations.push(`${manifestLocation}/${name}`); this.emitFile({ type: "asset", name, diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index e2ca2028..c2eef17e 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -338,7 +338,7 @@ export class Platform { document.querySelectorAll(".theme").forEach(e => e.remove()); // add new theme const styleTag = document.createElement("link"); - styleTag.href = `./${newPath}`; + styleTag.href = newPath; styleTag.rel = "stylesheet"; styleTag.type = "text/css"; styleTag.className = "theme"; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 8c9364bc..ee303b49 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -61,11 +61,11 @@ export class ThemeLoader { const results = await Promise.all( manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - results.forEach(({ body }) => this._populateThemeMap(body, log)); + results.forEach(({ body }, i) => this._populateThemeMap(body, manifestLocations[i], log)); }); } - private _populateThemeMap(manifest, log: ILogItem) { + private _populateThemeMap(manifest, manifestLocation: string, log: ILogItem) { log.wrap("populateThemeMap", (l) => { /* After build has finished, the source section of each theme manifest @@ -75,7 +75,12 @@ export class ThemeLoader { const builtAssets: Record = manifest.source?.["built-assets"]; const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (const [themeId, cssLocation] of Object.entries(builtAssets)) { + for (let [themeId, cssLocation] of Object.entries(builtAssets)) { + /** + * This cssLocation is relative to the location of the manifest file. + * So we first need to resolve it relative to the root of this hydrogen instance. + */ + cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; const variant = themeId.match(/.+-(.+)/)?.[1]; const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; const themeDisplayName = `${themeName} ${variantName}`; From fbdd512e0646bdc036f94b858d1d6172f06c13a4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 21:10:11 +0530 Subject: [PATCH 033/354] Split functions into smaller functions --- .../rollup-plugin-build-themes.js | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 89eaeec1..9ac2befe 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -43,10 +43,39 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { } } -function parseBundle(bundle) { +/** + * Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location. + * To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle. + * @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo + */ +function getMappingFromLocationToChunkArray(bundle) { const chunkMap = new Map(); + for (const [fileName, info] of Object.entries(bundle)) { + if (!fileName.endsWith(".css") || info.type === "asset" || info.facadeModuleId?.includes("type=runtime")) { + continue; + } + const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; + if (!location) { + throw new Error("Cannot find location of css chunk!"); + } + const array = chunkMap.get(location); + if (!array) { + chunkMap.set(location, [info]); + } + else { + array.push(info); + } + } + return chunkMap; +} + +/** + * Returns a mapping from unhashed file name (of css files) to AssetInfo. + * To understand what AssetInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle. + * @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo + */ +function getMappingFromFileNameToAssetInfo(bundle) { const assetMap = new Map(); - let runtimeThemeChunkMap = new Map(); for (const [fileName, info] of Object.entries(bundle)) { if (!fileName.endsWith(".css")) { continue; @@ -58,6 +87,20 @@ function parseBundle(bundle) { * searching through the bundle array later. */ assetMap.set(info.name, info); + } + } + return assetMap; +} + +/** + * Returns a mapping from location (of manifest file) to ChunkInfo of the runtime css asset + * To understand what ChunkInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle. + * @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo + */ +function getMappingFromLocationToRuntimeChunk(bundle) { + let runtimeThemeChunkMap = new Map(); + for (const [fileName, info] of Object.entries(bundle)) { + if (!fileName.endsWith(".css") || info.type === "asset") { continue; } const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; @@ -70,17 +113,9 @@ function parseBundle(bundle) { * so store this separately. */ runtimeThemeChunkMap.set(location, info); - continue; - } - const array = chunkMap.get(location); - if (!array) { - chunkMap.set(location, [info]); - } - else { - array.push(info); } } - return { chunkMap, assetMap, runtimeThemeChunkMap }; + return runtimeThemeChunkMap; } module.exports = function buildThemes(options) { @@ -246,10 +281,9 @@ module.exports = function buildThemes(options) { }, generateBundle(_, bundle) { - // assetMap: Mapping from asset-name (eg: element-dark.css) to AssetInfo - // chunkMap: Mapping from theme-location (eg: hydrogen-web/src/.../css/themes/element) to a list of ChunkInfo - // types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle - const { assetMap, chunkMap, runtimeThemeChunkMap } = parseBundle(bundle); + const assetMap = getMappingFromFileNameToAssetInfo(bundle); + const chunkMap = getMappingFromLocationToChunkArray(bundle); + const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle); const manifestLocations = []; // Location of the directory containing manifest relative to the root of the build output const manifestLocation = "assets"; From d688fa47375e999326bb0e1c6a61ca7b203b6bd6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 23 Jun 2022 15:06:22 +0530 Subject: [PATCH 034/354] Get the theme-collection id from manifest --- .../build-plugins/rollup-plugin-build-themes.js | 17 ++++++++++------- .../web/ui/css/themes/element/manifest.json | 1 + vite.config.js | 4 +--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 9ac2befe..6b8fdae0 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -123,6 +123,7 @@ module.exports = function buildThemes(options) { let isDevelopment = false; const virtualModuleId = '@theme/' const resolvedVirtualModuleId = '\0' + virtualModuleId; + const themeToManifestLocation = new Map(); return { name: "build-themes", @@ -137,20 +138,22 @@ module.exports = function buildThemes(options) { async buildStart() { if (isDevelopment) { return; } const { themeConfig } = options; - for (const [name, location] of Object.entries(themeConfig.themes)) { + for (const location of themeConfig.themes) { manifest = require(`${location}/manifest.json`); + const themeCollectionId = manifest.id; + themeToManifestLocation.set(themeCollectionId, location); variants = manifest.values.variants; for (const [variant, details] of Object.entries(variants)) { - const fileName = `theme-${name}-${variant}.css`; - if (name === themeConfig.default && details.default) { + const fileName = `theme-${themeCollectionId}-${variant}.css`; + if (themeCollectionId === themeConfig.default && details.default) { // This is the default theme, stash the file name for later if (details.dark) { defaultDark = fileName; - defaultThemes["dark"] = `${name}-${variant}`; + defaultThemes["dark"] = `${themeCollectionId}-${variant}`; } else { defaultLight = fileName; - defaultThemes["light"] = `${name}-${variant}`; + defaultThemes["light"] = `${themeCollectionId}-${variant}`; } } // emit the css as built theme bundle @@ -164,7 +167,7 @@ module.exports = function buildThemes(options) { this.emitFile({ type: "chunk", id: `${location}/theme.css?type=runtime`, - fileName: `theme-${name}-runtime.css`, + fileName: `theme-${themeCollectionId}-runtime.css`, }); } }, @@ -187,7 +190,7 @@ module.exports = function buildThemes(options) { if (theme === "default") { theme = options.themeConfig.default; } - const location = options.themeConfig.themes[theme]; + const location = themeToManifestLocation.get(theme); const manifest = require(`${location}/manifest.json`); const variants = manifest.values.variants; if (!variant || variant === "default") { diff --git a/src/platform/web/ui/css/themes/element/manifest.json b/src/platform/web/ui/css/themes/element/manifest.json index e183317c..cb21eaad 100644 --- a/src/platform/web/ui/css/themes/element/manifest.json +++ b/src/platform/web/ui/css/themes/element/manifest.json @@ -1,6 +1,7 @@ { "version": 1, "name": "Element", + "id": "element", "values": { "variants": { "light": { diff --git a/vite.config.js b/vite.config.js index 72be0184..10348218 100644 --- a/vite.config.js +++ b/vite.config.js @@ -33,9 +33,7 @@ export default defineConfig(({mode}) => { plugins: [ themeBuilder({ themeConfig: { - themes: { - element: "./src/platform/web/ui/css/themes/element", - }, + themes: ["./src/platform/web/ui/css/themes/element"], default: "element", }, compiledVariables, From 0ab611b0130f821007e4a7c41e5dc98dc99192a9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 1 Jul 2022 13:08:50 +0200 Subject: [PATCH 035/354] more detailed docs for IView, TemplateView and ListView --- doc/UI/ui.md | 204 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 doc/UI/ui.md diff --git a/doc/UI/ui.md b/doc/UI/ui.md new file mode 100644 index 00000000..40a8eccd --- /dev/null +++ b/doc/UI/ui.md @@ -0,0 +1,204 @@ +## IView components + +The [interface](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/types.ts) adopted by view components is agnostic of how they are rendered to the DOM. This has several benefits: + - it allows Hydrogen to not ship a [heavy view framework](https://bundlephobia.com/package/react-dom@18.2.0) that may or may not be used by its SDK users, and also keep bundle size of the app down. + - Given the interface is quite simple, is should be easy to integrate this interface into the render lifecycle of other frameworks. + - The main implementations used in Hydrogen are [`ListView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/ListView.ts) (rendering [`ObservableList`](https://github.com/vector-im/hydrogen-web/blob/master/src/observable/list/BaseObservableList.ts)s) and [`TemplateView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/TemplateView.ts) (easy templating and one-way databinding), each only a few 100 lines of code and tailored towards their specific use-case. They work straight with the DOM API and have no other dependencies. + - a common inteface allows us to mix and match between these different implementations (and gradually shift if need be in the future) with the code. + +## Templates + +### Template language + +TemplateView uses a mini-DSL language in pure javascript to express declarative templates. This template language is available without all the data-binding and event-handling bells and whistles in `ui/general/html.js`, and is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows: +```js +tag.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]); +tag.tag_name(child_element); +tag.tag_name([child_elements]); +``` +**tag_name** can be [most HTML or SVG tags](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/html.ts#L102-L110). + +eg: +Here is an example HTML segment followed with the code to create it in Hydrogen. +```html +
+

Demo

+ +
+``` +```js +tag.section({className: "main-section"},[ + tag.h1("Demo"), + tag.button({className:"btn_cool"}, "Click me") +]); +``` + +All these functions return DOM element nodes, e.g. the result of `document.createElement`. + +### TemplateView + +`TemplateView` builds on top of this by adopting the IView component model and adding easy event handling attributes and one-way databinding. +In views based on `TemplateView`, you will see `t` used instead of `tag`. +`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. As opposed to static templates with `tag`, you always use +`TemplateView` as an instance of a class, as there is some extra state to keep track (bindings, event handlers and subviews). + +You either subclass `TemplateView` and override the `render` method: +```js +class MyView extends TemplateView { + render(t, vm) { + return t.div(...); + } +} +``` + +Or you pass a render function to `InlineTemplateView`: +```js +new InlineTemplateView(vm, (t, vm) => { + return t.div(...); +}); +``` + +**Note:** the render function is only called once to build the initial DOM tree and setup bindings, etc ... Any subsequent updates to the DOM of a component happens through bindings. + +#### Event handlers + +Any attribute starting with `on` and having a function as a value will be attached as an event listener on the given node. The event handler will be removed during unmounting. + +```js +t.button({onClick: evt => { + vm.doSomething(evt.target.value); +}}, "Click me"); +``` + +#### Subviews + +`t.view(instance)` will mount the sub view (can be any IView) and return its root node so it can be attached in the DOM tree. +All subviews will be unmounted when the parent view gets unmounted. + +```js +t.div({className: "Container"}, t.view(new ChildView(vm.childViewModel))); +``` + +#### One-way data-binding + +A binding binds a part of the DOM to a value on the view model. The view model emits an update when any of its properties change to which the view can subscribe. When an update is received by the view, it will reevaluate all the bindings, and update the DOM accordingly. + +A binding can appear in many places where a static value can usually be used in the template tree. +To create a binding, you pass a function that maps the view value to a static value. + +##### Text binding + +```js +t.p(["I've got ", vm => vm.counter, " beans"]) +``` + +##### Attribute binding + +```js +t.button({disabled: vm => vm.isBusy}, "Submit"); +``` + +##### Class-name binding +```js +t.div({className: { + button: true, + active: vm => vm.isActive +}}) +``` +##### Subview binding + +So far, all the bindings can only change node values within our tree, but don't change the structure of the DOM. A sub view binding allows you to conditionally add a subview based on the result of a binding function. + +All sub view bindings return a DOM (element or comment) node and can be directly added to the DOM tree by including them in your template. + +###### map + +`t.mapView` allows you to choose a view based on the result of the binding function: + +```js +t.mapView(vm => vm.count, count => { + return count > 5 ? new LargeView(count) : new SmallView(count); +}); +``` + +Every time the first or binding function returns a different value, the second function is run to create a new view to replace the previous view. + +You can also return `null` or `undefined` from the second function to indicate a view should not be rendered. In this case a comment node will be used as a placeholder. + +There is also a `t.map` which will create a new template view (with the same value) and you directly provide a render function for it: + +```js +t.map(vm => vm.shape, (shape, t, vm) => { + switch (shape) { + case "rect": return t.rect(); + case "circle": return t.circle(); + } +}) +``` + +###### if + +`t.ifView` will render the subview if the binding returns a truthy value: + +```js +t.ifView(vm => vm.isActive, vm => new View(vm.someValue)); +``` + +You equally have `t.if`, which creates a `TemplateView` and passes you the `TemplateBuilder`: + +```js +t.if(vm => vm.isActive, (t, vm) => t.div("active!")); +``` + +##### Side-effects + +Sometimes you want to imperatively modify your DOM tree based on the value of a binding. +`mapSideEffect` makes this easy to do: + +```js +let node = t.div(); +t.mapSideEffect(vm => vm.color, (color, oldColor) => node.style.background = color); +return node; +``` + +**Note:** you shouldn't add any bindings, subviews or event handlers from the side-effect callback, +the safest is to not use the `t` argument at all. +If you do, they will be added every time the callback is run and only cleaned up when the view is unmounted. + +#### `tag` vs `t` + +**Note:** Although syntactically similar, `TemplateBuilder` and `tag` are not functionally equivalent. +Primarily `t` **supports** bindings and event handlers while `tag` **does not**. This is because to remove event listeners, we need to keep track of them, and thus we need to keep this state somewhere which +we can't do with a simple function call but we can insite the TemplateView class. + +```js + // The onClick here wont work!! + tag.button({className:"awesome-btn", onClick: () => this.foo()}); + +class MyView extends TemplateView { + render(t, vm){ + // The onClick works here. + t.button({className:"awesome-btn", onClick: () => this.foo()}); + } +} +``` + +## ListView + +A view component that renders and updates a list of sub views for every item in a `ObservableList`. + +```js +const list = new ListView({ + list: someObservableList +}, listValue => return new ChildView(listValue)) +``` + +As items are added, removed, moved (change position) and updated, the DOM will be kept in sync. + +There is also a `LazyListView` that only renders items in and around the current viewport. + +### Sub view updates + +Unless the `parentProvidesUpdates` option in the constructor is set to `false`, the ListView will call the `update` method on the child `IView` component when it receives an update event for one of the items in the `ObservableList`. + +This way, not every sub view has to have an individual listener on it's view model (a value from the observable list), and all updates go from the observable list to the list view, who then notifies the correct sub view. From d398e490ebf5b85a92a4ec40470a1af91eab9a05 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 1 Jul 2022 13:59:57 +0200 Subject: [PATCH 036/354] some rewording --- doc/UI/ui.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/UI/ui.md b/doc/UI/ui.md index 40a8eccd..fe60cf52 100644 --- a/doc/UI/ui.md +++ b/doc/UI/ui.md @@ -3,18 +3,18 @@ The [interface](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/types.ts) adopted by view components is agnostic of how they are rendered to the DOM. This has several benefits: - it allows Hydrogen to not ship a [heavy view framework](https://bundlephobia.com/package/react-dom@18.2.0) that may or may not be used by its SDK users, and also keep bundle size of the app down. - Given the interface is quite simple, is should be easy to integrate this interface into the render lifecycle of other frameworks. - - The main implementations used in Hydrogen are [`ListView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/ListView.ts) (rendering [`ObservableList`](https://github.com/vector-im/hydrogen-web/blob/master/src/observable/list/BaseObservableList.ts)s) and [`TemplateView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/TemplateView.ts) (easy templating and one-way databinding), each only a few 100 lines of code and tailored towards their specific use-case. They work straight with the DOM API and have no other dependencies. + - The main implementations used in Hydrogen are [`ListView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/ListView.ts) (rendering [`ObservableList`](https://github.com/vector-im/hydrogen-web/blob/master/src/observable/list/BaseObservableList.ts)s) and [`TemplateView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/TemplateView.ts) (templating and one-way databinding), each only a few 100 lines of code and tailored towards their specific use-case. They work straight with the DOM API and have no other dependencies. - a common inteface allows us to mix and match between these different implementations (and gradually shift if need be in the future) with the code. ## Templates ### Template language -TemplateView uses a mini-DSL language in pure javascript to express declarative templates. This template language is available without all the data-binding and event-handling bells and whistles in `ui/general/html.js`, and is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows: +Templates uses a mini-DSL language in pure javascript to express declarative templates. This is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows: ```js -tag.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]); -tag.tag_name(child_element); -tag.tag_name([child_elements]); +t.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]); +t.tag_name(child_element); +t.tag_name([child_elements]); ``` **tag_name** can be [most HTML or SVG tags](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/html.ts#L102-L110). @@ -37,10 +37,9 @@ All these functions return DOM element nodes, e.g. the result of `document.creat ### TemplateView -`TemplateView` builds on top of this by adopting the IView component model and adding easy event handling attributes and one-way databinding. -In views based on `TemplateView`, you will see `t` used instead of `tag`. -`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. As opposed to static templates with `tag`, you always use -`TemplateView` as an instance of a class, as there is some extra state to keep track (bindings, event handlers and subviews). +`TemplateView` builds on top of templating by adopting the IView component model and adding event handling attributes and one-way databinding. +In views based on `TemplateView`, you will see a render method with a `t` argument. +`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. You either subclass `TemplateView` and override the `render` method: ```js @@ -167,7 +166,10 @@ If you do, they will be added every time the callback is run and only cleaned up #### `tag` vs `t` -**Note:** Although syntactically similar, `TemplateBuilder` and `tag` are not functionally equivalent. +If you don't need a view component with data-binding, sub views and event handler attributes, the template language also is available in `ui/general/html.js` without any of these bells and whistles, exported as `tag`. As opposed to static templates with `tag`, you always use +`TemplateView` as an instance of a class, as there is some extra state to keep track (bindings, event handlers and subviews). + +Although syntactically similar, `TemplateBuilder` and `tag` are not functionally equivalent. Primarily `t` **supports** bindings and event handlers while `tag` **does not**. This is because to remove event listeners, we need to keep track of them, and thus we need to keep this state somewhere which we can't do with a simple function call but we can insite the TemplateView class. From fc93acfd8dcd6cf154daab6c0a5f9f5bfdf0aff9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 1 Jul 2022 14:09:06 +0200 Subject: [PATCH 037/354] some rewording --- doc/UI/ui.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/UI/ui.md b/doc/UI/ui.md index fe60cf52..e8d044e3 100644 --- a/doc/UI/ui.md +++ b/doc/UI/ui.md @@ -10,7 +10,7 @@ The [interface](https://github.com/vector-im/hydrogen-web/blob/master/src/platfo ### Template language -Templates uses a mini-DSL language in pure javascript to express declarative templates. This is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows: +Templates use a mini-DSL language in pure javascript to express declarative templates. This is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows: ```js t.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]); t.tag_name(child_element); @@ -27,9 +27,9 @@ Here is an example HTML segment followed with the code to create it in Hydrogen. ``` ```js -tag.section({className: "main-section"},[ - tag.h1("Demo"), - tag.button({className:"btn_cool"}, "Click me") +t.section({className: "main-section"},[ + t.h1("Demo"), + t.button({className:"btn_cool"}, "Click me") ]); ``` @@ -37,9 +37,9 @@ All these functions return DOM element nodes, e.g. the result of `document.creat ### TemplateView -`TemplateView` builds on top of templating by adopting the IView component model and adding event handling attributes and one-way databinding. +`TemplateView` builds on top of templating by adopting the IView component model and adding event handling attributes, sub views and one-way databinding. In views based on `TemplateView`, you will see a render method with a `t` argument. -`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. +`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. It also takes a data object to render and bind to, often called `vm`, short for view model from the MVVM pattern Hydrogen uses. You either subclass `TemplateView` and override the `render` method: ```js @@ -80,7 +80,7 @@ t.div({className: "Container"}, t.view(new ChildView(vm.childViewModel))); #### One-way data-binding -A binding binds a part of the DOM to a value on the view model. The view model emits an update when any of its properties change to which the view can subscribe. When an update is received by the view, it will reevaluate all the bindings, and update the DOM accordingly. +A binding couples a part of the DOM to a value on the view model. The view model emits an update when any of its properties change, to which the view can subscribe. When an update is received by the view, it will reevaluate all the bindings, and update the DOM accordingly. A binding can appear in many places where a static value can usually be used in the template tree. To create a binding, you pass a function that maps the view value to a static value. From ca94c65dac7396a7157e0f7b0424aa19de2bc549 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:19:56 +0200 Subject: [PATCH 038/354] clarify LazyListView constraints --- doc/UI/ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/UI/ui.md b/doc/UI/ui.md index e8d044e3..d3aa3893 100644 --- a/doc/UI/ui.md +++ b/doc/UI/ui.md @@ -197,7 +197,7 @@ const list = new ListView({ As items are added, removed, moved (change position) and updated, the DOM will be kept in sync. -There is also a `LazyListView` that only renders items in and around the current viewport. +There is also a `LazyListView` that only renders items in and around the current viewport, with the restriction that all items in the list must be rendered with the same height. ### Sub view updates From 8ec0bd7295a85ea8de66d23d1be7e40d9be03a3c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 5 Jul 2022 17:55:51 +0530 Subject: [PATCH 039/354] Check if lightbox url is available --- src/platform/web/ui/session/room/timeline/ImageView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/ImageView.js b/src/platform/web/ui/session/room/timeline/ImageView.js index 1668b09c..19591606 100644 --- a/src/platform/web/ui/session/room/timeline/ImageView.js +++ b/src/platform/web/ui/session/room/timeline/ImageView.js @@ -24,6 +24,6 @@ export class ImageView extends BaseMediaView { title: vm => vm.label, style: `max-width: ${vm.width}px; max-height: ${vm.height}px;` }); - return vm.isPending ? img : t.a({href: vm.lightboxUrl}, img); + return vm.isPending || !vm.lightboxUrl ? img : t.a({href: vm.lightboxUrl}, img); } } From 3bf6a46a3946b6c5e21dcefdfaeeef106cc22704 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 5 Jul 2022 16:02:47 +0200 Subject: [PATCH 040/354] release sdk 0.0.14 --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index a62888a7..e9887337 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.13", + "version": "0.0.14", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From dd82469ab4aa4db0f8a3f24b7c769e7eb2a621da Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 5 Jul 2022 20:07:48 +0530 Subject: [PATCH 041/354] Don't assume object is available --- scripts/postcss/css-compile-variables.js | 9 ++++++++- scripts/postcss/css-url-to-variables.js | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 63aef97f..80aedf60 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -112,7 +112,14 @@ function populateMapWithDerivedVariables(map, cssFileLocation, {resolvedMap, ali ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), ...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`)) ]; - map.set(location, { "derived-variables": derivedVariables }); + const sharedObject = map.get(location); + const output = { "derived-variables": derivedVariables }; + if (sharedObject) { + Object.assign(sharedObject, output); + } + else { + map.set(location, output); + } } /** diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 82ddae82..f9588434 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -55,7 +55,13 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVari function populateMapWithIcons(map, cssFileLocation, urlVariables) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; const sharedObject = map.get(location); - sharedObject["icon"] = Object.fromEntries(urlVariables); + const output = {"icon": Object.fromEntries(urlVariables)}; + if (sharedObject) { + Object.assign(sharedObject, output); + } + else { + map.set(location, output); + } } function *createCounter() { From a85d2c96d67c72d52adc94c43b3eb798875fc830 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 6 Jul 2022 10:06:00 +0100 Subject: [PATCH 042/354] Log the error when we can't get storage access This is quite useful when debugging why a session isn't working properly. --- src/matrix/storage/idb/StorageFactory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 5cb1b6e5..1f64baf3 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -42,6 +42,7 @@ async function requestPersistedStorage(): Promise { await glob.document.requestStorageAccess(); return true; } catch (err) { + console.warn("requestStorageAccess threw an error:", err); return false; } } else { From 204948db648fec5621cfe77d2dd72b568226f2a7 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Wed, 29 Jun 2022 22:18:29 -0400 Subject: [PATCH 043/354] changing filename to ts --- src/domain/login/{LoginViewModel.js => LoginViewModel.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/domain/login/{LoginViewModel.js => LoginViewModel.ts} (100%) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.ts similarity index 100% rename from src/domain/login/LoginViewModel.js rename to src/domain/login/LoginViewModel.ts From efd9f70e928cb2374c6c734ac9002fb77daec628 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 16:39:45 +0530 Subject: [PATCH 044/354] WIP --- src/domain/session/room/RoomViewModel.js | 37 ++++++++++++++++++- .../web/ui/css/themes/element/theme.css | 4 +- ...rchivedView.js => DisabledComposerView.js} | 4 +- src/platform/web/ui/session/room/RoomView.js | 19 ++++++---- 4 files changed, 50 insertions(+), 14 deletions(-) rename src/platform/web/ui/session/room/{RoomArchivedView.js => DisabledComposerView.js} (82%) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 66042ae5..1cf15969 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -37,9 +37,9 @@ export class RoomViewModel extends ViewModel { this._sendError = null; this._composerVM = null; if (room.isArchived) { - this._composerVM = new ArchivedViewModel(this.childOptions({archivedRoom: room})); + this._composerVM = this.track(new ArchivedViewModel(this.childOptions({archivedRoom: room}))); } else { - this._composerVM = new ComposerViewModel(this); + this._watchPowerLevelChange(); } this._clearUnreadTimout = null; this._closeUrl = this.urlCreator.urlUntilSegment("session"); @@ -67,6 +67,29 @@ export class RoomViewModel extends ViewModel { this._clearUnreadAfterDelay(); } + async _watchPowerLevelChange() { + const powerLevelObservable = await this._room.observePowerLevels(); + let oldCanSendMessage = powerLevelObservable.get().canSendType("m.room.message"); + const recreateComposer = newCanSendMessage => { + this._composerVM = this.disposeTracked(this._composerVM); + if (newCanSendMessage) { + this._composerVM = this.track(new ComposerViewModel(this)); + } + else { + this._composerVM = this.track(new LowerPowerLevelViewModel()); + } + this.emitChange("powerLevelObservable") + }; + this.track(powerLevelObservable.subscribe(newPowerLevel => { + const newCanSendMessage = newPowerLevel.canSendType("m.room.message"); + if (oldCanSendMessage !== newCanSendMessage) { + recreateComposer(newCanSendMessage); + oldCanSendMessage = newCanSendMessage; + } + })); + recreateComposer(oldCanSendMessage); + } + async _clearUnreadAfterDelay() { if (this._room.isArchived || this._clearUnreadTimout) { return; @@ -365,3 +388,13 @@ class ArchivedViewModel extends ViewModel { return "archived"; } } + +class LowerPowerLevelViewModel extends ViewModel { + get description() { + return this.i18n`You do not have the powerlevel necessary to send messages`; + } + + get kind() { + return "low-powerlevel"; + } +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 113ea254..632d5bc5 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -894,12 +894,12 @@ button.link { width: 100%; } -.RoomArchivedView { +.DisabledComposerView { padding: 12px; background-color: var(--background-color-secondary); } -.RoomArchivedView h3 { +.DisabledComposerView h3 { margin: 0; } diff --git a/src/platform/web/ui/session/room/RoomArchivedView.js b/src/platform/web/ui/session/room/DisabledComposerView.js similarity index 82% rename from src/platform/web/ui/session/room/RoomArchivedView.js rename to src/platform/web/ui/session/room/DisabledComposerView.js index 1db1c2d2..caa8eeb9 100644 --- a/src/platform/web/ui/session/room/RoomArchivedView.js +++ b/src/platform/web/ui/session/room/DisabledComposerView.js @@ -16,8 +16,8 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; -export class RoomArchivedView extends TemplateView { +export class DisabledComposerView extends TemplateView { render(t) { - return t.div({className: "RoomArchivedView"}, t.h3(vm => vm.description)); + return t.div({className: "DisabledComposerView"}, t.h3(vm => vm.description)); } } diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 76e26eab..5923355c 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -21,7 +21,7 @@ import {Menu} from "../../general/Menu.js"; import {TimelineView} from "./TimelineView"; import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; -import {RoomArchivedView} from "./RoomArchivedView.js"; +import {DisabledComposerView} from "./DisabledComposerView.js"; import {AvatarView} from "../../AvatarView.js"; export class RoomView extends TemplateView { @@ -32,12 +32,6 @@ export class RoomView extends TemplateView { } render(t, vm) { - let bottomView; - if (vm.composerViewModel.kind === "composer") { - bottomView = new MessageComposer(vm.composerViewModel, this._viewClassForTile); - } else if (vm.composerViewModel.kind === "archived") { - bottomView = new RoomArchivedView(vm.composerViewModel); - } return t.main({className: "RoomView middle"}, [ t.div({className: "RoomHeader middle-header"}, [ t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), @@ -58,7 +52,16 @@ export class RoomView extends TemplateView { new TimelineView(timelineViewModel, this._viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), - t.view(bottomView), + t.mapView(vm => vm.composerViewModel, + composerViewModel => { + switch (composerViewModel?.kind) { + case "composer": + return new MessageComposer(vm.composerViewModel, this._viewClassForTile); + case "archived": + case "low-powerlevel": + return new DisabledComposerView(vm.composerViewModel); + } + }), ]) ]); } From 3941b7e3f09a213ea03970d1512255fbe0ad2801 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 16:45:18 +0530 Subject: [PATCH 045/354] Rename method --- src/domain/session/room/RoomViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 1cf15969..fac0ed9d 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -39,7 +39,7 @@ export class RoomViewModel extends ViewModel { if (room.isArchived) { this._composerVM = this.track(new ArchivedViewModel(this.childOptions({archivedRoom: room}))); } else { - this._watchPowerLevelChange(); + this._recreateComposerOnPowerLevelChange(); } this._clearUnreadTimout = null; this._closeUrl = this.urlCreator.urlUntilSegment("session"); @@ -67,7 +67,7 @@ export class RoomViewModel extends ViewModel { this._clearUnreadAfterDelay(); } - async _watchPowerLevelChange() { + async _recreateComposerOnPowerLevelChange() { const powerLevelObservable = await this._room.observePowerLevels(); let oldCanSendMessage = powerLevelObservable.get().canSendType("m.room.message"); const recreateComposer = newCanSendMessage => { From cd9e00b847731d3acb37ab53fb00bff9bd2e6ccb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 17:17:05 +0530 Subject: [PATCH 046/354] Support power_level_content_override --- src/matrix/room/RoomBeingCreated.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/RoomBeingCreated.ts b/src/matrix/room/RoomBeingCreated.ts index 78202203..b2c9dafb 100644 --- a/src/matrix/room/RoomBeingCreated.ts +++ b/src/matrix/room/RoomBeingCreated.ts @@ -37,7 +37,8 @@ type CreateRoomPayload = { invite?: string[]; room_alias_name?: string; creation_content?: {"m.federate": boolean}; - initial_state: {type: string; state_key: string; content: Record}[] + initial_state: { type: string; state_key: string; content: Record }[]; + power_level_content_override?: Record; } type ImageInfo = { @@ -62,6 +63,7 @@ type Options = { invites?: string[]; avatar?: Avatar; alias?: string; + powerLevelContentOverride?: Record; } function defaultE2EEStatusForType(type: RoomType): boolean { @@ -151,6 +153,9 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { "m.federate": false }; } + if (this.options.powerLevelContentOverride) { + createOptions.power_level_content_override = this.options.powerLevelContentOverride; + } if (this.isEncrypted) { createOptions.initial_state.push(createRoomEncryptionEvent()); } From d292e1f5adc8ebdda19cd9c5db93b5148ff5bf00 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 17:23:23 +0530 Subject: [PATCH 047/354] Extract into function --- src/domain/session/room/RoomViewModel.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index fac0ed9d..74dda426 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -69,7 +69,8 @@ export class RoomViewModel extends ViewModel { async _recreateComposerOnPowerLevelChange() { const powerLevelObservable = await this._room.observePowerLevels(); - let oldCanSendMessage = powerLevelObservable.get().canSendType("m.room.message"); + const canSendMessage = () => powerLevelObservable.get().canSendType("m.room.message"); + let oldCanSendMessage = canSendMessage(); const recreateComposer = newCanSendMessage => { this._composerVM = this.disposeTracked(this._composerVM); if (newCanSendMessage) { @@ -80,8 +81,8 @@ export class RoomViewModel extends ViewModel { } this.emitChange("powerLevelObservable") }; - this.track(powerLevelObservable.subscribe(newPowerLevel => { - const newCanSendMessage = newPowerLevel.canSendType("m.room.message"); + this.track(powerLevelObservable.subscribe(() => { + const newCanSendMessage = canSendMessage(); if (oldCanSendMessage !== newCanSendMessage) { recreateComposer(newCanSendMessage); oldCanSendMessage = newCanSendMessage; From 0bdbb96036699bbbe1810aa0e010a9fbc16bd4e5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 17:26:43 +0530 Subject: [PATCH 048/354] Use same kind --- src/domain/session/room/RoomViewModel.js | 4 ++-- src/platform/web/ui/session/room/RoomView.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 74dda426..60a01d16 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -386,7 +386,7 @@ class ArchivedViewModel extends ViewModel { } get kind() { - return "archived"; + return "disabled"; } } @@ -396,6 +396,6 @@ class LowerPowerLevelViewModel extends ViewModel { } get kind() { - return "low-powerlevel"; + return "disabled"; } } diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 5923355c..d36466dd 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -57,8 +57,7 @@ export class RoomView extends TemplateView { switch (composerViewModel?.kind) { case "composer": return new MessageComposer(vm.composerViewModel, this._viewClassForTile); - case "archived": - case "low-powerlevel": + case "disabled": return new DisabledComposerView(vm.composerViewModel); } }), From f073f40e3106e60882b44c2bfe61e6dd129e42f0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Jul 2022 18:16:33 +0530 Subject: [PATCH 049/354] Fix error --- src/domain/session/room/RoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 60a01d16..94c78286 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -77,7 +77,7 @@ export class RoomViewModel extends ViewModel { this._composerVM = this.track(new ComposerViewModel(this)); } else { - this._composerVM = this.track(new LowerPowerLevelViewModel()); + this._composerVM = this.track(new LowerPowerLevelViewModel(this.childOptions())); } this.emitChange("powerLevelObservable") }; From 7b9e681d558f39fd77519bd7a99f8067cacd4ed4 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:25:17 +0200 Subject: [PATCH 050/354] sdk v0.0.15 --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index e9887337..25342baa 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.14", + "version": "0.0.15", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From 6718198d9cc9e5aac8d9e3c0ca5534440b53bdcd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 12:40:24 +0530 Subject: [PATCH 051/354] Continue with other items if this throws --- src/platform/web/ThemeLoader.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index ee303b49..89430663 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -76,11 +76,16 @@ export class ThemeLoader { const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (let [themeId, cssLocation] of Object.entries(builtAssets)) { - /** - * This cssLocation is relative to the location of the manifest file. - * So we first need to resolve it relative to the root of this hydrogen instance. - */ - cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; + try { + /** + * This cssLocation is relative to the location of the manifest file. + * So we first need to resolve it relative to the root of this hydrogen instance. + */ + cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; + } + catch { + continue; + } const variant = themeId.match(/.+-(.+)/)?.[1]; const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; const themeDisplayName = `${themeName} ${variantName}`; From 88808b0b069bc27276e6fcd9ddcaf513e9d35ed0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 15:50:01 +0530 Subject: [PATCH 052/354] Fix bug preventing yarn start --- .../build-plugins/rollup-plugin-build-themes.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 6b8fdae0..438203b7 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -136,7 +136,6 @@ module.exports = function buildThemes(options) { }, async buildStart() { - if (isDevelopment) { return; } const { themeConfig } = options; for (const location of themeConfig.themes) { manifest = require(`${location}/manifest.json`); @@ -157,18 +156,14 @@ module.exports = function buildThemes(options) { } } // emit the css as built theme bundle - this.emitFile({ - type: "chunk", - id: `${location}/theme.css?variant=${variant}${details.dark? "&dark=true": ""}`, - fileName, - }); + if (!isDevelopment) { + this.emitFile({ type: "chunk", id: `${location}/theme.css?variant=${variant}${details.dark ? "&dark=true" : ""}`, fileName, }); + } } // emit the css as runtime theme bundle - this.emitFile({ - type: "chunk", - id: `${location}/theme.css?type=runtime`, - fileName: `theme-${themeCollectionId}-runtime.css`, - }); + if (!isDevelopment) { + this.emitFile({ type: "chunk", id: `${location}/theme.css?type=runtime`, fileName: `theme-${themeCollectionId}-runtime.css`, }); + } } }, From d5e24bf6e8513b2768c61cd0f5837ce72c1c1fb2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 12:03:07 +0530 Subject: [PATCH 053/354] Convert color.js to color.mjs --- scripts/postcss/{color.js => color.mjs} | 5 ++--- vite.common-config.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) rename scripts/postcss/{color.js => color.mjs} (91%) diff --git a/scripts/postcss/color.js b/scripts/postcss/color.mjs similarity index 91% rename from scripts/postcss/color.js rename to scripts/postcss/color.mjs index b1ef7073..bd2ea3ea 100644 --- a/scripts/postcss/color.js +++ b/scripts/postcss/color.mjs @@ -13,10 +13,9 @@ 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 {offColor} from 'off-color'; -const offColor = require("off-color").offColor; - -module.exports.derive = function (value, operation, argument, isDark) { +export function derive(value, operation, argument, isDark) { const argumentAsNumber = parseInt(argument); if (isDark) { // For dark themes, invert the operation diff --git a/vite.common-config.js b/vite.common-config.js index 5d65f8e2..7f0e57a8 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,8 +8,8 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -const derive = require("./scripts/postcss/color").derive; const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG; +import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { logLevel: "warn", From 599e519f229031e4966c446e1be097af4a156883 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 12:17:33 +0530 Subject: [PATCH 054/354] Convert color code to use es6 module --- .../{svg-colorizer.js => svg-colorizer.mjs} | 18 +++++++++--------- vite.common-config.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) rename scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} (79%) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.mjs similarity index 79% rename from scripts/postcss/svg-colorizer.js rename to scripts/postcss/svg-colorizer.mjs index 06b7b14b..fbe48b55 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.mjs @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -const fs = require("fs"); -const path = require("path"); -const xxhash = require('xxhashjs'); +import {readFileSync, mkdirSync, writeFileSync} from "fs"; +import {resolve} from "path"; +import {h32} from "xxhashjs"; function createHash(content) { - const hasher = new xxhash.h32(0); + const hasher = new h32(0); hasher.update(content); return hasher.digest(); } @@ -30,8 +30,8 @@ function createHash(content) { * @param {string} primaryColor Primary color for the new svg * @param {string} secondaryColor Secondary color for the new svg */ -module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) { - const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"}); +export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) { + const svgCode = readFileSync(svgLocation, { encoding: "utf8"}); let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); if (svgCode === coloredSVGCode) { @@ -39,9 +39,9 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar } const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1]; const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; - const outputPath = path.resolve(__dirname, "../../.tmp"); + const outputPath = resolve(__dirname, "../../.tmp"); try { - fs.mkdirSync(outputPath); + mkdirSync(outputPath); } catch (e) { if (e.code !== "EEXIST") { @@ -49,6 +49,6 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar } } const outputFile = `${outputPath}/${outputName}`; - fs.writeFileSync(outputFile, coloredSVGCode); + writeFileSync(outputFile, coloredSVGCode); return outputFile; } diff --git a/vite.common-config.js b/vite.common-config.js index 7f0e57a8..aaea47a9 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,7 +8,7 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG; +import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-colorizer.mjs"; import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { From 8c02541b6971aec5fd15c65fe0048962538c1551 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Jun 2022 12:28:19 +0530 Subject: [PATCH 055/354] WIP - 1 --- src/platform/web/ThemeBuilder.ts | 155 +++++++++++++++++++++++++++++++ src/platform/web/ThemeLoader.ts | 40 +++++--- theme.json | 51 ++++++++++ 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 src/platform/web/ThemeBuilder.ts create mode 100644 theme.json diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts new file mode 100644 index 00000000..313a8183 --- /dev/null +++ b/src/platform/web/ThemeBuilder.ts @@ -0,0 +1,155 @@ +/* +Copyright 2020 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 type {ThemeInformation} from "./ThemeLoader"; +import {ColorSchemePreference} from "./ThemeLoader"; +import {offColor} from 'off-color'; + +function derive(value, operation, argument, isDark) { + const argumentAsNumber = parseInt(argument); + if (isDark) { + // For dark themes, invert the operation + if (operation === 'darker') { + operation = "lighter"; + } + else if (operation === 'lighter') { + operation = "darker"; + } + } + switch (operation) { + case "darker": { + const newColorString = offColor(value).darken(argumentAsNumber / 100).hex(); + return newColorString; + } + case "lighter": { + const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); + return newColorString; + } + } +} + +export class ThemeBuilder { + // todo: replace any with manifest type when PR is merged + private _idToManifest: Map; + private _themeMapping: Record = {}; + private _themeToVariables: Record = {}; + private _preferredColorScheme?: ColorSchemePreference; + + constructor(manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { + this._idToManifest = manifestMap; + this._preferredColorScheme = preferredColorScheme; + } + + populateDerivedTheme(manifest) { + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); + const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; + const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; + const derivedVariables = baseManifest.source?.["derived-variables"]; + const themeName = manifest.name; + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); + console.log("resolved", resolvedVariables); + Object.assign(variables, resolvedVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + /** + * This is a default variant! + * We'll add these to the themeMapping (separately) keyed with just the + * theme-name (i.e "Element" instead of "Element Dark"). + * We need to be able to distinguish them from other variants! + * + * This allows us to render radio-buttons with "dark" and + * "light" options. + */ + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + defaultVariant.variantName = variantName; + defaultVariant.id = themeId; + defaultVariant.cssLocation = cssLocation; + defaultVariant.variables = variables; + continue; + } + // Non-default variants are keyed in themeMapping with "theme_name variant_name" + // eg: "Element Dark" + this._themeMapping[themeDisplayName] = { + cssLocation, + id: themeId, + variables: variables, + }; + } + if (defaultDarkVariant.id && defaultLightVariant.id) { + /** + * As mentioned above, if there's both a default dark and a default light variant, + * add them to themeMapping separately. + */ + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; + } + else { + /** + * If only one default variant is found (i.e only dark default or light default but not both), + * treat it like any other variant. + */ + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + } + + get themeMapping() { + return this._themeMapping; + } + + injectCSSVariables(variables: Record) { + const root = document.documentElement; + for (const [variable, value] of Object.entries(variables)) { + root.style.setProperty(`--${variable}`, value); + } + } + + removeCSSVariables(variables: string[]) { + const root = document.documentElement; + for (const variable of variables) { + root.style.removeProperty(`--${variable}`); + } + } + + deriveVariables(variables: Record, derivedVariables: string[], isDark: boolean) { + const aliases: any = {}; + const resolvedVariables: any = {}; + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + for (const variable of derivedVariables) { + // If this is an alias, store it for processing later + const [alias, value] = variable.split("="); + if (value) { + aliases[alias] = value; + continue; + } + // Resolve derived variables + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = variables[baseVariable]; + const resolvedValue = derive(value, operation, argument, isDark); + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(aliases) as any) { + resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable]; + } + return resolvedVariables; + } +} diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 89430663..9806dd10 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -14,33 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {ILogItem} from "../../logging/types.js"; +import type {ILogItem} from "../../logging/types"; import type {Platform} from "./Platform.js"; +import {ThemeBuilder} from "./ThemeBuilder"; type NormalVariant = { id: string; cssLocation: string; + variables?: any; }; type DefaultVariant = { - dark: { - id: string; - cssLocation: string; + dark: NormalVariant & { variantName: string; }; - light: { - id: string; - cssLocation: string; + light: NormalVariant & { variantName: string; }; - default: { - id: string; - cssLocation: string; + default: NormalVariant & { variantName: string; }; } -type ThemeInformation = NormalVariant | DefaultVariant; +export type ThemeInformation = NormalVariant | DefaultVariant; export enum ColorSchemePreference { Dark, @@ -50,18 +46,31 @@ export enum ColorSchemePreference { export class ThemeLoader { private _platform: Platform; private _themeMapping: Record; + private _themeBuilder: ThemeBuilder; constructor(platform: Platform) { this._platform = platform; } async init(manifestLocations: string[], log?: ILogItem): Promise { + const idToManifest = new Map(); await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { this._themeMapping = {}; const results = await Promise.all( manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - results.forEach(({ body }, i) => this._populateThemeMap(body, manifestLocations[i], log)); + results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); + this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme); + results.forEach(({ body }, i) => { + if (body.extends) { + this._themeBuilder.populateDerivedTheme(body); + } + else { + this._populateThemeMap(body, manifestLocations[i], log); + } + }); + Object.assign(this._themeMapping, this._themeBuilder.themeMapping); + console.log("derived theme mapping", this._themeBuilder.themeMapping); }); } @@ -144,18 +153,23 @@ export class ThemeLoader { setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => { - let cssLocation: string; + let cssLocation: string, variables: Record; let themeDetails = this._themeMapping[themeName]; if ("id" in themeDetails) { cssLocation = themeDetails.cssLocation; + variables = themeDetails.variables; } else { if (!themeVariant) { throw new Error("themeVariant is undefined!"); } cssLocation = themeDetails[themeVariant].cssLocation; + variables = themeDetails[themeVariant].variables; } this._platform.replaceStylesheet(cssLocation); + if (variables) { + this._themeBuilder.injectCSSVariables(variables); + } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { this._platform.settingsStorage.setString("theme-variant", themeVariant); diff --git a/theme.json b/theme.json new file mode 100644 index 00000000..aee95368 --- /dev/null +++ b/theme.json @@ -0,0 +1,51 @@ +{ + "name": "Customer", + "extends": "element", + "id": "customer", + "values": { + "variants": { + "dark": { + "dark": true, + "default": true, + "name": "Dark", + "variables": { + "background-color-primary": "#21262b", + "background-color-secondary": "#2D3239", + "text-color": "#fff", + "accent-color": "#F03F5B", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + }, + "light": { + "default": true, + "name": "Dark", + "variables": { + "background-color-primary": "#21262b", + "background-color-secondary": "#2D3239", + "text-color": "#fff", + "accent-color": "#F03F5B", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + }, + "red": { + "name": "Red", + "variables": { + "background-color-primary": "#1F1F1F", + "background-color-secondary": "#2B243E", + "text-color": "#fff", + "accent-color": "#F23041", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#F23030", + "link-color": "#238cf5" + } + } + } + } +} From bf87ed7eae420ef905ceb47a48b0377a015ae8d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:41:06 +0530 Subject: [PATCH 056/354] Do not add variables to root for runtime theme --- scripts/postcss/css-url-to-variables.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index f9588434..9988a10e 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -81,7 +81,8 @@ module.exports = (opts = {}) => { const urlVariables = new Map(); const counter = createCounter(); root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter)); - if (urlVariables.size) { + const cssFileLocation = root.source.input.from; + if (urlVariables.size && !cssFileLocation.includes("type=runtime")) { addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables); } if (opts.compiledVariables){ From 43e8cc9e522413406d6431d36acd407e715ef64c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:42:02 +0530 Subject: [PATCH 057/354] Add svgo for optimizing svgs as dev dependency --- package.json | 1 + yarn.lock | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 82588f10..dd1a1554 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-value-parser": "^4.2.0", "regenerator-runtime": "^0.13.7", + "svgo": "^2.8.0", "text-encoding": "^0.7.0", "typescript": "^4.3.5", "vite": "^2.9.8", diff --git a/yarn.lock b/yarn.lock index 28efc3bc..1543f8d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,6 +77,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@types/json-schema@^7.0.7": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -347,6 +352,11 @@ commander@^6.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -382,11 +392,26 @@ css-select@^4.1.3: domutils "^2.6.0" nth-check "^2.0.0" +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-what@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" @@ -1197,6 +1222,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + mdn-polyfills@^5.20.0: version "5.20.0" resolved "https://registry.yarnpkg.com/mdn-polyfills/-/mdn-polyfills-5.20.0.tgz#ca8247edf20a4f60dec6804372229812b348260b" @@ -1500,7 +1530,7 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map@~0.6.1: +source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -1510,6 +1540,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1550,6 +1585,19 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svgo@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + table@^6.0.9: version "6.7.1" resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" From c8738045435e0335489b401c1b9b397ad564d7b3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:42:56 +0530 Subject: [PATCH 058/354] produce asset hashed icons --- .../rollup-plugin-build-themes.js | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 6b8fdae0..b523778b 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ const path = require('path').posix; +const {optimize} = require('svgo'); async function readCSSSource(location) { const fs = require("fs").promises; - const path = require("path"); const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`); const data = await fs.readFile(resolvedLocation); return data; @@ -43,6 +43,28 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { } } +/** + * Returns an object where keys are the svg file names and the values + * are the svg code (optimized) + * @param {*} icons Object where keys are css variable names and values are locations of the svg + * @param {*} manifestLocation Location of manifest used for resolving path + */ +async function generateIconSourceMap(icons, manifestLocation) { + const sources = {}; + const fs = require("fs").promises; + for (const icon of Object.values(icons)) { + const [location] = icon.split("?"); + const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); + const iconData = await fs.readFile(resolvedLocation); + const svgString = iconData.toString(); + const result = optimize(svgString); + const optimizedSvgString = result.data; + const fileName = path.basename(resolvedLocation); + sources[fileName] = optimizedSvgString; + } + return sources; +} + /** * Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location. * To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle. @@ -283,7 +305,7 @@ module.exports = function buildThemes(options) { ]; }, - generateBundle(_, bundle) { + async generateBundle(_, bundle) { const assetMap = getMappingFromFileNameToAssetInfo(bundle); const chunkMap = getMappingFromLocationToChunkArray(bundle); const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle); @@ -304,13 +326,28 @@ module.exports = function buildThemes(options) { const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot); builtAssets[`${name}-${variant}`] = locationRelativeToManifest; } + // Emit the base svg icons as asset + const nameToAssetHashedLocation = []; + const nameToSource = await generateIconSourceMap(icon, location); + for (const [name, source] of Object.entries(nameToSource)) { + const ref = this.emitFile({ type: "asset", name, source }); + const assetHashedName = this.getFileName(ref); + nameToAssetHashedLocation[name] = assetHashedName; + } + for (const [variable, location] of Object.entries(icon)) { + const [locationWithoutQueryParameters, queryParameters] = location.split("?"); + const name = path.basename(locationWithoutQueryParameters); + const locationRelativeToBuildRoot = nameToAssetHashedLocation[name]; + const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot); + icon[variable] = `${locationRelativeToManifest}?${queryParameters}`; + } const runtimeThemeChunk = runtimeThemeChunkMap.get(location); const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName); manifest.source = { "built-assets": builtAssets, "runtime-asset": runtimeAssetLocation, "derived-variables": derivedVariables, - "icon": icon + "icon": icon, }; const name = `theme-${themeKey}.json`; manifestLocations.push(`${manifestLocation}/${name}`); From 2947f9f6ff84834c4190616356d40f40460fff73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 17:14:10 +0530 Subject: [PATCH 059/354] Remove console.log --- src/platform/web/ThemeBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 313a8183..a192424c 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -63,7 +63,6 @@ export class ThemeBuilder { const themeId = `${manifest.id}-${variant}`; const { name: variantName, default: isDefault, dark, variables } = variantDetails; const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); - console.log("resolved", resolvedVariables); Object.assign(variables, resolvedVariables); const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { From 161e29b36e6d3864561ea08aa5ee4a30208875e8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 16:58:22 +0530 Subject: [PATCH 060/354] Use existing code --- src/platform/web/ThemeBuilder.ts | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index a192424c..b8e7274b 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -15,30 +15,7 @@ limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; import {ColorSchemePreference} from "./ThemeLoader"; -import {offColor} from 'off-color'; - -function derive(value, operation, argument, isDark) { - const argumentAsNumber = parseInt(argument); - if (isDark) { - // For dark themes, invert the operation - if (operation === 'darker') { - operation = "lighter"; - } - else if (operation === 'lighter') { - operation = "darker"; - } - } - switch (operation) { - case "darker": { - const newColorString = offColor(value).darken(argumentAsNumber / 100).hex(); - return newColorString; - } - case "lighter": { - const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); - return newColorString; - } - } -} +import {derive} from "../../../scripts/postcss/color.mjs"; export class ThemeBuilder { // todo: replace any with manifest type when PR is merged From 1ef382f3a95e85cf03829fc8d674e82a63f45d59 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:55:06 +0530 Subject: [PATCH 061/354] Add gruvbox color scheme --- theme.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/theme.json b/theme.json index aee95368..f5fbf3ed 100644 --- a/theme.json +++ b/theme.json @@ -36,14 +36,14 @@ "red": { "name": "Red", "variables": { - "background-color-primary": "#1F1F1F", - "background-color-secondary": "#2B243E", - "text-color": "#fff", - "accent-color": "#F23041", - "error-color": "#FF4B55", + "background-color-primary": "#282828", + "background-color-secondary": "#3c3836", + "text-color": "#fbf1c7", + "accent-color": "#8ec07c", + "error-color": "#fb4934", "fixed-white": "#fff", - "room-badge": "#F23030", - "link-color": "#238cf5" + "room-badge": "#cc241d", + "link-color": "#fe8019" } } } From 2f3db89e0a43db1e1d0cd7f37deb1506539eaf2d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:55:21 +0530 Subject: [PATCH 062/354] Let ts know that we can use replaceAll() --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index f46cc7eb..8f591a6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "noEmit": true, "target": "ES2020", "module": "ES2020", + "lib": ["ES2021", "DOM"], "moduleResolution": "node", "esModuleInterop": true }, From c5f4a75d4b7b3bee0218cfe7b6527fb66d812fb2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:57:00 +0530 Subject: [PATCH 063/354] Split code so that it can be reused --- .../{svg-colorizer.mjs => svg-builder.mjs} | 7 ++---- scripts/postcss/svg-colorizer.js | 24 +++++++++++++++++++ vite.common-config.js | 2 +- 3 files changed, 27 insertions(+), 6 deletions(-) rename scripts/postcss/{svg-colorizer.mjs => svg-builder.mjs} (82%) create mode 100644 scripts/postcss/svg-colorizer.js diff --git a/scripts/postcss/svg-colorizer.mjs b/scripts/postcss/svg-builder.mjs similarity index 82% rename from scripts/postcss/svg-colorizer.mjs rename to scripts/postcss/svg-builder.mjs index fbe48b55..1bbc4010 100644 --- a/scripts/postcss/svg-colorizer.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,6 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; +import {getColoredSvgString} from "./svg-colorizer.js"; function createHash(content) { const hasher = new h32(0); @@ -32,11 +33,7 @@ function createHash(content) { */ export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) { const svgCode = readFileSync(svgLocation, { encoding: "utf8"}); - let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); - coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); - if (svgCode === coloredSVGCode) { - throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); - } + const coloredSVGCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1]; const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; const outputPath = resolve(__dirname, "../../.tmp"); diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js new file mode 100644 index 00000000..cb291726 --- /dev/null +++ b/scripts/postcss/svg-colorizer.js @@ -0,0 +1,24 @@ +/* +Copyright 2021 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. +*/ + +export function getColoredSvgString(svgString, primaryColor, secondaryColor) { + let coloredSVGCode = svgString.replaceAll("#ff00ff", primaryColor); + coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); + if (svgString === coloredSVGCode) { + throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); + } + return coloredSVGCode; +} diff --git a/vite.common-config.js b/vite.common-config.js index aaea47a9..bcf17115 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,7 +8,7 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-colorizer.mjs"; +import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { From 5ba74b1d7588582081f934f3de4e45d3045bff8e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:58:07 +0530 Subject: [PATCH 064/354] Use script to copy over runtime theme after build --- scripts/test-theme.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 scripts/test-theme.sh diff --git a/scripts/test-theme.sh b/scripts/test-theme.sh new file mode 100755 index 00000000..9f94d3c3 --- /dev/null +++ b/scripts/test-theme.sh @@ -0,0 +1,5 @@ +#!/bin/zsh +cp theme.json target/assets/theme-customer.json +cat target/config.json | jq '.themeManifests += ["assets/theme-customer.json"]' | cat > target/config.temp.json +rm target/config.json +mv target/config.temp.json target/config.json From f7b302d34fb4f8e82372806e49804a581bf91f95 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:59:50 +0530 Subject: [PATCH 065/354] Don't optimzie colors --- scripts/build-plugins/rollup-plugin-build-themes.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index b523778b..72053a1f 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -57,7 +57,16 @@ async function generateIconSourceMap(icons, manifestLocation) { const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); const iconData = await fs.readFile(resolvedLocation); const svgString = iconData.toString(); - const result = optimize(svgString); + const result = optimize(svgString, { + plugins: [ + { + name: "preset-default", + params: { + overrides: { convertColors: false, }, + }, + }, + ], + }); const optimizedSvgString = result.data; const fileName = path.basename(resolvedLocation); sources[fileName] = optimizedSvgString; From d731eab51c10e9e7499c890f35a86a9099d4945f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 15:04:54 +0530 Subject: [PATCH 066/354] Support fetching text --- src/platform/web/dom/request/fetch.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index 497ad553..eb4caab6 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -115,6 +115,9 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { } else if (format === "buffer") { body = await response.arrayBuffer(); } + else if (format === "text") { + body = await response.text(); + } } catch (err) { // some error pages return html instead of json, ignore error if (!(err.name === "SyntaxError" && status >= 400)) { From ac7be0c7a1f66c549e634f082f7a858204d3dbaf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 15:05:50 +0530 Subject: [PATCH 067/354] WIP --- src/platform/web/DerivedVariables.ts | 54 +++++++++++++++ src/platform/web/IconColorizer.ts | 79 +++++++++++++++++++++ src/platform/web/ThemeBuilder.ts | 100 +++++++++------------------ src/platform/web/ThemeLoader.ts | 13 ++-- 4 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 src/platform/web/DerivedVariables.ts create mode 100644 src/platform/web/IconColorizer.ts diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/DerivedVariables.ts new file mode 100644 index 00000000..4e3a8b46 --- /dev/null +++ b/src/platform/web/DerivedVariables.ts @@ -0,0 +1,54 @@ +/* +Copyright 2020 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 {derive} from "../../../scripts/postcss/color.mjs"; + +export class DerivedVariables { + private _baseVariables: Record; + private _variablesToDerive: string[] + private _isDark: boolean + + constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) { + this._baseVariables = baseVariables; + this._variablesToDerive = variablesToDerive; + this._isDark = isDark; + } + + toVariables(): Record { + const aliases: any = {}; + const resolvedVariables: any = {}; + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + for (const variable of this._variablesToDerive) { + // If this is an alias, store it for processing later + const [alias, value] = variable.split("="); + if (value) { + aliases[alias] = value; + continue; + } + // Resolve derived variables + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = this._baseVariables[baseVariable]; + const resolvedValue = derive(value, operation, argument, this._isDark); + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(aliases) as any) { + resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + } + return resolvedVariables; + } +} diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/IconColorizer.ts new file mode 100644 index 00000000..81644603 --- /dev/null +++ b/src/platform/web/IconColorizer.ts @@ -0,0 +1,79 @@ +/* +Copyright 2020 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 type {Platform} from "./Platform.js"; +import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.js"; + +type ParsedStructure = { + [variableName: string]: { + svg: Promise<{ status: number; body: string }>; + primary: string | null; + secondary: string | null; + }; +}; + +export class IconColorizer { + private _iconVariables: Record; + private _resolvedVariables: Record; + private _manifestLocation: string; + private _platform: Platform; + + constructor(platform: Platform, iconVariables: Record, resolvedVariables: Record, manifestLocation: string) { + this._platform = platform; + this._iconVariables = iconVariables; + this._resolvedVariables = resolvedVariables; + this._manifestLocation = manifestLocation; + } + + async toVariables(): Promise> { + const { parsedStructure, promises } = await this._fetchAndParseIcons(); + await Promise.all(promises); + return this._produceColoredIconVariables(parsedStructure); + } + + private async _fetchAndParseIcons(): Promise<{ parsedStructure: ParsedStructure, promises: any[] }> { + const promises: any[] = []; + const parsedStructure: ParsedStructure = {}; + for (const [variable, url] of Object.entries(this._iconVariables)) { + const urlObject = new URL(`https://${url}`); + const pathWithoutQueryParams = urlObject.hostname; + const relativePath = new URL(pathWithoutQueryParams, new URL(this._manifestLocation, window.location.origin)); + const responsePromise = this._platform.request(relativePath, { method: "GET", format: "text", cache: true, }).response() + promises.push(responsePromise); + const searchParams = urlObject.searchParams; + parsedStructure[variable] = { + svg: responsePromise, + primary: searchParams.get("primary"), + secondary: searchParams.get("secondary") + }; + } + return { parsedStructure, promises }; + } + + private async _produceColoredIconVariables(parsedStructure: ParsedStructure): Promise> { + let coloredVariables: Record = {}; + for (const [variable, { svg, primary, secondary }] of Object.entries(parsedStructure)) { + const { body: svgCode } = await svg; + if (!primary) { + throw new Error(`Primary color variable ${primary} not in list of variables!`); + } + const primaryColor = this._resolvedVariables[primary], secondaryColor = this._resolvedVariables[secondary!]; + const coloredSvgCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); + const dataURI = `url('data:image/svg+xml;utf8,${encodeURIComponent(coloredSvgCode)}')`; + coloredVariables[variable] = dataURI; + } + return coloredVariables; + } +} diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b8e7274b..f05a24b1 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -14,72 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; +import type {Platform} from "./Platform.js"; import {ColorSchemePreference} from "./ThemeLoader"; -import {derive} from "../../../scripts/postcss/color.mjs"; +import {IconColorizer} from "./IconColorizer"; +import {DerivedVariables} from "./DerivedVariables"; export class ThemeBuilder { // todo: replace any with manifest type when PR is merged private _idToManifest: Map; private _themeMapping: Record = {}; - private _themeToVariables: Record = {}; private _preferredColorScheme?: ColorSchemePreference; + private _platform: Platform; + private _injectedVariables?: Record; - constructor(manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { + constructor(platform: Platform, manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { this._idToManifest = manifestMap; this._preferredColorScheme = preferredColorScheme; + this._platform = platform; } - populateDerivedTheme(manifest) { + async populateDerivedTheme(manifest) { const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; const derivedVariables = baseManifest.source?.["derived-variables"]; + const icons = baseManifest.source?.["icon"]; const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { - const themeId = `${manifest.id}-${variant}`; - const { name: variantName, default: isDefault, dark, variables } = variantDetails; - const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); - Object.assign(variables, resolvedVariables); - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - /** - * This is a default variant! - * We'll add these to the themeMapping (separately) keyed with just the - * theme-name (i.e "Element" instead of "Element Dark"). - * We need to be able to distinguish them from other variants! - * - * This allows us to render radio-buttons with "dark" and - * "light" options. - */ - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - defaultVariant.variantName = variantName; - defaultVariant.id = themeId; - defaultVariant.cssLocation = cssLocation; - defaultVariant.variables = variables; + try { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); + Object.assign(variables, resolvedVariables); + const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + Object.assign(variables, resolvedVariables, iconVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + continue; + } + this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; + } + catch (e) { + console.error(e); continue; } - // Non-default variants are keyed in themeMapping with "theme_name variant_name" - // eg: "Element Dark" - this._themeMapping[themeDisplayName] = { - cssLocation, - id: themeId, - variables: variables, - }; } if (defaultDarkVariant.id && defaultLightVariant.id) { - /** - * As mentioned above, if there's both a default dark and a default light variant, - * add them to themeMapping separately. - */ const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; } else { - /** - * If only one default variant is found (i.e only dark default or light default but not both), - * treat it like any other variant. - */ const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; } @@ -94,38 +81,17 @@ export class ThemeBuilder { for (const [variable, value] of Object.entries(variables)) { root.style.setProperty(`--${variable}`, value); } + this._injectedVariables = variables; } - removeCSSVariables(variables: string[]) { + removePreviousCSSVariables() { + if (!this._injectedVariables) { + return; + } const root = document.documentElement; - for (const variable of variables) { + for (const variable of Object.keys(this._injectedVariables)) { root.style.removeProperty(`--${variable}`); } - } - - deriveVariables(variables: Record, derivedVariables: string[], isDark: boolean) { - const aliases: any = {}; - const resolvedVariables: any = {}; - const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; - for (const variable of derivedVariables) { - // If this is an alias, store it for processing later - const [alias, value] = variable.split("="); - if (value) { - aliases[alias] = value; - continue; - } - // Resolve derived variables - const matches = variable.match(RE_VARIABLE_VALUE); - if (matches) { - const [, baseVariable, operation, argument] = matches; - const value = variables[baseVariable]; - const resolvedValue = derive(value, operation, argument, isDark); - resolvedVariables[variable] = resolvedValue; - } - } - for (const [alias, variable] of Object.entries(aliases) as any) { - resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable]; - } - return resolvedVariables; + this._injectedVariables = undefined; } } diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 9806dd10..b1e13161 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -60,17 +60,17 @@ export class ThemeLoader { manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); - this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme); - results.forEach(({ body }, i) => { + this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + for (let i = 0; i < results.length; ++i) { + const { body } = results[i]; if (body.extends) { - this._themeBuilder.populateDerivedTheme(body); + await this._themeBuilder.populateDerivedTheme(body); } else { this._populateThemeMap(body, manifestLocations[i], log); } - }); + } Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - console.log("derived theme mapping", this._themeBuilder.themeMapping); }); } @@ -170,6 +170,9 @@ export class ThemeLoader { if (variables) { this._themeBuilder.injectCSSVariables(variables); } + else { + this._themeBuilder.removePreviousCSSVariables(); + } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { this._platform.settingsStorage.setString("theme-variant", themeVariant); From a8cab98666002d0b8a2b80ef49842edd618a30d7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 16:07:52 +0530 Subject: [PATCH 068/354] Add mroe missing types --- src/platform/types/theme.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 9a984277..f57432eb 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -42,6 +42,12 @@ export type ThemeManifest = Partial<{ "runtime-asset": string; // Array of derived-variables "derived-variables": Array; + /** + * Mapping from icon variable to location of icon in build output with query parameters + * indicating how it should be colored for this particular theme. + * eg: "icon-url-1": "element-logo.86bc8565.svg?primary=accent-color" + */ + icon: Record; }; values: { /** @@ -60,6 +66,8 @@ type Variant = Partial<{ default: boolean; // A user-facing string that is the name for this variant. name: string; + // A boolean indicating whether this is a dark theme or not + dark: boolean; /** * Mapping from css variable to its value. * eg: {"background-color-primary": "#21262b", ...} From f4404578759b8e7bb197c9a9f6e5dade3cc580b5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 16:08:02 +0530 Subject: [PATCH 069/354] Use ThemeManifest type where possible --- src/platform/web/ThemeBuilder.ts | 28 +++++++++++++++++++++------- src/platform/web/ThemeLoader.ts | 12 ++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index f05a24b1..b093deb3 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -18,10 +18,10 @@ import type {Platform} from "./Platform.js"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; +import {ThemeManifest} from "../types/theme"; export class ThemeBuilder { - // todo: replace any with manifest type when PR is merged - private _idToManifest: Map; + private _idToManifest: Map; private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; @@ -34,11 +34,8 @@ export class ThemeBuilder { } async populateDerivedTheme(manifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); - const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; - const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; - const derivedVariables = baseManifest.source?.["derived-variables"]; - const icons = baseManifest.source?.["icon"]; + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends)!; + const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { @@ -72,6 +69,23 @@ export class ThemeBuilder { } } + private _getsourceData(manifest: ThemeManifest, location: string) { + const runtimeCSSLocation = manifest.source?.["runtime-asset"]; + if (!runtimeCSSLocation) { + throw new Error(`Run-time asset not found in source section for theme at ${location}`); + } + const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; + const derivedVariables = manifest.source?.["derived-variables"]; + if (!derivedVariables) { + throw new Error(`Derived variables not found in source section for theme at ${location}`); + } + const icons = manifest.source?.["icon"]; + if (!icons) { + throw new Error(`Icon mapping not found in source section for theme at ${location}`); + } + return { cssLocation, derivedVariables, icons }; + } + get themeMapping() { return this._themeMapping; } diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index b1e13161..7deeee1b 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -15,6 +15,7 @@ limitations under the License. */ import type {ILogItem} from "../../logging/types"; +import { ThemeManifest } from "../types/theme"; import type {Platform} from "./Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; @@ -74,7 +75,7 @@ export class ThemeLoader { }); } - private _populateThemeMap(manifest, manifestLocation: string, log: ILogItem) { + private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { log.wrap("populateThemeMap", (l) => { /* After build has finished, the source section of each theme manifest @@ -83,6 +84,9 @@ export class ThemeLoader { */ const builtAssets: Record = manifest.source?.["built-assets"]; const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest at ${manifestLocation}`); + } let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (let [themeId, cssLocation] of Object.entries(builtAssets)) { try { @@ -96,7 +100,11 @@ export class ThemeLoader { continue; } const variant = themeId.match(/.+-(.+)/)?.[1]; - const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; + const variantDetails = manifest.values?.variants[variant!]; + if (!variantDetails) { + throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); + } + const { name: variantName, default: isDefault, dark } = variantDetails; const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { /** From f15e23762ad2567f19f9ebda2acc2d65e1cdf5e3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:34:56 +0530 Subject: [PATCH 070/354] Add more missing keys to type --- src/platform/types/theme.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index f57432eb..9dd969de 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -22,6 +22,13 @@ export type ThemeManifest = Partial<{ version: number; // A user-facing string that is the name for this theme-collection. name: string; + // An identifier for this theme + id: string; + /** + * Id of the theme that this theme derives from. + * Only present for derived/runtime themes. + */ + extends: string; /** * This is added to the manifest during the build process and includes data * that is needed to load themes at runtime. From 80fb9536885b6c8abb00c959c63507cf8d2c4a55 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:35:29 +0530 Subject: [PATCH 071/354] Don't fail on erros; expect the code to throw! --- src/platform/web/ThemeLoader.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 7deeee1b..ec62b2c2 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -64,11 +64,16 @@ export class ThemeLoader { this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); for (let i = 0; i < results.length; ++i) { const { body } = results[i]; - if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body); + try { + if (body.extends) { + await this._themeBuilder.populateDerivedTheme(body); + } + else { + this._populateThemeMap(body, manifestLocations[i], log); + } } - else { - this._populateThemeMap(body, manifestLocations[i], log); + catch(e) { + console.error(e); } } Object.assign(this._themeMapping, this._themeBuilder.themeMapping); From 043cc9f12c2a95d97926bc612c21900a2ab31260 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:36:36 +0530 Subject: [PATCH 072/354] Use ThemeManifest type --- src/platform/web/ThemeBuilder.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b093deb3..b6f75be4 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -33,12 +33,15 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends)!; + async populateDerivedTheme(manifest: ThemeManifest) { + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends!)!; const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest!`); + } let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { + for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { try { const themeId = `${manifest.id}-${variant}`; const { name: variantName, default: isDefault, dark, variables } = variantDetails; From da0a918c180bb7aebbebb4efa568d713bd456883 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:44:21 +0530 Subject: [PATCH 073/354] This code should only run once --- src/platform/web/ThemeLoader.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index ec62b2c2..069be84d 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -76,6 +76,17 @@ export class ThemeLoader { console.error(e); } } + //Add the default-theme as an additional option to the mapping + const defaultThemeId = this.getDefaultTheme(); + if (defaultThemeId) { + const themeDetails = this._findThemeDetailsFromId(defaultThemeId); + if (themeDetails) { + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; + } + } + log.log({ l: "Default Theme", theme: defaultThemeId}); + log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); + log.log({ l: "Result", themeMapping: this._themeMapping }); Object.assign(this._themeMapping, this._themeBuilder.themeMapping); }); } @@ -150,17 +161,6 @@ export class ThemeLoader { const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; } - //Add the default-theme as an additional option to the mapping - const defaultThemeId = this.getDefaultTheme(); - if (defaultThemeId) { - const themeDetails = this._findThemeDetailsFromId(defaultThemeId); - if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; - } - } - l.log({ l: "Default Theme", theme: defaultThemeId}); - l.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); - l.log({ l: "Result", themeMapping: this._themeMapping }); }); } From ce5db477085ae05c950fea1c6fb26a1fd0a46a47 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 19:42:26 +0530 Subject: [PATCH 074/354] Support using derived theme as default theme --- src/platform/web/ThemeLoader.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 069be84d..c04d8e8c 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -25,16 +25,14 @@ type NormalVariant = { variables?: any; }; +type Variant = NormalVariant & { + variantName: string; +}; + type DefaultVariant = { - dark: NormalVariant & { - variantName: string; - }; - light: NormalVariant & { - variantName: string; - }; - default: NormalVariant & { - variantName: string; - }; + dark: Variant; + light: Variant; + default: Variant; } export type ThemeInformation = NormalVariant | DefaultVariant; @@ -76,18 +74,22 @@ export class ThemeLoader { console.error(e); } } + Object.assign(this._themeMapping, this._themeBuilder.themeMapping); //Add the default-theme as an additional option to the mapping const defaultThemeId = this.getDefaultTheme(); if (defaultThemeId) { const themeDetails = this._findThemeDetailsFromId(defaultThemeId); if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; + const variables = themeDetails.themeData.variables; + if (variables) { + this._themeMapping["Default"].variables = variables; + } } } log.log({ l: "Default Theme", theme: defaultThemeId}); log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); log.log({ l: "Result", themeMapping: this._themeMapping }); - Object.assign(this._themeMapping, this._themeBuilder.themeMapping); }); } @@ -222,16 +224,16 @@ export class ThemeLoader { } } - private _findThemeDetailsFromId(themeId: string): {themeName: string, cssLocation: string, variant?: string} | undefined { + private _findThemeDetailsFromId(themeId: string): {themeName: string, themeData: Partial} | undefined { for (const [themeName, themeData] of Object.entries(this._themeMapping)) { if ("id" in themeData && themeData.id === themeId) { - return { themeName, cssLocation: themeData.cssLocation }; + return { themeName, themeData }; } else if ("light" in themeData && themeData.light?.id === themeId) { - return { themeName, cssLocation: themeData.light.cssLocation, variant: "light" }; + return { themeName, themeData: themeData.light }; } else if ("dark" in themeData && themeData.dark?.id === themeId) { - return { themeName, cssLocation: themeData.dark.cssLocation, variant: "dark" }; + return { themeName, themeData: themeData.dark }; } } } From 9e2d355573cf5d580731ef47005225836bec81c7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 20:58:58 +0530 Subject: [PATCH 075/354] Add logging --- src/platform/web/ThemeBuilder.ts | 101 ++++++++++++++++--------------- src/platform/web/ThemeLoader.ts | 3 +- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b6f75be4..8035fe84 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -19,6 +19,7 @@ import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; import {ThemeManifest} from "../types/theme"; +import {ILogItem} from "../../logging/types"; export class ThemeBuilder { private _idToManifest: Map; @@ -33,60 +34,64 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends!)!; - const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); - const themeName = manifest.name; - if (!themeName) { - throw new Error(`Theme name not found in manifest!`); - } - let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { - try { - const themeId = `${manifest.id}-${variant}`; - const { name: variantName, default: isDefault, dark, variables } = variantDetails; - const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); - Object.assign(variables, resolvedVariables); - const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); - Object.assign(variables, resolvedVariables, iconVariables); - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem) { + await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { + const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; + const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); + const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest!`); + } + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { + try { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); + Object.assign(variables, resolvedVariables); + const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + Object.assign(variables, resolvedVariables, iconVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + continue; + } + this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; + } + catch (e) { + console.error(e); continue; } - this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; } - catch (e) { - console.error(e); - continue; + if (defaultDarkVariant.id && defaultLightVariant.id) { + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; } - } - if (defaultDarkVariant.id && defaultLightVariant.id) { - const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; - } - else { - const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; - } + else { + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + }); } - private _getsourceData(manifest: ThemeManifest, location: string) { - const runtimeCSSLocation = manifest.source?.["runtime-asset"]; - if (!runtimeCSSLocation) { - throw new Error(`Run-time asset not found in source section for theme at ${location}`); - } - const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; - const derivedVariables = manifest.source?.["derived-variables"]; - if (!derivedVariables) { - throw new Error(`Derived variables not found in source section for theme at ${location}`); - } - const icons = manifest.source?.["icon"]; - if (!icons) { - throw new Error(`Icon mapping not found in source section for theme at ${location}`); - } - return { cssLocation, derivedVariables, icons }; + private _getSourceData(manifest: ThemeManifest, location: string, log:ILogItem) { + return log.wrap("getSourceData", () => { + const runtimeCSSLocation = manifest.source?.["runtime-asset"]; + if (!runtimeCSSLocation) { + throw new Error(`Run-time asset not found in source section for theme at ${location}`); + } + const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; + const derivedVariables = manifest.source?.["derived-variables"]; + if (!derivedVariables) { + throw new Error(`Derived variables not found in source section for theme at ${location}`); + } + const icons = manifest.source?.["icon"]; + if (!icons) { + throw new Error(`Icon mapping not found in source section for theme at ${location}`); + } + return { cssLocation, derivedVariables, icons }; + }); } get themeMapping() { diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index c04d8e8c..da5f9658 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -64,7 +64,7 @@ export class ThemeLoader { const { body } = results[i]; try { if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body); + await this._themeBuilder.populateDerivedTheme(body, log); } else { this._populateThemeMap(body, manifestLocations[i], log); @@ -183,6 +183,7 @@ export class ThemeLoader { } this._platform.replaceStylesheet(cssLocation); if (variables) { + log?.log({l: "Derived Theme", variables}); this._themeBuilder.injectCSSVariables(variables); } else { From 9bdf9c500b60d5a1697f07223845228626012b31 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 21:07:04 +0530 Subject: [PATCH 076/354] Add return types --- src/platform/web/ThemeBuilder.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 8035fe84..c87e68fb 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -34,7 +34,7 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem) { + async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); @@ -75,7 +75,8 @@ export class ThemeBuilder { }); } - private _getSourceData(manifest: ThemeManifest, location: string, log:ILogItem) { + private _getSourceData(manifest: ThemeManifest, location: string, log: ILogItem) + : { cssLocation: string, derivedVariables: string[], icons: Record} { return log.wrap("getSourceData", () => { const runtimeCSSLocation = manifest.source?.["runtime-asset"]; if (!runtimeCSSLocation) { @@ -94,11 +95,11 @@ export class ThemeBuilder { }); } - get themeMapping() { + get themeMapping(): Record { return this._themeMapping; } - injectCSSVariables(variables: Record) { + injectCSSVariables(variables: Record): void { const root = document.documentElement; for (const [variable, value] of Object.entries(variables)) { root.style.setProperty(`--${variable}`, value); @@ -106,7 +107,7 @@ export class ThemeBuilder { this._injectedVariables = variables; } - removePreviousCSSVariables() { + removePreviousCSSVariables(): void { if (!this._injectedVariables) { return; } From b29287c47ee60323b5414bdf60cf696100ed5f41 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 22:29:36 +0530 Subject: [PATCH 077/354] await in loop --> Promise.all() --- src/platform/web/ThemeBuilder.ts | 2 +- src/platform/web/ThemeLoader.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index c87e68fb..85d1e336 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -35,7 +35,7 @@ export class ThemeBuilder { } async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { - await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { + await log.wrap("ThemeBuilder.populateThemeMap", async () => { const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); const themeName = manifest.name; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index da5f9658..b899ab5e 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -60,11 +60,13 @@ export class ThemeLoader { ); results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; try { if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body, log); + const promise = this._themeBuilder.populateDerivedTheme(body, log); + runtimeThemePromises.push(promise); } else { this._populateThemeMap(body, manifestLocations[i], log); @@ -74,8 +76,9 @@ export class ThemeLoader { console.error(e); } } + await Promise.all(runtimeThemePromises); Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - //Add the default-theme as an additional option to the mapping + // Add the default-theme as an additional option to the mapping const defaultThemeId = this.getDefaultTheme(); if (defaultThemeId) { const themeDetails = this._findThemeDetailsFromId(defaultThemeId); @@ -94,7 +97,7 @@ export class ThemeLoader { } private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("populateThemeMap", (l) => { + log.wrap("populateThemeMap", () => { /* After build has finished, the source section of each theme manifest contains `built-assets` which is a mapping from the theme-id to From dece42dce3db0dceff48ad1e7810953554622472 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 18 Jul 2022 14:55:13 +0530 Subject: [PATCH 078/354] Do not store all the manifests in memory --- src/platform/web/ThemeBuilder.ts | 13 +++++-------- src/platform/web/ThemeLoader.ts | 18 +++++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 85d1e336..2ff4108a 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -15,29 +15,26 @@ limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; import type {Platform} from "./Platform.js"; +import type {ThemeManifest} from "../types/theme"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; -import {ThemeManifest} from "../types/theme"; import {ILogItem} from "../../logging/types"; export class ThemeBuilder { - private _idToManifest: Map; private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; private _injectedVariables?: Record; - constructor(platform: Platform, manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { - this._idToManifest = manifestMap; + constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) { this._preferredColorScheme = preferredColorScheme; this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { + async populateDerivedTheme(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async () => { - const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; - const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); + const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; if (!themeName) { throw new Error(`Theme name not found in manifest!`); @@ -49,7 +46,7 @@ export class ThemeBuilder { const { name: variantName, default: isDefault, dark, variables } = variantDetails; const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); Object.assign(variables, resolvedVariables); - const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + const iconVariables = await new IconColorizer(this._platform, icons, variables, baseManifestLocation).toVariables(); Object.assign(variables, resolvedVariables, iconVariables); const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index b899ab5e..81114a54 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {ILogItem} from "../../logging/types"; -import { ThemeManifest } from "../types/theme"; +import type {ThemeManifest} from "../types/theme"; import type {Platform} from "./Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; @@ -52,20 +52,24 @@ export class ThemeLoader { } async init(manifestLocations: string[], log?: ILogItem): Promise { - const idToManifest = new Map(); await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { this._themeMapping = {}; const results = await Promise.all( - manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) + manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); - this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + this._themeBuilder = new ThemeBuilder(this._platform, this.preferredColorScheme); const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; try { if (body.extends) { - const promise = this._themeBuilder.populateDerivedTheme(body, log); + const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends); + if (indexOfBaseManifest === -1) { + throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`); + } + const {body: baseManifest} = results[indexOfBaseManifest]; + const baseManifestLocation = manifestLocations[indexOfBaseManifest]; + const promise = this._themeBuilder.populateDerivedTheme(body, baseManifest, baseManifestLocation, log); runtimeThemePromises.push(promise); } else { @@ -97,7 +101,7 @@ export class ThemeLoader { } private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("populateThemeMap", () => { + log.wrap("ThemeLoader.populateThemeMap", () => { /* After build has finished, the source section of each theme manifest contains `built-assets` which is a mapping from the theme-id to From 081de5afa81f5eb39b8f997c5e45febc19262677 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 14:52:53 +0530 Subject: [PATCH 079/354] .js --> .mjs --- scripts/postcss/svg-builder.mjs | 2 +- scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} | 0 src/platform/web/IconColorizer.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index 1bbc4010..ec94f1e5 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "./svg-colorizer.js"; +import {getColoredSvgString} from "./svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.mjs similarity index 100% rename from scripts/postcss/svg-colorizer.js rename to scripts/postcss/svg-colorizer.mjs diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/IconColorizer.ts index 81644603..8baf63b0 100644 --- a/src/platform/web/IconColorizer.ts +++ b/src/platform/web/IconColorizer.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {Platform} from "./Platform.js"; -import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.js"; +import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { From 07db5450b7be6e22c0db3d1f9b0400fa02424a03 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 14:55:42 +0530 Subject: [PATCH 080/354] Aliases can also be derived --- src/platform/web/DerivedVariables.ts | 75 ++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/DerivedVariables.ts index 4e3a8b46..3ace908c 100644 --- a/src/platform/web/DerivedVariables.ts +++ b/src/platform/web/DerivedVariables.ts @@ -19,6 +19,8 @@ export class DerivedVariables { private _baseVariables: Record; private _variablesToDerive: string[] private _isDark: boolean + private _aliases: Record = {}; + private _derivedAliases: string[] = []; constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) { this._baseVariables = baseVariables; @@ -27,28 +29,73 @@ export class DerivedVariables { } toVariables(): Record { - const aliases: any = {}; const resolvedVariables: any = {}; - const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + this._detectAliases(); + for (const variable of this._variablesToDerive) { + const resolvedValue = this._derive(variable); + if (resolvedValue) { + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(this._aliases) as any) { + resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + } + for (const variable of this._derivedAliases) { + const resolvedValue = this._deriveAlias(variable, resolvedVariables); + if (resolvedValue) { + resolvedVariables[variable] = resolvedValue; + } + } + return resolvedVariables; + } + + private _detectAliases() { + const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { // If this is an alias, store it for processing later const [alias, value] = variable.split("="); if (value) { - aliases[alias] = value; - continue; + this._aliases[alias] = value; } - // Resolve derived variables - const matches = variable.match(RE_VARIABLE_VALUE); - if (matches) { - const [, baseVariable, operation, argument] = matches; - const value = this._baseVariables[baseVariable]; - const resolvedValue = derive(value, operation, argument, this._isDark); - resolvedVariables[variable] = resolvedValue; + else { + newVariablesToDerive.push(variable); } } - for (const [alias, variable] of Object.entries(aliases) as any) { - resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + this._variablesToDerive = newVariablesToDerive; + } + + private _derive(variable: string): string | undefined { + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = this._baseVariables[baseVariable]; + if (!value ) { + if (this._aliases[baseVariable]) { + this._derivedAliases.push(variable); + return; + } + else { + throw new Error(`Cannot find value for base variable "${baseVariable}"!`); + } + } + const resolvedValue = derive(value, operation, argument, this._isDark); + return resolvedValue; + } + } + + + private _deriveAlias(variable: string, resolvedVariables: Record): string | undefined { + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = resolvedVariables[baseVariable]; + if (!value ) { + throw new Error(`Cannot find value for alias "${baseVariable}" when trying to derive ${variable}!`); + } + const resolvedValue = derive(value, operation, argument, this._isDark); + return resolvedValue; } - return resolvedVariables; } } From 7a1591e0ce87c8a1aae63cc89f615ee1d0bce150 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 15:05:07 +0530 Subject: [PATCH 081/354] Move code --- scripts/postcss/svg-builder.mjs | 2 +- src/platform/web/Platform.js | 2 +- src/platform/web/{ => theming}/DerivedVariables.ts | 4 ++-- src/platform/web/{ => theming}/IconColorizer.ts | 4 ++-- src/platform/web/{ => theming}/ThemeBuilder.ts | 6 +++--- src/platform/web/{ => theming}/ThemeLoader.ts | 0 .../postcss => src/platform/web/theming/actions}/color.mjs | 0 .../platform/web/theming/actions}/svg-colorizer.mjs | 0 vite.common-config.js | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) rename src/platform/web/{ => theming}/DerivedVariables.ts (98%) rename src/platform/web/{ => theming}/IconColorizer.ts (96%) rename src/platform/web/{ => theming}/ThemeBuilder.ts (97%) rename src/platform/web/{ => theming}/ThemeLoader.ts (100%) rename {scripts/postcss => src/platform/web/theming/actions}/color.mjs (100%) rename {scripts/postcss => src/platform/web/theming/actions}/svg-colorizer.mjs (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index ec94f1e5..6ef04860 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "./svg-colorizer.mjs"; +import {getColoredSvgString} from "../../src/platform/web/theming/actions/svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index c2eef17e..15923a86 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -38,7 +38,7 @@ import {downloadInIframe} from "./dom/download.js"; import {Disposables} from "../../utils/Disposables"; import {parseHTML} from "./parsehtml.js"; import {handleAvatarError} from "./ui/avatar"; -import {ThemeLoader} from "./ThemeLoader"; +import {ThemeLoader} from "./theming/ThemeLoader"; function addScript(src) { return new Promise(function (resolve, reject) { diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts similarity index 98% rename from src/platform/web/DerivedVariables.ts rename to src/platform/web/theming/DerivedVariables.ts index 3ace908c..be48a989 100644 --- a/src/platform/web/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -1,6 +1,6 @@ /* Copyright 2020 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 @@ -13,7 +13,7 @@ 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 {derive} from "../../../scripts/postcss/color.mjs"; +import {derive} from "./actions/color.mjs"; export class DerivedVariables { private _baseVariables: Record; diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/theming/IconColorizer.ts similarity index 96% rename from src/platform/web/IconColorizer.ts rename to src/platform/web/theming/IconColorizer.ts index 8baf63b0..82a9fd45 100644 --- a/src/platform/web/IconColorizer.ts +++ b/src/platform/web/theming/IconColorizer.ts @@ -13,8 +13,8 @@ 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 type {Platform} from "./Platform.js"; -import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.mjs"; +import type {Platform} from "../Platform.js"; +import {getColoredSvgString} from "./actions/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/theming/ThemeBuilder.ts similarity index 97% rename from src/platform/web/ThemeBuilder.ts rename to src/platform/web/theming/ThemeBuilder.ts index 2ff4108a..d086b6fc 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/theming/ThemeBuilder.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; -import type {Platform} from "./Platform.js"; -import type {ThemeManifest} from "../types/theme"; +import type {Platform} from "../Platform.js"; +import type {ThemeManifest} from "../../types/theme"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; -import {ILogItem} from "../../logging/types"; +import {ILogItem} from "../../../logging/types"; export class ThemeBuilder { private _themeMapping: Record = {}; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts similarity index 100% rename from src/platform/web/ThemeLoader.ts rename to src/platform/web/theming/ThemeLoader.ts diff --git a/scripts/postcss/color.mjs b/src/platform/web/theming/actions/color.mjs similarity index 100% rename from scripts/postcss/color.mjs rename to src/platform/web/theming/actions/color.mjs diff --git a/scripts/postcss/svg-colorizer.mjs b/src/platform/web/theming/actions/svg-colorizer.mjs similarity index 100% rename from scripts/postcss/svg-colorizer.mjs rename to src/platform/web/theming/actions/svg-colorizer.mjs diff --git a/vite.common-config.js b/vite.common-config.js index bcf17115..de464941 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -9,7 +9,7 @@ const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; -import {derive} from "./scripts/postcss/color.mjs"; +import {derive} from "./src/platform/web/theming/actions/color.mjs"; const commonOptions = { logLevel: "warn", From 83b5d3b68e6ee7e497bbdf0e1c407f2be03716d8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 15:11:05 +0530 Subject: [PATCH 082/354] Change directory name --- scripts/postcss/svg-builder.mjs | 2 +- src/platform/web/theming/DerivedVariables.ts | 2 +- src/platform/web/theming/IconColorizer.ts | 2 +- src/platform/web/theming/{actions => shared}/color.mjs | 0 src/platform/web/theming/{actions => shared}/svg-colorizer.mjs | 0 vite.common-config.js | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename src/platform/web/theming/{actions => shared}/color.mjs (100%) rename src/platform/web/theming/{actions => shared}/svg-colorizer.mjs (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index 6ef04860..3efff4ae 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "../../src/platform/web/theming/actions/svg-colorizer.mjs"; +import {getColoredSvgString} from "../../src/platform/web/theming/shared/svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index be48a989..619bab94 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -13,7 +13,7 @@ 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 {derive} from "./actions/color.mjs"; +import {derive} from "./shared/color.mjs"; export class DerivedVariables { private _baseVariables: Record; diff --git a/src/platform/web/theming/IconColorizer.ts b/src/platform/web/theming/IconColorizer.ts index 82a9fd45..e02c0971 100644 --- a/src/platform/web/theming/IconColorizer.ts +++ b/src/platform/web/theming/IconColorizer.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {Platform} from "../Platform.js"; -import {getColoredSvgString} from "./actions/svg-colorizer.mjs"; +import {getColoredSvgString} from "./shared/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { diff --git a/src/platform/web/theming/actions/color.mjs b/src/platform/web/theming/shared/color.mjs similarity index 100% rename from src/platform/web/theming/actions/color.mjs rename to src/platform/web/theming/shared/color.mjs diff --git a/src/platform/web/theming/actions/svg-colorizer.mjs b/src/platform/web/theming/shared/svg-colorizer.mjs similarity index 100% rename from src/platform/web/theming/actions/svg-colorizer.mjs rename to src/platform/web/theming/shared/svg-colorizer.mjs diff --git a/vite.common-config.js b/vite.common-config.js index de464941..2fa09d46 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -9,7 +9,7 @@ const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; -import {derive} from "./src/platform/web/theming/actions/color.mjs"; +import {derive} from "./src/platform/web/theming/shared/color.mjs"; const commonOptions = { logLevel: "warn", From e1ee2586306ef84c09f91e4770da78921f05c236 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 16:00:58 +0530 Subject: [PATCH 083/354] Change path --- src/platform/web/theming/ThemeLoader.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 81114a54..1c066577 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {ILogItem} from "../../logging/types"; -import type {ThemeManifest} from "../types/theme"; -import type {Platform} from "./Platform.js"; +import type {ILogItem} from "../../../logging/types"; +import type {ThemeManifest} from "../../types/theme"; +import type {Platform} from "../Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; type NormalVariant = { From ecb3a66dfcb9c23ca25fde50582c00f7d6052483 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 17:33:06 +0530 Subject: [PATCH 084/354] WIP --- src/platform/web/theming/ThemeLoader.ts | 169 ++++++------------ .../web/theming/parsers/BuiltThemeParser.ts | 106 +++++++++++ .../RuntimeThemeParser.ts} | 37 +--- src/platform/web/theming/parsers/types.ts | 38 ++++ 4 files changed, 203 insertions(+), 147 deletions(-) create mode 100644 src/platform/web/theming/parsers/BuiltThemeParser.ts rename src/platform/web/theming/{ThemeBuilder.ts => parsers/RuntimeThemeParser.ts} (78%) create mode 100644 src/platform/web/theming/parsers/types.ts diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 1c066577..10717975 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -15,37 +15,16 @@ limitations under the License. */ import type {ILogItem} from "../../../logging/types"; -import type {ThemeManifest} from "../../types/theme"; import type {Platform} from "../Platform.js"; -import {ThemeBuilder} from "./ThemeBuilder"; - -type NormalVariant = { - id: string; - cssLocation: string; - variables?: any; -}; - -type Variant = NormalVariant & { - variantName: string; -}; - -type DefaultVariant = { - dark: Variant; - light: Variant; - default: Variant; -} - -export type ThemeInformation = NormalVariant | DefaultVariant; - -export enum ColorSchemePreference { - Dark, - Light -}; +import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser"; +import type {Variant, ThemeInformation} from "./parsers/types"; +import {ColorSchemePreference} from "./parsers/types"; +import { BuiltThemeParser } from "./parsers/BuiltThemeParser"; export class ThemeLoader { private _platform: Platform; private _themeMapping: Record; - private _themeBuilder: ThemeBuilder; + private _injectedVariables?: Record; constructor(platform: Platform) { this._platform = platform; @@ -53,11 +32,11 @@ export class ThemeLoader { async init(manifestLocations: string[], log?: ILogItem): Promise { await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { - this._themeMapping = {}; const results = await Promise.all( manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - this._themeBuilder = new ThemeBuilder(this._platform, this.preferredColorScheme); + const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme); + const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme); const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; @@ -69,11 +48,11 @@ export class ThemeLoader { } const {body: baseManifest} = results[indexOfBaseManifest]; const baseManifestLocation = manifestLocations[indexOfBaseManifest]; - const promise = this._themeBuilder.populateDerivedTheme(body, baseManifest, baseManifestLocation, log); + const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log); runtimeThemePromises.push(promise); } else { - this._populateThemeMap(body, manifestLocations[i], log); + builtThemeParser.parse(body, manifestLocations[i], log); } } catch(e) { @@ -81,98 +60,14 @@ export class ThemeLoader { } } await Promise.all(runtimeThemePromises); - Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - // Add the default-theme as an additional option to the mapping - const defaultThemeId = this.getDefaultTheme(); - if (defaultThemeId) { - const themeDetails = this._findThemeDetailsFromId(defaultThemeId); - if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; - const variables = themeDetails.themeData.variables; - if (variables) { - this._themeMapping["Default"].variables = variables; - } - } - } - log.log({ l: "Default Theme", theme: defaultThemeId}); + this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping }; + Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping); + this._addDefaultThemeToMapping(log); log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); log.log({ l: "Result", themeMapping: this._themeMapping }); }); } - private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("ThemeLoader.populateThemeMap", () => { - /* - After build has finished, the source section of each theme manifest - contains `built-assets` which is a mapping from the theme-id to - cssLocation of theme - */ - const builtAssets: Record = manifest.source?.["built-assets"]; - const themeName = manifest.name; - if (!themeName) { - throw new Error(`Theme name not found in manifest at ${manifestLocation}`); - } - let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (let [themeId, cssLocation] of Object.entries(builtAssets)) { - try { - /** - * This cssLocation is relative to the location of the manifest file. - * So we first need to resolve it relative to the root of this hydrogen instance. - */ - cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; - } - catch { - continue; - } - const variant = themeId.match(/.+-(.+)/)?.[1]; - const variantDetails = manifest.values?.variants[variant!]; - if (!variantDetails) { - throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); - } - const { name: variantName, default: isDefault, dark } = variantDetails; - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - /** - * This is a default variant! - * We'll add these to the themeMapping (separately) keyed with just the - * theme-name (i.e "Element" instead of "Element Dark"). - * We need to be able to distinguish them from other variants! - * - * This allows us to render radio-buttons with "dark" and - * "light" options. - */ - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - defaultVariant.variantName = variantName; - defaultVariant.id = themeId - defaultVariant.cssLocation = cssLocation; - continue; - } - // Non-default variants are keyed in themeMapping with "theme_name variant_name" - // eg: "Element Dark" - this._themeMapping[themeDisplayName] = { - cssLocation, - id: themeId - }; - } - if (defaultDarkVariant.id && defaultLightVariant.id) { - /** - * As mentioned above, if there's both a default dark and a default light variant, - * add them to themeMapping separately. - */ - const defaultVariant = this.preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; - } - else { - /** - * If only one default variant is found (i.e only dark default or light default but not both), - * treat it like any other variant. - */ - const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; - } - }); - } - setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => { let cssLocation: string, variables: Record; @@ -191,10 +86,10 @@ export class ThemeLoader { this._platform.replaceStylesheet(cssLocation); if (variables) { log?.log({l: "Derived Theme", variables}); - this._themeBuilder.injectCSSVariables(variables); + this._injectCSSVariables(variables); } else { - this._themeBuilder.removePreviousCSSVariables(); + this._removePreviousCSSVariables(); } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { @@ -206,6 +101,25 @@ export class ThemeLoader { }); } + private _injectCSSVariables(variables: Record): void { + const root = document.documentElement; + for (const [variable, value] of Object.entries(variables)) { + root.style.setProperty(`--${variable}`, value); + } + this._injectedVariables = variables; + } + + private _removePreviousCSSVariables(): void { + if (!this._injectedVariables) { + return; + } + const root = document.documentElement; + for (const variable of Object.keys(this._injectedVariables)) { + root.style.removeProperty(`--${variable}`); + } + this._injectedVariables = undefined; + } + /** Maps theme display name to theme information */ get themeMapping(): Record { return this._themeMapping; @@ -246,6 +160,23 @@ export class ThemeLoader { } } + private _addDefaultThemeToMapping(log: ILogItem) { + log.wrap("addDefaultThemeToMapping", l => { + const defaultThemeId = this.getDefaultTheme(); + if (defaultThemeId) { + const themeDetails = this._findThemeDetailsFromId(defaultThemeId); + if (themeDetails) { + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; + const variables = themeDetails.themeData.variables; + if (variables) { + this._themeMapping["Default"].variables = variables; + } + } + } + l.log({ l: "Default Theme", theme: defaultThemeId}); + }); + } + get preferredColorScheme(): ColorSchemePreference | undefined { if (window.matchMedia("(prefers-color-scheme: dark)").matches) { return ColorSchemePreference.Dark; diff --git a/src/platform/web/theming/parsers/BuiltThemeParser.ts b/src/platform/web/theming/parsers/BuiltThemeParser.ts new file mode 100644 index 00000000..fbafadb8 --- /dev/null +++ b/src/platform/web/theming/parsers/BuiltThemeParser.ts @@ -0,0 +1,106 @@ +/* +Copyright 2020 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 type {ThemeInformation} from "./types"; +import type {ThemeManifest} from "../../../types/theme"; +import type {ILogItem} from "../../../../logging/types"; +import {ColorSchemePreference} from "./types"; + +export class BuiltThemeParser { + private _themeMapping: Record = {}; + private _preferredColorScheme?: ColorSchemePreference; + + constructor(preferredColorScheme?: ColorSchemePreference) { + this._preferredColorScheme = preferredColorScheme; + } + + parse(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { + log.wrap("BuiltThemeParser.parse", () => { + /* + After build has finished, the source section of each theme manifest + contains `built-assets` which is a mapping from the theme-id to + cssLocation of theme + */ + const builtAssets: Record = manifest.source?.["built-assets"]; + const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest at ${manifestLocation}`); + } + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (let [themeId, cssLocation] of Object.entries(builtAssets)) { + try { + /** + * This cssLocation is relative to the location of the manifest file. + * So we first need to resolve it relative to the root of this hydrogen instance. + */ + cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; + } + catch { + continue; + } + const variant = themeId.match(/.+-(.+)/)?.[1]; + const variantDetails = manifest.values?.variants[variant!]; + if (!variantDetails) { + throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); + } + const { name: variantName, default: isDefault, dark } = variantDetails; + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + /** + * This is a default variant! + * We'll add these to the themeMapping (separately) keyed with just the + * theme-name (i.e "Element" instead of "Element Dark"). + * We need to be able to distinguish them from other variants! + * + * This allows us to render radio-buttons with "dark" and + * "light" options. + */ + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + defaultVariant.variantName = variantName; + defaultVariant.id = themeId + defaultVariant.cssLocation = cssLocation; + continue; + } + // Non-default variants are keyed in themeMapping with "theme_name variant_name" + // eg: "Element Dark" + this._themeMapping[themeDisplayName] = { + cssLocation, + id: themeId + }; + } + if (defaultDarkVariant.id && defaultLightVariant.id) { + /** + * As mentioned above, if there's both a default dark and a default light variant, + * add them to themeMapping separately. + */ + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; + } + else { + /** + * If only one default variant is found (i.e only dark default or light default but not both), + * treat it like any other variant. + */ + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + }); + } + + get themeMapping(): Record { + return this._themeMapping; + } +} diff --git a/src/platform/web/theming/ThemeBuilder.ts b/src/platform/web/theming/parsers/RuntimeThemeParser.ts similarity index 78% rename from src/platform/web/theming/ThemeBuilder.ts rename to src/platform/web/theming/parsers/RuntimeThemeParser.ts index d086b6fc..2ab970b6 100644 --- a/src/platform/web/theming/ThemeBuilder.ts +++ b/src/platform/web/theming/parsers/RuntimeThemeParser.ts @@ -13,26 +13,25 @@ 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 type {ThemeInformation} from "./ThemeLoader"; -import type {Platform} from "../Platform.js"; -import type {ThemeManifest} from "../../types/theme"; -import {ColorSchemePreference} from "./ThemeLoader"; -import {IconColorizer} from "./IconColorizer"; -import {DerivedVariables} from "./DerivedVariables"; -import {ILogItem} from "../../../logging/types"; +import type {ThemeInformation} from "./types"; +import type {Platform} from "../../Platform.js"; +import type {ThemeManifest} from "../../../types/theme"; +import {ColorSchemePreference} from "./types"; +import {IconColorizer} from "../IconColorizer"; +import {DerivedVariables} from "../DerivedVariables"; +import {ILogItem} from "../../../../logging/types"; -export class ThemeBuilder { +export class RuntimeThemeParser { private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; - private _injectedVariables?: Record; constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) { this._preferredColorScheme = preferredColorScheme; this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { + async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async () => { const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; @@ -96,22 +95,4 @@ export class ThemeBuilder { return this._themeMapping; } - injectCSSVariables(variables: Record): void { - const root = document.documentElement; - for (const [variable, value] of Object.entries(variables)) { - root.style.setProperty(`--${variable}`, value); - } - this._injectedVariables = variables; - } - - removePreviousCSSVariables(): void { - if (!this._injectedVariables) { - return; - } - const root = document.documentElement; - for (const variable of Object.keys(this._injectedVariables)) { - root.style.removeProperty(`--${variable}`); - } - this._injectedVariables = undefined; - } } diff --git a/src/platform/web/theming/parsers/types.ts b/src/platform/web/theming/parsers/types.ts new file mode 100644 index 00000000..b357cf2c --- /dev/null +++ b/src/platform/web/theming/parsers/types.ts @@ -0,0 +1,38 @@ +/* +Copyright 2020 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. +*/ + +export type NormalVariant = { + id: string; + cssLocation: string; + variables?: any; +}; + +export type Variant = NormalVariant & { + variantName: string; +}; + +export type DefaultVariant = { + dark: Variant; + light: Variant; + default: Variant; +} + +export type ThemeInformation = NormalVariant | DefaultVariant; + +export enum ColorSchemePreference { + Dark, + Light +}; From 994667205f851074c71063e5e086c1a48020a334 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 19:38:36 +0530 Subject: [PATCH 085/354] Remove change --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 8f591a6e..f46cc7eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "noEmit": true, "target": "ES2020", "module": "ES2020", - "lib": ["ES2021", "DOM"], "moduleResolution": "node", "esModuleInterop": true }, From de02456641404c1e4cbb1987f1d71169c1f7c58e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 19:46:36 +0530 Subject: [PATCH 086/354] Add explaining comment --- scripts/build-plugins/rollup-plugin-build-themes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 72053a1f..f44406c6 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -343,6 +343,7 @@ module.exports = function buildThemes(options) { const assetHashedName = this.getFileName(ref); nameToAssetHashedLocation[name] = assetHashedName; } + // Update icon section in output manifest with paths to the icon in build output for (const [variable, location] of Object.entries(icon)) { const [locationWithoutQueryParameters, queryParameters] = location.split("?"); const name = path.basename(locationWithoutQueryParameters); From 7ac2c7c7faff035248c1e8d6fdc5ce771b3d3784 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:06:55 +0530 Subject: [PATCH 087/354] Get tests to work --- src/platform/web/theming/shared/color.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/theming/shared/color.mjs b/src/platform/web/theming/shared/color.mjs index bd2ea3ea..e777bfac 100644 --- a/src/platform/web/theming/shared/color.mjs +++ b/src/platform/web/theming/shared/color.mjs @@ -13,7 +13,8 @@ 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 {offColor} from 'off-color'; +import pkg from 'off-color'; +const offColor = pkg.offColor; export function derive(value, operation, argument, isDark) { const argumentAsNumber = parseInt(argument); From 8aa96e80310eb431f5cca14affaaf1db38f280c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:19:22 +0530 Subject: [PATCH 088/354] Update log label --- src/platform/web/theming/parsers/RuntimeThemeParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/theming/parsers/RuntimeThemeParser.ts b/src/platform/web/theming/parsers/RuntimeThemeParser.ts index 2ab970b6..9471740a 100644 --- a/src/platform/web/theming/parsers/RuntimeThemeParser.ts +++ b/src/platform/web/theming/parsers/RuntimeThemeParser.ts @@ -32,7 +32,7 @@ export class RuntimeThemeParser { } async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { - await log.wrap("ThemeBuilder.populateThemeMap", async () => { + await log.wrap("RuntimeThemeParser.parse", async () => { const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; if (!themeName) { From 612b878793ca608a408b70ed3faa34dfe4a911d2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:21:35 +0530 Subject: [PATCH 089/354] Update theme name --- theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.json b/theme.json index f5fbf3ed..ea7492d4 100644 --- a/theme.json +++ b/theme.json @@ -34,7 +34,7 @@ } }, "red": { - "name": "Red", + "name": "Gruvbox", "variables": { "background-color-primary": "#282828", "background-color-secondary": "#3c3836", From 313e65e00cbc7e2323e87ffbb32081ed21f52ed4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 12:30:41 +0530 Subject: [PATCH 090/354] Write tests --- src/platform/web/theming/DerivedVariables.ts | 32 +++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index 619bab94..e0450e94 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -52,7 +52,6 @@ export class DerivedVariables { private _detectAliases() { const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { - // If this is an alias, store it for processing later const [alias, value] = variable.split("="); if (value) { this._aliases[alias] = value; @@ -99,3 +98,34 @@ export class DerivedVariables { } } } + +import pkg from "off-color"; +const {offColor} = pkg; + +export function tests() { + return { + "Simple variable derivation": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], false); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").darken(5/100).hex(); + assert.deepEqual(result, {"background-color--darker-5": resultColor}); + }, + + "For dark themes, lighten and darken are inverted": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], true); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").lighten(5/100).hex(); + assert.deepEqual(result, {"background-color--darker-5": resultColor}); + }, + + "Aliases can be derived": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["my-awesome-alias=background-color","my-awesome-alias--darker-5"], false); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").darken(5/100).hex(); + assert.deepEqual(result, { + "my-awesome-alias": "#ff00ff", + "my-awesome-alias--darker-5": resultColor, + }); + }, + } +} From 1456e308a836ed397106a05c5644d72652754ac3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 15:36:02 +0530 Subject: [PATCH 091/354] Add type and fix formatting --- src/platform/web/theming/DerivedVariables.ts | 3 +-- src/platform/web/theming/ThemeLoader.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index e0450e94..5a626bfa 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -49,7 +49,7 @@ export class DerivedVariables { return resolvedVariables; } - private _detectAliases() { + private _detectAliases(): void { const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { const [alias, value] = variable.split("="); @@ -83,7 +83,6 @@ export class DerivedVariables { } } - private _deriveAlias(variable: string, resolvedVariables: Record): string | undefined { const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; const matches = variable.match(RE_VARIABLE_VALUE); diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 10717975..be1bafc0 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -19,7 +19,7 @@ import type {Platform} from "../Platform.js"; import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser"; import type {Variant, ThemeInformation} from "./parsers/types"; import {ColorSchemePreference} from "./parsers/types"; -import { BuiltThemeParser } from "./parsers/BuiltThemeParser"; +import {BuiltThemeParser} from "./parsers/BuiltThemeParser"; export class ThemeLoader { private _platform: Platform; From 7feaa479c0f4c79f012c6690b4fe2ee769f94bb6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 15:55:11 +0530 Subject: [PATCH 092/354] Typescript update to support .mjs files --- package.json | 2 +- tsconfig.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index dd1a1554..9361098c 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "regenerator-runtime": "^0.13.7", "svgo": "^2.8.0", "text-encoding": "^0.7.0", - "typescript": "^4.3.5", + "typescript": "^4.7.0", "vite": "^2.9.8", "xxhashjs": "^0.2.2" }, diff --git a/tsconfig.json b/tsconfig.json index f46cc7eb..72d9d3ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "noImplicitAny": false, "noEmit": true, "target": "ES2020", - "module": "ES2020", + "module": "Node16", "moduleResolution": "node", "esModuleInterop": true }, diff --git a/yarn.lock b/yarn.lock index 1543f8d0..c48d2719 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1665,10 +1665,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.7.0: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" From d79e5f7806a2b90c352ce947915dd85fa0597fb0 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 20 Jul 2022 15:20:23 +0200 Subject: [PATCH 093/354] create key share operations for invitees when history visibility=invited --- src/matrix/e2ee/RoomEncryption.js | 51 +++++++++++++++++-- src/matrix/room/Room.js | 19 ++++--- src/matrix/room/common.ts | 71 +++++++++++++++++++++++++++ src/matrix/room/members/RoomMember.js | 4 ++ 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 80f57507..0ec7212e 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -19,13 +19,23 @@ import {groupEventsBySession} from "./megolm/decryption/utils"; import {mergeMap} from "../../utils/mergeMap"; import {groupBy} from "../../utils/groupBy"; import {makeTxnId} from "../common.js"; +import {iterateResponseStateEvents} from "../room/common"; const ENCRYPTED_TYPE = "m.room.encrypted"; +const ROOM_HISTORY_VISIBILITY_TYPE = "m.room.history_visibility"; // how often ensureMessageKeyIsShared can check if it needs to // create a new outbound session // note that encrypt could still create a new session const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min +// Use enum when converting to TS +const HistoryVisibility = Object.freeze({ + Joined: "joined", + Invited: "invited", + WorldReadable: "world_readable", + Shared: "shared", +}); + // TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap export class RoomEncryption { constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) { @@ -45,6 +55,7 @@ export class RoomEncryption { this._isFlushingRoomKeyShares = false; this._lastKeyPreShareTime = null; this._keySharePromise = null; + this._historyVisibility = undefined; this._disposed = false; } @@ -77,7 +88,13 @@ export class RoomEncryption { this._senderDeviceCache = new Map(); // purge the sender device cache } - async writeMemberChanges(memberChanges, txn, log) { + async writeSync(roomResponse, memberChanges, txn, log) { + let historyVisibility = this._historyVisibility; + iterateResponseStateEvents(roomResponse, event => { + if(event.state_key === "" && event.type === ROOM_HISTORY_VISIBILITY_TYPE) { + historyVisibility = event?.content?.history_visibility; + } + }); let shouldFlush = false; const memberChangesArray = Array.from(memberChanges.values()); // this also clears our session if we leave the room ourselves @@ -89,10 +106,35 @@ export class RoomEncryption { this._megolmEncryption.discardOutboundSession(this._room.id, txn); } if (memberChangesArray.some(m => m.hasJoined)) { - shouldFlush = await this._addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log); + const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId); + shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log) + || shouldFlush; } + if (memberChangesArray.some(m => m.wasInvited)) { + historyVisibility = await this._loadHistoryVisibilityIfNeeded(historyVisibility, txn); + if (historyVisibility === HistoryVisibility.Invited) { + const userIds = memberChangesArray.filter(m => m.wasInvited).map(m => m.userId); + shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log) + || shouldFlush; + } + } + await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); - return shouldFlush; + return {shouldFlush, historyVisibility}; + } + + afterSync({historyVisibility}) { + this._historyVisibility = historyVisibility; + } + + async _loadHistoryVisibilityIfNeeded(historyVisibility, txn) { + if (!historyVisibility) { + const visibilityEntry = await txn.roomState.get(this.id, ROOM_HISTORY_VISIBILITY_TYPE, ""); + if (visibilityEntry) { + return event?.content?.history_visibility; + } + } + return historyVisibility; } async prepareDecryptAll(events, newKeys, source, txn) { @@ -288,8 +330,7 @@ export class RoomEncryption { await this._processShareRoomKeyOperation(operation, hsApi, log); } - async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn, log) { - const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId); + async _addShareRoomKeyOperationForMembers(userIds, txn, log) { const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage( this._room.id, txn); if (roomKeyMessage) { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 12c17580..8cc87845 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -139,11 +139,11 @@ export class Room extends BaseRoom { } log.set("newEntries", newEntries.length); log.set("updatedEntries", updatedEntries.length); - let shouldFlushKeyShares = false; + let encryptionChanges; // pass member changes to device tracker - if (roomEncryption && this.isTrackingMembers && memberChanges?.size) { - shouldFlushKeyShares = await roomEncryption.writeMemberChanges(memberChanges, txn, log); - log.set("shouldFlushKeyShares", shouldFlushKeyShares); + if (roomEncryption) { + encryptionChanges = await roomEncryption.writeSync(roomResponse, memberChanges, txn, log); + log.set("shouldFlushKeyShares", encryptionChanges.shouldFlush); } const allEntries = newEntries.concat(updatedEntries); // also apply (decrypted) timeline entries to the summary changes @@ -188,7 +188,7 @@ export class Room extends BaseRoom { memberChanges, heroChanges, powerLevelsEvent, - shouldFlushKeyShares, + encryptionChanges, }; } @@ -201,11 +201,14 @@ export class Room extends BaseRoom { const { summaryChanges, newEntries, updatedEntries, newLiveKey, removedPendingEvents, memberChanges, powerLevelsEvent, - heroChanges, roomEncryption + heroChanges, roomEncryption, encryptionChanges } = changes; log.set("id", this.id); this._syncWriter.afterSync(newLiveKey); this._setEncryption(roomEncryption); + if (this._roomEncryption) { + this._roomEncryption.afterSync(encryptionChanges); + } if (memberChanges.size) { if (this._changedMembersDuringSync) { for (const [userId, memberChange] of memberChanges.entries()) { @@ -288,8 +291,8 @@ export class Room extends BaseRoom { } } - needsAfterSyncCompleted({shouldFlushKeyShares}) { - return shouldFlushKeyShares; + needsAfterSyncCompleted({encryptionChanges}) { + return encryptionChanges?.shouldFlush; } /** diff --git a/src/matrix/room/common.ts b/src/matrix/room/common.ts index 57ab7023..a03703a4 100644 --- a/src/matrix/room/common.ts +++ b/src/matrix/room/common.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type {StateEvent} from "../storage/types"; + export function getPrevContentFromStateEvent(event) { // where to look for prev_content is a bit of a mess, // see https://matrix.to/#/!NasysSDfxKxZBzJJoE:matrix.org/$DvrAbZJiILkOmOIuRsNoHmh2v7UO5CWp_rYhlGk34fQ?via=matrix.org&via=pixie.town&via=amorgan.xyz @@ -40,3 +42,72 @@ export enum RoomType { Private, Public } + +type RoomResponse = { + state?: { + events?: Array + }, + timeline?: { + events?: Array + } +} + +/** iterates over any state events in a sync room response, in the order that they should be applied (from older to younger events) */ +export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: (StateEvent) => void) { + // first iterate over state events, they precede the timeline + const stateEvents = roomResponse.state?.events; + if (stateEvents) { + for (let i = 0; i < stateEvents.length; i++) { + callback(stateEvents[i]); + } + } + // now see if there are any state events within the timeline + let timelineEvents = roomResponse.timeline?.events; + if (timelineEvents) { + for (let i = 0; i < timelineEvents.length; i++) { + const event = timelineEvents[i]; + if (typeof event.state_key === "string") { + callback(event); + } + } + } +} + +export function tests() { + return { + "test iterateResponseStateEvents with both state and timeline sections": assert => { + const roomResponse = { + state: { + events: [ + {type: "m.room.member", state_key: "1"}, + {type: "m.room.member", state_key: "2", content: {a: 1}}, + ] + }, + timeline: { + events: [ + {type: "m.room.message"}, + {type: "m.room.member", state_key: "3"}, + {type: "m.room.message"}, + {type: "m.room.member", state_key: "2", content: {a: 2}}, + ] + } + } as unknown as RoomResponse; + const expectedStateKeys = ["1", "2", "3", "2"]; + const expectedAForMember2 = [1, 2]; + iterateResponseStateEvents(roomResponse, event => { + assert.strictEqual(event.type, "m.room.member"); + assert.strictEqual(expectedStateKeys.shift(), event.state_key); + if (event.state_key === "2") { + assert.strictEqual(expectedAForMember2.shift(), event.content.a); + } + }); + assert.strictEqual(expectedStateKeys.length, 0); + assert.strictEqual(expectedAForMember2.length, 0); + }, + "test iterateResponseStateEvents with empty response": assert => { + iterateResponseStateEvents({}, () => { + assert.fail("no events expected"); + }); + } + } +} diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index dabff972..8e00f5de 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -137,6 +137,10 @@ export class MemberChange { return this.member.membership; } + get wasInvited() { + return this.previousMembership === "invite" && this.membership !== "invite"; + } + get hasLeft() { return this.previousMembership === "join" && this.membership !== "join"; } From c8a8eb10b5aa34467e7894ec3eb525ce7d7ca6d1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 20 Jul 2022 15:21:33 +0200 Subject: [PATCH 094/354] get user ids for sharing a new key when the message is sent rather than when the key happens to get sent --- src/matrix/e2ee/RoomEncryption.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 0ec7212e..8839556f 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -316,10 +316,13 @@ export class RoomEncryption { } async _shareNewRoomKey(roomKeyMessage, hsApi, log) { + const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, this._historyVisibility, hsApi, log); + const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); + let writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); let operation; try { - operation = this._writeRoomKeyShareOperation(roomKeyMessage, null, writeOpTxn); + operation = this._writeRoomKeyShareOperation(roomKeyMessage, userIds, writeOpTxn); } catch (err) { writeOpTxn.abort(); throw err; @@ -385,16 +388,7 @@ export class RoomEncryption { log.set("id", operation.id); await this._deviceTracker.trackRoom(this._room, log); - let devices; - if (operation.userIds === null) { - devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); - const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); - operation.userIds = userIds; - await this._updateOperationsStore(operations => operations.update(operation)); - } else { - devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log); - } - + const devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log); const messages = await log.wrap("olm encrypt", log => this._olmEncryption.encrypt( "m.room_key", operation.roomKeyMessage, devices, hsApi, log)); const missingDevices = devices.filter(d => !messages.some(m => m.device === d)); From 8d766ac5048e47221ac7db177bfaa3e3b26eba64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 21 Jul 2022 12:05:10 +0530 Subject: [PATCH 095/354] Remove await within loop --- .../build-plugins/rollup-plugin-build-themes.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index f44406c6..bb1c65c1 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -51,12 +51,20 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { */ async function generateIconSourceMap(icons, manifestLocation) { const sources = {}; + const fileNames = []; + const promises = []; const fs = require("fs").promises; for (const icon of Object.values(icons)) { const [location] = icon.split("?"); const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); - const iconData = await fs.readFile(resolvedLocation); - const svgString = iconData.toString(); + const iconData = fs.readFile(resolvedLocation); + promises.push(iconData); + const fileName = path.basename(resolvedLocation); + fileNames.push(fileName); + } + const results = await Promise.all(promises); + for (let i = 0; i < results.length; ++i) { + const svgString = results[i].toString(); const result = optimize(svgString, { plugins: [ { @@ -68,8 +76,7 @@ async function generateIconSourceMap(icons, manifestLocation) { ], }); const optimizedSvgString = result.data; - const fileName = path.basename(resolvedLocation); - sources[fileName] = optimizedSvgString; + sources[fileNames[i]] = optimizedSvgString; } return sources; } From faa8cae532ee9899542808e667ee533a078cb270 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:55:23 +0200 Subject: [PATCH 096/354] Added the possibility to join a room using /join (also added the global commands uses, and some others commands like /shrug .) --- src/domain/session/room/RoomViewModel.js | 80 ++++++++++++++++--- .../web/ui/css/themes/element/theme.css | 12 +++ 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 94c78286..dfda5767 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -197,18 +197,80 @@ export class RoomViewModel extends ViewModel { } } + async _getMessageInformations (message) { + let msgtype = "m.text"; + if (message.startsWith("/")) { + let command=message.substring(1).split(" "); + switch (command[0]) { + case "me": + message = message.substring(4).trim(); + msgtype = "m.emote"; + break; + case "join": + if (command.length==2) { + let roomname = message.substring(5).trim(); + try { + await this._options.client.session.joinRoom(roomname); + } catch (exc) { + if (exc.statusCode??exc.status==400) { + this._sendError=new Error("/join : '"+roomname+"' was not legal room ID or room alias"); + } else if ((exc.statusCode??exc.status==502) || (exc.message="Internal Server Error")) { + this._sendError=new Error("/join : room not found"); + } else { + this._sendError=new Error("join syntax: /join "); + } + this._timelineError = null; + this.emitChange("error"); + } + } else { + this._sendError=new Error("join syntax: /join "); + this._timelineError = null; + this.emitChange("error"); + } + msgtype=undefined; + message=undefined; + break; + case "shrug": + message="¯\\_(ツ)_/¯ "+message.substring(7); + break; + case "tableflip": + message="(╯°□°)╯︵ ┻━┻ "+message.substring(11); + break; + case "unflip": + message="┬──┬ ノ( ゜-゜ノ) "+message.substring(8); + break; + case "lenny": + message="( ͡° ͜ʖ ͡°) "+message.substring(7); + break; + default: + if (command[0][0]=="/") { + message = message.substring(1).trim(); + break; + } else { + this._sendError=new Error("no command name '"+command[0]+"'"); + this._timelineError = null; + this.emitChange("error"); + msgtype=undefined; + message=undefined; + } + } + } + return [msgtype, message]; + } + async _sendMessage(message, replyingTo) { if (!this._room.isArchived && message) { + let messinfo = await this._getMessageInformations(message); try { - let msgtype = "m.text"; - if (message.startsWith("/me ")) { - message = message.substr(4).trim(); - msgtype = "m.emote"; - } - if (replyingTo) { - await replyingTo.reply(msgtype, message); - } else { - await this._room.sendEvent("m.room.message", {msgtype, body: message}); + let msgtype = messinfo[0]; + let message = messinfo[1]; + console.log("messinfo :",messinfo); + if (msgtype && message) { + if (replyingTo) { + await replyingTo.reply(msgtype, message); + } else { + await this._room.sendEvent("m.room.message", {msgtype, body: message}); + } } } catch (err) { console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 632d5bc5..16dfea12 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -521,6 +521,18 @@ a { .RoomView_error { color: var(--error-color); + background : #efefef; + padding-right : 20px; + padding-left : 20px; + overflow : hidden; + height : 0px; + transition : 0.25s all ease-out; +} + +.RoomView_error:not(:empty) { + height : auto; + padding-top : 20px; + padding-bottom : 20px; } .MessageComposer_replyPreview .Timeline_message { From 22831e710ca456009eb2f819c86e08b0ed0875bd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 14:15:26 +0200 Subject: [PATCH 097/354] support async callback in iterateResponseStateEvents --- src/matrix/room/common.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/matrix/room/common.ts b/src/matrix/room/common.ts index a03703a4..5a7fc98e 100644 --- a/src/matrix/room/common.ts +++ b/src/matrix/room/common.ts @@ -53,12 +53,20 @@ type RoomResponse = { } /** iterates over any state events in a sync room response, in the order that they should be applied (from older to younger events) */ -export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: (StateEvent) => void) { +export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: (StateEvent) => Promise | void): Promise | void { + let promises: Promise[] | undefined = undefined; + const callCallback = stateEvent => { + const result = callback(stateEvent); + if (result instanceof Promise) { + promises = promises ?? []; + promises.push(result); + } + }; // first iterate over state events, they precede the timeline const stateEvents = roomResponse.state?.events; if (stateEvents) { for (let i = 0; i < stateEvents.length; i++) { - callback(stateEvents[i]); + callCallback(stateEvents[i]); } } // now see if there are any state events within the timeline @@ -67,10 +75,13 @@ export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: for (let i = 0; i < timelineEvents.length; i++) { const event = timelineEvents[i]; if (typeof event.state_key === "string") { - callback(event); + callCallback(event); } } } + if (promises) { + return Promise.all(promises); + } } export function tests() { From fb58d9c9efd9a3efa70dd2ddfa988daffd078036 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:08:53 +0200 Subject: [PATCH 098/354] Corrected some syntax dismiss --- src/domain/session/room/RoomViewModel.js | 51 ++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index dfda5767..5ded5108 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -200,71 +200,72 @@ export class RoomViewModel extends ViewModel { async _getMessageInformations (message) { let msgtype = "m.text"; if (message.startsWith("/")) { - let command=message.substring(1).split(" "); - switch (command[0]) { + const [commandName, ...args] = message.substring(1).split(" "); + switch (commandName) { case "me": message = message.substring(4).trim(); msgtype = "m.emote"; break; case "join": - if (command.length==2) { - let roomname = message.substring(5).trim(); + if (args.length == 1) { + let roomName = args[0]; try { - await this._options.client.session.joinRoom(roomname); + await this._options.client.session.joinRoom(roomName); + const internalId = await this._options.client.session.joinRoom(roomName); + this.navigation.push("room", internalId); } catch (exc) { - if (exc.statusCode??exc.status==400) { - this._sendError=new Error("/join : '"+roomname+"' was not legal room ID or room alias"); - } else if ((exc.statusCode??exc.status==502) || (exc.message="Internal Server Error")) { - this._sendError=new Error("/join : room not found"); + if (exc.statusCode ?? exc.status === 400) { + this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); + } else if ((exc.statusCode ?? exc.status === 404) || (exc.statusCode ?? exc.status === 502) || (exc.message == "Internal Server Error")) { + this._sendError = new Error(`/join : room '${roomName}' not found`); } else { - this._sendError=new Error("join syntax: /join "); + this._sendError = new Error("join syntax: /join "); } this._timelineError = null; this.emitChange("error"); } } else { - this._sendError=new Error("join syntax: /join "); + this._sendError = new Error("join syntax: /join "); this._timelineError = null; this.emitChange("error"); } - msgtype=undefined; - message=undefined; + msgtype = undefined; + message = undefined; break; case "shrug": - message="¯\\_(ツ)_/¯ "+message.substring(7); + message = "¯\\_(ツ)_/¯ " + message.substring(7); break; case "tableflip": - message="(╯°□°)╯︵ ┻━┻ "+message.substring(11); + message="(╯°□°)╯︵ ┻━┻ " + message.substring(11); break; case "unflip": - message="┬──┬ ノ( ゜-゜ノ) "+message.substring(8); + message="┬──┬ ノ( ゜-゜ノ) " + message.substring(8); break; case "lenny": - message="( ͡° ͜ʖ ͡°) "+message.substring(7); + message="( ͡° ͜ʖ ͡°) " + message.substring(7); break; default: - if (command[0][0]=="/") { + if (commandName[0] == "/") { message = message.substring(1).trim(); break; } else { - this._sendError=new Error("no command name '"+command[0]+"'"); + this._sendError = new Error(`no command name "${commandName}". To send the message instead of executing, please type "/${message}"`); this._timelineError = null; this.emitChange("error"); - msgtype=undefined; - message=undefined; + msgtype = undefined; + message = undefined; } } } - return [msgtype, message]; + return {type: msgtype, message: message}; } async _sendMessage(message, replyingTo) { if (!this._room.isArchived && message) { let messinfo = await this._getMessageInformations(message); try { - let msgtype = messinfo[0]; - let message = messinfo[1]; - console.log("messinfo :",messinfo); + let msgtype = messinfo.type; + let message = messinfo.message; if (msgtype && message) { if (replyingTo) { await replyingTo.reply(msgtype, message); From 8b393464094f0c2d0b734e6511621440735fa950 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:34:52 +0200 Subject: [PATCH 099/354] The error message can now be closed --- src/domain/session/room/RoomViewModel.js | 5 ++ .../web/ui/css/themes/element/theme.css | 49 ++++++++++++++++++- src/platform/web/ui/session/room/RoomView.js | 10 +++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 5ded5108..5415d6dd 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -416,6 +416,11 @@ export class RoomViewModel extends ViewModel { this._composerVM.setReplyingTo(entry); } } + + dismissError(evt) { + this._sendError = null; + this.emitChange("error"); + } } function videoToInfo(video) { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 16dfea12..f14100b2 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -526,15 +526,62 @@ a { padding-left : 20px; overflow : hidden; height : 0px; + font-weight : bold; transition : 0.25s all ease-out; + position : relative; + display : flex; } .RoomView_error:not(:empty) { - height : auto; + height : 40px; + align-items : center; padding-top : 20px; padding-bottom : 20px; } +.RoomView_error p { + position : relative; + display : block; + width : 100%; + height : auto; + margin : 0; +} + +.RoomView_error button { + width : 40px; + padding-top : 20px; + padding-bottom : 20px; + background : none; + border : none; + position : relative; +} + +.RoomView_error button:hover { + background : #cfcfcf; +} + +.RoomView_error button:after { + content:""; + position : absolute; + top : 10px; + right: 16px; + background : red; + width : 5px; + height : 20px; + transform: rotate(45deg); +} + +.RoomView_error button:before { + content:""; + position : absolute; + top : 17px; + left: 10px; + background : red; + width : 20px; + height : 5px; + transform: rotate(45deg); +} + .MessageComposer_replyPreview .Timeline_message { margin: 0; margin-top: 5px; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index d36466dd..7cfd7d85 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -46,7 +46,13 @@ export class RoomView extends TemplateView { }) ]), t.div({className: "RoomView_body"}, [ - t.div({className: "RoomView_error"}, vm => vm.error), + t.div({className: "RoomView_error"}, [ + t.if(vm => vm.error, t => t.p({}, vm => vm.error)), + t.if(vm => vm.error, t => t.button({ + className: "RoomView_error_closerButton", + onClick: evt => vm.dismissError(evt) + })) + ]), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? new TimelineView(timelineViewModel, this._viewClassForTile) : @@ -64,7 +70,7 @@ export class RoomView extends TemplateView { ]) ]); } - + _toggleOptionsMenu(evt) { if (this._optionsPopup && this._optionsPopup.isOpen) { this._optionsPopup.close(); From be8962cec29aa23ea56515ec3cc8a8232b940bd6 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:59:48 +0200 Subject: [PATCH 100/354] Fixed priority operations when checking request status --- src/domain/session/room/RoomViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 5415d6dd..1786741e 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -214,9 +214,9 @@ export class RoomViewModel extends ViewModel { const internalId = await this._options.client.session.joinRoom(roomName); this.navigation.push("room", internalId); } catch (exc) { - if (exc.statusCode ?? exc.status === 400) { + if ((exc.statusCode ?? exc.status) === 400) { this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status === 404) || (exc.statusCode ?? exc.status === 502) || (exc.message == "Internal Server Error")) { + } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { this._sendError = new Error(`/join : room '${roomName}' not found`); } else { this._sendError = new Error("join syntax: /join "); From e345d0b33e1247833c873382f976dd2b81df8f24 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:06:09 +0200 Subject: [PATCH 101/354] Added the 403 status when joining an unknown room --- src/domain/session/room/RoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 1786741e..66f936ea 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -216,7 +216,7 @@ export class RoomViewModel extends ViewModel { } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + } else if ((exc.statusCode ?? exc.status) === 403 || (exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { this._sendError = new Error(`/join : room '${roomName}' not found`); } else { this._sendError = new Error("join syntax: /join "); From 66a59e6f4d626f04dbc90cf934e6fe8abb8ab354 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:09:43 +0200 Subject: [PATCH 102/354] Error of interpretation of the 403 status at the last update. Fixed --- src/domain/session/room/RoomViewModel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 66f936ea..1ad92dd5 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -216,8 +216,10 @@ export class RoomViewModel extends ViewModel { } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 403 || (exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + } else if (exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { this._sendError = new Error(`/join : room '${roomName}' not found`); + } else if (exc.statusCode ?? exc.status) === 403) { + this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); } else { this._sendError = new Error("join syntax: /join "); } From b7fd22c7f949783cda851c9bc10711f9c3029011 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:10:29 +0200 Subject: [PATCH 103/354] SyntaxError fixed --- src/domain/session/room/RoomViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 1ad92dd5..b3fc703c 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -216,9 +216,9 @@ export class RoomViewModel extends ViewModel { } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if (exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { this._sendError = new Error(`/join : room '${roomName}' not found`); - } else if (exc.statusCode ?? exc.status) === 403) { + } else if ((exc.statusCode ?? exc.status) === 403) { this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); } else { this._sendError = new Error("join syntax: /join "); From f3379402027a13ff41943aec4d62b77a8490b5eb Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:46:29 +0200 Subject: [PATCH 104/354] this migration shouldn't be needed anymore and undoes the export of addRoomToIdentity, which is somewhat internal --- src/matrix/e2ee/DeviceTracker.js | 2 +- src/matrix/storage/idb/schema.ts | 52 ++++---------------------------- 2 files changed, 7 insertions(+), 47 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index f8c3bca8..207aee05 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -19,7 +19,7 @@ import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; -export function addRoomToIdentity(identity, userId, roomId) { +function addRoomToIdentity(identity, userId, roomId) { if (!identity) { identity = { userId: userId, diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 7819130e..9b875f9c 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -2,7 +2,6 @@ import {IDOMStorage} from "./types"; import {ITransaction} from "./QueryTarget"; import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; -import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; @@ -183,51 +182,12 @@ function createTimelineRelationsStore(db: IDBDatabase) : void { db.createObjectStore("timelineRelations", {keyPath: "key"}); } -//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470) -async function fixMissingRoomsInUserIdentities(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) { - const roomSummaryStore = txn.objectStore("roomSummary"); - const trackedRoomIds: string[] = []; - await iterateCursor(roomSummaryStore.openCursor(), roomSummary => { - if (roomSummary.isTrackingMembers) { - trackedRoomIds.push(roomSummary.roomId); - } - return NOT_DONE; - }); - const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions"); - const userIdentitiesStore: IDBObjectStore = txn.objectStore("userIdentities"); - const roomMemberStore = txn.objectStore("roomMembers"); - for (const roomId of trackedRoomIds) { - let foundMissing = false; - const joinedUserIds: string[] = []; - const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); - await log.wrap({l: "room", id: roomId}, async log => { - await iterateCursor(roomMemberStore.openCursor(memberRange), member => { - if (member.membership === "join") { - joinedUserIds.push(member.userId); - } - return NOT_DONE; - }); - log.set("joinedUserIds", joinedUserIds.length); - for (const userId of joinedUserIds) { - const identity = await reqAsPromise(userIdentitiesStore.get(userId)); - const originalRoomCount = identity?.roomIds?.length; - const updatedIdentity = addRoomToIdentity(identity, userId, roomId); - if (updatedIdentity) { - log.log({l: `fixing up`, id: userId, - roomsBefore: originalRoomCount, roomsAfter: updatedIdentity.roomIds.length}); - userIdentitiesStore.put(updatedIdentity); - foundMissing = true; - } - } - log.set("foundMissing", foundMissing); - if (foundMissing) { - // clear outbound megolm session, - // so we'll create a new one on the next message that will be properly shared - outboundGroupSessionsStore.delete(roomId); - } - }); - } -} +//v11 doesn't change the schema, +// but ensured all userIdentities have all the roomIds they should (see #470) + +// 2022-07-20: The fix dated from August 2021, and have removed it now because of a +// refactoring needed in the device tracker, which made it inconvenient to expose addRoomToIdentity +function fixMissingRoomsInUserIdentities() {} // v12 move ssssKey to e2ee:ssssKey so it will get backed up in the next step async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) { From 86c0e9e66936d3c18aca88fa4c9aab53acfd8d3d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:46:53 +0200 Subject: [PATCH 105/354] logic for whether a key should be shared by membership and h. visibility --- src/matrix/e2ee/common.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/matrix/e2ee/common.js b/src/matrix/e2ee/common.js index 2b9d46b9..cc3bfff5 100644 --- a/src/matrix/e2ee/common.js +++ b/src/matrix/e2ee/common.js @@ -69,3 +69,28 @@ export function createRoomEncryptionEvent() { } } } + + +// Use enum when converting to TS +export const HistoryVisibility = Object.freeze({ + Joined: "joined", + Invited: "invited", + WorldReadable: "world_readable", + Shared: "shared", +}); + +export function shouldShareKey(membership, historyVisibility) { + switch (historyVisibility) { + case HistoryVisibility.WorldReadable: + return true; + case HistoryVisibility.Shared: + // was part of room at some time + return membership !== undefined; + case HistoryVisibility.Joined: + return membership === "join"; + case HistoryVisibility.Invited: + return membership === "invite" || membership === "join"; + default: + return false; + } +} From f6011f3f348215d0829af9e1f8f7e6553bc4a87b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:48:26 +0200 Subject: [PATCH 106/354] take history visibility into account in device tracker and return added and removed userids to their userIdentity for the given room, so room encryption can share and discard the keys for them --- src/matrix/e2ee/DeviceTracker.js | 71 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 207aee05..7ce601d2 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -15,6 +15,7 @@ limitations under the License. */ import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; +import {shouldShareKey} from "./common.js"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; @@ -79,13 +80,38 @@ export class DeviceTracker { })); } - writeMemberChanges(room, memberChanges, txn) { - return Promise.all(Array.from(memberChanges.values()).map(async memberChange => { - return this._applyMemberChange(memberChange, txn); + /** @return Promise<{added: string[], removed: string[]}> the user ids for who the room was added or removed to the userIdentity, + * and with who a key should be now be shared + **/ + async writeMemberChanges(room, memberChanges, historyVisibility, txn) { + const added = []; + const removed = []; + await Promise.all(Array.from(memberChanges.values()).map(async memberChange => { + // keys should now be shared with this member? + // add the room to the userIdentity if so + if (shouldShareKey(memberChange.membership, historyVisibility)) { + if (await this._addRoomToUserIdentity(memberChange.roomId, memberChange.userId, txn)) { + added.push(memberChange.userId); + } + } else if (memberChange.hasLeft) { + // remove room + const {roomId} = memberChange; + // if we left the room, remove room from all user identities in the room + if (memberChange.userId === this._ownUserId) { + const userIds = await txn.roomMembers.getAllUserIds(roomId); + await Promise.all(userIds.map(userId => { + return this._removeRoomFromUserIdentity(roomId, userId, txn); + })); + } else { + await this._removeRoomFromUserIdentity(roomId, memberChange.userId, txn); + } + removed.push(memberChange.userId); + } })); + return {added, removed}; } - async trackRoom(room, log) { + async trackRoom(room, historyVisibility, log) { if (room.isTrackingMembers || !room.isEncrypted) { return; } @@ -100,7 +126,11 @@ export class DeviceTracker { isTrackingChanges = room.writeIsTrackingMembers(true, txn); const members = Array.from(memberList.members.values()); log.set("members", members.length); - await this._writeJoinedMembers(members, txn); + await Promise.all(members.map(async member => { + if (shouldShareKey(member.membership, historyVisibility)) { + await this._addRoomToUserIdentity(member.roomId, member.userId, txn); + } + })); } catch (err) { txn.abort(); throw err; @@ -120,13 +150,15 @@ export class DeviceTracker { })); } - async _writeMember(member, txn) { + async _addRoomToUserIdentity(roomId, userId, txn) { const {userIdentities} = txn; - const identity = await userIdentities.get(member.userId); - const updatedIdentity = addRoomToIdentity(identity, member.userId, member.roomId); + const identity = await userIdentities.get(userId); + const updatedIdentity = addRoomToIdentity(identity, userId, roomId); if (updatedIdentity) { userIdentities.set(updatedIdentity); + return true; } + return false; } async _removeRoomFromUserIdentity(roomId, userId, txn) { @@ -141,28 +173,9 @@ export class DeviceTracker { } else { userIdentities.set(identity); } + return true; } - } - - async _applyMemberChange(memberChange, txn) { - // TODO: depends whether we encrypt for invited users?? - // add room - if (memberChange.hasJoined) { - await this._writeMember(memberChange.member, txn); - } - // remove room - else if (memberChange.hasLeft) { - const {roomId} = memberChange; - // if we left the room, remove room from all user identities in the room - if (memberChange.userId === this._ownUserId) { - const userIds = await txn.roomMembers.getAllUserIds(roomId); - await Promise.all(userIds.map(userId => { - return this._removeRoomFromUserIdentity(roomId, userId, txn); - })); - } else { - await this._removeRoomFromUserIdentity(roomId, memberChange.userId, txn); - } - } + return false; } async _queryKeys(userIds, hsApi, log) { From 17f42f523a66afa0351ed15c51b62119c97794c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:49:26 +0200 Subject: [PATCH 107/354] add write method for when history visibility changes also returning added and removed user ids --- src/matrix/e2ee/DeviceTracker.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 7ce601d2..8bed487d 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -16,6 +16,7 @@ limitations under the License. import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; import {shouldShareKey} from "./common.js"; +import {RoomMember} from "../room/members/RoomMember.js"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; @@ -142,12 +143,29 @@ export class DeviceTracker { } } - async _writeJoinedMembers(members, txn) { - await Promise.all(members.map(async member => { - if (member.membership === "join") { - await this._writeMember(member, txn); - } - })); + async writeHistoryVisibility(room, historyVisibility, syncTxn, log) { + const added = []; + const removed = []; + if (room.isTrackingMembers && room.isEncrypted) { + await log.wrap("rewriting userIdentities", async log => { + // don't use room.loadMemberList here because we want to use the syncTxn to load the members + const memberDatas = await syncTxn.roomMembers.getAll(room.id); + const members = memberDatas.map(d => new RoomMember(d)); + log.set("members", members.length); + await Promise.all(members.map(async member => { + if (shouldShareKey(member.membership, historyVisibility)) { + if (await this._addRoomToUserIdentity(member.roomId, member.userId, syncTxn)) { + added.push(member.userId); + } + } else { + if (await this._removeRoomFromUserIdentity(member.roomId, member.userId, syncTxn)) { + removed.push(member.userId); + } + } + })); + }); + } + return {added, removed}; } async _addRoomToUserIdentity(roomId, userId, txn) { From a23df8a5457e63cd0f0c10941f88f1631c504624 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 22 Jul 2022 17:49:59 +0200 Subject: [PATCH 108/354] pass history visibility to device tracker and delegate adding and removing members to share keys with to it --- src/matrix/e2ee/RoomEncryption.js | 74 +++++++++++++++++-------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 8839556f..a841a6bd 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -28,14 +28,6 @@ const ROOM_HISTORY_VISIBILITY_TYPE = "m.room.history_visibility"; // note that encrypt could still create a new session const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min -// Use enum when converting to TS -const HistoryVisibility = Object.freeze({ - Joined: "joined", - Invited: "invited", - WorldReadable: "world_readable", - Shared: "shared", -}); - // TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap export class RoomEncryption { constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) { @@ -89,37 +81,49 @@ export class RoomEncryption { } async writeSync(roomResponse, memberChanges, txn, log) { - let historyVisibility = this._historyVisibility; - iterateResponseStateEvents(roomResponse, event => { + let historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility, txn); + const addedMembers = []; + const removedMembers = []; + // update the historyVisibility if needed + await iterateResponseStateEvents(roomResponse, event => { + // TODO: can the same state event appear twice? Hence we would be rewriting the useridentities twice... + // we'll see in the logs if(event.state_key === "" && event.type === ROOM_HISTORY_VISIBILITY_TYPE) { - historyVisibility = event?.content?.history_visibility; + const newHistoryVisibility = event?.content?.history_visibility; + if (newHistoryVisibility !== historyVisibility) { + return log.wrap({ + l: "history_visibility changed", + from: historyVisibility, + to: newHistoryVisibility + }, async log => { + historyVisibility = newHistoryVisibility; + const result = await this._deviceTracker.writeHistoryVisibility(this._room, historyVisibility, txn, log); + addedMembers.push(...result.added); + removedMembers.push(...result.removed); + }); + } } }); - let shouldFlush = false; - const memberChangesArray = Array.from(memberChanges.values()); - // this also clears our session if we leave the room ourselves - if (memberChangesArray.some(m => m.hasLeft)) { + // process member changes + if (memberChanges.size) { + const result = await this._deviceTracker.writeMemberChanges( + this._room, memberChanges, historyVisibility, txn); + addedMembers.push(...result.added); + removedMembers.push(...result.removed); + } + // discard key if somebody (including ourselves) left + if (removedMembers.length) { log.log({ l: "discardOutboundSession", - leftUsers: memberChangesArray.filter(m => m.hasLeft).map(m => m.userId), + leftUsers: removedMembers, }); this._megolmEncryption.discardOutboundSession(this._room.id, txn); } - if (memberChangesArray.some(m => m.hasJoined)) { - const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId); - shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log) - || shouldFlush; + let shouldFlush = false; + // add room to userIdentities if needed, and share the current key with them + if (addedMembers.length) { + shouldFlush = await this._addShareRoomKeyOperationForMembers(addedMembers, txn, log); } - if (memberChangesArray.some(m => m.wasInvited)) { - historyVisibility = await this._loadHistoryVisibilityIfNeeded(historyVisibility, txn); - if (historyVisibility === HistoryVisibility.Invited) { - const userIds = memberChangesArray.filter(m => m.wasInvited).map(m => m.userId); - shouldFlush = await this._addShareRoomKeyOperationForMembers(userIds, txn, log) - || shouldFlush; - } - } - - await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); return {shouldFlush, historyVisibility}; } @@ -127,8 +131,11 @@ export class RoomEncryption { this._historyVisibility = historyVisibility; } - async _loadHistoryVisibilityIfNeeded(historyVisibility, txn) { + async _loadHistoryVisibilityIfNeeded(historyVisibility, txn = undefined) { if (!historyVisibility) { + if (!txn) { + txn = await this._storage.readTxn([this._storage.storeNames.roomState]); + } const visibilityEntry = await txn.roomState.get(this.id, ROOM_HISTORY_VISIBILITY_TYPE, ""); if (visibilityEntry) { return event?.content?.history_visibility; @@ -316,6 +323,7 @@ export class RoomEncryption { } async _shareNewRoomKey(roomKeyMessage, hsApi, log) { + this._historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility); const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, this._historyVisibility, hsApi, log); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); @@ -386,8 +394,8 @@ export class RoomEncryption { async _processShareRoomKeyOperation(operation, hsApi, log) { log.set("id", operation.id); - - await this._deviceTracker.trackRoom(this._room, log); + this._historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility); + await this._deviceTracker.trackRoom(this._room, this._historyVisibility, log); const devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi, log); const messages = await log.wrap("olm encrypt", log => this._olmEncryption.encrypt( "m.room_key", operation.roomKeyMessage, devices, hsApi, log)); From 8a976861fb15ce7b6b5935dd7862b105e790a348 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 25 Jul 2022 11:31:14 +0530 Subject: [PATCH 109/354] Add type --- src/platform/types/config.ts | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/platform/types/config.ts diff --git a/src/platform/types/config.ts b/src/platform/types/config.ts new file mode 100644 index 00000000..2e08657e --- /dev/null +++ b/src/platform/types/config.ts @@ -0,0 +1,63 @@ +/* +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. +*/ + +export type Config = { + /** + * The default homeserver used by Hydrogen; auto filled in the login UI. + * eg: https://matrix.org + * REQUIRED + */ + defaultHomeServer: string; + /** + * The submit endpoint for your preferred rageshake server. + * eg: https://element.io/bugreports/submit + * Read more about rageshake at https://github.com/matrix-org/rageshake + * OPTIONAL + */ + bugReportEndpointUrl: string; + /** + * Paths to theme-manifests + * eg: ["assets/theme-element.json", "assets/theme-awesome.json"] + * REQUIRED + */ + themeManifests: string[]; + /** + * This configures the default theme(s) used by Hydrogen. + * These themes appear as "Default" option in the theme chooser UI and are also + * used as a fallback when other themes fail to load. + * Whether the dark or light variant is used depends on the system preference. + * OPTIONAL + */ + defaultTheme: { + // id of light theme + light: string; + // id of dark theme + dark: string; + }; + /** + * Configuration for push notifications. + * See https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset + * OPTIONAL + */ + push: { + // See app_id in the request body in above link + appId: string; + // The host used for pushing notification + gatewayUrl: string; + // See pushkey in above link + applicationServerKey: string; + }; +}; From 63bdbee39ccd59f7964ad260cfe254c8eb24fae2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 25 Jul 2022 11:33:22 +0530 Subject: [PATCH 110/354] Make optional fields optional --- src/platform/types/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/types/config.ts b/src/platform/types/config.ts index 2e08657e..165bd22b 100644 --- a/src/platform/types/config.ts +++ b/src/platform/types/config.ts @@ -27,7 +27,7 @@ export type Config = { * Read more about rageshake at https://github.com/matrix-org/rageshake * OPTIONAL */ - bugReportEndpointUrl: string; + bugReportEndpointUrl?: string; /** * Paths to theme-manifests * eg: ["assets/theme-element.json", "assets/theme-awesome.json"] @@ -41,7 +41,7 @@ export type Config = { * Whether the dark or light variant is used depends on the system preference. * OPTIONAL */ - defaultTheme: { + defaultTheme?: { // id of light theme light: string; // id of dark theme @@ -52,7 +52,7 @@ export type Config = { * See https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset * OPTIONAL */ - push: { + push?: { // See app_id in the request body in above link appId: string; // The host used for pushing notification From fdd60a7516efdf7b8ad331119e68a24a66cc593f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 25 Jul 2022 11:38:50 +0530 Subject: [PATCH 111/354] Add documentation for derived themes --- doc/THEMING.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/doc/THEMING.md b/doc/THEMING.md index b6af27cf..5ffadf44 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -167,3 +167,38 @@ To find the theme-id of some theme, you can look at the built-asset section of t This default theme will render as "Default" option in the theme-chooser dropdown. If the device preference is for dark theme, the dark default is selected and vice versa. **You'll need to reload twice so that Hydrogen picks up the config changes!** + +# Derived Theme(Collection) +This allows users to theme Hydrogen without the need for rebuilding. Derived theme collections can be thought of as extensions (derivations) of some existing build time theme. + +## Creating a derived theme: +Here's how you create a new derived theme: +1. You create a new theme manifest file (eg: theme-awesome.json) and mention which build time theme you're basing your new theme on using the `extends` field. The base css file of the mentioned theme is used for your new theme. +2. You configue the theme manifest as usual by populating the `variants` field with your desired colors. +3. You add your new theme manifest to the list of themes in `config.json`. + +Reload Hydrogen twice and the new theme should show up in the theme chooser. + +## How does it work? + +For every theme collection in hydrogen, the build process emits a runtime css file which like the built theme css file contains variables in the css code. But unlike the theme css file, the runtime css file lacks the definition for these variables: + +CSS for the built theme: +```css +:root { + --background-color-primary: #f2f20f; +} + +body { + background-color: var(--background-color-primary); +} +``` +and the corresponding runtime theme: +```css +/* Notice the lack of definiton for --background-color-primary here! */ +body { + background-color: var(--background-color-primary); +} +``` + +When hydrogen loads a derived theme, it takes the runtime css file of the extended theme and dynamically adds the variable definition based on the values specified in the manifest. Icons are also colored dynamically and injected as variables using Data URIs. From 0bf021ea87f24ea029a6c86ea9c8859214aa0a33 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Mon, 25 Jul 2022 13:37:03 +0200 Subject: [PATCH 112/354] The room is now joined after having actualised the rooms list, to avoid the synchronisations waits that can sometimes disable to enter the room (message "You're not into this room" or simply "You're not in this room yet. *Join the room*") --- src/domain/session/room/RoomViewModel.js | 2 +- src/matrix/Session.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index b3fc703c..a15447b8 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -210,8 +210,8 @@ export class RoomViewModel extends ViewModel { if (args.length == 1) { let roomName = args[0]; try { - await this._options.client.session.joinRoom(roomName); const internalId = await this._options.client.session.joinRoom(roomName); + await this._options.client.session.waitForRoomFromSync(internalId); this.navigation.push("room", internalId); } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ae1dea61..f45cb5b3 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -948,6 +948,23 @@ export class Session { return body.room_id; }); } + + waitForRoomFromSync(roomId) { + let resolve; + const promise = new Promise(r => { resolve = r; }) + const subscription = { + onAdd: (_, value) => { + if (value.id === roomId) { + this._session.rooms.unsubscribe(subscription); + resolve(); + } + }, + onUpdate: () => undefined, + onRemove: () => undefined, + }; + this._session.rooms.subscribe(subscription); + return promise; + } } export function tests() { From 1e5179f8356ecf16221bb9ca1ddc2057702a60f6 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:22:06 +0200 Subject: [PATCH 113/354] =?UTF-8?q?=20-=20Application=20des=20diff=C3=A9re?= =?UTF-8?q?nts=20commentaires=20du=20Pull=20Request=20(#809)=20=20-=20Corr?= =?UTF-8?q?ection=20des=20erreurs=20d'indentations.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/session/room/RoomViewModel.js | 124 +++++++++--------- src/matrix/Session.js | 17 --- src/matrix/common.js | 18 +-- .../web/ui/css/themes/element/theme.css | 51 +++---- src/platform/web/ui/session/room/RoomView.js | 12 +- 5 files changed, 105 insertions(+), 117 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index a15447b8..9b6aa966 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -197,77 +197,77 @@ export class RoomViewModel extends ViewModel { } } - async _getMessageInformations (message) { - let msgtype = "m.text"; - if (message.startsWith("/")) { - const [commandName, ...args] = message.substring(1).split(" "); - switch (commandName) { - case "me": - message = message.substring(4).trim(); - msgtype = "m.emote"; - break; - case "join": - if (args.length == 1) { - let roomName = args[0]; - try { - const internalId = await this._options.client.session.joinRoom(roomName); - await this._options.client.session.waitForRoomFromSync(internalId); - this.navigation.push("room", internalId); - } catch (exc) { - if ((exc.statusCode ?? exc.status) === 400) { - this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - this._sendError = new Error(`/join : room '${roomName}' not found`); - } else if ((exc.statusCode ?? exc.status) === 403) { - this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); - } else { - this._sendError = new Error("join syntax: /join "); - } - this._timelineError = null; - this.emitChange("error"); - } - } else { - this._sendError = new Error("join syntax: /join "); - this._timelineError = null; - this.emitChange("error"); - } - msgtype = undefined; - message = undefined; - break; - case "shrug": - message = "¯\\_(ツ)_/¯ " + message.substring(7); - break; - case "tableflip": - message="(╯°□°)╯︵ ┻━┻ " + message.substring(11); - break; - case "unflip": - message="┬──┬ ノ( ゜-゜ノ) " + message.substring(8); - break; - case "lenny": - message="( ͡° ͜ʖ ͡°) " + message.substring(7); - break; - default: - if (commandName[0] == "/") { - message = message.substring(1).trim(); - break; - } else { - this._sendError = new Error(`no command name "${commandName}". To send the message instead of executing, please type "/${message}"`); + async _processCommand (message) { + let msgtype = undefined; + const [commandName, ...args] = message.substring(1).split(" "); + switch (commandName) { + case "me": + message = message.substring(4).trim(); + msgtype = "m.emote"; + break; + case "join": + if (args.length == 1) { + let roomName = args[0]; + try { + const roomId = await this._options.client.session.joinRoom(roomName); + await session.observeRoomStatus(roomId).waitFor(status === RoomStatus.Joined); + this.navigation.push("room", roomId); + } catch (exc) { + if ((exc.statusCode ?? exc.status) === 400) { + this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); + } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + this._sendError = new Error(`/join : room '${roomName}' not found`); + } else if ((exc.statusCode ?? exc.status) === 403) { + this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); + } else { + this._sendError = new Error("join syntax: /join "); + } this._timelineError = null; this.emitChange("error"); - msgtype = undefined; - message = undefined; - } - } + } + } else { + this._sendError = new Error("join syntax: /join "); + this._timelineError = null; + this.emitChange("error"); + } + break; + case "shrug": + message = "¯\\_(ツ)_/¯ " + message.substring(7); + msgtype = "m.text"; + break; + case "tableflip": + message="(╯°□°)╯︵ ┻━┻ " + message.substring(11); + msgtype = "m.text"; + break; + case "unflip": + message="┬──┬ ノ( ゜-゜ノ) " + message.substring(8); + msgtype = "m.text"; + break; + case "lenny": + message="( ͡° ͜ʖ ͡°) " + message.substring(7); + msgtype = "m.text"; + break; + default: + this._sendError = new Error(`no command name "${commandName}". To send the message instead of executing, please type "/${message}"`); + this._timelineError = null; + this.emitChange("error"); + msgtype = undefined; + message = undefined; } return {type: msgtype, message: message}; } async _sendMessage(message, replyingTo) { if (!this._room.isArchived && message) { - let messinfo = await this._getMessageInformations(message); + let messinfo = {msgtype : "m.text", message : message}; + if (message.startsWith("//")) { + messinfo.message = message.substring(1).trim(); + } else if (message.startsWith("/")) { + messinfo = await this._processCommand(message); + } try { - let msgtype = messinfo.type; - let message = messinfo.message; + const msgtype = messinfo.type; + const message = messinfo.message; if (msgtype && message) { if (replyingTo) { await replyingTo.reply(msgtype, message); diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f45cb5b3..ae1dea61 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -948,23 +948,6 @@ export class Session { return body.room_id; }); } - - waitForRoomFromSync(roomId) { - let resolve; - const promise = new Promise(r => { resolve = r; }) - const subscription = { - onAdd: (_, value) => { - if (value.id === roomId) { - this._session.rooms.unsubscribe(subscription); - resolve(); - } - }, - onUpdate: () => undefined, - onRemove: () => undefined, - }; - this._session.rooms.subscribe(subscription); - return promise; - } } export function tests() { diff --git a/src/matrix/common.js b/src/matrix/common.js index ba7876ed..abd74a57 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -22,16 +22,16 @@ export function makeTxnId() { } export function isTxnId(txnId) { - return txnId.startsWith("t") && txnId.length === 15; + return txnId.startsWith("t") && txnId.length === 15; } export function tests() { - return { - "isTxnId succeeds on result of makeTxnId": assert => { - assert(isTxnId(makeTxnId())); - }, - "isTxnId fails on event id": assert => { - assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); - }, - } + return { + "isTxnId succeeds on result of makeTxnId": assert => { + assert(isTxnId(makeTxnId())); + }, + "isTxnId fails on event id": assert => { + assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); + }, + } } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index f14100b2..f611c767 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -522,26 +522,31 @@ a { .RoomView_error { color: var(--error-color); background : #efefef; - padding-right : 20px; - padding-left : 20px; - overflow : hidden; height : 0px; font-weight : bold; transition : 0.25s all ease-out; + padding-right : 20px; + padding-left : 20px; +} + +.RoomView_error div{ + overflow : hidden; + height: 100%; + width: 100%; position : relative; display : flex; + align-items : center; } .RoomView_error:not(:empty) { height : 40px; - align-items : center; padding-top : 20px; padding-bottom : 20px; } .RoomView_error p { - position : relative; - display : block; + position : relative; + display : block; width : 100%; height : auto; margin : 0; @@ -557,29 +562,29 @@ a { } .RoomView_error button:hover { - background : #cfcfcf; + background : #cfcfcf; } .RoomView_error button:after { - content:""; - position : absolute; - top : 10px; - right: 16px; - background : red; - width : 5px; - height : 20px; - transform: rotate(45deg); + content:""; + position : absolute; + top : 10px; + right: 16px; + background : red; + width : 5px; + height : 20px; + transform: rotate(45deg); } .RoomView_error button:before { - content:""; - position : absolute; - top : 17px; - left: 10px; - background : red; - width : 20px; - height : 5px; - transform: rotate(45deg); + content:""; + position : absolute; + top : 17px; + left: 10px; + background : red; + width : 20px; + height : 5px; + transform: rotate(45deg); } .MessageComposer_replyPreview .Timeline_message { diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 7cfd7d85..e3eb0587 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -47,12 +47,12 @@ export class RoomView extends TemplateView { ]), t.div({className: "RoomView_body"}, [ t.div({className: "RoomView_error"}, [ - t.if(vm => vm.error, t => t.p({}, vm => vm.error)), - t.if(vm => vm.error, t => t.button({ - className: "RoomView_error_closerButton", - onClick: evt => vm.dismissError(evt) - })) - ]), + t.if(vm => vm.error, t => t.div( + [ + t.p({}, vm => vm.error), + t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) }) + ]) + )]), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? new TimelineView(timelineViewModel, this._viewClassForTile) : From adfecf0778838b7167c1264b4125e6d2d9fdef73 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:02:20 +0200 Subject: [PATCH 114/354] Fix restoring the last url at start The last session url is now remembered for being restored at the beginning of the session. Thanks for the help of @bwindels --- src/domain/navigation/URLRouter.js | 4 ++-- src/matrix/common.js | 18 +++++++++--------- src/platform/web/dom/History.js | 5 +++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index 586eec8a..ab5dc5ce 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -27,7 +27,7 @@ export class URLRouter { } _getLastSessionId() { - const navPath = this._urlAsNavPath(this._history.getLastUrl() || ""); + const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || ""); const sessionId = navPath.get("session")?.value; if (typeof sessionId === "string") { return sessionId; @@ -84,7 +84,7 @@ export class URLRouter { } tryRestoreLastUrl() { - const lastNavPath = this._urlAsNavPath(this._history.getLastUrl() || ""); + const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || ""); if (lastNavPath.segments.length !== 0) { this._applyNavPathToNavigation(lastNavPath); return true; diff --git a/src/matrix/common.js b/src/matrix/common.js index ba7876ed..abd74a57 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -22,16 +22,16 @@ export function makeTxnId() { } export function isTxnId(txnId) { - return txnId.startsWith("t") && txnId.length === 15; + return txnId.startsWith("t") && txnId.length === 15; } export function tests() { - return { - "isTxnId succeeds on result of makeTxnId": assert => { - assert(isTxnId(makeTxnId())); - }, - "isTxnId fails on event id": assert => { - assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); - }, - } + return { + "isTxnId succeeds on result of makeTxnId": assert => { + assert(isTxnId(makeTxnId())); + }, + "isTxnId fails on event id": assert => { + assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); + }, + } } diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index d51974bb..49102e5c 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -65,6 +65,7 @@ export class History extends BaseObservableValue { } onSubscribeFirst() { + this._lastSessionHash = window.localStorage?.getItem("hydrogen_last_url_hash"); window.addEventListener('hashchange', this); } @@ -76,7 +77,7 @@ export class History extends BaseObservableValue { window.localStorage?.setItem("hydrogen_last_url_hash", hash); } - getLastUrl() { - return window.localStorage?.getItem("hydrogen_last_url_hash"); + getLastSessionUrl() { + return this._lastSessionHash; } } From 09fd1a5113963079a6fe2f30d225af3bd3736bc0 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:37:05 +0200 Subject: [PATCH 115/354] Use "args.join" instead of "message.substring" into RoomViewModel._processCommands --- src/domain/session/room/RoomViewModel.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 9b6aa966..a1939f75 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -202,7 +202,7 @@ export class RoomViewModel extends ViewModel { const [commandName, ...args] = message.substring(1).split(" "); switch (commandName) { case "me": - message = message.substring(4).trim(); + message = args.join(" "); msgtype = "m.emote"; break; case "join": @@ -232,19 +232,19 @@ export class RoomViewModel extends ViewModel { } break; case "shrug": - message = "¯\\_(ツ)_/¯ " + message.substring(7); + message = "¯\\_(ツ)_/¯ " + args.join(" "); msgtype = "m.text"; break; case "tableflip": - message="(╯°□°)╯︵ ┻━┻ " + message.substring(11); + message="(╯°□°)╯︵ ┻━┻ " + args.join(" "); msgtype = "m.text"; break; case "unflip": - message="┬──┬ ノ( ゜-゜ノ) " + message.substring(8); + message="┬──┬ ノ( ゜-゜ノ) " + args.join(" "); msgtype = "m.text"; break; case "lenny": - message="( ͡° ͜ʖ ͡°) " + message.substring(7); + message="( ͡° ͜ʖ ͡°) " + args.join(" "); msgtype = "m.text"; break; default: From 0718f1e77ebd3a8af4566df7ec2062254a1843f2 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Tue, 26 Jul 2022 11:11:16 +0200 Subject: [PATCH 116/354] Fixed the https://github.com/vector-im/hydrogen-web/pull/816#discussion_r929692693 comment Added the _lastSessionHash attribute inside the History constructor --- src/platform/web/dom/History.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index 49102e5c..ab47e18a 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -17,6 +17,12 @@ limitations under the License. import {BaseObservableValue} from "../../../observable/ObservableValue"; export class History extends BaseObservableValue { + + constructor() { + super(); + this._lastSessionHash = null; + } + handleEvent(event) { if (event.type === "hashchange") { this.emit(this.get()); From f9f49b76403482ef657d7e2638660c4d3c48ca8e Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Tue, 26 Jul 2022 14:48:03 +0200 Subject: [PATCH 117/354] Fixed an error and improving css If the /join command success, an error was thrown, because of a copy-pasted command not well integrated The button of the error on "theme.css" contains now an unicode cross. The :after/:before cross was disformed when opening the room informations. --- src/domain/session/room/RoomViewModel.js | 5 ++-- .../web/ui/css/themes/element/theme.css | 24 +++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index a1939f75..039996cd 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -23,6 +23,7 @@ import {imageToInfo} from "../common.js"; // TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry // this is a breaking SDK change though to make this option mandatory import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; +import {RoomStatus} from "../../../matrix/room/common"; export class RoomViewModel extends ViewModel { constructor(options) { @@ -210,7 +211,7 @@ export class RoomViewModel extends ViewModel { let roomName = args[0]; try { const roomId = await this._options.client.session.joinRoom(roomName); - await session.observeRoomStatus(roomId).waitFor(status === RoomStatus.Joined); + await (await this._options.client.session.observeRoomStatus(roomId)).waitFor(status => status === RoomStatus.Joined); this.navigation.push("room", roomId); } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { @@ -220,7 +221,7 @@ export class RoomViewModel extends ViewModel { } else if ((exc.statusCode ?? exc.status) === 403) { this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); } else { - this._sendError = new Error("join syntax: /join "); + this._sendError = exc; } this._timelineError = null; this.emitChange("error"); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index f611c767..0f40bdf3 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -559,32 +559,24 @@ a { background : none; border : none; position : relative; + border-radius : 5px; + transition: 0.1s all ease-out; + cursor: pointer; } .RoomView_error button:hover { background : #cfcfcf; } -.RoomView_error button:after { - content:""; - position : absolute; - top : 10px; - right: 16px; - background : red; - width : 5px; - height : 20px; - transform: rotate(45deg); -} - .RoomView_error button:before { - content:""; + content:"\274c"; position : absolute; - top : 17px; + top : 15px; left: 10px; - background : red; width : 20px; - height : 5px; - transform: rotate(45deg); + height : 10px; + font-size : 10px; + align-self : middle; } .MessageComposer_replyPreview .Timeline_message { From 4c17612b0588143a84d35a2f24928a646a117f70 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:53:02 +0200 Subject: [PATCH 118/354] allow passing txn to loadMembers so we can do it as part of sync txn to rewrite useridentities upon receiving new history visibility --- src/matrix/e2ee/DeviceTracker.js | 13 +++++++------ src/matrix/room/BaseRoom.js | 5 ++++- src/matrix/room/members/load.js | 10 ++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 8bed487d..1ba59d0a 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -116,12 +116,13 @@ export class DeviceTracker { if (room.isTrackingMembers || !room.isEncrypted) { return; } - const memberList = await room.loadMemberList(log); + const txn = await this._storage.readWriteTxn([ + this._storage.storeNames.roomSummary, + this._storage.storeNames.userIdentities, + ]); + let memberList; try { - const txn = await this._storage.readWriteTxn([ - this._storage.storeNames.roomSummary, - this._storage.storeNames.userIdentities, - ]); + memberList = await room.loadMemberList(txn, log); let isTrackingChanges; try { isTrackingChanges = room.writeIsTrackingMembers(true, txn); @@ -139,7 +140,7 @@ export class DeviceTracker { await txn.complete(); room.applyIsTrackingMembersChanges(isTrackingChanges); } finally { - memberList.release(); + memberList?.release(); } } diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index dda3e2e5..57d2a7b2 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -243,7 +243,7 @@ export class BaseRoom extends EventEmitter { /** @public */ - async loadMemberList(log = null) { + async loadMemberList(txn = undefined, log = null) { if (this._memberList) { // TODO: also await fetchOrLoadMembers promise here this._memberList.retain(); @@ -254,6 +254,9 @@ export class BaseRoom extends EventEmitter { roomId: this._roomId, hsApi: this._hsApi, storage: this._storage, + // pass in a transaction if we know we won't need to fetch (which would abort the transaction) + // and we want to make this operation part of the larger transaction + txn, syncToken: this._getSyncToken(), // to handle race between /members and /sync setChangedMembersMap: map => this._changedMembersDuringSync = map, diff --git a/src/matrix/room/members/load.js b/src/matrix/room/members/load.js index 5077d793..3d0556fc 100644 --- a/src/matrix/room/members/load.js +++ b/src/matrix/room/members/load.js @@ -17,10 +17,12 @@ limitations under the License. import {RoomMember} from "./RoomMember.js"; -async function loadMembers({roomId, storage}) { - const txn = await storage.readTxn([ - storage.storeNames.roomMembers, - ]); +async function loadMembers({roomId, storage, txn}) { + if (!txn) { + txn = await storage.readTxn([ + storage.storeNames.roomMembers, + ]); + } const memberDatas = await txn.roomMembers.getAll(roomId); return memberDatas.map(d => new RoomMember(d)); } From dea3852425eda1ec668b89f9afaa0df7843d3b26 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:57:28 +0200 Subject: [PATCH 119/354] add some tests for sharing keys with invitees --- src/matrix/e2ee/DeviceTracker.js | 165 +++++++++++++++++++++++++++---- 1 file changed, 144 insertions(+), 21 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 1ba59d0a..582a0e31 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -15,7 +15,7 @@ limitations under the License. */ import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; -import {shouldShareKey} from "./common.js"; +import {HistoryVisibility, shouldShareKey} from "./common.js"; import {RoomMember} from "../room/members/RoomMember.js"; const TRACKING_STATUS_OUTDATED = 0; @@ -150,20 +150,24 @@ export class DeviceTracker { if (room.isTrackingMembers && room.isEncrypted) { await log.wrap("rewriting userIdentities", async log => { // don't use room.loadMemberList here because we want to use the syncTxn to load the members - const memberDatas = await syncTxn.roomMembers.getAll(room.id); - const members = memberDatas.map(d => new RoomMember(d)); - log.set("members", members.length); - await Promise.all(members.map(async member => { - if (shouldShareKey(member.membership, historyVisibility)) { - if (await this._addRoomToUserIdentity(member.roomId, member.userId, syncTxn)) { - added.push(member.userId); + const memberList = await room.loadMemberList(syncTxn, log); + try { + const members = Array.from(memberList.members.values()); + log.set("members", members.length); + await Promise.all(members.map(async member => { + if (shouldShareKey(member.membership, historyVisibility)) { + if (await this._addRoomToUserIdentity(member.roomId, member.userId, syncTxn)) { + added.push(member.userId); + } + } else { + if (await this._removeRoomFromUserIdentity(member.roomId, member.userId, syncTxn)) { + removed.push(member.userId); + } } - } else { - if (await this._removeRoomFromUserIdentity(member.roomId, member.userId, syncTxn)) { - removed.push(member.userId); - } - } - })); + })); + } finally { + memberList.release(); + } }); } return {added, removed}; @@ -399,6 +403,7 @@ export class DeviceTracker { import {createMockStorage} from "../../mocks/Storage"; import {Instance as NullLoggerInstance} from "../../logging/NullLogger"; +import {MemberChange} from "../room/members/RoomMember"; export function tests() { @@ -407,8 +412,8 @@ export function tests() { isTrackingMembers: false, isEncrypted: true, loadMemberList: () => { - const joinedMembers = joinedUserIds.map(userId => {return {membership: "join", roomId, userId};}); - const invitedMembers = invitedUserIds.map(userId => {return {membership: "invite", roomId, userId};}); + const joinedMembers = joinedUserIds.map(userId => {return RoomMember.fromUserId(roomId, userId, "join");}); + const invitedMembers = invitedUserIds.map(userId => {return RoomMember.fromUserId(roomId, userId, "invite");}); const members = joinedMembers.concat(invitedMembers); const memberMap = members.reduce((map, member) => { map.set(member.userId, member); @@ -472,10 +477,29 @@ export function tests() { } }; } + + async function writeMemberListToStorage(room, storage) { + const txn = await storage.readWriteTxn([ + storage.storeNames.roomMembers, + ]); + const memberList = await room.loadMemberList(txn); + try { + for (const member of memberList.members.values()) { + txn.roomMembers.set(member.serialize()); + } + } catch (err) { + txn.abort(); + throw err; + } finally { + memberList.release(); + } + await txn.complete(); + } + const roomId = "!abc:hs.tld"; return { - "trackRoom only writes joined members": async assert => { + "trackRoom only writes joined members with history visibility of joined": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, @@ -485,7 +509,7 @@ export function tests() { ownDeviceId: "ABCD", }); const room = createUntrackedRoomMock(roomId, ["@alice:hs.tld", "@bob:hs.tld"], ["@charly:hs.tld"]); - await tracker.trackRoom(room, NullLoggerInstance.item); + await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); const txn = await storage.readTxn([storage.storeNames.userIdentities]); assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), { userId: "@alice:hs.tld", @@ -509,7 +533,7 @@ export function tests() { ownDeviceId: "ABCD", }); const room = createUntrackedRoomMock(roomId, ["@alice:hs.tld", "@bob:hs.tld"]); - await tracker.trackRoom(room, NullLoggerInstance.item); + await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); const hsApi = createQueryKeysHSApiMock(); const devices = await tracker.devicesForRoomMembers(roomId, ["@alice:hs.tld", "@bob:hs.tld"], hsApi, NullLoggerInstance.item); assert.equal(devices.length, 2); @@ -526,7 +550,7 @@ export function tests() { ownDeviceId: "ABCD", }); const room = createUntrackedRoomMock(roomId, ["@alice:hs.tld", "@bob:hs.tld"]); - await tracker.trackRoom(room, NullLoggerInstance.item); + await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); const hsApi = createQueryKeysHSApiMock(); // query devices first time await tracker.devicesForRoomMembers(roomId, ["@alice:hs.tld", "@bob:hs.tld"], hsApi, NullLoggerInstance.item); @@ -544,6 +568,105 @@ export function tests() { const txn2 = await storage.readTxn([storage.storeNames.deviceIdentities]); // also check the modified key was not stored assert.equal((await txn2.deviceIdentities.get("@alice:hs.tld", "device1")).ed25519Key, "ed25519:@alice:hs.tld:device1:key"); - } + }, + "change history visibility from joined to invited adds invitees": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + // alice is joined, bob is invited + const room = await createUntrackedRoomMock(roomId, + ["@alice:hs.tld"], ["@bob:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); + const {added, removed} = await tracker.writeHistoryVisibility(room, HistoryVisibility.Invited, txn, NullLoggerInstance.item); + assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + assert.deepEqual(added, ["@bob:hs.tld"]); + assert.deepEqual(removed, []); + }, + "change history visibility from invited to joined removes invitees": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + // alice is joined, bob is invited + const room = await createUntrackedRoomMock(roomId, + ["@alice:hs.tld"], ["@bob:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + const {added, removed} = await tracker.writeHistoryVisibility(room, HistoryVisibility.Joined, txn, NullLoggerInstance.item); + assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); + assert.deepEqual(added, []); + assert.deepEqual(removed, ["@bob:hs.tld"]); + }, + "adding invitee with history visibility of invited adds room to userIdentities": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + // inviting a new member + const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "invite")); + const {added, removed} = await tracker.writeMemberChanges(room, [inviteChange], HistoryVisibility.Invited, txn); + assert.deepEqual(added, ["@bob:hs.tld"]); + assert.deepEqual(removed, []); + assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + }, + "adding invitee with history visibility of joined doesn't add room": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + // inviting a new member + const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "invite")); + const memberChanges = new Map([[inviteChange.userId, inviteChange]]); + const {added, removed} = await tracker.writeMemberChanges(room, memberChanges, HistoryVisibility.Joined, txn); + assert.deepEqual(added, []); + assert.deepEqual(removed, []); + assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); + }, + "getting all devices after changing history visibility now includes invitees": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + const room = createUntrackedRoomMock(roomId, ["@alice:hs.tld"], ["@bob:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); + const hsApi = createQueryKeysHSApiMock(); + // write memberlist from room mock to mock storage, + // as devicesForTrackedRoom reads directly from roomMembers store. + await writeMemberListToStorage(room, storage); + const devices = await tracker.devicesForTrackedRoom(roomId, hsApi, NullLoggerInstance.item); + assert.equal(devices.length, 2); + assert.equal(devices.find(d => d.userId === "@alice:hs.tld").ed25519Key, "ed25519:@alice:hs.tld:device1:key"); + assert.equal(devices.find(d => d.userId === "@bob:hs.tld").ed25519Key, "ed25519:@bob:hs.tld:device1:key"); + }, } } From dd878bb8d6e7ef36994d7e8a53b0983112ea98b7 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:58:07 +0200 Subject: [PATCH 120/354] also take rejecting invites into account to remove user identity --- src/matrix/e2ee/DeviceTracker.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 582a0e31..3fe88357 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -94,8 +94,8 @@ export class DeviceTracker { if (await this._addRoomToUserIdentity(memberChange.roomId, memberChange.userId, txn)) { added.push(memberChange.userId); } - } else if (memberChange.hasLeft) { - // remove room + } else if (shouldShareKey(memberChange.previousMembership, historyVisibility)) { + // try to remove room we were previously sharing the key with the member but not anymore const {roomId} = memberChange; // if we left the room, remove room from all user identities in the room if (memberChange.userId === this._ownUserId) { @@ -668,5 +668,26 @@ export function tests() { assert.equal(devices.find(d => d.userId === "@alice:hs.tld").ed25519Key, "ed25519:@alice:hs.tld:device1:key"); assert.equal(devices.find(d => d.userId === "@bob:hs.tld").ed25519Key, "ed25519:@bob:hs.tld:device1:key"); }, + "rejecting invite with history visibility of invited removes room from user identity": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + // alice is joined, bob is invited + const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"], ["@bob:hs.tld"]); + await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + // reject invite + const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "leave"), "invite"); + const memberChanges = new Map([[inviteChange.userId, inviteChange]]); + const {added, removed} = await tracker.writeMemberChanges(room, memberChanges, HistoryVisibility.Invited, txn); + assert.deepEqual(added, []); + assert.deepEqual(removed, ["@bob:hs.tld"]); + assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); + }, } } From 544afef90249c851849f24c405ab8e4b6208ab8f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:41:26 +0200 Subject: [PATCH 121/354] test adding and removing when tracking multiple rooms --- src/matrix/e2ee/DeviceTracker.js | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 3fe88357..edf3c133 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -409,6 +409,7 @@ export function tests() { function createUntrackedRoomMock(roomId, joinedUserIds, invitedUserIds = []) { return { + id: roomId, isTrackingMembers: false, isEncrypted: true, loadMemberList: () => { @@ -689,5 +690,48 @@ export function tests() { assert.deepEqual(removed, ["@bob:hs.tld"]); assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); }, + "remove room from user identity sharing multiple rooms with us preserves other room": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + // alice is joined, bob is invited + const room1 = await createUntrackedRoomMock("!abc:hs.tld", ["@alice:hs.tld", "@bob:hs.tld"]); + const room2 = await createUntrackedRoomMock("!def:hs.tld", ["@alice:hs.tld", "@bob:hs.tld"]); + await tracker.trackRoom(room1, HistoryVisibility.Joined, NullLoggerInstance.item); + await tracker.trackRoom(room2, HistoryVisibility.Joined, NullLoggerInstance.item); + const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld", "!def:hs.tld"]); + const leaveChange = new MemberChange(RoomMember.fromUserId(room2.id, "@bob:hs.tld", "leave"), "join"); + const memberChanges = new Map([[leaveChange.userId, leaveChange]]); + const txn2 = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + await tracker.writeMemberChanges(room2, memberChanges, HistoryVisibility.Joined, txn2); + await txn2.complete(); + const txn3 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn3.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld"]); + }, + "add room to user identity sharing multiple rooms with us preserves other room": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + // alice is joined, bob is invited + const room1 = await createUntrackedRoomMock("!abc:hs.tld", ["@alice:hs.tld", "@bob:hs.tld"]); + const room2 = await createUntrackedRoomMock("!def:hs.tld", ["@alice:hs.tld", "@bob:hs.tld"]); + await tracker.trackRoom(room1, HistoryVisibility.Joined, NullLoggerInstance.item); + const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld"]); + await tracker.trackRoom(room2, HistoryVisibility.Joined, NullLoggerInstance.item); + const txn2 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn2.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld", "!def:hs.tld"]); + }, } } From bfaba63f473c84e65413806752b712b87fbc035b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:55:21 +0200 Subject: [PATCH 122/354] fix ts error --- src/matrix/room/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/common.ts b/src/matrix/room/common.ts index 5a7fc98e..4556302f 100644 --- a/src/matrix/room/common.ts +++ b/src/matrix/room/common.ts @@ -80,7 +80,7 @@ export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: } } if (promises) { - return Promise.all(promises); + return Promise.all(promises).then(() => undefined); } } From 9b0ab0c8f1d8cbd9fef72b565b635a2be89687d0 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 09:19:36 +0200 Subject: [PATCH 123/354] Used "null" instead of "undefined" When creating the this._lastSessionHash attribute of History --- src/platform/web/dom/History.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index ab47e18a..d40f501b 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -20,7 +20,7 @@ export class History extends BaseObservableValue { constructor() { super(); - this._lastSessionHash = null; + this._lastSessionHash = undefined; } handleEvent(event) { From 50b6ee91d72d69e45cd97d1f480fa6d1127113f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 27 Jul 2022 11:39:36 +0200 Subject: [PATCH 124/354] don't need history visibility here --- src/matrix/e2ee/RoomEncryption.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index a841a6bd..f49d5c97 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -323,8 +323,7 @@ export class RoomEncryption { } async _shareNewRoomKey(roomKeyMessage, hsApi, log) { - this._historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility); - const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, this._historyVisibility, hsApi, log); + const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); let writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); From f18520a2fe2da30a3c4028be00d98c60467cec1a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 27 Jul 2022 11:39:50 +0200 Subject: [PATCH 125/354] let loadMembers use own txn in case members haven't been fetched yet if they haven't, it will need a network request, meaning that the txn will get closed, so we can't reuse it afterwards --- src/matrix/e2ee/DeviceTracker.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index edf3c133..55051994 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -116,13 +116,12 @@ export class DeviceTracker { if (room.isTrackingMembers || !room.isEncrypted) { return; } + const memberList = await room.loadMemberList(undefined, log); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.roomSummary, this._storage.storeNames.userIdentities, ]); - let memberList; try { - memberList = await room.loadMemberList(txn, log); let isTrackingChanges; try { isTrackingChanges = room.writeIsTrackingMembers(true, txn); @@ -140,7 +139,7 @@ export class DeviceTracker { await txn.complete(); room.applyIsTrackingMembersChanges(isTrackingChanges); } finally { - memberList?.release(); + memberList.release(); } } From 0df66b5aea6b448ea7ec03c3c26f65ca63c45f93 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:06:55 +0200 Subject: [PATCH 126/354] track room before listing user ids when sharing key --- src/matrix/e2ee/RoomEncryption.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index f49d5c97..49ca1a2f 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -323,6 +323,8 @@ export class RoomEncryption { } async _shareNewRoomKey(roomKeyMessage, hsApi, log) { + this._historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility); + await this._deviceTracker.trackRoom(this._room, this._historyVisibility, log); const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); From 550b9db4dc365c3f361e046141119e94fb932562 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:21:00 +0200 Subject: [PATCH 127/354] Separated the join instructions into a executeJoinCommand method --- src/domain/session/room/RoomViewModel.js | 52 +++++++++++++----------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 039996cd..82fcc785 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -198,6 +198,33 @@ export class RoomViewModel extends ViewModel { } } + async executeJoinCommand(args) { + if (args.length == 1) { + let roomName = args[0]; + try { + const roomId = await this._options.client.session.joinRoom(roomName); + await (await this._options.client.session.observeRoomStatus(roomId)).waitFor(status => status === RoomStatus.Joined); + this.navigation.push("room", roomId); + } catch (exc) { + if ((exc.statusCode ?? exc.status) === 400) { + this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); + } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + this._sendError = new Error(`/join : room '${roomName}' not found`); + } else if ((exc.statusCode ?? exc.status) === 403) { + this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); + } else { + this._sendError = exc; + } + this._timelineError = null; + this.emitChange("error"); + } + } else { + this._sendError = new Error("join syntax: /join "); + this._timelineError = null; + this.emitChange("error"); + } + } + async _processCommand (message) { let msgtype = undefined; const [commandName, ...args] = message.substring(1).split(" "); @@ -207,30 +234,7 @@ export class RoomViewModel extends ViewModel { msgtype = "m.emote"; break; case "join": - if (args.length == 1) { - let roomName = args[0]; - try { - const roomId = await this._options.client.session.joinRoom(roomName); - await (await this._options.client.session.observeRoomStatus(roomId)).waitFor(status => status === RoomStatus.Joined); - this.navigation.push("room", roomId); - } catch (exc) { - if ((exc.statusCode ?? exc.status) === 400) { - this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - this._sendError = new Error(`/join : room '${roomName}' not found`); - } else if ((exc.statusCode ?? exc.status) === 403) { - this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); - } else { - this._sendError = exc; - } - this._timelineError = null; - this.emitChange("error"); - } - } else { - this._sendError = new Error("join syntax: /join "); - this._timelineError = null; - this.emitChange("error"); - } + await this.executeJoinCommand(args); break; case "shrug": message = "¯\\_(ツ)_/¯ " + args.join(" "); From 2d3b6fe973b2b1e66a5db985d357a60e1674bd71 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:40:19 +0200 Subject: [PATCH 128/354] Canceled indentation modification. --- src/matrix/common.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/matrix/common.js b/src/matrix/common.js index abd74a57..ba7876ed 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -22,16 +22,16 @@ export function makeTxnId() { } export function isTxnId(txnId) { - return txnId.startsWith("t") && txnId.length === 15; + return txnId.startsWith("t") && txnId.length === 15; } export function tests() { - return { - "isTxnId succeeds on result of makeTxnId": assert => { - assert(isTxnId(makeTxnId())); - }, - "isTxnId fails on event id": assert => { - assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); - }, - } + return { + "isTxnId succeeds on result of makeTxnId": assert => { + assert(isTxnId(makeTxnId())); + }, + "isTxnId fails on event id": assert => { + assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); + }, + } } From ab64ce02b22d8002ee4874751ecdec9fb2c8424d Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 15:18:32 +0200 Subject: [PATCH 129/354] Separated the _processCommand and the joinRoom command - renamed executeJoinCommand as joinRoom; - separated the joinRoom process and the parse and result process --- src/domain/session/room/RoomViewModel.js | 57 ++++++++++++++---------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 82fcc785..b66545c6 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -198,30 +198,23 @@ export class RoomViewModel extends ViewModel { } } - async executeJoinCommand(args) { - if (args.length == 1) { - let roomName = args[0]; - try { - const roomId = await this._options.client.session.joinRoom(roomName); - await (await this._options.client.session.observeRoomStatus(roomId)).waitFor(status => status === RoomStatus.Joined); - this.navigation.push("room", roomId); - } catch (exc) { - if ((exc.statusCode ?? exc.status) === 400) { - this._sendError = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - this._sendError = new Error(`/join : room '${roomName}' not found`); - } else if ((exc.statusCode ?? exc.status) === 403) { - this._sendError = new Error(`/join : you're not invited to join '${roomName}'`); - } else { - this._sendError = exc; - } - this._timelineError = null; - this.emitChange("error"); + async joinRoom(roomName) { + try { + const roomId = await this._options.client.session.joinRoom(roomName); + const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId); + await roomStatusObserver.waitFor(status => status === RoomStatus.Joined); + this.navigation.push("room", roomId); + return true; + } catch (exc) { + if ((exc.statusCode ?? exc.status) === 400) { + return `'${roomName}' was not legal room ID or room alias`; + } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { + return `room '${roomName}' not found`; + } else if ((exc.statusCode ?? exc.status) === 403) { + return `you're not invited to join '${roomName}'`; + } else { + return exc; } - } else { - this._sendError = new Error("join syntax: /join "); - this._timelineError = null; - this.emitChange("error"); } } @@ -234,7 +227,23 @@ export class RoomViewModel extends ViewModel { msgtype = "m.emote"; break; case "join": - await this.executeJoinCommand(args); + if (args.length == 1) { + const roomName = args[0]; + const exc = await this.joinRoom(roomName); + if (exc!==true) { + if (exc && exc.stack && exc.message) { + this._sendError = exc; + } else { + this._sendError = new Error("/join : " + exc); + } + this._timelineError = null; + this.emitChange("error"); + } + } else { + this._sendError = new Error("join syntax: /join "); + this._timelineError = null; + this.emitChange("error"); + } break; case "shrug": message = "¯\\_(ツ)_/¯ " + args.join(" "); From a40bb59dc0461caf39cc1b066978f36da4c3ecf4 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 16:36:58 +0200 Subject: [PATCH 130/354] Some fixes : - fixed a pretty syntax miss (a !== b); - fixed a type error : replaced "msgtype" by "type" when instantied the "messinfo" variable; - some indentation fixes --- src/domain/session/room/RoomViewModel.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index b66545c6..a6db142b 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -230,7 +230,7 @@ export class RoomViewModel extends ViewModel { if (args.length == 1) { const roomName = args[0]; const exc = await this.joinRoom(roomName); - if (exc!==true) { + if (exc !== true) { if (exc && exc.stack && exc.message) { this._sendError = exc; } else { @@ -273,21 +273,21 @@ export class RoomViewModel extends ViewModel { async _sendMessage(message, replyingTo) { if (!this._room.isArchived && message) { - let messinfo = {msgtype : "m.text", message : message}; + let messinfo = {type : "m.text", message : message}; if (message.startsWith("//")) { messinfo.message = message.substring(1).trim(); } else if (message.startsWith("/")) { messinfo = await this._processCommand(message); } try { - const msgtype = messinfo.type; - const message = messinfo.message; - if (msgtype && message) { - if (replyingTo) { - await replyingTo.reply(msgtype, message); - } else { - await this._room.sendEvent("m.room.message", {msgtype, body: message}); - } + const msgtype = messinfo.type; + const message = messinfo.message; + if (msgtype && message) { + if (replyingTo) { + await replyingTo.reply(msgtype, message); + } else { + await this._room.sendEvent("m.room.message", {msgtype, body: message}); + } } } catch (err) { console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); From 176caf340fb1ddf53ec411a38f33c5e009bf6b76 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Wed, 27 Jul 2022 16:42:44 +0200 Subject: [PATCH 131/354] Placed the join command outside of the processCommand method --- src/domain/session/room/RoomViewModel.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index a6db142b..74cb3d05 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -218,6 +218,19 @@ export class RoomViewModel extends ViewModel { } } + async _processCommandJoin(roomName) { + const exc = await this.joinRoom(roomName); + if (exc !== true) { + if (exc && exc.stack && exc.message) { + this._sendError = exc; + } else { + this._sendError = new Error("/join : " + exc); + } + this._timelineError = null; + this.emitChange("error"); + } + } + async _processCommand (message) { let msgtype = undefined; const [commandName, ...args] = message.substring(1).split(" "); @@ -229,16 +242,7 @@ export class RoomViewModel extends ViewModel { case "join": if (args.length == 1) { const roomName = args[0]; - const exc = await this.joinRoom(roomName); - if (exc !== true) { - if (exc && exc.stack && exc.message) { - this._sendError = exc; - } else { - this._sendError = new Error("/join : " + exc); - } - this._timelineError = null; - this.emitChange("error"); - } + await this._processCommandJoin(roomName); } else { this._sendError = new Error("join syntax: /join "); this._timelineError = null; From d7657dcc4d850094c9cfeedf13d1450cbecc49ef Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 30 Jun 2022 21:20:13 -0400 Subject: [PATCH 132/354] first draft of fully typescriptified LoginViewModel.ts --- .ts-eslintrc.js | 3 +- src/domain/LogoutViewModel.ts | 8 +-- src/domain/RootViewModel.js | 4 +- src/domain/ViewModel.ts | 6 +- src/domain/login/LoginViewModel.ts | 97 +++++++++++++++++++----------- src/matrix/Client.js | 10 +-- 6 files changed, 79 insertions(+), 49 deletions(-) diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index 1974e07b..cf1fc3bf 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -19,6 +19,7 @@ module.exports = { ], rules: { "@typescript-eslint/no-floating-promises": 2, - "@typescript-eslint/no-misused-promises": 2 + "@typescript-eslint/no-misused-promises": 2, + "semi": ["error", "always"] } }; diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index 3edfcad5..b0409edd 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Options, ViewModel} from "./ViewModel"; +import {Options as BaseOptions, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; -type LogoutOptions = { sessionId: string; } & Options; +type Options = { sessionId: string; } & BaseOptions; -export class LogoutViewModel extends ViewModel { +export class LogoutViewModel extends ViewModel { private _sessionId: string; private _busy: boolean; private _showConfirm: boolean; private _error?: Error; - constructor(options: LogoutOptions) { + constructor(options: Options) { super(options); this._sessionId = options.sessionId; this._busy = false; diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 2711cd2f..a4729b9e 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {Client} from "../matrix/Client.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -import {LoginViewModel} from "./login/LoginViewModel.js"; +import {LoginViewModel} from "./login/LoginViewModel.ts"; import {LogoutViewModel} from "./LogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel"; @@ -118,7 +118,7 @@ export class RootViewModel extends ViewModel { // but we also want the change of screen to go through the navigation // so we store the session container in a temporary variable that will be // consumed by _applyNavigation, triggered by the navigation change - // + // // Also, we should not call _setSection before the navigation is in the correct state, // as url creation (e.g. in RoomTileViewModel) // won't be using the correct navigation base path. diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 0bc52f6e..63e64676 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -62,7 +62,7 @@ export class ViewModel extends EventEmitter<{change const segmentObservable = this.navigation.observe(type); const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { onChange(value, type); - }) + }); this.track(unsubscribe); } @@ -100,7 +100,7 @@ export class ViewModel extends EventEmitter<{change // TODO: this will need to support binding // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves - // + // // translated string should probably always be bindings, unless we're fine with a refresh when changing the language? // we probably are, if we're using routing with a url, we could just refresh. i18n(parts: TemplateStringsArray, ...expr: any[]) { @@ -115,7 +115,7 @@ export class ViewModel extends EventEmitter<{change return result; } - emitChange(changedProps: any): void { + emitChange(changedProps?: any): void { if (this._options.emitChange) { this._options.emitChange(changedProps); } else { diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index bf77e624..34c86b2d 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -15,34 +15,51 @@ limitations under the License. */ import {Client} from "../../matrix/Client.js"; -import {ViewModel} from "../ViewModel"; +import {Options as BaseOptions, ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; -export class LoginViewModel extends ViewModel { - constructor(options) { +// TODO(isaiah): make these default exported +import {PasswordLoginMethod} from "../../matrix/login/PasswordLoginMethod"; +import {SSOLoginHelper} from "../../matrix/login/SSOLoginHelper"; +import {TokenLoginMethod} from "../../matrix/login/TokenLoginMethod"; + +type Options = { + defaultHomeserver: string; + ready: ReadyFn; + loginToken?: string; +} & BaseOptions; + +export class LoginViewModel extends ViewModel { + private _ready: ReadyFn; + private _loginToken?: string; + private _client: Client; + private _loginOptions?: LoginOptions; + private _passwordLoginViewModel?: PasswordLoginViewModel; + private _startSSOLoginViewModel?: StartSSOLoginViewModel; + private _completeSSOLoginViewModel?: CompleteSSOLoginViewModel; + private _loadViewModel?: SessionLoadViewModel; + private _loadViewModelSubscription?: () => void; + private _homeserver: string; + private _queriedHomeserver?: string; + private _abortHomeserverQueryTimeout?: () => void; + private _abortQueryOperation?: () => void; + + private _hideHomeserver = false; + private _isBusy = false; + private _errorMessage = ""; + + constructor(options: Readonly) { super(options); const {ready, defaultHomeserver, loginToken} = options; this._ready = ready; this._loginToken = loginToken; this._client = new Client(this.platform); - this._loginOptions = null; - this._passwordLoginViewModel = null; - this._startSSOLoginViewModel = null; - this._completeSSOLoginViewModel = null; - this._loadViewModel = null; - this._loadViewModelSubscription = null; this._homeserver = defaultHomeserver; - this._queriedHomeserver = null; - this._errorMessage = ""; - this._hideHomeserver = false; - this._isBusy = false; - this._abortHomeserverQueryTimeout = null; - this._abortQueryOperation = null; - this._initViewModels(); + void this._initViewModels(); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } @@ -67,7 +84,7 @@ export class LoginViewModel extends ViewModel { this.childOptions( { client: this._client, - attemptLogin: loginMethod => this.attemptLogin(loginMethod), + attemptLogin: (loginMethod: PasswordLoginMethod) => this.attemptLogin(loginMethod), loginToken: this._loginToken }))); this.emitChange("completeSSOLoginViewModel"); @@ -81,7 +98,7 @@ export class LoginViewModel extends ViewModel { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ loginOptions: this._loginOptions, - attemptLogin: loginMethod => this.attemptLogin(loginMethod) + attemptLogin: (loginMethod: PasswordLoginMethod) => this.attemptLogin(loginMethod) }))); this.emitChange("passwordLoginViewModel"); } @@ -93,23 +110,23 @@ export class LoginViewModel extends ViewModel { this.emitChange("startSSOLoginViewModel"); } - _showError(message) { + _showError(message: string) { this._errorMessage = message; this.emitChange("errorMessage"); } - _setBusy(status) { + _setBusy(status: boolean) { this._isBusy = status; this._passwordLoginViewModel?.setBusy(status); this._startSSOLoginViewModel?.setBusy(status); this.emitChange("isBusy"); } - async attemptLogin(loginMethod) { + async attemptLogin(loginMethod: PasswordLoginMethod) { this._setBusy(true); - this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); + await this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); const loadStatus = this._client.loadStatus; - const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); + const handle = loadStatus.waitFor((status: LoadStatus) => status !== LoadStatus.Login); await handle.promise; this._setBusy(false); const status = loadStatus.get(); @@ -119,11 +136,11 @@ export class LoginViewModel extends ViewModel { this._hideHomeserver = true; this.emitChange("hideHomeserver"); this._disposeViewModels(); - this._createLoadViewModel(); + await this._createLoadViewModel(); return null; } - _createLoadViewModel() { + async _createLoadViewModel() { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.track( @@ -139,7 +156,7 @@ export class LoginViewModel extends ViewModel { }) ) ); - this._loadViewModel.start(); + await this._loadViewModel.start(); this.emitChange("loadViewModel"); this._loadViewModelSubscription = this.track( this._loadViewModel.disposableOn("change", () => { @@ -152,7 +169,7 @@ export class LoginViewModel extends ViewModel { } _disposeViewModels() { - this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + this._startSSOLoginViewModel = this.disposeTracked(this._startSSOLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); this.emitChange("disposeViewModels"); @@ -161,8 +178,8 @@ export class LoginViewModel extends ViewModel { async setHomeserver(newHomeserver) { this._homeserver = newHomeserver; // clear everything set by queryHomeserver - this._loginOptions = null; - this._queriedHomeserver = null; + this._loginOptions = undefined; + this._queriedHomeserver = undefined; this._showError(""); this._disposeViewModels(); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); @@ -181,9 +198,9 @@ export class LoginViewModel extends ViewModel { } } this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); - this.queryHomeserver(); + await this.queryHomeserver(); } - + async queryHomeserver() { // don't repeat a query we've just done if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { @@ -210,7 +227,7 @@ export class LoginViewModel extends ViewModel { if (e.name === "AbortError") { return; //aborted, bail out } else { - this._loginOptions = null; + this._loginOptions = undefined; } } finally { this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); @@ -221,19 +238,29 @@ export class LoginViewModel extends ViewModel { if (this._loginOptions.password) { this._showPasswordLogin(); } if (!this._loginOptions.sso && !this._loginOptions.password) { this._showError("This homeserver supports neither SSO nor password based login flows"); - } + } } else { this._showError(`Could not query login methods supported by ${this.homeserver}`); } } - dispose() { + async dispose() { super.dispose(); if (this._client) { // if we move away before we're done with initial sync // delete the session - this._client.deleteSession(); + await this._client.deleteSession(); } } } + +type ReadyFn = (client: Client) => void; + +// TODO: move to Client.js when its converted to typescript. +type LoginOptions = { + homeserver: string; + password?: (username: string, password: string) => PasswordLoginMethod; + sso?: SSOLoginHelper; + token?: (loginToken: string) => TokenLoginMethod; +}; \ No newline at end of file diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 21175a7f..44643cc1 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -100,6 +100,8 @@ export class Client { }); } + // TODO: When converted to typescript this should return the same type + // as this._loginOptions is in LoginViewModel.ts (LoginOptions). _parseLoginOptions(options, homeserver) { /* Take server response and return new object which has two props password and sso which @@ -136,7 +138,7 @@ export class Client { const request = this._platform.request; const hsApi = new HomeServerApi({homeserver, request}); const registration = new Registration(hsApi, { - username, + username, password, initialDeviceDisplayName, }, @@ -196,7 +198,7 @@ export class Client { sessionInfo.deviceId = dehydratedDevice.deviceId; } } - await this._platform.sessionInfoStorage.add(sessionInfo); + await this._platform.sessionInfoStorage.add(sessionInfo); // loading the session can only lead to // LoadStatus.Error in case of an error, // so separate try/catch @@ -266,7 +268,7 @@ export class Client { this._status.set(LoadStatus.SessionSetup); await log.wrap("createIdentity", log => this._session.createIdentity(log)); } - + this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session, logger: this._platform.logger}); // notify sync and session when back online this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { @@ -311,7 +313,7 @@ export class Client { this._waitForFirstSyncHandle = this._sync.status.waitFor(s => { if (s === SyncStatus.Stopped) { // keep waiting if there is a ConnectionError - // as the reconnector above will call + // as the reconnector above will call // sync.start again to retry in this case return this._sync.error?.name !== "ConnectionError"; } From ad0bd82bdaddd339cea0819dac5064b8106084d6 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 30 Jun 2022 21:36:00 -0400 Subject: [PATCH 133/354] creating default exports --- src/domain/login/LoginViewModel.ts | 6 +----- src/matrix/login/index.ts | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 src/matrix/login/index.ts diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 34c86b2d..57366e7a 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -21,11 +21,7 @@ import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; - -// TODO(isaiah): make these default exported -import {PasswordLoginMethod} from "../../matrix/login/PasswordLoginMethod"; -import {SSOLoginHelper} from "../../matrix/login/SSOLoginHelper"; -import {TokenLoginMethod} from "../../matrix/login/TokenLoginMethod"; +import {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod} from "../../matrix/login"; type Options = { defaultHomeserver: string; diff --git a/src/matrix/login/index.ts b/src/matrix/login/index.ts new file mode 100644 index 00000000..7018449d --- /dev/null +++ b/src/matrix/login/index.ts @@ -0,0 +1,5 @@ +import {PasswordLoginMethod} from "./PasswordLoginMethod"; +import {SSOLoginHelper} from "./SSOLoginHelper"; +import {TokenLoginMethod} from "./TokenLoginMethod"; + +export {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod}; \ No newline at end of file From 7b7b19476c44b697e240d63efb6302001a41e697 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Wed, 6 Jul 2022 20:57:57 -0400 Subject: [PATCH 134/354] updates some signatures to be more verbose, fixes wrong type for attemptLogin --- src/domain/login/LoginViewModel.ts | 63 ++++++++++++++++++++++-------- src/matrix/login/index.ts | 4 +- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 57366e7a..71cff624 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -21,7 +21,7 @@ import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; -import {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod} from "../../matrix/login"; +import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login"; type Options = { defaultHomeserver: string; @@ -44,9 +44,9 @@ export class LoginViewModel extends ViewModel { private _abortHomeserverQueryTimeout?: () => void; private _abortQueryOperation?: () => void; - private _hideHomeserver = false; - private _isBusy = false; - private _errorMessage = ""; + private _hideHomeserver: boolean = false; + private _isBusy: boolean = false; + private _errorMessage: string = ""; constructor(options: Readonly) { super(options); @@ -58,16 +58,45 @@ export class LoginViewModel extends ViewModel { void this._initViewModels(); } - get passwordLoginViewModel() { return this._passwordLoginViewModel; } - get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } - get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } - get homeserver() { return this._homeserver; } - get resolvedHomeserver() { return this._loginOptions?.homeserver; } - get errorMessage() { return this._errorMessage; } - get showHomeserver() { return !this._hideHomeserver; } - get loadViewModel() {return this._loadViewModel; } - get isBusy() { return this._isBusy; } - get isFetchingLoginOptions() { return !!this._abortQueryOperation; } + get passwordLoginViewModel(): PasswordLoginViewModel { + return this._passwordLoginViewModel; + } + + get startSSOLoginViewModel(): StartSSOLoginViewModel { + return this._startSSOLoginViewModel; + } + + get completeSSOLoginViewModel(): CompleteSSOLoginViewModel { + return this._completeSSOLoginViewModel; + } + + get homeserver(): string { + return this._homeserver; + } + + get resolvedHomeserver(): string | undefined { + return this._loginOptions?.homeserver; + } + + get errorMessage(): string { + return this._errorMessage; + } + + get showHomeserver(): boolean { + return !this._hideHomeserver; + } + + get loadViewModel(): SessionLoadViewModel { + return this._loadViewModel; + } + + get isBusy(): boolean { + return this._isBusy; + } + + get isFetchingLoginOptions(): boolean { + return !!this._abortQueryOperation; + } goBack() { this.navigation.push("session"); @@ -80,7 +109,7 @@ export class LoginViewModel extends ViewModel { this.childOptions( { client: this._client, - attemptLogin: (loginMethod: PasswordLoginMethod) => this.attemptLogin(loginMethod), + attemptLogin: (loginMethod: TokenLoginMethod) => this.attemptLogin(loginMethod), loginToken: this._loginToken }))); this.emitChange("completeSSOLoginViewModel"); @@ -118,7 +147,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("isBusy"); } - async attemptLogin(loginMethod: PasswordLoginMethod) { + async attemptLogin(loginMethod: ILoginMethod) { this._setBusy(true); await this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); const loadStatus = this._client.loadStatus; @@ -171,7 +200,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - async setHomeserver(newHomeserver) { + async setHomeserver(newHomeserver: string) { this._homeserver = newHomeserver; // clear everything set by queryHomeserver this._loginOptions = undefined; diff --git a/src/matrix/login/index.ts b/src/matrix/login/index.ts index 7018449d..ba133a26 100644 --- a/src/matrix/login/index.ts +++ b/src/matrix/login/index.ts @@ -1,5 +1,7 @@ +import {ILoginMethod} from "./LoginMethod"; import {PasswordLoginMethod} from "./PasswordLoginMethod"; import {SSOLoginHelper} from "./SSOLoginHelper"; import {TokenLoginMethod} from "./TokenLoginMethod"; -export {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod}; \ No newline at end of file + +export {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod}; \ No newline at end of file From aeed9787895da4f517015a1d3ea259f3924588dd Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 26 Jul 2022 20:44:15 -0700 Subject: [PATCH 135/354] changes signature of emitChange to require changedProps --- src/domain/RootViewModel.js | 2 +- src/domain/ViewModel.ts | 2 +- src/domain/login/LoginViewModel.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index a4729b9e..4094d864 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {Client} from "../matrix/Client.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -import {LoginViewModel} from "./login/LoginViewModel.ts"; +import {LoginViewModel} from "./login/LoginViewModel"; import {LogoutViewModel} from "./LogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel"; diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 63e64676..7cf21861 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -115,7 +115,7 @@ export class ViewModel extends EventEmitter<{change return result; } - emitChange(changedProps?: any): void { + emitChange(changedProps: any): void { if (this._options.emitChange) { this._options.emitChange(changedProps); } else { diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 71cff624..d9bf2e64 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -208,7 +208,7 @@ export class LoginViewModel extends ViewModel { this._showError(""); this._disposeViewModels(); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this.emitChange(); // multiple fields changing + this.emitChange("loginViewModels"); // multiple fields changing // also clear the timeout if it is still running this.disposeTracked(this._abortHomeserverQueryTimeout); const timeout = this.clock.createTimeout(1000); From a5b9cb6b95027a01e52c7ab3c95aa6c8b308bc73 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 26 Jul 2022 20:54:06 -0700 Subject: [PATCH 136/354] removes unnecessary awaits --- src/domain/login/LoginViewModel.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index d9bf2e64..d0ae9d9a 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -147,9 +147,9 @@ export class LoginViewModel extends ViewModel { this.emitChange("isBusy"); } - async attemptLogin(loginMethod: ILoginMethod) { + async attemptLogin(loginMethod: ILoginMethod): Promise { this._setBusy(true); - await this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); + void this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); const loadStatus = this._client.loadStatus; const handle = loadStatus.waitFor((status: LoadStatus) => status !== LoadStatus.Login); await handle.promise; @@ -161,11 +161,11 @@ export class LoginViewModel extends ViewModel { this._hideHomeserver = true; this.emitChange("hideHomeserver"); this._disposeViewModels(); - await this._createLoadViewModel(); + void this._createLoadViewModel(); return null; } - async _createLoadViewModel() { + _createLoadViewModel(): void { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.track( @@ -181,7 +181,7 @@ export class LoginViewModel extends ViewModel { }) ) ); - await this._loadViewModel.start(); + void this._loadViewModel.start(); this.emitChange("loadViewModel"); this._loadViewModelSubscription = this.track( this._loadViewModel.disposableOn("change", () => { @@ -200,7 +200,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - async setHomeserver(newHomeserver: string) { + async setHomeserver(newHomeserver: string): void { this._homeserver = newHomeserver; // clear everything set by queryHomeserver this._loginOptions = undefined; @@ -223,10 +223,10 @@ export class LoginViewModel extends ViewModel { } } this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); - await this.queryHomeserver(); + void this.queryHomeserver(); } - async queryHomeserver() { + async queryHomeserver(): void { // don't repeat a query we've just done if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { return; @@ -270,12 +270,12 @@ export class LoginViewModel extends ViewModel { } } - async dispose() { + dispose(): void { super.dispose(); if (this._client) { // if we move away before we're done with initial sync // delete the session - await this._client.deleteSession(); + void this._client.deleteSession(); } } } From 8b91d8fac83a238c12dac17804eedfd2bb48c43e Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 26 Jul 2022 20:55:17 -0700 Subject: [PATCH 137/354] adds newline --- src/domain/login/LoginViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index d0ae9d9a..2a80465a 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -288,4 +288,4 @@ type LoginOptions = { password?: (username: string, password: string) => PasswordLoginMethod; sso?: SSOLoginHelper; token?: (loginToken: string) => TokenLoginMethod; -}; \ No newline at end of file +}; From cadca709460a5e701c962de20be33ff174f4f55a Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 26 Jul 2022 21:05:49 -0700 Subject: [PATCH 138/354] fixes linter errors and removes some unneeded async/await --- src/domain/ViewModel.ts | 4 ++-- src/domain/login/LoginViewModel.ts | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 7cf21861..00ae847b 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -58,7 +58,7 @@ export class ViewModel extends EventEmitter<{change return this._options[name]; } - observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) { + observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void): void { const segmentObservable = this.navigation.observe(type); const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { onChange(value, type); @@ -103,7 +103,7 @@ export class ViewModel extends EventEmitter<{change // // translated string should probably always be bindings, unless we're fine with a refresh when changing the language? // we probably are, if we're using routing with a url, we could just refresh. - i18n(parts: TemplateStringsArray, ...expr: any[]) { + i18n(parts: TemplateStringsArray, ...expr: any[]): string { // just concat for now let result = ""; for (let i = 0; i < parts.length; ++i) { diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 2a80465a..67deede8 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -55,7 +55,7 @@ export class LoginViewModel extends ViewModel { this._loginToken = loginToken; this._client = new Client(this.platform); this._homeserver = defaultHomeserver; - void this._initViewModels(); + this._initViewModels(); } get passwordLoginViewModel(): PasswordLoginViewModel { @@ -98,11 +98,11 @@ export class LoginViewModel extends ViewModel { return !!this._abortQueryOperation; } - goBack() { + goBack(): void { this.navigation.push("session"); } - async _initViewModels() { + _initViewModels(): void { if (this._loginToken) { this._hideHomeserver = true; this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( @@ -115,11 +115,11 @@ export class LoginViewModel extends ViewModel { this.emitChange("completeSSOLoginViewModel"); } else { - await this.queryHomeserver(); + void this.queryHomeserver(); } } - _showPasswordLogin() { + _showPasswordLogin(): void { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ loginOptions: this._loginOptions, @@ -128,19 +128,19 @@ export class LoginViewModel extends ViewModel { this.emitChange("passwordLoginViewModel"); } - _showSSOLogin() { + _showSSOLogin(): void { this._startSSOLoginViewModel = this.track( new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) ); this.emitChange("startSSOLoginViewModel"); } - _showError(message: string) { + _showError(message: string): void { this._errorMessage = message; this.emitChange("errorMessage"); } - _setBusy(status: boolean) { + _setBusy(status: boolean): void { this._isBusy = status; this._passwordLoginViewModel?.setBusy(status); this._startSSOLoginViewModel?.setBusy(status); @@ -193,14 +193,14 @@ export class LoginViewModel extends ViewModel { ); } - _disposeViewModels() { + _disposeViewModels(): void { this._startSSOLoginViewModel = this.disposeTracked(this._startSSOLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); this.emitChange("disposeViewModels"); } - async setHomeserver(newHomeserver: string): void { + async setHomeserver(newHomeserver: string): Promise { this._homeserver = newHomeserver; // clear everything set by queryHomeserver this._loginOptions = undefined; @@ -226,7 +226,7 @@ export class LoginViewModel extends ViewModel { void this.queryHomeserver(); } - async queryHomeserver(): void { + async queryHomeserver(): Promise { // don't repeat a query we've just done if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { return; From a82df95b82b417b551eda2177eb1f8fa9c9dd73b Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Wed, 27 Jul 2022 22:07:46 -0700 Subject: [PATCH 139/354] marking private methods as such --- src/domain/login/LoginViewModel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 67deede8..aaeca54f 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -102,7 +102,7 @@ export class LoginViewModel extends ViewModel { this.navigation.push("session"); } - _initViewModels(): void { + private _initViewModels(): void { if (this._loginToken) { this._hideHomeserver = true; this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( @@ -119,7 +119,7 @@ export class LoginViewModel extends ViewModel { } } - _showPasswordLogin(): void { + private _showPasswordLogin(): void { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ loginOptions: this._loginOptions, @@ -128,19 +128,19 @@ export class LoginViewModel extends ViewModel { this.emitChange("passwordLoginViewModel"); } - _showSSOLogin(): void { + private _showSSOLogin(): void { this._startSSOLoginViewModel = this.track( new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) ); this.emitChange("startSSOLoginViewModel"); } - _showError(message: string): void { + private _showError(message: string): void { this._errorMessage = message; this.emitChange("errorMessage"); } - _setBusy(status: boolean): void { + private _setBusy(status: boolean): void { this._isBusy = status; this._passwordLoginViewModel?.setBusy(status); this._startSSOLoginViewModel?.setBusy(status); @@ -165,7 +165,7 @@ export class LoginViewModel extends ViewModel { return null; } - _createLoadViewModel(): void { + private _createLoadViewModel(): void { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.track( @@ -193,7 +193,7 @@ export class LoginViewModel extends ViewModel { ); } - _disposeViewModels(): void { + private _disposeViewModels(): void { this._startSSOLoginViewModel = this.disposeTracked(this._startSSOLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); From 3c64f7d49b31564bb4e27dd91aab7108d9554372 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 28 Jul 2022 09:23:30 +0200 Subject: [PATCH 140/354] Finals checks about https://github.com/vector-im/hydrogen-web/pull/809#pullrequestreview-1053501341 - joined the processJoinRoom and joinRoom methods; - fixed some precisions miss; - removed some useless code; - change the error message height from absolute (40px) to relative (auto) --- src/domain/session/room/RoomViewModel.js | 27 ++++++------------- .../web/ui/css/themes/element/theme.css | 4 +-- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 74cb3d05..47d8c628 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -199,40 +199,30 @@ export class RoomViewModel extends ViewModel { } async joinRoom(roomName) { + } + + async _processCommandJoin(roomName) { try { const roomId = await this._options.client.session.joinRoom(roomName); const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId); await roomStatusObserver.waitFor(status => status === RoomStatus.Joined); this.navigation.push("room", roomId); - return true; } catch (exc) { if ((exc.statusCode ?? exc.status) === 400) { - return `'${roomName}' was not legal room ID or room alias`; + exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - return `room '${roomName}' not found`; + exc = new Error(`/join : room '${roomName}' not found`); } else if ((exc.statusCode ?? exc.status) === 403) { - return `you're not invited to join '${roomName}'`; - } else { - return exc; - } - } - } - - async _processCommandJoin(roomName) { - const exc = await this.joinRoom(roomName); - if (exc !== true) { - if (exc && exc.stack && exc.message) { - this._sendError = exc; - } else { - this._sendError = new Error("/join : " + exc); + exc = new Error(`/join : you're not invited to join '${roomName}'`); } + this._sendError = exc; this._timelineError = null; this.emitChange("error"); } } async _processCommand (message) { - let msgtype = undefined; + let msgtype; const [commandName, ...args] = message.substring(1).split(" "); switch (commandName) { case "me": @@ -269,7 +259,6 @@ export class RoomViewModel extends ViewModel { this._sendError = new Error(`no command name "${commandName}". To send the message instead of executing, please type "/${message}"`); this._timelineError = null; this.emitChange("error"); - msgtype = undefined; message = undefined; } return {type: msgtype, message: message}; diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 0f40bdf3..05681cbb 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -539,7 +539,7 @@ a { } .RoomView_error:not(:empty) { - height : 40px; + height : auto; padding-top : 20px; padding-bottom : 20px; } @@ -572,7 +572,7 @@ a { content:"\274c"; position : absolute; top : 15px; - left: 10px; + left: 9px; width : 20px; height : 10px; font-size : 10px; From fb7932674702fe4bbd35ef186cbb95b70a77b34f Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 28 Jul 2022 09:26:08 +0200 Subject: [PATCH 141/354] Forgot one change --- src/domain/session/room/RoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 47d8c628..124f6e9c 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -230,7 +230,7 @@ export class RoomViewModel extends ViewModel { msgtype = "m.emote"; break; case "join": - if (args.length == 1) { + if (args.length === 1) { const roomName = args[0]; await this._processCommandJoin(roomName); } else { From 302131c447d30300ae7411b6e7df31f5acb120c7 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 28 Jul 2022 10:14:21 +0200 Subject: [PATCH 142/354] Review last checks --- src/domain/session/room/RoomViewModel.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 124f6e9c..cecbb27f 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -198,22 +198,21 @@ export class RoomViewModel extends ViewModel { } } - async joinRoom(roomName) { - } - async _processCommandJoin(roomName) { try { const roomId = await this._options.client.session.joinRoom(roomName); const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId); await roomStatusObserver.waitFor(status => status === RoomStatus.Joined); this.navigation.push("room", roomId); - } catch (exc) { + } catch (err) { if ((exc.statusCode ?? exc.status) === 400) { - exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); + const exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - exc = new Error(`/join : room '${roomName}' not found`); + const exc = new Error(`/join : room '${roomName}' not found`); } else if ((exc.statusCode ?? exc.status) === 403) { - exc = new Error(`/join : you're not invited to join '${roomName}'`); + const exc = new Error(`/join : you're not invited to join '${roomName}'`); + } else { + const exc = err; } this._sendError = exc; this._timelineError = null; From f5dacb4e42bd3daf6b6633cebb0613d362494b61 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 28 Jul 2022 10:26:59 +0200 Subject: [PATCH 143/354] Fixed last check --- src/domain/session/room/RoomViewModel.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index cecbb27f..249d04f0 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -205,14 +205,15 @@ export class RoomViewModel extends ViewModel { await roomStatusObserver.waitFor(status => status === RoomStatus.Joined); this.navigation.push("room", roomId); } catch (err) { - if ((exc.statusCode ?? exc.status) === 400) { - const exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((exc.statusCode ?? exc.status) === 404 || (exc.statusCode ?? exc.status) === 502 || exc.message == "Internal Server Error") { - const exc = new Error(`/join : room '${roomName}' not found`); - } else if ((exc.statusCode ?? exc.status) === 403) { - const exc = new Error(`/join : you're not invited to join '${roomName}'`); + let exc; + if ((err.statusCode ?? err.status) === 400) { + exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); + } else if ((err.statusCode ?? err.status) === 404 || (err.statusCode ?? err.status) === 502 || err.message == "Internal Server Error") { + exc = new Error(`/join : room '${roomName}' not found`); + } else if ((err.statusCode ?? err.status) === 403) { + exc = new Error(`/join : you're not invited to join '${roomName}'`); } else { - const exc = err; + exc = err; } this._sendError = exc; this._timelineError = null; From 319ec37864017a38abda67c76af0901dcc3bc4d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 28 Jul 2022 11:44:50 +0200 Subject: [PATCH 144/354] fix typos preventing to load the history visibility --- src/matrix/e2ee/RoomEncryption.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 49ca1a2f..fa1ecf6e 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -136,9 +136,9 @@ export class RoomEncryption { if (!txn) { txn = await this._storage.readTxn([this._storage.storeNames.roomState]); } - const visibilityEntry = await txn.roomState.get(this.id, ROOM_HISTORY_VISIBILITY_TYPE, ""); + const visibilityEntry = await txn.roomState.get(this._room.id, ROOM_HISTORY_VISIBILITY_TYPE, ""); if (visibilityEntry) { - return event?.content?.history_visibility; + return visibilityEntry.event?.content?.history_visibility; } } return historyVisibility; From b73e3e9883dffc0f5937d4a49eafb06bf61dba35 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Thu, 28 Jul 2022 13:06:47 +0200 Subject: [PATCH 145/354] Don't add a memeber to the members list if it left the DM --- src/matrix/room/members/Heroes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/members/Heroes.js b/src/matrix/room/members/Heroes.js index 1d2ab39e..97630edd 100644 --- a/src/matrix/room/members/Heroes.js +++ b/src/matrix/room/members/Heroes.js @@ -85,7 +85,9 @@ export class Heroes { this._members.delete(userId); } for (const member of updatedHeroMembers) { - this._members.set(member.userId, member); + if (!removedUserIds.includes(member.userId)) { + this._members.set(member.userId, member); + } } const sortedMembers = Array.from(this._members.values()).sort((a, b) => a.name.localeCompare(b.name)); this._roomName = calculateRoomName(sortedMembers, summaryData, log); From 62b3a67e33c9d53446ac55c79e155728f7dc5255 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 28 Jul 2022 17:09:41 +0200 Subject: [PATCH 146/354] write unit tests for correctly reading history visibility when needed --- src/matrix/e2ee/RoomEncryption.js | 140 ++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index fa1ecf6e..36424b02 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -551,3 +551,143 @@ class BatchDecryptionResult { })); } } + +import {createMockStorage} from "../../mocks/Storage"; +import {Clock as MockClock} from "../../mocks/Clock"; +import {poll} from "../../mocks/poll"; +import {Instance as NullLoggerInstance} from "../../logging/NullLogger"; +import {ConsoleLogger} from "../../logging/ConsoleLogger"; +import {HomeServer as MockHomeServer} from "../../mocks/HomeServer.js"; + +export function tests() { + const roomId = "!abc:hs.tld"; + return { + "ensureMessageKeyIsShared tracks room and passes correct history visibility to deviceTracker": async assert => { + const storage = await createMockStorage(); + const megolmMock = { + async ensureOutboundSession() { return { }; } + }; + const olmMock = { + async encrypt() { return []; } + } + let isRoomTracked = false; + let isDevicesRequested = false; + const deviceTracker = { + async trackRoom(room, historyVisibility) { + // only assert on first call + if (isRoomTracked) { return; } + assert(!isDevicesRequested); + assert.equal(room.id, roomId); + assert.equal(historyVisibility, "invited"); + isRoomTracked = true; + }, + async devicesForTrackedRoom() { + assert(isRoomTracked); + isDevicesRequested = true; + return []; + }, + async devicesForRoomMembers() { + return []; + } + } + const writeTxn = await storage.readWriteTxn([storage.storeNames.roomState]); + writeTxn.roomState.set(roomId, {state_key: "", type: ROOM_HISTORY_VISIBILITY_TYPE, content: { + history_visibility: "invited" + }}); + await writeTxn.complete(); + const roomEncryption = new RoomEncryption({ + room: {id: roomId}, + megolmEncryption: megolmMock, + olmEncryption: olmMock, + storage, + deviceTracker, + clock: new MockClock() + }); + const homeServer = new MockHomeServer(); + const promise = roomEncryption.ensureMessageKeyIsShared(homeServer.api, NullLoggerInstance.item); + // need to poll because sendToDevice isn't first async step + const request = await poll(() => homeServer.requests.sendToDevice?.[0]); + request.respond({}); + await promise; + assert(isRoomTracked); + assert(isDevicesRequested); + }, + "encrypt tracks room and passes correct history visibility to deviceTracker": async assert => { + const storage = await createMockStorage(); + const megolmMock = { + async encrypt() { return { roomKeyMessage: {} }; } + }; + const olmMock = { + async encrypt() { return []; } + } + let isRoomTracked = false; + let isDevicesRequested = false; + const deviceTracker = { + async trackRoom(room, historyVisibility) { + // only assert on first call + if (isRoomTracked) { return; } + assert(!isDevicesRequested); + assert.equal(room.id, roomId); + assert.equal(historyVisibility, "invited"); + isRoomTracked = true; + }, + async devicesForTrackedRoom() { + assert(isRoomTracked); + isDevicesRequested = true; + return []; + }, + async devicesForRoomMembers() { + return []; + } + } + const writeTxn = await storage.readWriteTxn([storage.storeNames.roomState]); + writeTxn.roomState.set(roomId, {state_key: "", type: ROOM_HISTORY_VISIBILITY_TYPE, content: { + history_visibility: "invited" + }}); + await writeTxn.complete(); + const roomEncryption = new RoomEncryption({ + room: {id: roomId}, + megolmEncryption: megolmMock, + olmEncryption: olmMock, + storage, + deviceTracker + }); + const homeServer = new MockHomeServer(); + const promise = roomEncryption.encrypt("m.room.message", {body: "hello"}, homeServer.api, NullLoggerInstance.item); + // need to poll because sendToDevice isn't first async step + const request = await poll(() => homeServer.requests.sendToDevice?.[0]); + request.respond({}); + await promise; + assert(isRoomTracked); + assert(isDevicesRequested); + }, + "writeSync passes correct history visibility to deviceTracker": async assert => { + const storage = await createMockStorage(); + let isMemberChangesCalled = false; + const deviceTracker = { + async writeMemberChanges(room, memberChanges, historyVisibility, txn) { + assert.equal(historyVisibility, "invited"); + isMemberChangesCalled = true; + return {removed: [], added: []}; + }, + async devicesForRoomMembers() { + return []; + } + } + const writeTxn = await storage.readWriteTxn([storage.storeNames.roomState]); + writeTxn.roomState.set(roomId, {state_key: "", type: ROOM_HISTORY_VISIBILITY_TYPE, content: { + history_visibility: "invited" + }}); + const memberChanges = new Map([["@alice:hs.tld", {}]]); + const roomEncryption = new RoomEncryption({ + room: {id: roomId}, + storage, + deviceTracker + }); + const roomResponse = {}; + const txn = await storage.readWriteTxn([storage.storeNames.roomState]); + await roomEncryption.writeSync(roomResponse, memberChanges, txn, NullLoggerInstance.item); + assert(isMemberChangesCalled); + }, + } +} From d937b9b14b3f2ef5c950c59a658db6659d6e3d51 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 29 Jul 2022 10:39:41 +0100 Subject: [PATCH 147/354] use logging items --- src/matrix/storage/idb/StorageFactory.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 1f64baf3..6d5f2720 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -21,6 +21,7 @@ import { exportSession, importSession, Export } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; import { ILogItem } from "../../../logging/types"; +import { LogLevel } from "../../../logging/LogFilter"; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: ILogItem) { @@ -32,7 +33,7 @@ interface ServiceWorkerHandler { preventConcurrentSessionAccess: (sessionId: string) => Promise; } -async function requestPersistedStorage(): Promise { +async function requestPersistedStorage(log: ILogItem): Promise { // don't assume browser so we can run in node with fake-idb const glob = this; if (glob?.navigator?.storage?.persist) { @@ -42,7 +43,8 @@ async function requestPersistedStorage(): Promise { await glob.document.requestStorageAccess(); return true; } catch (err) { - console.warn("requestStorageAccess threw an error:", err); + const item = log.log("requestStorageAccess threw an error:", LogLevel.Warn); + item.error = err; return false; } } else { @@ -65,10 +67,10 @@ export class StorageFactory { async create(sessionId: string, log: ILogItem): Promise { await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); - requestPersistedStorage().then(persisted => { + requestPersistedStorage(log).then(persisted => { // Firefox lies here though, and returns true even if the user denied the request if (!persisted) { - console.warn("no persisted storage, database can be evicted by browser"); + log.log("no persisted storage, database can be evicted by browser:", LogLevel.Warn); } }); From 58a2d1f34c3d6216e7effdbdfe56a85594d65f35 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 29 Jul 2022 11:44:23 +0200 Subject: [PATCH 148/354] Restored the common.js indentation --- src/matrix/common.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/matrix/common.js b/src/matrix/common.js index abd74a57..ba7876ed 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -22,16 +22,16 @@ export function makeTxnId() { } export function isTxnId(txnId) { - return txnId.startsWith("t") && txnId.length === 15; + return txnId.startsWith("t") && txnId.length === 15; } export function tests() { - return { - "isTxnId succeeds on result of makeTxnId": assert => { - assert(isTxnId(makeTxnId())); - }, - "isTxnId fails on event id": assert => { - assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); - }, - } + return { + "isTxnId succeeds on result of makeTxnId": assert => { + assert(isTxnId(makeTxnId())); + }, + "isTxnId fails on event id": assert => { + assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); + }, + } } From 5b5c8524016c8ab06899c8e43af8b2a7eb40d4cb Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 29 Jul 2022 10:44:37 +0100 Subject: [PATCH 149/354] Revert "use logging items" This reverts commit d937b9b14b3f2ef5c950c59a658db6659d6e3d51. --- src/matrix/storage/idb/StorageFactory.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 6d5f2720..1f64baf3 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -21,7 +21,6 @@ import { exportSession, importSession, Export } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; import { ILogItem } from "../../../logging/types"; -import { LogLevel } from "../../../logging/LogFilter"; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: ILogItem) { @@ -33,7 +32,7 @@ interface ServiceWorkerHandler { preventConcurrentSessionAccess: (sessionId: string) => Promise; } -async function requestPersistedStorage(log: ILogItem): Promise { +async function requestPersistedStorage(): Promise { // don't assume browser so we can run in node with fake-idb const glob = this; if (glob?.navigator?.storage?.persist) { @@ -43,8 +42,7 @@ async function requestPersistedStorage(log: ILogItem): Promise { await glob.document.requestStorageAccess(); return true; } catch (err) { - const item = log.log("requestStorageAccess threw an error:", LogLevel.Warn); - item.error = err; + console.warn("requestStorageAccess threw an error:", err); return false; } } else { @@ -67,10 +65,10 @@ export class StorageFactory { async create(sessionId: string, log: ILogItem): Promise { await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); - requestPersistedStorage(log).then(persisted => { + requestPersistedStorage().then(persisted => { // Firefox lies here though, and returns true even if the user denied the request if (!persisted) { - log.log("no persisted storage, database can be evicted by browser:", LogLevel.Warn); + console.warn("no persisted storage, database can be evicted by browser"); } }); From f512bfcfc169928e24831948b228f856317adaa5 Mon Sep 17 00:00:00 2001 From: Kaki In <91763754+Kaki-In@users.noreply.github.com> Date: Fri, 29 Jul 2022 11:47:47 +0200 Subject: [PATCH 150/354] Pretty syntaxed the RoomViewModel --- src/domain/session/room/RoomViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 249d04f0..fcda95fc 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -244,15 +244,15 @@ export class RoomViewModel extends ViewModel { msgtype = "m.text"; break; case "tableflip": - message="(╯°□°)╯︵ ┻━┻ " + args.join(" "); + message = "(╯°□°)╯︵ ┻━┻ " + args.join(" "); msgtype = "m.text"; break; case "unflip": - message="┬──┬ ノ( ゜-゜ノ) " + args.join(" "); + message = "┬──┬ ノ( ゜-゜ノ) " + args.join(" "); msgtype = "m.text"; break; case "lenny": - message="( ͡° ͜ʖ ͡°) " + args.join(" "); + message = "( ͡° ͜ʖ ͡°) " + args.join(" "); msgtype = "m.text"; break; default: From 170460f5a956f164578b35319d2d632c668616d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 12:02:09 +0200 Subject: [PATCH 151/354] add link to sygnal webpush docs as well --- src/platform/types/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/types/config.ts b/src/platform/types/config.ts index 165bd22b..8a5eabf2 100644 --- a/src/platform/types/config.ts +++ b/src/platform/types/config.ts @@ -50,6 +50,7 @@ export type Config = { /** * Configuration for push notifications. * See https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset + * and https://github.com/matrix-org/sygnal/blob/main/docs/applications.md#webpush * OPTIONAL */ push?: { From 02bc7d1d7e9eef5915b9cb6284fb289868905e44 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 10:14:41 +0000 Subject: [PATCH 152/354] fix typo --- doc/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index 5ffadf44..39729830 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -174,7 +174,7 @@ This allows users to theme Hydrogen without the need for rebuilding. Derived the ## Creating a derived theme: Here's how you create a new derived theme: 1. You create a new theme manifest file (eg: theme-awesome.json) and mention which build time theme you're basing your new theme on using the `extends` field. The base css file of the mentioned theme is used for your new theme. -2. You configue the theme manifest as usual by populating the `variants` field with your desired colors. +2. You configure the theme manifest as usual by populating the `variants` field with your desired colors. 3. You add your new theme manifest to the list of themes in `config.json`. Reload Hydrogen twice and the new theme should show up in the theme chooser. From 06da5a8ae485a0b83f682711306239b14b20d916 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 10:14:58 +0000 Subject: [PATCH 153/354] clarification --- doc/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index 39729830..599434bd 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -177,7 +177,7 @@ Here's how you create a new derived theme: 2. You configure the theme manifest as usual by populating the `variants` field with your desired colors. 3. You add your new theme manifest to the list of themes in `config.json`. -Reload Hydrogen twice and the new theme should show up in the theme chooser. +Refresh Hydrogen twice (once to refresh cache, and once to load) and the new theme should show up in the theme chooser. ## How does it work? From b6f795505d90073d1da22eaa2599b51810372d16 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 12:21:16 +0200 Subject: [PATCH 154/354] fix lint --- src/domain/session/room/RoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index fcda95fc..75f90730 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -426,7 +426,7 @@ export class RoomViewModel extends ViewModel { } } - dismissError(evt) { + dismissError() { this._sendError = null; this.emitChange("error"); } From 708637e390250db305e11bbc2c7b5bf8cc30479e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 16:45:25 +0530 Subject: [PATCH 155/354] No need for this complex resolve --- scripts/build-plugins/rollup-plugin-build-themes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index bb1c65c1..5b913448 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -56,7 +56,8 @@ async function generateIconSourceMap(icons, manifestLocation) { const fs = require("fs").promises; for (const icon of Object.values(icons)) { const [location] = icon.split("?"); - const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); + // resolve location against manifestLocation + const resolvedLocation = path.resolve(manifestLocation, location); const iconData = fs.readFile(resolvedLocation); promises.push(iconData); const fileName = path.basename(resolvedLocation); From 39817dc36bb8952aa7ce55045c7ead6f506c776f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 17:33:33 +0530 Subject: [PATCH 156/354] Revert back option --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 72d9d3ae..f46cc7eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "noImplicitAny": false, "noEmit": true, "target": "ES2020", - "module": "Node16", + "module": "ES2020", "moduleResolution": "node", "esModuleInterop": true }, From cb0ac846c7bf9691dd1dd8951da33511135e0399 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 16:22:01 +0200 Subject: [PATCH 157/354] remove obsolete comment --- src/matrix/e2ee/DeviceTracker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 55051994..484a6d0b 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -148,7 +148,6 @@ export class DeviceTracker { const removed = []; if (room.isTrackingMembers && room.isEncrypted) { await log.wrap("rewriting userIdentities", async log => { - // don't use room.loadMemberList here because we want to use the syncTxn to load the members const memberList = await room.loadMemberList(syncTxn, log); try { const members = Array.from(memberList.members.values()); From 5e83eca3b9598de03b9898380457853f25cc1ebf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 16:43:28 +0200 Subject: [PATCH 158/354] move semi-automatic test for runtime themes into dedicated directory --- scripts/{ => test-derived-theme}/test-theme.sh | 4 ++-- theme.json => scripts/test-derived-theme/theme.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename scripts/{ => test-derived-theme}/test-theme.sh (67%) rename theme.json => scripts/test-derived-theme/theme.json (100%) diff --git a/scripts/test-theme.sh b/scripts/test-derived-theme/test-theme.sh similarity index 67% rename from scripts/test-theme.sh rename to scripts/test-derived-theme/test-theme.sh index 9f94d3c3..6ac9b128 100755 --- a/scripts/test-theme.sh +++ b/scripts/test-derived-theme/test-theme.sh @@ -1,5 +1,5 @@ -#!/bin/zsh -cp theme.json target/assets/theme-customer.json +#!/bin/sh +cp scripts/test-derived-theme/theme.json target/assets/theme-customer.json cat target/config.json | jq '.themeManifests += ["assets/theme-customer.json"]' | cat > target/config.temp.json rm target/config.json mv target/config.temp.json target/config.json diff --git a/theme.json b/scripts/test-derived-theme/theme.json similarity index 100% rename from theme.json rename to scripts/test-derived-theme/theme.json From db2b4e693c1e130cc0d142ad10a1d128fefe3897 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 29 Jul 2022 17:10:24 +0200 Subject: [PATCH 159/354] release v0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9361098c..1823ef45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.33", + "version": "0.3.0", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 50910907953fc2483757e41f3146c852c254c941 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 23:11:17 +0530 Subject: [PATCH 160/354] Produce .tmp directory within root --- scripts/postcss/svg-builder.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index 3efff4ae..cbfd3637 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -36,7 +36,7 @@ export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) { const coloredSVGCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1]; const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; - const outputPath = resolve(__dirname, "../../.tmp"); + const outputPath = resolve(__dirname, "./.tmp"); try { mkdirSync(outputPath); } From 4571ecd85177b1f04fa06a218d27bdc77f977a43 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 23:45:58 +0530 Subject: [PATCH 161/354] Specify theme as array --- vite.sdk-assets-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js index d7d4d064..5c1f3196 100644 --- a/vite.sdk-assets-config.js +++ b/vite.sdk-assets-config.js @@ -36,7 +36,7 @@ export default mergeOptions(commonOptions, { plugins: [ themeBuilder({ themeConfig: { - themes: { element: "./src/platform/web/ui/css/themes/element" }, + themes: ["./src/platform/web/ui/css/themes/element"], default: "element", }, compiledVariables, From ef9f90bc3609e859938633d6f6a107c79184f0c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 23:40:12 +0530 Subject: [PATCH 162/354] Fix imports breaking on dev --- src/platform/web/theming/DerivedVariables.ts | 2 +- src/platform/web/theming/shared/color.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index 5a626bfa..92e5ac20 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -98,7 +98,7 @@ export class DerivedVariables { } } -import pkg from "off-color"; +import * as pkg from "off-color"; const {offColor} = pkg; export function tests() { diff --git a/src/platform/web/theming/shared/color.mjs b/src/platform/web/theming/shared/color.mjs index e777bfac..d4a15e56 100644 --- a/src/platform/web/theming/shared/color.mjs +++ b/src/platform/web/theming/shared/color.mjs @@ -13,7 +13,7 @@ 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 pkg from 'off-color'; +import * as pkg from 'off-color'; const offColor = pkg.offColor; export function derive(value, operation, argument, isDark) { From ba8cdea6b477a8d5a1d499348d033fdcec7ae050 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 1 Aug 2022 13:30:17 +0530 Subject: [PATCH 163/354] Use default import if other not found --- src/platform/web/theming/DerivedVariables.ts | 2 +- src/platform/web/theming/shared/color.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index 92e5ac20..bc7a8346 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -99,7 +99,7 @@ export class DerivedVariables { } import * as pkg from "off-color"; -const {offColor} = pkg; +const offColor = pkg.offColor ?? pkg.default.offColor; export function tests() { return { diff --git a/src/platform/web/theming/shared/color.mjs b/src/platform/web/theming/shared/color.mjs index d4a15e56..8af76b6b 100644 --- a/src/platform/web/theming/shared/color.mjs +++ b/src/platform/web/theming/shared/color.mjs @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import * as pkg from 'off-color'; -const offColor = pkg.offColor; +const offColor = pkg.offColor ?? pkg.default.offColor; export function derive(value, operation, argument, isDark) { const argumentAsNumber = parseInt(argument); From 236a4ab49bac40a80503b2b610d2f8826a0ee1cb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 1 Aug 2022 16:51:39 +0530 Subject: [PATCH 164/354] Ignore error --- src/platform/web/theming/DerivedVariables.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index bc7a8346..ca46a8fd 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -99,6 +99,7 @@ export class DerivedVariables { } import * as pkg from "off-color"; +// @ts-ignore const offColor = pkg.offColor ?? pkg.default.offColor; export function tests() { From 832597447accd0a04f6aec444ec8a64a1d82c90f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 1 Aug 2022 17:00:56 +0530 Subject: [PATCH 165/354] Add explaining doc --- doc/IMPORT-ISSUES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/IMPORT-ISSUES.md diff --git a/doc/IMPORT-ISSUES.md b/doc/IMPORT-ISSUES.md new file mode 100644 index 00000000..d4bb62fe --- /dev/null +++ b/doc/IMPORT-ISSUES.md @@ -0,0 +1,11 @@ +## How to import common-js dependency using ES6 syntax +--- +Until [#6632](https://github.com/vitejs/vite/issues/6632) is fixed, such imports should be done as follows: + +```ts +import * as pkg from "off-color"; +// @ts-ignore +const offColor = pkg.offColor ?? pkg.default.offColor; +``` + +This way build, dev server and unit tests should all work. From 97391663d36fb01d24af55e6b211ee7c5dcc796f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:32:26 +0200 Subject: [PATCH 166/354] sdk version 0.1.0 --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index 25342baa..b00b0b62 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.15", + "version": "0.1.0", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From c898bcb46a59d907e30f34a6f3301a529ff69243 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:16:55 +0200 Subject: [PATCH 167/354] release v0.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1823ef45..f49ee21b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.0", + "version": "0.3.1", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From b1fd5f1ad530a2d1f3eb38cfa4f13aa073a31ea3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 4 Aug 2022 16:33:59 +0530 Subject: [PATCH 168/354] Do not fill gap when offline --- src/domain/session/room/timeline/tiles/GapTile.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 6caa4b9b..e2e5b041 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -16,6 +16,8 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {UpdateAction} from "../UpdateAction.js"; +import {ConnectionError} from "../../../../../matrix/error.js"; +import {ConnectionStatus} from "../../../../../matrix/net/Reconnector"; export class GapTile extends SimpleTile { constructor(entry, options) { @@ -29,6 +31,7 @@ export class GapTile extends SimpleTile { async fill() { if (!this._loading && !this._entry.edgeReached) { this._loading = true; + this._error = null; this.emitChange("isLoading"); try { await this._room.fillGap(this._entry, 10); @@ -55,7 +58,15 @@ export class GapTile extends SimpleTile { let canFillMore; this._siblingChanged = false; do { - canFillMore = await this.fill(); + try { + canFillMore = await this.fill(); + } + catch (e) { + if (e instanceof ConnectionError) { + await this.options.client.reconnector.connectionStatus.waitFor(status => status === ConnectionStatus.Online).promise; + canFillMore = true; + } + } depth = depth + 1; } while (depth < 10 && !this._siblingChanged && canFillMore && !this.isDisposed); } From d01a95aae3da88577fab0b74d209b3ae2460cffd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 4 Aug 2022 16:37:28 +0530 Subject: [PATCH 169/354] UI improvements --- .../session/room/timeline/tiles/GapTile.js | 5 ++++- .../web/ui/css/themes/element/timeline.css | 9 ++++++++ .../web/ui/session/room/timeline/GapView.js | 22 +++++++++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index e2e5b041..9bebb7b5 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -111,8 +111,11 @@ export class GapTile extends SimpleTile { get error() { if (this._error) { + if (this._error instanceof ConnectionError) { + return { message: "Waiting for reconnection", showSpinner: true }; + } const dir = this._entry.prev_batch ? "previous" : "next"; - return `Could not load ${dir} messages: ${this._error.message}`; + return { message: `Could not load ${dir} messages: ${this._error.message}`, showSpinner: false }; } return null; } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 43c57d19..47f9a365 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -422,3 +422,12 @@ only loads when the top comes into view*/ .GapView.isAtTop { padding: 52px 20px 12px 20px; } + +.GapView__container { + display: flex; + align-items: center; +} + +.GapView__container .spinner { + margin-right: 10px; +} diff --git a/src/platform/web/ui/session/room/timeline/GapView.js b/src/platform/web/ui/session/room/timeline/GapView.js index db6cda59..03f87fcf 100644 --- a/src/platform/web/ui/session/room/timeline/GapView.js +++ b/src/platform/web/ui/session/room/timeline/GapView.js @@ -29,10 +29,24 @@ export class GapView extends TemplateView { isLoading: vm => vm.isLoading, isAtTop: vm => vm.isAtTop, }; - return t.li({className}, [ - spinner(t), - t.div(vm => vm.isLoading ? vm.i18n`Loading more messages …` : vm.i18n`Not loading!`), - t.if(vm => vm.error, t => t.strong(vm => vm.error)) + return t.li({ className }, [ + t.map(vm => vm.error, + (error, t, vm) => { + let elements; + if (error) { + elements = [t.strong(() => error.message)]; + if (error.showSpinner) { + elements.unshift(spinner(t)); + } + } + else if (vm.isLoading) { + elements = [spinner(t), t.span(vm.i18n`Loading more messages …`)]; + } + else { + elements = t.span(vm.i18n`Not loading!`); + } + return t.div({ className: "GapView__container" }, elements); + }) ]); } From b33db1df3643dfdaf6b674cb6600fae3ff6c2341 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 4 Jul 2022 19:14:34 -0400 Subject: [PATCH 170/354] refactors ObservableMap --- .eslintrc.js | 34 +-- src/domain/SessionPickerViewModel.js | 2 +- .../room/timeline/ReactionsViewModel.js | 2 +- src/matrix/Session.js | 8 +- src/matrix/room/members/MemberList.js | 2 +- src/matrix/room/timeline/Timeline.js | 14 +- src/observable/{index.js => index.ts} | 32 +- src/observable/list/SortedMapList.js | 4 +- src/observable/map/BaseObservableMap.ts | 15 + src/observable/map/FilteredMap.js | 2 +- src/observable/map/JoinedMap.js | 2 +- src/observable/map/ObservableMap.ts | 288 ++++++++++-------- 12 files changed, 234 insertions(+), 171 deletions(-) rename src/observable/{index.js => index.ts} (56%) diff --git a/.eslintrc.js b/.eslintrc.js index cb28f4c8..cf1fc3bf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,25 @@ module.exports = { - "env": { + root: true, + env: { "browser": true, "es6": true }, - "extends": "eslint:recommended", - "parserOptions": { + extends: [ + // "plugin:@typescript-eslint/recommended", + // "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + parser: '@typescript-eslint/parser', + parserOptions: { "ecmaVersion": 2020, - "sourceType": "module" + "sourceType": "module", + "project": "./tsconfig.json" }, - "rules": { - "no-console": "off", - "no-empty": "off", - "no-prototype-builtins": "off", - "no-unused-vars": "warn" - }, - "globals": { - "DEFINE_VERSION": "readonly", - "DEFINE_GLOBAL_HASH": "readonly", - // only available in sw.js - "DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly", - "DEFINE_HASHED_PRECACHED_ASSETS": "readonly", - "DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly" + plugins: [ + '@typescript-eslint', + ], + rules: { + "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-misused-promises": 2, + "semi": ["error", "always"] } }; diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index e486c64f..f4a16f1c 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {SortedArray} from "../observable/index.js"; +import {SortedArray} from "../observable"; import {ViewModel} from "./ViewModel"; import {avatarInitials, getIdentifierColorNumber} from "./avatar"; diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 1977b6f4..c5ea1224 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -13,7 +13,7 @@ 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 {ObservableMap} from "../../../../observable/map/ObservableMap"; +import {ObservableMap} from "../../../../observable"; export class ReactionsViewModel { constructor(parentTile) { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ae1dea61..048ddbc8 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -21,7 +21,7 @@ import {RoomStatus} from "./room/common"; import {RoomBeingCreated} from "./room/RoomBeingCreated"; import {Invite} from "./room/Invite.js"; import {Pusher} from "./push/Pusher"; -import { ObservableMap } from "../observable/index.js"; +import {ObservableMap} from "../observable"; import {User} from "./User.js"; import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Account as E2EEAccount} from "./e2ee/Account.js"; @@ -192,7 +192,7 @@ export class Session { /** * Enable secret storage by providing the secret storage credential. * This will also see if there is a megolm key backup and try to enable that if so. - * + * * @param {string} type either "passphrase" or "recoverykey" * @param {string} credential either the passphrase or the recovery key, depending on the type * @return {Promise} resolves or rejects after having tried to enable secret storage @@ -663,7 +663,7 @@ export class Session { if (this._e2eeAccount && deviceOneTimeKeysCount) { changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log); } - + const deviceLists = syncResponse.device_lists; if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) { await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log)); @@ -908,7 +908,7 @@ export class Session { Creates an empty (summary isn't loaded) the archived room if it isn't loaded already, assuming sync will either remove it (when rejoining) or write a full summary adopting it from the joined room when leaving - + @internal */ createOrGetArchivedRoomForSync(roomId) { diff --git a/src/matrix/room/members/MemberList.js b/src/matrix/room/members/MemberList.js index f32a63d3..74b3fe7d 100644 --- a/src/matrix/room/members/MemberList.js +++ b/src/matrix/room/members/MemberList.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ObservableMap} from "../../../observable/map/ObservableMap"; +import {ObservableMap} from "../../../observable"; import {RetainedValue} from "../../../utils/RetainedValue"; export class MemberList extends RetainedValue { diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3332a5b0..a721092e 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js"; +import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable"; import {Disposables} from "../../../utils/Disposables"; import {Direction} from "./Direction"; import {TimelineReader} from "./persistence/TimelineReader.js"; @@ -45,7 +45,7 @@ export class Timeline { }); this._readerRequest = null; this._allEntries = null; - /** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */ + /** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */ this._contextEntriesNotInTimeline = new Map(); /** Only used to decrypt non-persisted context entries fetched from the homeserver */ this._decryptEntries = null; @@ -189,7 +189,7 @@ export class Timeline { // before it has any subscriptions, we bail out if this isn't // the case yet. This can happen when sync adds or replaces entries // before load has finished and the view has subscribed to the timeline. - // + // // Once the subscription is setup, MappedList will set up the local // relations as needed with _applyAndEmitLocalRelationChange, // so we're not missing anything by bailing out. @@ -239,7 +239,7 @@ export class Timeline { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment // that is currently not loaded into the FragmentIdComparer, it will - // throw a CompareError, and it means that the event is not loaded + // throw a CompareError, and it means that the event is not loaded // in the timeline (like when receiving a relation for an event // that is not loaded in memory) so we can just drop this error as // replacing an event that is not already loaded is a no-op. @@ -311,7 +311,7 @@ export class Timeline { * - timeline * - storage * - homeserver - * @param {EventEntry[]} entries + * @param {EventEntry[]} entries */ async _loadContextEntriesWhereNeeded(entries) { for (const entry of entries) { @@ -392,7 +392,7 @@ export class Timeline { * [loadAtTop description] * @param {[type]} amount [description] * @return {boolean} true if the top of the timeline has been reached - * + * */ async loadAtTop(amount) { if (this._disposables.isDisposed) { @@ -547,7 +547,7 @@ export function tests() { content: {}, relatedEventId: event2.event_id }})); - // 4. subscribe (it's now safe to iterate timeline.entries) + // 4. subscribe (it's now safe to iterate timeline.entries) timeline.entries.subscribe(new ListObserver()); // 5. check the local relation got correctly aggregated const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting); diff --git a/src/observable/index.js b/src/observable/index.ts similarity index 56% rename from src/observable/index.js rename to src/observable/index.ts index 6057174b..1b05f798 100644 --- a/src/observable/index.js +++ b/src/observable/index.ts @@ -18,14 +18,40 @@ import {SortedMapList} from "./list/SortedMapList.js"; import {FilteredMap} from "./map/FilteredMap.js"; import {MappedMap} from "./map/MappedMap.js"; import {JoinedMap} from "./map/JoinedMap.js"; -import {BaseObservableMap} from "./map/BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./map/BaseObservableMap"; +import {ObservableMapInternal} from "./map/ObservableMap"; // re-export "root" (of chain) collections export { ObservableArray } from "./list/ObservableArray"; export { SortedArray } from "./list/SortedArray"; export { MappedList } from "./list/MappedList"; export { AsyncMappedList } from "./list/AsyncMappedList"; export { ConcatList } from "./list/ConcatList"; -export { ObservableMap } from "./map/ObservableMap"; + +// avoid circular dependency between these classes +// and BaseObservableMap (as they extend it) +function config(): BaseObservableMapConfig { + return { + join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { + return new JoinedMap([_this].concat(otherMaps)); + }, + mapValues: (_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap => { + return new MappedMap(_this, mapper, updater); + }, + sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { + return new SortedMapList(_this, comparator); + }, + filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { + return new FilteredMap(_this, filter); + } + }; +}; + + +export class ObservableMap extends ObservableMapInternal { + constructor(initialValues?: (readonly [K, V])[]) { + super(config(), initialValues); + } +} // avoid circular dependency between these classes // and BaseObservableMap (as they extend it) @@ -45,4 +71,4 @@ Object.assign(BaseObservableMap.prototype, { join(...otherMaps) { return new JoinedMap([this].concat(otherMaps)); } -}); +}); \ No newline at end of file diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index d74dbade..21a3aa55 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -53,7 +53,7 @@ export class SortedMapList extends BaseObservableList { this._sortedPairs = null; this._mapSubscription = null; } - + onAdd(key, value) { const pair = {key, value}; const idx = sortedIndex(this._sortedPairs, pair, this._comparator); @@ -133,7 +133,7 @@ export class SortedMapList extends BaseObservableList { } } -import {ObservableMap} from "../map/ObservableMap"; +import {ObservableMap} from "../"; export function tests() { return { diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 694c017e..39189cdc 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -15,6 +15,10 @@ limitations under the License. */ import {BaseObservable} from "../BaseObservable"; +import {JoinedMap} from "../map/JoinedMap.js"; +import {MappedMap} from "../map/MappedMap.js"; +import {FilteredMap} from "../map/FilteredMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; export interface IMapObserver { onReset(): void; @@ -23,6 +27,13 @@ export interface IMapObserver { onRemove(key: K, value: V): void } +export type BaseObservableMapConfig = { + join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; + mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; + sortValues(_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList; + filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; +} + export abstract class BaseObservableMap extends BaseObservable> { emitReset() { for(let h of this._handlers) { @@ -49,6 +60,10 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; + abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; + abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; + abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; abstract get(key: K): V | undefined; diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.js index d7e11fbe..98dc4650 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.js @@ -166,7 +166,7 @@ class FilterIterator { } } -import {ObservableMap} from "./ObservableMap"; +import {ObservableMap} from "../"; export function tests() { return { "filter preloaded list": assert => { diff --git a/src/observable/map/JoinedMap.js b/src/observable/map/JoinedMap.js index d97c5677..ea5ad784 100644 --- a/src/observable/map/JoinedMap.js +++ b/src/observable/map/JoinedMap.js @@ -191,7 +191,7 @@ class SourceSubscriptionHandler { } -import { ObservableMap } from "./ObservableMap"; +import {ObservableMap} from "../"; export function tests() { diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index d604ab0a..ced58df1 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -14,16 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {JoinedMap} from "../map/JoinedMap.js"; +import {MappedMap} from "../map/MappedMap.js"; +import {FilteredMap} from "../map/FilteredMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; -export class ObservableMap extends BaseObservableMap { +export class ObservableMapInternal extends BaseObservableMap { + private _config: BaseObservableMapConfig private readonly _values: Map; - constructor(initialValues?: (readonly [K, V])[]) { + constructor(config: BaseObservableMapConfig, initialValues?: (readonly [K, V])[]) { super(); + this._config = config; this._values = new Map(initialValues); } + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); + } + update(key: K, params?: any): boolean { const value = this._values.get(key); if (value !== undefined) { @@ -61,7 +83,7 @@ export class ObservableMap extends BaseObservableMap { // We set the value here because update only supports inline updates this._values.set(key, value); return this.update(key, undefined); - } + } else { return this.add(key, value); } @@ -91,139 +113,139 @@ export class ObservableMap extends BaseObservableMap { keys(): Iterator { return this._values.keys(); } -} +}; -export function tests() { - return { - test_initial_values(assert) { - const map = new ObservableMap([ - ["a", 5], - ["b", 10] - ]); - assert.equal(map.size, 2); - assert.equal(map.get("a"), 5); - assert.equal(map.get("b"), 10); - }, +// export function tests() { +// return { +// test_initial_values(assert) { +// const map = new ObservableMap([ +// ["a", 5], +// ["b", 10] +// ]); +// assert.equal(map.size, 2); +// assert.equal(map.get("a"), 5); +// assert.equal(map.get("b"), 10); +// }, - test_add(assert) { - let fired = 0; - const map = new ObservableMap(); - map.subscribe({ - onAdd(key, value) { - fired += 1; - assert.equal(key, 1); - assert.deepEqual(value, {value: 5}); - }, - onUpdate() {}, - onRemove() {}, - onReset() {} - }); - map.add(1, {value: 5}); - assert.equal(map.size, 1); - assert.equal(fired, 1); - }, +// test_add(assert) { +// let fired = 0; +// const map = new ObservableMap(); +// map.subscribe({ +// onAdd(key, value) { +// fired += 1; +// assert.equal(key, 1); +// assert.deepEqual(value, {value: 5}); +// }, +// onUpdate() {}, +// onRemove() {}, +// onReset() {} +// }); +// map.add(1, {value: 5}); +// assert.equal(map.size, 1); +// assert.equal(fired, 1); +// }, - test_update(assert) { - let fired = 0; - const map = new ObservableMap(); - const value = {number: 5}; - map.add(1, value); - map.subscribe({ - onUpdate(key, value, params) { - fired += 1; - assert.equal(key, 1); - assert.deepEqual(value, {number: 6}); - assert.equal(params, "test"); - }, - onAdd() {}, - onRemove() {}, - onReset() {} - }); - value.number = 6; - map.update(1, "test"); - assert.equal(fired, 1); - }, +// test_update(assert) { +// let fired = 0; +// const map = new ObservableMap(); +// const value = {number: 5}; +// map.add(1, value); +// map.subscribe({ +// onUpdate(key, value, params) { +// fired += 1; +// assert.equal(key, 1); +// assert.deepEqual(value, {number: 6}); +// assert.equal(params, "test"); +// }, +// onAdd() {}, +// onRemove() {}, +// onReset() {} +// }); +// value.number = 6; +// map.update(1, "test"); +// assert.equal(fired, 1); +// }, - test_update_unknown(assert) { - let fired = 0; - const map = new ObservableMap(); - map.subscribe({ - onUpdate() { fired += 1; }, - onAdd() {}, - onRemove() {}, - onReset() {} - }); - const result = map.update(1); - assert.equal(fired, 0); - assert.equal(result, false); - }, +// test_update_unknown(assert) { +// let fired = 0; +// const map = new ObservableMap(); +// map.subscribe({ +// onUpdate() { fired += 1; }, +// onAdd() {}, +// onRemove() {}, +// onReset() {} +// }); +// const result = map.update(1); +// assert.equal(fired, 0); +// assert.equal(result, false); +// }, - test_set(assert) { - let add_fired = 0, update_fired = 0; - const map = new ObservableMap(); - map.subscribe({ - onAdd(key, value) { - add_fired += 1; - assert.equal(key, 1); - assert.deepEqual(value, {value: 5}); - }, - onUpdate(key, value/*, params*/) { - update_fired += 1; - assert.equal(key, 1); - assert.deepEqual(value, {value: 7}); - }, - onRemove() {}, - onReset() {} - }); - // Add - map.set(1, {value: 5}); - assert.equal(map.size, 1); - assert.equal(add_fired, 1); - // Update - map.set(1, {value: 7}); - assert.equal(map.size, 1); - assert.equal(update_fired, 1); - }, +// test_set(assert) { +// let add_fired = 0, update_fired = 0; +// const map = new ObservableMap(); +// map.subscribe({ +// onAdd(key, value) { +// add_fired += 1; +// assert.equal(key, 1); +// assert.deepEqual(value, {value: 5}); +// }, +// onUpdate(key, value/*, params*/) { +// update_fired += 1; +// assert.equal(key, 1); +// assert.deepEqual(value, {value: 7}); +// }, +// onRemove() {}, +// onReset() {} +// }); +// // Add +// map.set(1, {value: 5}); +// assert.equal(map.size, 1); +// assert.equal(add_fired, 1); +// // Update +// map.set(1, {value: 7}); +// assert.equal(map.size, 1); +// assert.equal(update_fired, 1); +// }, - test_remove(assert) { - let fired = 0; - const map = new ObservableMap(); - const value = {value: 5}; - map.add(1, value); - map.subscribe({ - onRemove(key, value) { - fired += 1; - assert.equal(key, 1); - assert.deepEqual(value, {value: 5}); - }, - onAdd() {}, - onUpdate() {}, - onReset() {} - }); - map.remove(1); - assert.equal(map.size, 0); - assert.equal(fired, 1); - }, +// test_remove(assert) { +// let fired = 0; +// const map = new ObservableMap(); +// const value = {value: 5}; +// map.add(1, value); +// map.subscribe({ +// onRemove(key, value) { +// fired += 1; +// assert.equal(key, 1); +// assert.deepEqual(value, {value: 5}); +// }, +// onAdd() {}, +// onUpdate() {}, +// onReset() {} +// }); +// map.remove(1); +// assert.equal(map.size, 0); +// assert.equal(fired, 1); +// }, - test_iterate(assert) { - const results: any[] = []; - const map = new ObservableMap(); - map.add(1, {number: 5}); - map.add(2, {number: 6}); - map.add(3, {number: 7}); - for (let e of map) { - results.push(e); - } - assert.equal(results.length, 3); - assert.equal(results.find(([key]) => key === 1)[1].number, 5); - assert.equal(results.find(([key]) => key === 2)[1].number, 6); - assert.equal(results.find(([key]) => key === 3)[1].number, 7); - }, - test_size(assert) { - const map = new ObservableMap(); - map.add(1, {number: 5}); - map.add(2, {number: 6}); - assert.equal(map.size, 2); - }, - } -} +// test_iterate(assert) { +// const results: any[] = []; +// const map = new ObservableMap(); +// map.add(1, {number: 5}); +// map.add(2, {number: 6}); +// map.add(3, {number: 7}); +// for (let e of map) { +// results.push(e); +// } +// assert.equal(results.length, 3); +// assert.equal(results.find(([key]) => key === 1)[1].number, 5); +// assert.equal(results.find(([key]) => key === 2)[1].number, 6); +// assert.equal(results.find(([key]) => key === 3)[1].number, 7); +// }, +// test_size(assert) { +// const map = new ObservableMap(); +// map.add(1, {number: 5}); +// map.add(2, {number: 6}); +// assert.equal(map.size, 2); +// }, +// } +// } From 7645eb875374c33593ba2f85a7891061ac78e503 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 4 Jul 2022 19:47:43 -0400 Subject: [PATCH 171/354] moves config into its own file --- src/observable/index.ts | 31 ++---------------- src/observable/map/BaseObservableMap.ts | 7 ++++ src/observable/map/ObservableMap.ts | 14 ++++---- src/observable/map/config.ts | 43 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 src/observable/map/config.ts diff --git a/src/observable/index.ts b/src/observable/index.ts index 1b05f798..47ab84ca 100644 --- a/src/observable/index.ts +++ b/src/observable/index.ts @@ -18,40 +18,15 @@ import {SortedMapList} from "./list/SortedMapList.js"; import {FilteredMap} from "./map/FilteredMap.js"; import {MappedMap} from "./map/MappedMap.js"; import {JoinedMap} from "./map/JoinedMap.js"; -import {BaseObservableMap, BaseObservableMapConfig} from "./map/BaseObservableMap"; -import {ObservableMapInternal} from "./map/ObservableMap"; -// re-export "root" (of chain) collections +import {BaseObservableMap} from "./map/BaseObservableMap"; +// re-export "root" (of chain) collection +export { ObservableMap } from "./map/ObservableMap"; export { ObservableArray } from "./list/ObservableArray"; export { SortedArray } from "./list/SortedArray"; export { MappedList } from "./list/MappedList"; export { AsyncMappedList } from "./list/AsyncMappedList"; export { ConcatList } from "./list/ConcatList"; -// avoid circular dependency between these classes -// and BaseObservableMap (as they extend it) -function config(): BaseObservableMapConfig { - return { - join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { - return new JoinedMap([_this].concat(otherMaps)); - }, - mapValues: (_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap => { - return new MappedMap(_this, mapper, updater); - }, - sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { - return new SortedMapList(_this, comparator); - }, - filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { - return new FilteredMap(_this, filter); - } - }; -}; - - -export class ObservableMap extends ObservableMapInternal { - constructor(initialValues?: (readonly [K, V])[]) { - super(config(), initialValues); - } -} // avoid circular dependency between these classes // and BaseObservableMap (as they extend it) diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 39189cdc..5eee5f95 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -60,10 +60,17 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; + abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; abstract get(key: K): V | undefined; diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index ced58df1..85e46703 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -15,18 +15,20 @@ limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {JoinedMap} from "../map/JoinedMap.js"; -import {MappedMap} from "../map/MappedMap.js"; -import {FilteredMap} from "../map/FilteredMap.js"; +import {config} from "./config"; +import {JoinedMap} from "./JoinedMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {FilteredMap} from "./FilteredMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; -export class ObservableMapInternal extends BaseObservableMap { + +export class ObservableMap extends BaseObservableMap { private _config: BaseObservableMapConfig private readonly _values: Map; - constructor(config: BaseObservableMapConfig, initialValues?: (readonly [K, V])[]) { + constructor(initialValues?: (readonly [K, V])[]) { super(); - this._config = config; + this._config = config(); this._values = new Map(initialValues); } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts new file mode 100644 index 00000000..7a7ea0c4 --- /dev/null +++ b/src/observable/map/config.ts @@ -0,0 +1,43 @@ +/* +Copyright 2022 Isaiah Becker-Mayer + +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 {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {FilteredMap} from "./FilteredMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {JoinedMap} from "./JoinedMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; + + +// This function is used as a default implementation of +// the respective abstract functions in BaseObservableMap. +// We implement it this way in order to avoid a circular +// dependency between the classes that are instantiated here +// (i.e. `new JoinedMap()`) and BaseObservableMap (as they extend it). +export function config(): BaseObservableMapConfig { + return { + join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { + return new JoinedMap([_this].concat(otherMaps)); + }, + mapValues: (_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap => { + return new MappedMap(_this, mapper, updater); + }, + sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { + return new SortedMapList(_this, comparator); + }, + filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { + return new FilteredMap(_this, filter); + } + }; +}; \ No newline at end of file From f1751a24b0f9568df6741bdc94128eb75cd65ba0 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 4 Jul 2022 20:30:18 -0400 Subject: [PATCH 172/354] FilteredMap --- src/observable/BaseObservable.ts | 2 +- src/observable/map/BaseObservableMap.ts | 8 +- .../map/{FilteredMap.js => FilteredMap.ts} | 119 +++++++++++++----- src/observable/map/ObservableMap.ts | 2 +- src/observable/map/config.ts | 2 +- 5 files changed, 93 insertions(+), 40 deletions(-) rename src/observable/map/{FilteredMap.js => FilteredMap.ts} (60%) diff --git a/src/observable/BaseObservable.ts b/src/observable/BaseObservable.ts index 44d716ac..11ecd8a0 100644 --- a/src/observable/BaseObservable.ts +++ b/src/observable/BaseObservable.ts @@ -80,5 +80,5 @@ export function tests() { assert.equal(c.firstSubscribeCalls, 1); assert.equal(c.firstUnsubscribeCalls, 1); } - } + }; } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 5eee5f95..4e8c88d0 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -31,7 +31,7 @@ export type BaseObservableMapConfig = { join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; sortValues(_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList; - filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; + filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; } export abstract class BaseObservableMap extends BaseObservable> { @@ -48,13 +48,13 @@ export abstract class BaseObservableMap extends BaseObservable extends BaseObservable): JoinedMap; abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; - abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; + abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.ts similarity index 60% rename from src/observable/map/FilteredMap.js rename to src/observable/map/FilteredMap.ts index 98dc4650..a2250f21 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.ts @@ -14,19 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {SubscriptionHandle} from "../BaseObservable"; +import {config} from "./config"; +import {JoinedMap} from "./JoinedMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; -export class FilteredMap extends BaseObservableMap { - constructor(source, filter) { +export class FilteredMap extends BaseObservableMap { + private _source: BaseObservableMap; + private _config: BaseObservableMapConfig + private _filter: (value: V, key: K) => boolean; + private _included?: Map; + private _subscription?: SubscriptionHandle; + + constructor(source: BaseObservableMap, filter: (value: V, key: K) => boolean) { super(); this._source = source; this._filter = filter; - /** @type {Map} */ - this._included = null; - this._subscription = null; + this._config = config(); } - setFilter(filter) { + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); + } + + setFilter(filter: (value: V, key: K) => boolean) { this._filter = filter; if (this._subscription) { this._reapplyFilter(); @@ -58,30 +83,38 @@ export class FilteredMap extends BaseObservableMap { } } } - this._included = null; + this._included = undefined; } } - onAdd(key, value) { + onAdd(key: K, value: V) { if (this._filter) { - const included = this._filter(value, key); - this._included.set(key, included); - if (!included) { - return; + if (this._included) { + const included = this._filter(value, key); + this._included.set(key, included); + if (!included) { + return; + } + } else { + throw new Error("Internal logic error: FilteredMap._included used before initialized"); } } this.emitAdd(key, value); } - onRemove(key, value) { - const wasIncluded = !this._filter || this._included.get(key); - this._included.delete(key); - if (wasIncluded) { - this.emitRemove(key, value); + onRemove(key: K, value: V) { + const wasIncluded = !this._filter || this._included?.get(key); + if (this._included) { + this._included.delete(key); + if (wasIncluded) { + this.emitRemove(key, value); + } + } else { + throw new Error("Internal logic error: FilteredMap._included used before initialized"); } } - onUpdate(key, value, params) { + onUpdate(key: K, value: V, params: any) { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._included) { return; @@ -96,7 +129,7 @@ export class FilteredMap extends BaseObservableMap { } } - _emitForUpdate(wasIncluded, isIncluded, key, value, params = null) { + _emitForUpdate(wasIncluded: boolean | undefined, isIncluded: boolean, key: K, value: V, params: any = null) { if (wasIncluded && !isIncluded) { this.emitRemove(key, value); } else if (!wasIncluded && isIncluded) { @@ -114,8 +147,10 @@ export class FilteredMap extends BaseObservableMap { onUnsubscribeLast() { super.onUnsubscribeLast(); - this._included = null; - this._subscription = this._subscription(); + this._included = undefined; + if (this._subscription) { + this._subscription = this._subscription(); + } } onReset() { @@ -124,12 +159,12 @@ export class FilteredMap extends BaseObservableMap { } [Symbol.iterator]() { - return new FilterIterator(this._source, this._included); + return new FilterIterator(this._source, this._included); } get size() { let count = 0; - this._included.forEach(included => { + this._included?.forEach(included => { if (included) { count += 1; } @@ -145,9 +180,11 @@ export class FilteredMap extends BaseObservableMap { } } -class FilterIterator { - constructor(map, _included) { - this._included = _included; +class FilterIterator { + private _included?: Map + private _sourceIterator: Iterator<[K, V], any, undefined> + constructor(map: BaseObservableMap, included?: Map) { + this._included = included; this._sourceIterator = map[Symbol.iterator](); } @@ -159,14 +196,14 @@ class FilterIterator { return sourceResult; } const key = sourceResult.value[0]; - if (this._included.get(key)) { + if (this._included?.get(key)) { return sourceResult; } } } } -import {ObservableMap} from "../"; +import {ObservableMap} from ".."; export function tests() { return { "filter preloaded list": assert => { @@ -174,9 +211,22 @@ export function tests() { source.add("one", 1); source.add("two", 2); source.add("three", 3); - const oddNumbers = new FilteredMap(source, x => x % 2 !== 0); + const oddNumbers = new FilteredMap(source, (x: number) => x % 2 !== 0); // can only iterate after subscribing - oddNumbers.subscribe({}); + oddNumbers.subscribe({ + onAdd() { + return; + }, + onRemove() { + return; + }, + onUpdate() { + return; + }, + onReset() { + return; + }, + }); assert.equal(oddNumbers.size, 2); const it = oddNumbers[Symbol.iterator](); assert.deepEqual(it.next().value, ["one", 1]); @@ -199,7 +249,7 @@ export function tests() { source.add("num1", 1); source.add("num2", 2); source.add("num3", 3); - const oddMap = new FilteredMap(source, x => x % 2 !== 0); + const oddMap = new FilteredMap(source, (x: number) => x % 2 !== 0); oddMap.subscribe({ onAdd() { count_add += 1; @@ -209,6 +259,9 @@ export function tests() { }, onUpdate() { count_update += 1; + }, + onReset() { + return; } }); source.set("num3", 4); @@ -218,5 +271,5 @@ export function tests() { assert.strictEqual(count_update, 1); assert.strictEqual(count_remove, 1); } - } + }; } diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 85e46703..f4625a6f 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -44,7 +44,7 @@ export class ObservableMap extends BaseObservableMap { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { return this._config.filterValues(this, filter); } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index 7a7ea0c4..ce280d2f 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -36,7 +36,7 @@ export function config(): BaseObservableMapConfig { sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { return new SortedMapList(_this, comparator); }, - filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { + filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { return new FilteredMap(_this, filter); } }; From 3ba2bab59f9bca9ce89c1fc80a30dc3d1efa5380 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Wed, 6 Jul 2022 20:23:37 -0400 Subject: [PATCH 173/354] JoinedMap to typescript --- .eslintrc.js | 34 +++--- src/observable/map/BaseObservableMap.ts | 4 +- src/observable/map/FilteredMap.ts | 2 +- .../map/{JoinedMap.js => JoinedMap.ts} | 102 ++++++++++++------ src/observable/map/ObservableMap.ts | 2 +- src/observable/map/config.ts | 4 +- 6 files changed, 90 insertions(+), 58 deletions(-) rename src/observable/map/{JoinedMap.js => JoinedMap.ts} (72%) diff --git a/.eslintrc.js b/.eslintrc.js index cf1fc3bf..cb28f4c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,25 @@ module.exports = { - root: true, - env: { + "env": { "browser": true, "es6": true }, - extends: [ - // "plugin:@typescript-eslint/recommended", - // "plugin:@typescript-eslint/recommended-requiring-type-checking", - ], - parser: '@typescript-eslint/parser', - parserOptions: { + "extends": "eslint:recommended", + "parserOptions": { "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" + "sourceType": "module" }, - plugins: [ - '@typescript-eslint', - ], - rules: { - "@typescript-eslint/no-floating-promises": 2, - "@typescript-eslint/no-misused-promises": 2, - "semi": ["error", "always"] + "rules": { + "no-console": "off", + "no-empty": "off", + "no-prototype-builtins": "off", + "no-unused-vars": "warn" + }, + "globals": { + "DEFINE_VERSION": "readonly", + "DEFINE_GLOBAL_HASH": "readonly", + // only available in sw.js + "DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly", + "DEFINE_HASHED_PRECACHED_ASSETS": "readonly", + "DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly" } }; diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 4e8c88d0..c4f7daae 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -28,7 +28,7 @@ export interface IMapObserver { } export type BaseObservableMapConfig = { - join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; + join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; sortValues(_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList; filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; @@ -66,7 +66,7 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; + abstract join(...otherMaps: Array): JoinedMap; abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index a2250f21..c4810d9b 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -35,7 +35,7 @@ export class FilteredMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { + join(...otherMaps: Array): JoinedMap { return this._config.join(this, ...otherMaps); } diff --git a/src/observable/map/JoinedMap.js b/src/observable/map/JoinedMap.ts similarity index 72% rename from src/observable/map/JoinedMap.js rename to src/observable/map/JoinedMap.ts index ea5ad784..11589ba5 100644 --- a/src/observable/map/JoinedMap.js +++ b/src/observable/map/JoinedMap.ts @@ -14,16 +14,41 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {config} from "./config"; +import {FilteredMap} from "./FilteredMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; -export class JoinedMap extends BaseObservableMap { - constructor(sources) { + +export class JoinedMap extends BaseObservableMap { + protected _sources: BaseObservableMap[]; + private _config: BaseObservableMapConfig + private _subscriptions?: SourceSubscriptionHandler[]; + + constructor(sources: BaseObservableMap[]) { super(); this._sources = sources; - this._subscriptions = null; + this._config = config(); } - onAdd(source, key, value) { + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); + } + + onAdd(source: BaseObservableMap, key: K, value: V) { if (!this._isKeyAtSourceOccluded(source, key)) { const occludingValue = this._getValueFromOccludedSources(source, key); if (occludingValue !== undefined) { @@ -35,7 +60,7 @@ export class JoinedMap extends BaseObservableMap { } } - onRemove(source, key, value) { + onRemove(source: BaseObservableMap, key: K, value: V) { if (!this._isKeyAtSourceOccluded(source, key)) { this.emitRemove(key, value); const occludedValue = this._getValueFromOccludedSources(source, key); @@ -47,7 +72,7 @@ export class JoinedMap extends BaseObservableMap { } } - onUpdate(source, key, value, params) { + onUpdate(source: BaseObservableMap, key: K, value: V, params: any) { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._subscriptions) { return; @@ -66,7 +91,7 @@ export class JoinedMap extends BaseObservableMap { super.onSubscribeFirst(); } - _isKeyAtSourceOccluded(source, key) { + _isKeyAtSourceOccluded(source: BaseObservableMap, key: K) { // sources that come first in the sources array can // hide the keys in later sources, to prevent events // being emitted for the same key and different values, @@ -81,7 +106,7 @@ export class JoinedMap extends BaseObservableMap { } // get the value that the given source and key occlude, if any - _getValueFromOccludedSources(source, key) { + _getValueFromOccludedSources(source: BaseObservableMap, key: K) { // sources that come first in the sources array can // hide the keys in later sources, to prevent events // being emitted for the same key and different values, @@ -99,51 +124,55 @@ export class JoinedMap extends BaseObservableMap { onUnsubscribeLast() { super.onUnsubscribeLast(); - for (const s of this._subscriptions) { - s.dispose(); + if (this._subscriptions) { + for (const s of this._subscriptions) { + s.dispose(); + } } } [Symbol.iterator]() { - return new JoinedIterator(this._sources); + return new JoinedIterator(this._sources); } get size() { return this._sources.reduce((sum, s) => sum + s.size, 0); } - get(key) { + get(key: K): V | undefined{ for (const s of this._sources) { const value = s.get(key); if (value) { return value; } } - return null; + return undefined; } } -class JoinedIterator { - constructor(sources) { +class JoinedIterator implements Iterator<[K, V]> { + private _sources: {[Symbol.iterator](): Iterator<[K, V]>}[]; + private _sourceIndex = -1; + private _encounteredKeys = new Set(); + private _currentIterator?: Iterator<[K, V]> + + constructor(sources: {[Symbol.iterator](): Iterator<[K, V]>}[]) { this._sources = sources; - this._sourceIndex = -1; - this._currentIterator = null; - this._encounteredKeys = new Set(); } - next() { - let result; + next(): IteratorYieldResult<[K, V]> | IteratorReturnResult { + let result: IteratorYieldResult<[K, V]> | undefined = undefined; while (!result) { if (!this._currentIterator) { this._sourceIndex += 1; if (this._sources.length <= this._sourceIndex) { - return {done: true}; + return {done: true, value: null}; } this._currentIterator = this._sources[this._sourceIndex][Symbol.iterator](); } - const sourceResult = this._currentIterator.next(); - if (sourceResult.done) { - this._currentIterator = null; + const sourceResult = this._currentIterator?.next(); + if (!sourceResult || sourceResult.done) { + this._currentIterator = undefined; continue; } else { const key = sourceResult.value[0]; @@ -191,26 +220,29 @@ class SourceSubscriptionHandler { } -import {ObservableMap} from "../"; +import {ObservableMap} from ".."; export function tests() { - function observeMap(map) { - const events = []; + function observeMap(map: JoinedMap) { + const events: { type: string, key: any, value: any, params?: any }[] = []; map.subscribe({ - onAdd(key, value) { events.push({type: "add", key, value}); }, - onRemove(key, value) { events.push({type: "remove", key, value}); }, - onUpdate(key, value, params) { events.push({type: "update", key, value, params}); } + onAdd(key, value) { events.push({ type: "add", key, value }); }, + onRemove(key, value) { events.push({ type: "remove", key, value }); }, + onUpdate(key, value, params) { events.push({ type: "update", key, value, params }); }, + onReset: function (): void { + return; + } }); return events; } return { "joined iterator": assert => { - const firstKV = ["a", 1]; - const secondKV = ["b", 2]; - const thirdKV = ["c", 3]; - const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]); + const firstKV: [string, number] = ["a", 1]; + const secondKV: [string, number] = ["b", 2]; + const thirdKV: [string, number] = ["c", 3]; + const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]); assert.equal(it.next().value, firstKV); assert.equal(it.next().value, secondKV); assert.equal(it.next().value, thirdKV); diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index f4625a6f..42d47c8d 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -32,7 +32,7 @@ export class ObservableMap extends BaseObservableMap { this._values = new Map(initialValues); } - join(...otherMaps: Array): JoinedMap { + join(...otherMaps: Array): JoinedMap { return this._config.join(this, ...otherMaps); } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index ce280d2f..a0a63014 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -27,7 +27,7 @@ import {SortedMapList} from "../list/SortedMapList.js"; // (i.e. `new JoinedMap()`) and BaseObservableMap (as they extend it). export function config(): BaseObservableMapConfig { return { - join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { + join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { return new JoinedMap([_this].concat(otherMaps)); }, mapValues: (_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap => { @@ -40,4 +40,4 @@ export function config(): BaseObservableMapConfig { return new FilteredMap(_this, filter); } }; -}; \ No newline at end of file +} \ No newline at end of file From bd58674626d33c80fcd82335b51b6466e5c7cd81 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 7 Jul 2022 22:26:58 -0400 Subject: [PATCH 174/354] Updates .ts-eslintrc with new settings, see https://github.com/import-js/eslint-plugin-import/issues/653#issuecomment-840228881 and https://typescript-eslint.io/docs/linting/troubleshooting/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors for the impetus --- .ts-eslintrc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index cf1fc3bf..ac73bb34 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -20,6 +20,8 @@ module.exports = { rules: { "@typescript-eslint/no-floating-promises": 2, "@typescript-eslint/no-misused-promises": 2, - "semi": ["error", "always"] + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['warn'], + 'no-undef': 'off', } }; From 63e9b49ebe7cbefcf30f04a305d9f627da9874d2 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 7 Jul 2022 22:29:28 -0400 Subject: [PATCH 175/354] uncommenting tests --- src/observable/map/ObservableMap.ts | 256 ++++++++++++++-------------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 42d47c8d..7ee3d666 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -117,137 +117,137 @@ export class ObservableMap extends BaseObservableMap { } }; -// export function tests() { -// return { -// test_initial_values(assert) { -// const map = new ObservableMap([ -// ["a", 5], -// ["b", 10] -// ]); -// assert.equal(map.size, 2); -// assert.equal(map.get("a"), 5); -// assert.equal(map.get("b"), 10); -// }, +export function tests() { + return { + test_initial_values(assert) { + const map = new ObservableMap([ + ["a", 5], + ["b", 10] + ]); + assert.equal(map.size, 2); + assert.equal(map.get("a"), 5); + assert.equal(map.get("b"), 10); + }, -// test_add(assert) { -// let fired = 0; -// const map = new ObservableMap(); -// map.subscribe({ -// onAdd(key, value) { -// fired += 1; -// assert.equal(key, 1); -// assert.deepEqual(value, {value: 5}); -// }, -// onUpdate() {}, -// onRemove() {}, -// onReset() {} -// }); -// map.add(1, {value: 5}); -// assert.equal(map.size, 1); -// assert.equal(fired, 1); -// }, + test_add(assert) { + let fired = 0; + const map = new ObservableMap(); + map.subscribe({ + onAdd(key, value) { + fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 5}); + }, + onUpdate() {}, + onRemove() {}, + onReset() {} + }); + map.add(1, {value: 5}); + assert.equal(map.size, 1); + assert.equal(fired, 1); + }, -// test_update(assert) { -// let fired = 0; -// const map = new ObservableMap(); -// const value = {number: 5}; -// map.add(1, value); -// map.subscribe({ -// onUpdate(key, value, params) { -// fired += 1; -// assert.equal(key, 1); -// assert.deepEqual(value, {number: 6}); -// assert.equal(params, "test"); -// }, -// onAdd() {}, -// onRemove() {}, -// onReset() {} -// }); -// value.number = 6; -// map.update(1, "test"); -// assert.equal(fired, 1); -// }, + test_update(assert) { + let fired = 0; + const map = new ObservableMap(); + const value = {number: 5}; + map.add(1, value); + map.subscribe({ + onUpdate(key, value, params) { + fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {number: 6}); + assert.equal(params, "test"); + }, + onAdd() {}, + onRemove() {}, + onReset() {} + }); + value.number = 6; + map.update(1, "test"); + assert.equal(fired, 1); + }, -// test_update_unknown(assert) { -// let fired = 0; -// const map = new ObservableMap(); -// map.subscribe({ -// onUpdate() { fired += 1; }, -// onAdd() {}, -// onRemove() {}, -// onReset() {} -// }); -// const result = map.update(1); -// assert.equal(fired, 0); -// assert.equal(result, false); -// }, + test_update_unknown(assert) { + let fired = 0; + const map = new ObservableMap(); + map.subscribe({ + onUpdate() { fired += 1; }, + onAdd() {}, + onRemove() {}, + onReset() {} + }); + const result = map.update(1); + assert.equal(fired, 0); + assert.equal(result, false); + }, -// test_set(assert) { -// let add_fired = 0, update_fired = 0; -// const map = new ObservableMap(); -// map.subscribe({ -// onAdd(key, value) { -// add_fired += 1; -// assert.equal(key, 1); -// assert.deepEqual(value, {value: 5}); -// }, -// onUpdate(key, value/*, params*/) { -// update_fired += 1; -// assert.equal(key, 1); -// assert.deepEqual(value, {value: 7}); -// }, -// onRemove() {}, -// onReset() {} -// }); -// // Add -// map.set(1, {value: 5}); -// assert.equal(map.size, 1); -// assert.equal(add_fired, 1); -// // Update -// map.set(1, {value: 7}); -// assert.equal(map.size, 1); -// assert.equal(update_fired, 1); -// }, + test_set(assert) { + let add_fired = 0, update_fired = 0; + const map = new ObservableMap(); + map.subscribe({ + onAdd(key, value) { + add_fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 5}); + }, + onUpdate(key, value/*, params*/) { + update_fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 7}); + }, + onRemove() {}, + onReset() {} + }); + // Add + map.set(1, {value: 5}); + assert.equal(map.size, 1); + assert.equal(add_fired, 1); + // Update + map.set(1, {value: 7}); + assert.equal(map.size, 1); + assert.equal(update_fired, 1); + }, -// test_remove(assert) { -// let fired = 0; -// const map = new ObservableMap(); -// const value = {value: 5}; -// map.add(1, value); -// map.subscribe({ -// onRemove(key, value) { -// fired += 1; -// assert.equal(key, 1); -// assert.deepEqual(value, {value: 5}); -// }, -// onAdd() {}, -// onUpdate() {}, -// onReset() {} -// }); -// map.remove(1); -// assert.equal(map.size, 0); -// assert.equal(fired, 1); -// }, + test_remove(assert) { + let fired = 0; + const map = new ObservableMap(); + const value = {value: 5}; + map.add(1, value); + map.subscribe({ + onRemove(key, value) { + fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 5}); + }, + onAdd() {}, + onUpdate() {}, + onReset() {} + }); + map.remove(1); + assert.equal(map.size, 0); + assert.equal(fired, 1); + }, -// test_iterate(assert) { -// const results: any[] = []; -// const map = new ObservableMap(); -// map.add(1, {number: 5}); -// map.add(2, {number: 6}); -// map.add(3, {number: 7}); -// for (let e of map) { -// results.push(e); -// } -// assert.equal(results.length, 3); -// assert.equal(results.find(([key]) => key === 1)[1].number, 5); -// assert.equal(results.find(([key]) => key === 2)[1].number, 6); -// assert.equal(results.find(([key]) => key === 3)[1].number, 7); -// }, -// test_size(assert) { -// const map = new ObservableMap(); -// map.add(1, {number: 5}); -// map.add(2, {number: 6}); -// assert.equal(map.size, 2); -// }, -// } -// } + test_iterate(assert) { + const results: any[] = []; + const map = new ObservableMap(); + map.add(1, {number: 5}); + map.add(2, {number: 6}); + map.add(3, {number: 7}); + for (let e of map) { + results.push(e); + } + assert.equal(results.length, 3); + assert.equal(results.find(([key]) => key === 1)[1].number, 5); + assert.equal(results.find(([key]) => key === 2)[1].number, 6); + assert.equal(results.find(([key]) => key === 3)[1].number, 7); + }, + test_size(assert) { + const map = new ObservableMap(); + map.add(1, {number: 5}); + map.add(2, {number: 6}); + assert.equal(map.size, 2); + }, + } +} From edeec896ae6aa7d55a89d41400f35a62087a193a Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Fri, 8 Jul 2022 20:39:07 -0400 Subject: [PATCH 176/354] typescriptifies SourceSubscriptionHandler --- src/observable/map/JoinedMap.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 11589ba5..6f285fd9 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -19,12 +19,13 @@ import {config} from "./config"; import {FilteredMap} from "./FilteredMap.js"; import {MappedMap} from "./MappedMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; +import {SubscriptionHandle} from "../BaseObservable" export class JoinedMap extends BaseObservableMap { protected _sources: BaseObservableMap[]; private _config: BaseObservableMapConfig - private _subscriptions?: SourceSubscriptionHandler[]; + private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { super(); @@ -186,11 +187,15 @@ class JoinedIterator implements Iterator<[K, V]> { } } -class SourceSubscriptionHandler { - constructor(source, joinedMap) { +class SourceSubscriptionHandler { + private _source: BaseObservableMap; + private _joinedMap: JoinedMap; + private _subscription?: SubscriptionHandle; + + constructor(source: BaseObservableMap, joinedMap: JoinedMap) { this._source = source; this._joinedMap = joinedMap; - this._subscription = null; + this._subscription = undefined; } subscribe() { @@ -199,23 +204,23 @@ class SourceSubscriptionHandler { } dispose() { - this._subscription = this._subscription(); + if (this._subscription) this._subscription = this._subscription(); } - onAdd(key, value) { + onAdd(key: K, value: V) { this._joinedMap.onAdd(this._source, key, value); } - onRemove(key, value) { + onRemove(key: K, value: V) { this._joinedMap.onRemove(this._source, key, value); } - onUpdate(key, value, params) { + onUpdate(key: K, value: V, params: any) { this._joinedMap.onUpdate(this._source, key, value, params); } onReset() { - this._joinedMap.onReset(this._source); + this._joinedMap.onReset(); } } From d060d337b63493e890bad3fb3bb7b17b82334f4a Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Fri, 8 Jul 2022 20:58:30 -0400 Subject: [PATCH 177/354] typescriptifies LogMap --- src/observable/map/LogMap.js | 70 ----------------------- src/observable/map/LogMap.ts | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 70 deletions(-) delete mode 100644 src/observable/map/LogMap.js create mode 100644 src/observable/map/LogMap.ts diff --git a/src/observable/map/LogMap.js b/src/observable/map/LogMap.js deleted file mode 100644 index 1beb4846..00000000 --- a/src/observable/map/LogMap.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -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 {BaseObservableMap} from "./BaseObservableMap"; - -export class LogMap extends BaseObservableMap { - constructor(source, log) { - super(); - this._source = source; - this.log = log; - this._subscription = null; - } - - onAdd(key, value) { - this.log("add", key, value); - this.emitAdd(key, value); - } - - onRemove(key, value) { - this.log("remove", key, value); - this.emitRemove(key, value); - } - - onUpdate(key, value, params) { - this.log("update", key, value, params); - this.emitUpdate(key, value, params); - } - - onSubscribeFirst() { - this.log("subscribeFirst"); - this._subscription = this._source.subscribe(this); - super.onSubscribeFirst(); - } - - onUnsubscribeLast() { - super.onUnsubscribeLast(); - this._subscription = this._subscription(); - this.log("unsubscribeLast"); - } - - onReset() { - this.log("reset"); - this.emitReset(); - } - - [Symbol.iterator]() { - return this._source[Symbol.iterator](); - } - - get size() { - return this._source.size; - } - - get(key) { - return this._source.get(key); - } -} diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts new file mode 100644 index 00000000..988aa71b --- /dev/null +++ b/src/observable/map/LogMap.ts @@ -0,0 +1,104 @@ +/* +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 {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {config} from "./config"; +import {FilteredMap} from "./FilteredMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {JoinedMap} from "./JoinedMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; +import {SubscriptionHandle} from "../BaseObservable" +import {ILogItem, LabelOrValues} from "../../logging/types"; +import {LogLevel} from "../../logging/LogFilter"; + +export class LogMap extends BaseObservableMap { + private _source: BaseObservableMap; + private _subscription?: SubscriptionHandle; + private _log: ILogItem; + private _config: BaseObservableMapConfig + + + constructor(source: BaseObservableMap, log: ILogItem) { + super(); + this._source = source; + this._log = log; + this._config = config(); + } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); + } + + private log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem { + return this._log.log(labelOrValues, logLevel); + } + + onAdd(key: K, value: V) { + this.log("add " + JSON.stringify({key, value})); + this.emitAdd(key, value); + } + + onRemove(key: K, value: V) { + this.log("remove " + JSON.stringify({key, value})); + this.emitRemove(key, value); + } + + onUpdate(key: K, value: V, params: any) { + this.log("update" + JSON.stringify({key, value, params})); + this.emitUpdate(key, value, params); + } + + onSubscribeFirst() { + this.log("subscribeFirst"); + this._subscription = this._source.subscribe(this); + super.onSubscribeFirst(); + } + + onUnsubscribeLast() { + super.onUnsubscribeLast(); + if (this._subscription) this._subscription = this._subscription(); + this.log("unsubscribeLast"); + } + + onReset() { + this.log("reset"); + this.emitReset(); + } + + [Symbol.iterator]() { + return this._source[Symbol.iterator](); + } + + get size() { + return this._source.size; + } + + get(key: K) { + return this._source.get(key); + } +} From 95c65280ef303b1ebd44315f53b5ec8e71e581fd Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Fri, 8 Jul 2022 22:07:36 -0400 Subject: [PATCH 178/354] typescriptifying MappedMap --- .../session/rightpanel/MemberListViewModel.js | 2 +- src/observable/map/BaseObservableMap.ts | 4 +- src/observable/map/FilteredMap.ts | 2 +- src/observable/map/JoinedMap.ts | 4 +- src/observable/map/LogMap.ts | 2 +- .../map/{MappedMap.js => MappedMap.ts} | 65 +++++++++++++++---- src/observable/map/ObservableMap.ts | 2 +- src/observable/map/config.ts | 2 +- 8 files changed, 62 insertions(+), 21 deletions(-) rename src/observable/map/{MappedMap.js => MappedMap.ts} (57%) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index b75a3d1c..1633936a 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -47,7 +47,7 @@ export class MemberListViewModel extends ViewModel { this.nameDisambiguator.disambiguate(vm); return vm; } - const updater = (vm, params, newMember) => { + const updater = (params, vm, newMember) => { vm.updateFrom(newMember); this.nameDisambiguator.disambiguate(vm); }; diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index c4f7daae..b2f63c03 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -29,7 +29,7 @@ export interface IMapObserver { export type BaseObservableMapConfig = { join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; - mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; + mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; sortValues(_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList; filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; } @@ -67,7 +67,7 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; - abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; + abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index c4810d9b..a9603d6d 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -39,7 +39,7 @@ export class FilteredMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ return this._config.mapValues(this, mapper, updater); } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 6f285fd9..fbf2f39d 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -24,7 +24,7 @@ import {SubscriptionHandle} from "../BaseObservable" export class JoinedMap extends BaseObservableMap { protected _sources: BaseObservableMap[]; - private _config: BaseObservableMapConfig + private _config: BaseObservableMapConfig; private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { @@ -37,7 +37,7 @@ export class JoinedMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + mapValues(mapper: any, updater?: (params: any) => void): MappedMap { return this._config.mapValues(this, mapper, updater); } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 988aa71b..a287014a 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -42,7 +42,7 @@ export class LogMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + mapValues(mapper: any, updater?: (params: any) => void): MappedMap { return this._config.mapValues(this, mapper, updater); } diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.ts similarity index 57% rename from src/observable/map/MappedMap.js rename to src/observable/map/MappedMap.ts index a6b65c41..ff522621 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.ts @@ -14,49 +14,83 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {config} from "./config"; +import {JoinedMap} from "./JoinedMap.js"; +import {FilteredMap} from "./FilteredMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; +import {SubscriptionHandle} from "../BaseObservable"; + /* so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ -export class MappedMap extends BaseObservableMap { - constructor(source, mapper, updater) { +export class MappedMap extends BaseObservableMap { + private _source: BaseObservableMap; + private _mapper: Mapper; + private _updater?: Updater; + private _mappedValues: Map; + private _subscription?: SubscriptionHandle; + private _config: BaseObservableMapConfig + + constructor( + source: BaseObservableMap, + mapper: Mapper, + updater?: Updater + ) { super(); this._source = source; this._mapper = mapper; this._updater = updater; - this._mappedValues = new Map(); + this._mappedValues = new Map(); + this._config = config(); } - _emitSpontaneousUpdate(key, params) { + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); + } + + _emitSpontaneousUpdate(key: K, params: any) { const value = this._mappedValues.get(key); if (value) { this.emitUpdate(key, value, params); } } - onAdd(key, value) { + onAdd(key: K, value: V) { const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key); const mappedValue = this._mapper(value, emitSpontaneousUpdate); this._mappedValues.set(key, mappedValue); this.emitAdd(key, mappedValue); } - onRemove(key/*, _value*/) { + onRemove(key: K/*, _value*/) { const mappedValue = this._mappedValues.get(key); if (this._mappedValues.delete(key)) { - this.emitRemove(key, mappedValue); + if (mappedValue) this.emitRemove(key, mappedValue); } } - onUpdate(key, value, params) { + onUpdate(key: K, value: V, params: any) { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._mappedValues) { return; } const mappedValue = this._mappedValues.get(key); if (mappedValue !== undefined) { - this._updater?.(mappedValue, params, value); + this._updater?.(params, mappedValue, value); // TODO: map params somehow if needed? this.emitUpdate(key, mappedValue, params); } @@ -74,7 +108,7 @@ export class MappedMap extends BaseObservableMap { onUnsubscribeLast() { super.onUnsubscribeLast(); - this._subscription = this._subscription(); + if (this._subscription) this._subscription = this._subscription(); this._mappedValues.clear(); } @@ -91,7 +125,14 @@ export class MappedMap extends BaseObservableMap { return this._mappedValues.size; } - get(key) { + get(key: K): V | undefined { return this._mappedValues.get(key); } } + +type Mapper = ( + value: V, + emitSpontaneousUpdate: any, +) => V; + +type Updater = (params: any, mappedValue?: V, value?: V) => void; \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 7ee3d666..06776927 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -36,7 +36,7 @@ export class ObservableMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + mapValues(mapper: any, updater?: (params: any) => void): MappedMap { return this._config.mapValues(this, mapper, updater); } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index a0a63014..c91cfe1a 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -30,7 +30,7 @@ export function config(): BaseObservableMapConfig { join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { return new JoinedMap([_this].concat(otherMaps)); }, - mapValues: (_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap => { + mapValues: (_this: BaseObservableMap, mapper: any, updater: (params: any) => void): MappedMap => { return new MappedMap(_this, mapper, updater); }, sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { From ab6a8ad3aac1ba9493119ab00c824f48da745c0e Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 10:16:46 -0400 Subject: [PATCH 179/354] typescriptifying ApplyMap --- .../session/leftpanel/LeftPanelViewModel.js | 4 +- src/observable/index.ts | 29 +--------- .../map/{ApplyMap.js => ApplyMap.ts} | 54 ++++++++++++++----- src/observable/map/FilteredMap.ts | 2 +- 4 files changed, 47 insertions(+), 42 deletions(-) rename src/observable/map/{ApplyMap.js => ApplyMap.ts} (52%) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 8c8d71a2..8e814151 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -20,8 +20,8 @@ import {RoomTileViewModel} from "./RoomTileViewModel.js"; 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"; +import {ApplyMap} from "../../../observable/map/ApplyMap"; +import {addPanelIfNeeded} from "../../navigation"; export class LeftPanelViewModel extends ViewModel { constructor(options) { diff --git a/src/observable/index.ts b/src/observable/index.ts index 47ab84ca..dfd272fd 100644 --- a/src/observable/index.ts +++ b/src/observable/index.ts @@ -14,36 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {SortedMapList} from "./list/SortedMapList.js"; -import {FilteredMap} from "./map/FilteredMap.js"; -import {MappedMap} from "./map/MappedMap.js"; -import {JoinedMap} from "./map/JoinedMap.js"; -import {BaseObservableMap} from "./map/BaseObservableMap"; + // re-export "root" (of chain) collection export { ObservableMap } from "./map/ObservableMap"; export { ObservableArray } from "./list/ObservableArray"; export { SortedArray } from "./list/SortedArray"; export { MappedList } from "./list/MappedList"; export { AsyncMappedList } from "./list/AsyncMappedList"; -export { ConcatList } from "./list/ConcatList"; - - -// avoid circular dependency between these classes -// and BaseObservableMap (as they extend it) -Object.assign(BaseObservableMap.prototype, { - sortValues(comparator) { - return new SortedMapList(this, comparator); - }, - - mapValues(mapper, updater) { - return new MappedMap(this, mapper, updater); - }, - - filterValues(filter) { - return new FilteredMap(this, filter); - }, - - join(...otherMaps) { - return new JoinedMap([this].concat(otherMaps)); - } -}); \ No newline at end of file +export { ConcatList } from "./list/ConcatList"; \ No newline at end of file diff --git a/src/observable/map/ApplyMap.js b/src/observable/map/ApplyMap.ts similarity index 52% rename from src/observable/map/ApplyMap.js rename to src/observable/map/ApplyMap.ts index 6be7278a..da485af9 100644 --- a/src/observable/map/ApplyMap.js +++ b/src/observable/map/ApplyMap.ts @@ -14,45 +14,73 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {SubscriptionHandle} from "../BaseObservable"; +import {config} from "./config"; +import {JoinedMap} from "./JoinedMap.js"; +import {MappedMap} from "./MappedMap.js"; +import {FilteredMap} from "./FilteredMap.js"; +import {SortedMapList} from "../list/SortedMapList.js"; -export class ApplyMap extends BaseObservableMap { - constructor(source, apply) { + +export class ApplyMap extends BaseObservableMap { + private _source: BaseObservableMap; + private _subscription?: SubscriptionHandle; + private _apply?: Apply; + private _config: BaseObservableMapConfig; + + constructor(source: BaseObservableMap, apply?: Apply) { super(); this._source = source; this._apply = apply; - this._subscription = null; + this._config = config(); + } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + return this._config.filterValues(this, filter); } hasApply() { return !!this._apply; } - setApply(apply) { + setApply(apply?: Apply) { this._apply = apply; - if (apply) { + if (this._apply) { this.applyOnce(this._apply); } } - applyOnce(apply) { + applyOnce(apply: Apply) { for (const [key, value] of this._source) { apply(key, value); } } - onAdd(key, value) { + onAdd(key: K, value: V) { if (this._apply) { this._apply(key, value); } this.emitAdd(key, value); } - onRemove(key, value) { + onRemove(key: K, value: V) { this.emitRemove(key, value); } - onUpdate(key, value, params) { + onUpdate(key: K, value: V, params: any) { if (this._apply) { this._apply(key, value, params); } @@ -69,7 +97,7 @@ export class ApplyMap extends BaseObservableMap { onUnsubscribeLast() { super.onUnsubscribeLast(); - this._subscription = this._subscription(); + if (this._subscription) this._subscription = this._subscription(); } onReset() { @@ -87,7 +115,9 @@ export class ApplyMap extends BaseObservableMap { return this._source.size; } - get(key) { + get(key: K) { return this._source.get(key); } } + +type Apply = (key: K, value: V, params?: any) => void; \ No newline at end of file diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index a9603d6d..8b544072 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -23,7 +23,7 @@ import {SortedMapList} from "../list/SortedMapList.js"; export class FilteredMap extends BaseObservableMap { private _source: BaseObservableMap; - private _config: BaseObservableMapConfig + private _config: BaseObservableMapConfig; private _filter: (value: V, key: K) => boolean; private _included?: Map; private _subscription?: SubscriptionHandle; From 081cc05fa65868ddeb5601ae33b64321db078ad5 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 11:53:09 -0400 Subject: [PATCH 180/354] Updates comparator --- src/observable/list/BaseObservableList.ts | 2 +- src/observable/list/SortedArray.ts | 6 +++--- src/observable/list/SortedMapList.js | 4 ++-- src/observable/map/ApplyMap.ts | 2 +- src/observable/map/BaseObservableMap.ts | 4 ++-- src/observable/map/FilteredMap.ts | 2 +- src/observable/map/JoinedMap.ts | 2 +- src/observable/map/LogMap.ts | 4 ++-- src/observable/map/MappedMap.ts | 2 +- src/observable/map/ObservableMap.ts | 4 ++-- src/observable/map/config.ts | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/observable/list/BaseObservableList.ts b/src/observable/list/BaseObservableList.ts index d103eb64..e2806c72 100644 --- a/src/observable/list/BaseObservableList.ts +++ b/src/observable/list/BaseObservableList.ts @@ -31,7 +31,7 @@ export function defaultObserverWith(overrides: { [key in keyof IListObserver< onUpdate(){}, onRemove(){}, onMove(){}, - } + }; return Object.assign(defaults, overrides); } diff --git a/src/observable/list/SortedArray.ts b/src/observable/list/SortedArray.ts index c85cca27..7df285a5 100644 --- a/src/observable/list/SortedArray.ts +++ b/src/observable/list/SortedArray.ts @@ -87,7 +87,7 @@ export class SortedArray extends BaseObservableList { const idx = sortedIndex(this._items, item, this._comparator); if (idx >= this._items.length || this._comparator(this._items[idx], item) !== 0) { this._items.splice(idx, 0, item); - this.emitAdd(idx, item) + this.emitAdd(idx, item); } else { this._items[idx] = item; this.emitUpdate(idx, item, updateParams); @@ -156,7 +156,7 @@ export function tests() { assert.equal(sa.get(0), "a"); assert.equal(sa.get(1), "b"); assert.equal(sa.get(2), "c"); - }, + }, "_getNext": assert => { const sa = new SortedArray((a, b) => a.localeCompare(b)); sa.setManyUnsorted(["b", "a", "f"]); @@ -183,5 +183,5 @@ export function tests() { // check done persists assert.equal(it.next().done, true); } - } + }; } diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index 21a3aa55..6f4be123 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -129,7 +129,7 @@ export class SortedMapList extends BaseObservableList { } return v; } - } + }; } } @@ -267,5 +267,5 @@ export function tests() { assert.equal(updateFired, 1); assert.deepEqual(Array.from(list).map(v => v.number), [1, 3, 11]); }, - } + }; } diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index da485af9..23b6f6ea 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -44,7 +44,7 @@ export class ApplyMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index b2f63c03..37597932 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -30,7 +30,7 @@ export interface IMapObserver { export type BaseObservableMapConfig = { join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; - sortValues(_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList; + sortValues(_this: BaseObservableMap, comparator: (a: V, b: V) => number): SortedMapList; filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; } @@ -68,7 +68,7 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; - abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList; + abstract sortValues(comparator: (a: V, b: V) => number): SortedMapList; abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; abstract [Symbol.iterator](): Iterator<[K, V]>; diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 8b544072..41017bfd 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -43,7 +43,7 @@ export class FilteredMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index fbf2f39d..8973f1bf 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -41,7 +41,7 @@ export class JoinedMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index a287014a..1417b14c 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -20,7 +20,7 @@ import {FilteredMap} from "./FilteredMap.js"; import {MappedMap} from "./MappedMap.js"; import {JoinedMap} from "./JoinedMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; -import {SubscriptionHandle} from "../BaseObservable" +import {SubscriptionHandle} from "../BaseObservable"; import {ILogItem, LabelOrValues} from "../../logging/types"; import {LogLevel} from "../../logging/LogFilter"; @@ -46,7 +46,7 @@ export class LogMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index ff522621..a3dbb000 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -54,7 +54,7 @@ export class MappedMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 06776927..169d2b58 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -40,7 +40,7 @@ export class ObservableMap extends BaseObservableMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator?: (a: any, b: any) => number): SortedMapList { + sortValues(comparator: (a: V, b: V) => number): SortedMapList { return this._config.sortValues(this, comparator); } @@ -249,5 +249,5 @@ export function tests() { map.add(2, {number: 6}); assert.equal(map.size, 2); }, - } + }; } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index c91cfe1a..8557031d 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -33,7 +33,7 @@ export function config(): BaseObservableMapConfig { mapValues: (_this: BaseObservableMap, mapper: any, updater: (params: any) => void): MappedMap => { return new MappedMap(_this, mapper, updater); }, - sortValues: (_this: BaseObservableMap, comparator?: (a: any, b: any) => number): SortedMapList => { + sortValues: (_this: BaseObservableMap, comparator: (a: V, b: V) => number): SortedMapList => { return new SortedMapList(_this, comparator); }, filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { From 674e7bd1c6f88a9dbaf19ee07eab646bfb25b0c1 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 11:57:58 -0400 Subject: [PATCH 181/354] fixing linting errors --- src/observable/ObservableValue.ts | 6 +++--- src/observable/list/AsyncMappedList.ts | 30 +++++++++++++------------- src/observable/list/ConcatList.ts | 4 ++-- src/observable/map/JoinedMap.ts | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index ad0a226d..dab8fb52 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -201,7 +201,7 @@ export function tests() { "waitFor promise resolves on matching update": async assert => { const a = new ObservableValue(5); const handle = a.waitFor(v => v === 6); - Promise.resolve().then(() => { + await Promise.resolve().then(() => { a.set(6); }); await handle.promise; @@ -210,7 +210,7 @@ export function tests() { "waitFor promise rejects when disposed": async assert => { const a = new ObservableValue(0); const handle = a.waitFor(() => false); - Promise.resolve().then(() => { + await Promise.resolve().then(() => { handle.dispose(); }); await assert.rejects(handle.promise, AbortError); @@ -244,5 +244,5 @@ export function tests() { count.set(5); assert.deepEqual(updates, [0, 5]); } - } + }; } diff --git a/src/observable/list/AsyncMappedList.ts b/src/observable/list/AsyncMappedList.ts index 0a919cdc..53edde21 100644 --- a/src/observable/list/AsyncMappedList.ts +++ b/src/observable/list/AsyncMappedList.ts @@ -16,13 +16,13 @@ limitations under the License. */ import {IListObserver} from "./BaseObservableList"; -import {BaseMappedList, Mapper, Updater, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList"; +import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList"; export class AsyncMappedList extends BaseMappedList> implements IListObserver { private _eventQueue: AsyncEvent[] | null = null; private _flushing: boolean = false; - onSubscribeFirst(): void { + async onSubscribeFirst(): Promise { this._sourceUnsubscribe = this._sourceList.subscribe(this); this._eventQueue = []; this._mappedValues = []; @@ -31,7 +31,7 @@ export class AsyncMappedList extends BaseMappedList> impleme this._eventQueue.push(new AddEvent(idx, item)); idx += 1; } - this._flush(); + await this._flush(); } async _flush(): Promise { @@ -49,38 +49,38 @@ export class AsyncMappedList extends BaseMappedList> impleme } } - onReset(): void { + async onReset(): Promise { if (this._eventQueue) { this._eventQueue.push(new ResetEvent()); - this._flush(); + await this._flush(); } } - onAdd(index: number, value: F): void { + async onAdd(index: number, value: F): Promise { if (this._eventQueue) { this._eventQueue.push(new AddEvent(index, value)); - this._flush(); + await this._flush(); } } - onUpdate(index: number, value: F, params: any): void { + async onUpdate(index: number, value: F, params: any): Promise { if (this._eventQueue) { this._eventQueue.push(new UpdateEvent(index, value, params)); - this._flush(); + await this._flush(); } } - onRemove(index: number): void { + async onRemove(index: number): Promise { if (this._eventQueue) { this._eventQueue.push(new RemoveEvent(index)); - this._flush(); + await this._flush(); } } - onMove(fromIdx: number, toIdx: number): void { + async onMove(fromIdx: number, toIdx: number): Promise { if (this._eventQueue) { this._eventQueue.push(new MoveEvent(fromIdx, toIdx)); - this._flush(); + await this._flush(); } } @@ -150,7 +150,7 @@ export function tests() { mapper.subscribe(observer); source.append(2); // will sleep this amount, so second append would take less time source.append(1); - source.update(0, 7, "lucky seven") + source.update(0, 7, "lucky seven"); source.remove(0); { const {type, index, value} = await observer.next(); @@ -182,5 +182,5 @@ export function tests() { assert.equal(value.n, 49); } } - } + }; } diff --git a/src/observable/list/ConcatList.ts b/src/observable/list/ConcatList.ts index 5822468a..8ef7326c 100644 --- a/src/observable/list/ConcatList.ts +++ b/src/observable/list/ConcatList.ts @@ -47,7 +47,7 @@ export class ConcatList extends BaseObservableList implements IListObserve onReset(): void { // TODO: not ideal if other source lists are large // but working impl for now - // reset, and + // reset, and this.emitReset(); let idx = 0; for(const item of this) { @@ -102,7 +102,7 @@ export class ConcatList extends BaseObservableList implements IListObserve } return result; } - } + }; } } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 8973f1bf..fb67e934 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -19,7 +19,7 @@ import {config} from "./config"; import {FilteredMap} from "./FilteredMap.js"; import {MappedMap} from "./MappedMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; -import {SubscriptionHandle} from "../BaseObservable" +import {SubscriptionHandle} from "../BaseObservable"; export class JoinedMap extends BaseObservableMap { From be570cafb0912440cb7ff9956653eab693e2f4e9 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 12:15:32 -0400 Subject: [PATCH 182/354] Adds types for common functions --- .../session/rightpanel/MemberListViewModel.js | 2 +- src/observable/map/ApplyMap.ts | 8 ++++---- src/observable/map/BaseObservableMap.ts | 13 +++++++------ src/observable/map/FilteredMap.ts | 14 +++++++------- src/observable/map/JoinedMap.ts | 8 ++++---- src/observable/map/LogMap.ts | 8 ++++---- src/observable/map/MappedMap.ts | 17 +++++------------ src/observable/map/ObservableMap.ts | 8 ++++---- src/observable/map/config.ts | 19 +++++++++++++++---- 9 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 1633936a..1c878c85 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -46,7 +46,7 @@ export class MemberListViewModel extends ViewModel { const vm = new MemberTileViewModel(this.childOptions({member, emitChange, mediaRepository})); this.nameDisambiguator.disambiguate(vm); return vm; - } + }; const updater = (params, vm, newMember) => { vm.updateFrom(newMember); this.nameDisambiguator.disambiguate(vm); diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 23b6f6ea..4f79a9df 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -16,7 +16,7 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {JoinedMap} from "./JoinedMap.js"; import {MappedMap} from "./MappedMap.js"; import {FilteredMap} from "./FilteredMap.js"; @@ -40,15 +40,15 @@ export class ApplyMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + mapValues(mapper: Mapper, updater?: Updater): MappedMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 37597932..d8a7d43b 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -19,6 +19,7 @@ import {JoinedMap} from "../map/JoinedMap.js"; import {MappedMap} from "../map/MappedMap.js"; import {FilteredMap} from "../map/FilteredMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; +import {Mapper, Updater, Comparator, Filter} from "./config"; export interface IMapObserver { onReset(): void; @@ -29,9 +30,9 @@ export interface IMapObserver { export type BaseObservableMapConfig = { join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; - mapValues(_this: BaseObservableMap, mapper: any, updater?: (params: any) => void): MappedMap; - sortValues(_this: BaseObservableMap, comparator: (a: V, b: V) => number): SortedMapList; - filterValues(_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap; + mapValues(_this: BaseObservableMap, mapper: any, updater?: Updater): MappedMap; + sortValues(_this: BaseObservableMap, comparator: Comparator): SortedMapList; + filterValues(_this: BaseObservableMap, filter: Filter): FilteredMap; } export abstract class BaseObservableMap extends BaseObservable> { @@ -67,9 +68,9 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; - abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap; - abstract sortValues(comparator: (a: V, b: V) => number): SortedMapList; - abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap; + abstract mapValues(mapper: Mapper, updater?: Updater): MappedMap; + abstract sortValues(comparator: Comparator): SortedMapList; + abstract filterValues(filter: Filter): FilteredMap; abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 41017bfd..4e3f8ee6 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -16,7 +16,7 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {JoinedMap} from "./JoinedMap.js"; import {MappedMap} from "./MappedMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; @@ -24,11 +24,11 @@ import {SortedMapList} from "../list/SortedMapList.js"; export class FilteredMap extends BaseObservableMap { private _source: BaseObservableMap; private _config: BaseObservableMapConfig; - private _filter: (value: V, key: K) => boolean; + private _filter: Filter; private _included?: Map; private _subscription?: SubscriptionHandle; - constructor(source: BaseObservableMap, filter: (value: V, key: K) => boolean) { + constructor(source: BaseObservableMap, filter: Filter) { super(); this._source = source; this._filter = filter; @@ -39,19 +39,19 @@ export class FilteredMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + mapValues(mapper: Mapper, updater?: Updater): MappedMap{ return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } - setFilter(filter: (value: V, key: K) => boolean) { + setFilter(filter: Filter) { this._filter = filter; if (this._subscription) { this._reapplyFilter(); diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index fb67e934..67a85368 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {FilteredMap} from "./FilteredMap.js"; import {MappedMap} from "./MappedMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; @@ -37,15 +37,15 @@ export class JoinedMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + mapValues(mapper: Mapper, updater?: Updater): MappedMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 1417b14c..44b0a59c 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {FilteredMap} from "./FilteredMap.js"; import {MappedMap} from "./MappedMap.js"; import {JoinedMap} from "./JoinedMap.js"; @@ -42,15 +42,15 @@ export class LogMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + mapValues(mapper: Mapper, updater?: Updater): MappedMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index a3dbb000..950273ec 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {JoinedMap} from "./JoinedMap.js"; import {FilteredMap} from "./FilteredMap.js"; import {SortedMapList} from "../list/SortedMapList.js"; @@ -50,15 +50,15 @@ export class MappedMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap{ + mapValues(mapper: Mapper, updater?: Updater): MappedMap{ return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } @@ -128,11 +128,4 @@ export class MappedMap extends BaseObservableMap { get(key: K): V | undefined { return this._mappedValues.get(key); } -} - -type Mapper = ( - value: V, - emitSpontaneousUpdate: any, -) => V; - -type Updater = (params: any, mappedValue?: V, value?: V) => void; \ No newline at end of file +} \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 169d2b58..1be7b2c1 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config} from "./config"; +import {config, Mapper, Updater, Comparator, Filter} from "./config"; import {JoinedMap} from "./JoinedMap.js"; import {MappedMap} from "./MappedMap.js"; import {FilteredMap} from "./FilteredMap.js"; @@ -36,15 +36,15 @@ export class ObservableMap extends BaseObservableMap { return this._config.join(this, ...otherMaps); } - mapValues(mapper: any, updater?: (params: any) => void): MappedMap { + mapValues(mapper: Mapper, updater?: Updater): MappedMap { return this._config.mapValues(this, mapper, updater); } - sortValues(comparator: (a: V, b: V) => number): SortedMapList { + sortValues(comparator: Comparator): SortedMapList { return this._config.sortValues(this, comparator); } - filterValues(filter: (v: V, k: K) => boolean): FilteredMap { + filterValues(filter: Filter): FilteredMap { return this._config.filterValues(this, filter); } diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index 8557031d..54e291e2 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -30,14 +30,25 @@ export function config(): BaseObservableMapConfig { join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { return new JoinedMap([_this].concat(otherMaps)); }, - mapValues: (_this: BaseObservableMap, mapper: any, updater: (params: any) => void): MappedMap => { + mapValues: (_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap => { return new MappedMap(_this, mapper, updater); }, - sortValues: (_this: BaseObservableMap, comparator: (a: V, b: V) => number): SortedMapList => { + sortValues: (_this: BaseObservableMap, comparator: Comparator): SortedMapList => { return new SortedMapList(_this, comparator); }, - filterValues: (_this: BaseObservableMap, filter: (v: V, k: K) => boolean): FilteredMap => { + filterValues: (_this: BaseObservableMap, filter: Filter): FilteredMap => { return new FilteredMap(_this, filter); } }; -} \ No newline at end of file +} + +export type Mapper = ( + value: V, + emitSpontaneousUpdate: any, +) => V; + +export type Updater = (params: any, mappedValue?: V, value?: V) => void; + +export type Comparator = (a: V, b: V) => number; + +export type Filter = (v: V, k: K) => boolean; \ No newline at end of file From deab8bdaf074aafe3a88b77ad529a9552cb4063c Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 12:17:59 -0400 Subject: [PATCH 183/354] moves boilerplate to bottom of classes --- src/observable/map/ApplyMap.ts | 33 +++++++++++++++-------------- src/observable/map/FilteredMap.ts | 32 ++++++++++++++-------------- src/observable/map/JoinedMap.ts | 33 +++++++++++++++-------------- src/observable/map/LogMap.ts | 33 +++++++++++++++-------------- src/observable/map/MappedMap.ts | 32 ++++++++++++++-------------- src/observable/map/ObservableMap.ts | 32 ++++++++++++++-------------- 6 files changed, 99 insertions(+), 96 deletions(-) diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 4f79a9df..4dbbc3d3 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -36,22 +36,6 @@ export class ApplyMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - hasApply() { return !!this._apply; } @@ -118,6 +102,23 @@ export class ApplyMap extends BaseObservableMap { get(key: K) { return this._source.get(key); } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } + } type Apply = (key: K, value: V, params?: any) => void; \ No newline at end of file diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 4e3f8ee6..782a67fe 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -35,22 +35,6 @@ export class FilteredMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - setFilter(filter: Filter) { this._filter = filter; if (this._subscription) { @@ -178,6 +162,22 @@ export class FilteredMap extends BaseObservableMap { return value; } } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } } class FilterIterator { diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 67a85368..2be95bdf 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -33,22 +33,6 @@ export class JoinedMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - onAdd(source: BaseObservableMap, key: K, value: V) { if (!this._isKeyAtSourceOccluded(source, key)) { const occludingValue = this._getValueFromOccludedSources(source, key); @@ -149,6 +133,23 @@ export class JoinedMap extends BaseObservableMap { } return undefined; } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } + } class JoinedIterator implements Iterator<[K, V]> { diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 44b0a59c..6f3bf676 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -38,22 +38,6 @@ export class LogMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - private log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem { return this._log.log(labelOrValues, logLevel); } @@ -101,4 +85,21 @@ export class LogMap extends BaseObservableMap { get(key: K) { return this._source.get(key); } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } + } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 950273ec..396802bb 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -46,22 +46,6 @@ export class MappedMap extends BaseObservableMap { this._config = config(); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - _emitSpontaneousUpdate(key: K, params: any) { const value = this._mappedValues.get(key); if (value) { @@ -128,4 +112,20 @@ export class MappedMap extends BaseObservableMap { get(key: K): V | undefined { return this._mappedValues.get(key); } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap{ + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } } \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 1be7b2c1..6ce6ea91 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -32,22 +32,6 @@ export class ObservableMap extends BaseObservableMap { this._values = new Map(initialValues); } - join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); - } - update(key: K, params?: any): boolean { const value = this._values.get(key); if (value !== undefined) { @@ -115,6 +99,22 @@ export class ObservableMap extends BaseObservableMap { keys(): Iterator { return this._values.keys(); } + + join(...otherMaps: Array): JoinedMap { + return this._config.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return this._config.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._config.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._config.filterValues(this, filter); + } }; export function tests() { From 0203ece3bd593608b693e14806ed6a53f97d6ba2 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 12:43:24 -0400 Subject: [PATCH 184/354] updates ts-eslint and fixes errors in src/observable --- .ts-eslintrc.js | 8 ++-- src/observable/BaseObservable.ts | 9 +++-- src/observable/ObservableValue.ts | 29 +++++++------- src/observable/list/AsyncMappedList.ts | 6 ++- src/observable/list/BaseMappedList.ts | 5 ++- src/observable/list/BaseObservableList.ts | 12 +++--- src/observable/list/ConcatList.ts | 13 ++++-- src/observable/list/MappedList.ts | 19 +++++---- src/observable/list/ObservableArray.ts | 1 + src/observable/list/SortedArray.ts | 9 +++-- src/observable/list/common.ts | 7 +++- src/observable/map/ApplyMap.ts | 23 ++++++----- src/observable/map/BaseObservableMap.ts | 8 ++-- src/observable/map/FilteredMap.ts | 36 +++++++++-------- src/observable/map/JoinedMap.ts | 49 ++++++++++++----------- src/observable/map/LogMap.ts | 17 ++++---- src/observable/map/MappedMap.ts | 17 ++++---- src/observable/map/ObservableMap.ts | 17 ++++---- 18 files changed, 159 insertions(+), 126 deletions(-) diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index ac73bb34..ae7233ed 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -20,8 +20,10 @@ module.exports = { rules: { "@typescript-eslint/no-floating-promises": 2, "@typescript-eslint/no-misused-promises": 2, - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['warn'], - 'no-undef': 'off', + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn"], + "no-undef": "off", + "semi": ["error", "always"], + "@typescript-eslint/explicit-function-return-type": ["error"] } }; diff --git a/src/observable/BaseObservable.ts b/src/observable/BaseObservable.ts index 11ecd8a0..edbdd8bc 100644 --- a/src/observable/BaseObservable.ts +++ b/src/observable/BaseObservable.ts @@ -34,7 +34,7 @@ export abstract class BaseObservable { if (this._handlers.size === 1) { this.onSubscribeFirst(); } - return () => { + return (): undefined => { return this.unsubscribe(handler); }; } @@ -63,17 +63,18 @@ export abstract class BaseObservable { // Add iterator over handlers here } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { class Collection extends BaseObservable<{}> { firstSubscribeCalls: number = 0; firstUnsubscribeCalls: number = 0; - onSubscribeFirst() { this.firstSubscribeCalls += 1; } - onUnsubscribeLast() { this.firstUnsubscribeCalls += 1; } + onSubscribeFirst(): void { this.firstSubscribeCalls += 1; } + onUnsubscribeLast(): void { this.firstUnsubscribeCalls += 1; } } return { - test_unsubscribe(assert) { + test_unsubscribe(assert): void { const c = new Collection(); const unsubscribe = c.subscribe({}); unsubscribe(); diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index dab8fb52..96791f91 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -20,7 +20,7 @@ import type {SubscriptionHandle} from "./BaseObservable"; // like an EventEmitter, but doesn't have an event type export abstract class BaseObservableValue extends BaseObservable<(value: T) => void> { - emit(argument: T) { + emit(argument: T): void { for (const h of this._handlers) { h(argument); } @@ -68,7 +68,7 @@ class WaitForHandle implements IWaitHandle { return this._promise; } - dispose() { + dispose(): void { if (this._subscription) { this._subscription(); this._subscription = null; @@ -82,7 +82,7 @@ class WaitForHandle implements IWaitHandle { class ResolvedWaitForHandle implements IWaitHandle { constructor(public promise: Promise) {} - dispose() {} + dispose(): void {} } export class ObservableValue extends BaseObservableValue { @@ -113,7 +113,7 @@ export class RetainedObservableValue extends ObservableValue { this._freeCallback = freeCallback; } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); this._freeCallback(); } @@ -130,7 +130,7 @@ export class FlatMapObservableValue extends BaseObservableValue extends BaseObservableValue { this.updateTargetSubscription(); @@ -147,7 +147,7 @@ export class FlatMapObservableValue extends BaseObservableValue extends BaseObservableValue { + "set emits an update": (assert): void => { const a = new ObservableValue(0); let fired = false; const subscription = a.subscribe(v => { @@ -187,7 +188,7 @@ export function tests() { assert(fired); subscription(); }, - "set doesn't emit if value hasn't changed": assert => { + "set doesn't emit if value hasn't changed": (assert): void => { const a = new ObservableValue(5); let fired = false; const subscription = a.subscribe(() => { @@ -198,7 +199,7 @@ export function tests() { assert(!fired); subscription(); }, - "waitFor promise resolves on matching update": async assert => { + "waitFor promise resolves on matching update": async (assert): Promise => { const a = new ObservableValue(5); const handle = a.waitFor(v => v === 6); await Promise.resolve().then(() => { @@ -207,7 +208,7 @@ export function tests() { await handle.promise; assert.strictEqual(a.get(), 6); }, - "waitFor promise rejects when disposed": async assert => { + "waitFor promise rejects when disposed": async (assert): Promise => { const a = new ObservableValue(0); const handle = a.waitFor(() => false); await Promise.resolve().then(() => { @@ -215,7 +216,7 @@ export function tests() { }); await assert.rejects(handle.promise, AbortError); }, - "flatMap.get": assert => { + "flatMap.get": (assert): void => { const a = new ObservableValue}>(undefined); const countProxy = a.flatMap(a => a!.count); assert.strictEqual(countProxy.get(), undefined); @@ -223,7 +224,7 @@ export function tests() { a.set({count}); assert.strictEqual(countProxy.get(), 0); }, - "flatMap update from source": assert => { + "flatMap update from source": (assert): void => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; a.flatMap(a => a!.count).subscribe(count => { @@ -233,7 +234,7 @@ export function tests() { a.set({count}); assert.deepEqual(updates, [0]); }, - "flatMap update from target": assert => { + "flatMap update from target": (assert): void => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; a.flatMap(a => a!.count).subscribe(count => { diff --git a/src/observable/list/AsyncMappedList.ts b/src/observable/list/AsyncMappedList.ts index 53edde21..f1785c13 100644 --- a/src/observable/list/AsyncMappedList.ts +++ b/src/observable/list/AsyncMappedList.ts @@ -135,10 +135,12 @@ class ResetEvent { import {ObservableArray} from "./ObservableArray"; import {ListObserver} from "../../mocks/ListObserver.js"; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { return { - "events are emitted in order": async assert => { - const double = n => n * n; + "events are emitted in order": async (assert): Promise => { + const double = (n: number): number => n * n; const source = new ObservableArray(); const mapper = new AsyncMappedList(source, async n => { await new Promise(r => setTimeout(r, n)); diff --git a/src/observable/list/BaseMappedList.ts b/src/observable/list/BaseMappedList.ts index 4e3d05e0..0435a760 100644 --- a/src/observable/list/BaseMappedList.ts +++ b/src/observable/list/BaseMappedList.ts @@ -37,14 +37,15 @@ export class BaseMappedList extends BaseObservableList { this._removeCallback = removeCallback; } - findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false) { + findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false): boolean { return findAndUpdateInArray(predicate, this._mappedValues!, this, updater); } - get length() { + get length(): number { return this._mappedValues!.length; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return this._mappedValues!.values(); } diff --git a/src/observable/list/BaseObservableList.ts b/src/observable/list/BaseObservableList.ts index e2806c72..1fd82c25 100644 --- a/src/observable/list/BaseObservableList.ts +++ b/src/observable/list/BaseObservableList.ts @@ -26,17 +26,17 @@ export interface IListObserver { export function defaultObserverWith(overrides: { [key in keyof IListObserver]?: IListObserver[key] }): IListObserver { const defaults = { - onReset(){}, - onAdd(){}, - onUpdate(){}, - onRemove(){}, - onMove(){}, + onReset(): void {}, + onAdd(): void {}, + onUpdate(): void {}, + onRemove(): void {}, + onMove(): void {}, }; return Object.assign(defaults, overrides); } export abstract class BaseObservableList extends BaseObservable> implements Iterable { - emitReset() { + emitReset(): void { for(let h of this._handlers) { h.onReset(this); } diff --git a/src/observable/list/ConcatList.ts b/src/observable/list/ConcatList.ts index 8ef7326c..aaad5a6a 100644 --- a/src/observable/list/ConcatList.ts +++ b/src/observable/list/ConcatList.ts @@ -86,10 +86,12 @@ export class ConcatList extends BaseObservableList implements IListObserve return len; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { let sourceListIdx = 0; let it = this._sourceLists[0][Symbol.iterator](); return { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type next: () => { let result = it.next(); while (result.done) { @@ -108,16 +110,19 @@ export class ConcatList extends BaseObservableList implements IListObserve import {ObservableArray} from "./ObservableArray"; import {defaultObserverWith} from "./BaseObservableList"; + + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function tests() { return { - test_length(assert) { + test_length(assert): void { const all = new ConcatList( new ObservableArray([1, 2, 3]), new ObservableArray([11, 12, 13]) ); assert.equal(all.length, 6); }, - test_iterator(assert) { + test_iterator(assert): void { const all = new ConcatList( new ObservableArray([1, 2, 3]), new ObservableArray([11, 12, 13]) @@ -131,7 +136,7 @@ export async function tests() { assert.equal(it.next().value, 13); assert(it.next().done); }, - test_add(assert) { + test_add(assert): void { const list1 = new ObservableArray([1, 2, 3]); const list2 = new ObservableArray([11, 12, 13]); const all = new ConcatList(list1, list2); @@ -146,7 +151,7 @@ export async function tests() { list2.insert(1, 11.5); assert(fired); }, - test_update(assert) { + test_update(assert): void { const list1 = new ObservableArray([1, 2, 3]); const list2 = new ObservableArray([11, 12, 13]); const all = new ConcatList(list1, list2); diff --git a/src/observable/list/MappedList.ts b/src/observable/list/MappedList.ts index ebb418d3..2ddae698 100644 --- a/src/observable/list/MappedList.ts +++ b/src/observable/list/MappedList.ts @@ -19,7 +19,7 @@ import {IListObserver} from "./BaseObservableList"; import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList"; export class MappedList extends BaseMappedList implements IListObserver { - onSubscribeFirst() { + onSubscribeFirst(): void { this._sourceUnsubscribe = this._sourceList.subscribe(this); this._mappedValues = []; for (const item of this._sourceList) { @@ -61,18 +61,21 @@ import {ObservableArray} from "./ObservableArray"; import {BaseObservableList} from "./BaseObservableList"; import {defaultObserverWith} from "./BaseObservableList"; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function tests() { class MockList extends BaseObservableList { - get length() { + get length(): 0 { return 0; } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return [].values(); } } return { - test_add(assert) { + test_add(assert): void { const source = new MockList(); const mapped = new MappedList(source, n => {return {n: n*n};}); let fired = false; @@ -87,7 +90,7 @@ export async function tests() { assert(fired); unsubscribe(); }, - test_update(assert) { + test_update(assert): void { const source = new MockList(); const mapped = new MappedList( source, @@ -109,7 +112,7 @@ export async function tests() { assert(fired); unsubscribe(); }, - "test findAndUpdate not found": assert => { + "test findAndUpdate not found": (assert): void => { const source = new ObservableArray([1, 3, 4]); const mapped = new MappedList( source, @@ -123,7 +126,7 @@ export async function tests() { () => assert.fail() ), false); }, - "test findAndUpdate found but updater bails out of update": assert => { + "test findAndUpdate found but updater bails out of update": (assert): void => { const source = new ObservableArray([1, 3, 4]); const mapped = new MappedList( source, @@ -143,7 +146,7 @@ export async function tests() { ), true); assert.equal(fired, true); }, - "test findAndUpdate emits update": assert => { + "test findAndUpdate emits update": (assert): void => { const source = new ObservableArray([1, 3, 4]); const mapped = new MappedList( source, @@ -161,6 +164,6 @@ export async function tests() { assert.equal(mapped.findAndUpdate(n => n === 9, () => "param"), true); assert.equal(fired, true); }, - + }; } diff --git a/src/observable/list/ObservableArray.ts b/src/observable/list/ObservableArray.ts index 0771d0f6..662f715e 100644 --- a/src/observable/list/ObservableArray.ts +++ b/src/observable/list/ObservableArray.ts @@ -75,6 +75,7 @@ export class ObservableArray extends BaseObservableList { return this._items.length; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return this._items.values(); } diff --git a/src/observable/list/SortedArray.ts b/src/observable/list/SortedArray.ts index 7df285a5..c956f7b8 100644 --- a/src/observable/list/SortedArray.ts +++ b/src/observable/list/SortedArray.ts @@ -112,6 +112,7 @@ export class SortedArray extends BaseObservableList { return this._items.length; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return new Iterator(this); } @@ -127,6 +128,7 @@ class Iterator { this._current = null; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type next() { if (this._sortedArray) { if (this._current) { @@ -147,9 +149,10 @@ class Iterator { } } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { return { - "setManyUnsorted": assert => { + "setManyUnsorted": (assert): void => { const sa = new SortedArray((a, b) => a.localeCompare(b)); sa.setManyUnsorted(["b", "a", "c"]); assert.equal(sa.length, 3); @@ -157,7 +160,7 @@ export function tests() { assert.equal(sa.get(1), "b"); assert.equal(sa.get(2), "c"); }, - "_getNext": assert => { + "_getNext": (assert): void => { const sa = new SortedArray((a, b) => a.localeCompare(b)); sa.setManyUnsorted(["b", "a", "f"]); assert.equal(sa._getNext("a"), "b"); @@ -166,7 +169,7 @@ export function tests() { assert.equal(sa._getNext("c"), "f"); assert.equal(sa._getNext("f"), undefined); }, - "iterator with removals": assert => { + "iterator with removals": (assert): void => { const queue = new SortedArray<{idx: number}>((a, b) => a.idx - b.idx); queue.setManyUnsorted([{idx: 5}, {idx: 3}, {idx: 1}, {idx: 4}, {idx: 2}]); const it = queue[Symbol.iterator](); diff --git a/src/observable/list/common.ts b/src/observable/list/common.ts index c67a841b..20f3a8bf 100644 --- a/src/observable/list/common.ts +++ b/src/observable/list/common.ts @@ -17,7 +17,12 @@ limitations under the License. import {BaseObservableList} from "./BaseObservableList"; /* inline update of item in collection backed by array, without replacing the preexising item */ -export function findAndUpdateInArray(predicate: (value: T) => boolean, array: T[], observable: BaseObservableList, updater: (value: T) => any | false) { +export function findAndUpdateInArray( + predicate: (value: T) => boolean, + array: T[], + observable: BaseObservableList, + updater: (value: T) => any | false +): boolean { const index = array.findIndex(predicate); if (index !== -1) { const value = array[index]; diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 4dbbc3d3..4d45b519 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -36,42 +36,42 @@ export class ApplyMap extends BaseObservableMap { this._config = config(); } - hasApply() { + hasApply(): boolean { return !!this._apply; } - setApply(apply?: Apply) { + setApply(apply?: Apply): void { this._apply = apply; if (this._apply) { this.applyOnce(this._apply); } } - applyOnce(apply: Apply) { + applyOnce(apply: Apply): void { for (const [key, value] of this._source) { apply(key, value); } } - onAdd(key: K, value: V) { + onAdd(key: K, value: V): void { if (this._apply) { this._apply(key, value); } this.emitAdd(key, value); } - onRemove(key: K, value: V) { + onRemove(key: K, value: V): void { this.emitRemove(key, value); } - onUpdate(key: K, value: V, params: any) { + onUpdate(key: K, value: V, params: any): void { if (this._apply) { this._apply(key, value, params); } this.emitUpdate(key, value, params); } - onSubscribeFirst() { + onSubscribeFirst(): void { this._subscription = this._source.subscribe(this); if (this._apply) { this.applyOnce(this._apply); @@ -79,27 +79,28 @@ export class ApplyMap extends BaseObservableMap { super.onSubscribeFirst(); } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); if (this._subscription) this._subscription = this._subscription(); } - onReset() { + onReset(): void { if (this._apply) { this.applyOnce(this._apply); } this.emitReset(); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return this._source[Symbol.iterator](); } - get size() { + get size(): number { return this._source.size; } - get(key: K) { + get(key: K): V | undefined { return this._source.get(key); } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index d8a7d43b..206770ee 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -36,26 +36,26 @@ export type BaseObservableMapConfig = { } export abstract class BaseObservableMap extends BaseObservable> { - emitReset() { + emitReset(): void { for(let h of this._handlers) { h.onReset(); } } // we need batch events, mostly on index based collection though? // maybe we should get started without? - emitAdd(key: K, value: V) { + emitAdd(key: K, value: V): void { for(let h of this._handlers) { h.onAdd(key, value); } } - emitUpdate(key: K, value: V, params: any) { + emitUpdate(key: K, value: V, params: any): void { for(let h of this._handlers) { h.onUpdate(key, value, params); } } - emitRemove(key: K, value: V) { + emitRemove(key: K, value: V): void { for(let h of this._handlers) { h.onRemove(key, value); } diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 782a67fe..613b1439 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -35,7 +35,7 @@ export class FilteredMap extends BaseObservableMap { this._config = config(); } - setFilter(filter: Filter) { + setFilter(filter: Filter): void { this._filter = filter; if (this._subscription) { this._reapplyFilter(); @@ -45,7 +45,7 @@ export class FilteredMap extends BaseObservableMap { /** * reapply the filter */ - _reapplyFilter(silent = false) { + _reapplyFilter(silent = false): void { if (this._filter) { const oldIncluded = this._included; this._included = this._included || new Map(); @@ -71,7 +71,7 @@ export class FilteredMap extends BaseObservableMap { } } - onAdd(key: K, value: V) { + onAdd(key: K, value: V): void { if (this._filter) { if (this._included) { const included = this._filter(value, key); @@ -86,7 +86,7 @@ export class FilteredMap extends BaseObservableMap { this.emitAdd(key, value); } - onRemove(key: K, value: V) { + onRemove(key: K, value: V): void { const wasIncluded = !this._filter || this._included?.get(key); if (this._included) { this._included.delete(key); @@ -98,7 +98,7 @@ export class FilteredMap extends BaseObservableMap { } } - onUpdate(key: K, value: V, params: any) { + onUpdate(key: K, value: V, params: any): void { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._included) { return; @@ -113,7 +113,7 @@ export class FilteredMap extends BaseObservableMap { } } - _emitForUpdate(wasIncluded: boolean | undefined, isIncluded: boolean, key: K, value: V, params: any = null) { + _emitForUpdate(wasIncluded: boolean | undefined, isIncluded: boolean, key: K, value: V, params: any = null): void { if (wasIncluded && !isIncluded) { this.emitRemove(key, value); } else if (!wasIncluded && isIncluded) { @@ -123,13 +123,13 @@ export class FilteredMap extends BaseObservableMap { } } - onSubscribeFirst() { + onSubscribeFirst(): void { this._subscription = this._source.subscribe(this); this._reapplyFilter(true); super.onSubscribeFirst(); } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); this._included = undefined; if (this._subscription) { @@ -137,16 +137,17 @@ export class FilteredMap extends BaseObservableMap { } } - onReset() { + onReset(): void { this._reapplyFilter(); this.emitReset(); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return new FilterIterator(this._source, this._included); } - get size() { + get size(): number { let count = 0; this._included?.forEach(included => { if (included) { @@ -156,7 +157,7 @@ export class FilteredMap extends BaseObservableMap { return count; } - get(key) { + get(key): V | undefined{ const value = this._source.get(key); if (value && this._filter(value, key)) { return value; @@ -188,6 +189,7 @@ class FilterIterator { this._sourceIterator = map[Symbol.iterator](); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type next() { // eslint-disable-next-line no-constant-condition while (true) { @@ -204,9 +206,11 @@ class FilterIterator { } import {ObservableMap} from ".."; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { return { - "filter preloaded list": assert => { + "filter preloaded list": (assert): void => { const source = new ObservableMap(); source.add("one", 1); source.add("two", 2); @@ -233,17 +237,17 @@ export function tests() { assert.deepEqual(it.next().value, ["three", 3]); assert.equal(it.next().done, true); }, - // "filter added values": assert => { + // "filter added values": (assert): void => { // }, - // "filter removed values": assert => { + // "filter removed values": (assert): void => { // }, - // "filter changed values": assert => { + // "filter changed values": (assert): void => { // }, - "emits must trigger once": assert => { + "emits must trigger once": (assert): void => { const source = new ObservableMap(); let count_add = 0, count_update = 0, count_remove = 0; source.add("num1", 1); diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 2be95bdf..1b145d85 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -33,7 +33,7 @@ export class JoinedMap extends BaseObservableMap { this._config = config(); } - onAdd(source: BaseObservableMap, key: K, value: V) { + onAdd(source: BaseObservableMap, key: K, value: V): void { if (!this._isKeyAtSourceOccluded(source, key)) { const occludingValue = this._getValueFromOccludedSources(source, key); if (occludingValue !== undefined) { @@ -45,7 +45,7 @@ export class JoinedMap extends BaseObservableMap { } } - onRemove(source: BaseObservableMap, key: K, value: V) { + onRemove(source: BaseObservableMap, key: K, value: V): void { if (!this._isKeyAtSourceOccluded(source, key)) { this.emitRemove(key, value); const occludedValue = this._getValueFromOccludedSources(source, key); @@ -57,7 +57,7 @@ export class JoinedMap extends BaseObservableMap { } } - onUpdate(source: BaseObservableMap, key: K, value: V, params: any) { + onUpdate(source: BaseObservableMap, key: K, value: V, params: any): void { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._subscriptions) { return; @@ -67,16 +67,16 @@ export class JoinedMap extends BaseObservableMap { } } - onReset() { + onReset(): void { this.emitReset(); } - onSubscribeFirst() { + onSubscribeFirst(): void { this._subscriptions = this._sources.map(source => new SourceSubscriptionHandler(source, this).subscribe()); super.onSubscribeFirst(); } - _isKeyAtSourceOccluded(source: BaseObservableMap, key: K) { + _isKeyAtSourceOccluded(source: BaseObservableMap, key: K): boolean { // sources that come first in the sources array can // hide the keys in later sources, to prevent events // being emitted for the same key and different values, @@ -91,7 +91,7 @@ export class JoinedMap extends BaseObservableMap { } // get the value that the given source and key occlude, if any - _getValueFromOccludedSources(source: BaseObservableMap, key: K) { + _getValueFromOccludedSources(source: BaseObservableMap, key: K): V | undefined{ // sources that come first in the sources array can // hide the keys in later sources, to prevent events // being emitted for the same key and different values, @@ -107,7 +107,7 @@ export class JoinedMap extends BaseObservableMap { return undefined; } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); if (this._subscriptions) { for (const s of this._subscriptions) { @@ -116,15 +116,16 @@ export class JoinedMap extends BaseObservableMap { } } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return new JoinedIterator(this._sources); } - get size() { + get size(): number { return this._sources.reduce((sum, s) => sum + s.size, 0); } - get(key: K): V | undefined{ + get(key: K): V | undefined { for (const s of this._sources) { const value = s.get(key); if (value) { @@ -199,28 +200,28 @@ class SourceSubscriptionHandler { this._subscription = undefined; } - subscribe() { + subscribe(): this { this._subscription = this._source.subscribe(this); return this; } - dispose() { + dispose(): void { if (this._subscription) this._subscription = this._subscription(); } - onAdd(key: K, value: V) { + onAdd(key: K, value: V): void { this._joinedMap.onAdd(this._source, key, value); } - onRemove(key: K, value: V) { + onRemove(key: K, value: V): void { this._joinedMap.onRemove(this._source, key, value); } - onUpdate(key: K, value: V, params: any) { + onUpdate(key: K, value: V, params: any): void { this._joinedMap.onUpdate(this._source, key, value, params); } - onReset() { + onReset(): void { this._joinedMap.onReset(); } } @@ -228,9 +229,9 @@ class SourceSubscriptionHandler { import {ObservableMap} from ".."; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { - - function observeMap(map: JoinedMap) { + function observeMap(map: JoinedMap): { type: string; key: any; value: any; params?: any; }[] { const events: { type: string, key: any, value: any, params?: any }[] = []; map.subscribe({ onAdd(key, value) { events.push({ type: "add", key, value }); }, @@ -244,7 +245,7 @@ export function tests() { } return { - "joined iterator": assert => { + "joined iterator": (assert): void => { const firstKV: [string, number] = ["a", 1]; const secondKV: [string, number] = ["b", 2]; const thirdKV: [string, number] = ["c", 3]; @@ -254,7 +255,7 @@ export function tests() { assert.equal(it.next().value, thirdKV); assert.equal(it.next().done, true); }, - "prevent key collision during iteration": assert => { + "prevent key collision during iteration": (assert): void => { const first = new ObservableMap(); const second = new ObservableMap(); const join = new JoinedMap([first, second]); @@ -266,7 +267,7 @@ export function tests() { assert.deepEqual(it.next().value, ["b", 3]); assert.equal(it.next().done, true); }, - "adding occluded key doesn't emit add": assert => { + "adding occluded key doesn't emit add": (assert): void => { const first = new ObservableMap(); const second = new ObservableMap(); const join = new JoinedMap([first, second]); @@ -278,7 +279,7 @@ export function tests() { assert.equal(events[0].key, "a"); assert.equal(events[0].value, 1); }, - "updating occluded key doesn't emit update": assert => { + "updating occluded key doesn't emit update": (assert): void => { const first = new ObservableMap(); const second = new ObservableMap(); const join = new JoinedMap([first, second]); @@ -288,7 +289,7 @@ export function tests() { second.update("a", 3); assert.equal(events.length, 0); }, - "removal of occluding key emits add after remove": assert => { + "removal of occluding key emits add after remove": (assert): void => { const first = new ObservableMap(); const second = new ObservableMap(); const join = new JoinedMap([first, second]); @@ -304,7 +305,7 @@ export function tests() { assert.equal(events[1].key, "a"); assert.equal(events[1].value, 2); }, - "adding occluding key emits remove first": assert => { + "adding occluding key emits remove first": (assert): void => { const first = new ObservableMap(); const second = new ObservableMap(); const join = new JoinedMap([first, second]); diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 6f3bf676..2a084401 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -42,47 +42,48 @@ export class LogMap extends BaseObservableMap { return this._log.log(labelOrValues, logLevel); } - onAdd(key: K, value: V) { + onAdd(key: K, value: V): void { this.log("add " + JSON.stringify({key, value})); this.emitAdd(key, value); } - onRemove(key: K, value: V) { + onRemove(key: K, value: V): void { this.log("remove " + JSON.stringify({key, value})); this.emitRemove(key, value); } - onUpdate(key: K, value: V, params: any) { + onUpdate(key: K, value: V, params: any): void { this.log("update" + JSON.stringify({key, value, params})); this.emitUpdate(key, value, params); } - onSubscribeFirst() { + onSubscribeFirst(): void { this.log("subscribeFirst"); this._subscription = this._source.subscribe(this); super.onSubscribeFirst(); } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); if (this._subscription) this._subscription = this._subscription(); this.log("unsubscribeLast"); } - onReset() { + onReset(): void { this.log("reset"); this.emitReset(); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return this._source[Symbol.iterator](); } - get size() { + get size(): number { return this._source.size; } - get(key: K) { + get(key: K): V | undefined{ return this._source.get(key); } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 396802bb..e37e313b 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -46,28 +46,28 @@ export class MappedMap extends BaseObservableMap { this._config = config(); } - _emitSpontaneousUpdate(key: K, params: any) { + _emitSpontaneousUpdate(key: K, params: any): void { const value = this._mappedValues.get(key); if (value) { this.emitUpdate(key, value, params); } } - onAdd(key: K, value: V) { + onAdd(key: K, value: V): void { const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key); const mappedValue = this._mapper(value, emitSpontaneousUpdate); this._mappedValues.set(key, mappedValue); this.emitAdd(key, mappedValue); } - onRemove(key: K/*, _value*/) { + onRemove(key: K/*, _value*/): void { const mappedValue = this._mappedValues.get(key); if (this._mappedValues.delete(key)) { if (mappedValue) this.emitRemove(key, mappedValue); } } - onUpdate(key: K, value: V, params: any) { + onUpdate(key: K, value: V, params: any): void { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._mappedValues) { return; @@ -80,7 +80,7 @@ export class MappedMap extends BaseObservableMap { } } - onSubscribeFirst() { + onSubscribeFirst(): void { this._subscription = this._source.subscribe(this); for (let [key, value] of this._source) { const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key); @@ -90,22 +90,23 @@ export class MappedMap extends BaseObservableMap { super.onSubscribeFirst(); } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); if (this._subscription) this._subscription = this._subscription(); this._mappedValues.clear(); } - onReset() { + onReset(): void { this._mappedValues.clear(); this.emitReset(); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type [Symbol.iterator]() { return this._mappedValues.entries(); } - get size() { + get size(): number { return this._mappedValues.size; } diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 6ce6ea91..c49d31dc 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -117,9 +117,10 @@ export class ObservableMap extends BaseObservableMap { } }; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { return { - test_initial_values(assert) { + test_initial_values(assert): void { const map = new ObservableMap([ ["a", 5], ["b", 10] @@ -129,7 +130,7 @@ export function tests() { assert.equal(map.get("b"), 10); }, - test_add(assert) { + test_add(assert): void { let fired = 0; const map = new ObservableMap(); map.subscribe({ @@ -147,7 +148,7 @@ export function tests() { assert.equal(fired, 1); }, - test_update(assert) { + test_update(assert): void { let fired = 0; const map = new ObservableMap(); const value = {number: 5}; @@ -168,7 +169,7 @@ export function tests() { assert.equal(fired, 1); }, - test_update_unknown(assert) { + test_update_unknown(assert): void { let fired = 0; const map = new ObservableMap(); map.subscribe({ @@ -182,7 +183,7 @@ export function tests() { assert.equal(result, false); }, - test_set(assert) { + test_set(assert): void { let add_fired = 0, update_fired = 0; const map = new ObservableMap(); map.subscribe({ @@ -209,7 +210,7 @@ export function tests() { assert.equal(update_fired, 1); }, - test_remove(assert) { + test_remove(assert): void { let fired = 0; const map = new ObservableMap(); const value = {value: 5}; @@ -229,7 +230,7 @@ export function tests() { assert.equal(fired, 1); }, - test_iterate(assert) { + test_iterate(assert): void { const results: any[] = []; const map = new ObservableMap(); map.add(1, {number: 5}); @@ -243,7 +244,7 @@ export function tests() { assert.equal(results.find(([key]) => key === 2)[1].number, 6); assert.equal(results.find(([key]) => key === 3)[1].number, 7); }, - test_size(assert) { + test_size(assert): void { const map = new ObservableMap(); map.add(1, {number: 5}); map.add(2, {number: 6}); From bed66ada883317447700a3ad296a5ab988b41456 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 9 Jul 2022 13:01:53 -0400 Subject: [PATCH 185/354] removes .js to files that are now typescripted --- src/observable/map/ApplyMap.ts | 6 +++--- src/observable/map/BaseObservableMap.ts | 6 +++--- src/observable/map/FilteredMap.ts | 4 ++-- src/observable/map/JoinedMap.ts | 4 ++-- src/observable/map/LogMap.ts | 6 +++--- src/observable/map/MappedMap.ts | 4 ++-- src/observable/map/ObservableMap.ts | 6 +++--- src/observable/map/config.ts | 6 +++--- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 4d45b519..0fef91c9 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -17,9 +17,9 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {JoinedMap} from "./JoinedMap.js"; -import {MappedMap} from "./MappedMap.js"; -import {FilteredMap} from "./FilteredMap.js"; +import {JoinedMap} from "./JoinedMap"; +import {MappedMap} from "./MappedMap"; +import {FilteredMap} from "./FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 206770ee..98781955 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -15,9 +15,9 @@ limitations under the License. */ import {BaseObservable} from "../BaseObservable"; -import {JoinedMap} from "../map/JoinedMap.js"; -import {MappedMap} from "../map/MappedMap.js"; -import {FilteredMap} from "../map/FilteredMap.js"; +import {JoinedMap} from "../map/JoinedMap"; +import {MappedMap} from "../map/MappedMap"; +import {FilteredMap} from "../map/FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; import {Mapper, Updater, Comparator, Filter} from "./config"; diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 613b1439..00006a6d 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -17,8 +17,8 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {JoinedMap} from "./JoinedMap.js"; -import {MappedMap} from "./MappedMap.js"; +import {JoinedMap} from "./JoinedMap"; +import {MappedMap} from "./MappedMap"; import {SortedMapList} from "../list/SortedMapList.js"; export class FilteredMap extends BaseObservableMap { diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index 1b145d85..bf9f8b97 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -16,8 +16,8 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {FilteredMap} from "./FilteredMap.js"; -import {MappedMap} from "./MappedMap.js"; +import {FilteredMap} from "./FilteredMap"; +import {MappedMap} from "./MappedMap"; import {SortedMapList} from "../list/SortedMapList.js"; import {SubscriptionHandle} from "../BaseObservable"; diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 2a084401..70fe623a 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -16,9 +16,9 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {FilteredMap} from "./FilteredMap.js"; -import {MappedMap} from "./MappedMap.js"; -import {JoinedMap} from "./JoinedMap.js"; +import {FilteredMap} from "./FilteredMap"; +import {MappedMap} from "./MappedMap"; +import {JoinedMap} from "./JoinedMap"; import {SortedMapList} from "../list/SortedMapList.js"; import {SubscriptionHandle} from "../BaseObservable"; import {ILogItem, LabelOrValues} from "../../logging/types"; diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index e37e313b..95f49c2b 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -16,8 +16,8 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {JoinedMap} from "./JoinedMap.js"; -import {FilteredMap} from "./FilteredMap.js"; +import {JoinedMap} from "./JoinedMap"; +import {FilteredMap} from "./FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; import {SubscriptionHandle} from "../BaseObservable"; diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index c49d31dc..950fb818 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -16,9 +16,9 @@ limitations under the License. import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; import {config, Mapper, Updater, Comparator, Filter} from "./config"; -import {JoinedMap} from "./JoinedMap.js"; -import {MappedMap} from "./MappedMap.js"; -import {FilteredMap} from "./FilteredMap.js"; +import {JoinedMap} from "./JoinedMap"; +import {MappedMap} from "./MappedMap"; +import {FilteredMap} from "./FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts index 54e291e2..fab0bcf8 100644 --- a/src/observable/map/config.ts +++ b/src/observable/map/config.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {FilteredMap} from "./FilteredMap.js"; -import {MappedMap} from "./MappedMap.js"; -import {JoinedMap} from "./JoinedMap.js"; +import {FilteredMap} from "./FilteredMap"; +import {MappedMap} from "./MappedMap"; +import {JoinedMap} from "./JoinedMap"; import {SortedMapList} from "../list/SortedMapList.js"; From 73b83fdab8f055600ee043f1e57415b0326b895b Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 10 Jul 2022 00:21:30 -0400 Subject: [PATCH 186/354] Changes config from a function that returns an objectinto a more aptly named BaseObservableMapDefaults class --- src/observable/map/ApplyMap.ts | 16 +++--- src/observable/map/BaseObservableMap.ts | 18 ++---- .../map/BaseObservableMapDefaults.ts | 55 +++++++++++++++++++ src/observable/map/FilteredMap.ts | 15 +++-- src/observable/map/JoinedMap.ts | 15 +++-- src/observable/map/LogMap.ts | 16 +++--- src/observable/map/MappedMap.ts | 16 +++--- src/observable/map/ObservableMap.ts | 15 +++-- src/observable/map/config.ts | 54 ------------------ 9 files changed, 105 insertions(+), 115 deletions(-) create mode 100644 src/observable/map/BaseObservableMapDefaults.ts delete mode 100644 src/observable/map/config.ts diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 0fef91c9..f8d09914 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {JoinedMap} from "./JoinedMap"; import {MappedMap} from "./MappedMap"; import {FilteredMap} from "./FilteredMap"; @@ -24,16 +24,16 @@ import {SortedMapList} from "../list/SortedMapList.js"; export class ApplyMap extends BaseObservableMap { + private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; private _apply?: Apply; - private _config: BaseObservableMapConfig; + constructor(source: BaseObservableMap, apply?: Apply) { super(); this._source = source; this._apply = apply; - this._config = config(); } hasApply(): boolean { @@ -105,19 +105,19 @@ export class ApplyMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 98781955..02dedce5 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -19,7 +19,7 @@ import {JoinedMap} from "../map/JoinedMap"; import {MappedMap} from "../map/MappedMap"; import {FilteredMap} from "../map/FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; -import {Mapper, Updater, Comparator, Filter} from "./config"; +import {Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; export interface IMapObserver { onReset(): void; @@ -28,13 +28,6 @@ export interface IMapObserver { onRemove(key: K, value: V): void } -export type BaseObservableMapConfig = { - join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap; - mapValues(_this: BaseObservableMap, mapper: any, updater?: Updater): MappedMap; - sortValues(_this: BaseObservableMap, comparator: Comparator): SortedMapList; - filterValues(_this: BaseObservableMap, filter: Filter): FilteredMap; -} - export abstract class BaseObservableMap extends BaseObservable> { emitReset(): void { for(let h of this._handlers) { @@ -62,10 +55,11 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; abstract mapValues(mapper: Mapper, updater?: Updater): MappedMap; diff --git a/src/observable/map/BaseObservableMapDefaults.ts b/src/observable/map/BaseObservableMapDefaults.ts new file mode 100644 index 00000000..46d9148e --- /dev/null +++ b/src/observable/map/BaseObservableMapDefaults.ts @@ -0,0 +1,55 @@ +/* +Copyright 2022 Isaiah Becker-Mayer + +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 {BaseObservableMap} from "./BaseObservableMap"; +import {FilteredMap} from "./FilteredMap"; +import {MappedMap} from "./MappedMap"; +import {JoinedMap} from "./JoinedMap"; +import {SortedMapList} from "../list/SortedMapList.js"; + + +// This class is used as a default implementation of +// the respective abstract functions in BaseObservableMap. +// It is kept as its own class in its own file in order to avoid a circular +// dependency between the classes that extend BaseObservableMap which are +// instantiated here (i.e. `new JoinedMap()`). +export class BaseObservableMapDefaults { + join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap { + return new JoinedMap([_this].concat(otherMaps)); + } + + mapValues(_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap { + return new MappedMap(_this, mapper, updater); + } + + sortValues(_this: BaseObservableMap, comparator: Comparator): SortedMapList { + return new SortedMapList(_this, comparator); + } + + filterValues(_this: BaseObservableMap, filter: Filter): FilteredMap { + return new FilteredMap(_this, filter); + } +} + +export type Mapper = ( + value: V, + emitSpontaneousUpdate: any, +) => V; + +export type Updater = (params: any, mappedValue?: V, value?: V) => void; + +export type Comparator = (a: V, b: V) => number; + +export type Filter = (v: V, k: K) => boolean; \ No newline at end of file diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 00006a6d..b0ed2e82 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; +import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {JoinedMap} from "./JoinedMap"; import {MappedMap} from "./MappedMap"; import {SortedMapList} from "../list/SortedMapList.js"; export class FilteredMap extends BaseObservableMap { + private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; - private _config: BaseObservableMapConfig; private _filter: Filter; private _included?: Map; private _subscription?: SubscriptionHandle; @@ -32,7 +32,6 @@ export class FilteredMap extends BaseObservableMap { super(); this._source = source; this._filter = filter; - this._config = config(); } setFilter(filter: Filter): void { @@ -165,19 +164,19 @@ export class FilteredMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index bf9f8b97..d4164f79 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {FilteredMap} from "./FilteredMap"; import {MappedMap} from "./MappedMap"; import {SortedMapList} from "../list/SortedMapList.js"; @@ -23,14 +23,13 @@ import {SubscriptionHandle} from "../BaseObservable"; export class JoinedMap extends BaseObservableMap { + private _defaults = new BaseObservableMapDefaults(); protected _sources: BaseObservableMap[]; - private _config: BaseObservableMapConfig; private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { super(); this._sources = sources; - this._config = config(); } onAdd(source: BaseObservableMap, key: K, value: V): void { @@ -136,19 +135,19 @@ export class JoinedMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 70fe623a..7b857678 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {FilteredMap} from "./FilteredMap"; import {MappedMap} from "./MappedMap"; import {JoinedMap} from "./JoinedMap"; @@ -25,17 +25,15 @@ import {ILogItem, LabelOrValues} from "../../logging/types"; import {LogLevel} from "../../logging/LogFilter"; export class LogMap extends BaseObservableMap { + private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; private _log: ILogItem; - private _config: BaseObservableMapConfig - constructor(source: BaseObservableMap, log: ILogItem) { super(); this._source = source; this._log = log; - this._config = config(); } private log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem { @@ -88,19 +86,19 @@ export class LogMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 95f49c2b..3d9e3a3c 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {JoinedMap} from "./JoinedMap"; import {FilteredMap} from "./FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; @@ -26,12 +26,13 @@ so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ export class MappedMap extends BaseObservableMap { + private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _mapper: Mapper; private _updater?: Updater; private _mappedValues: Map; private _subscription?: SubscriptionHandle; - private _config: BaseObservableMapConfig + constructor( source: BaseObservableMap, @@ -43,7 +44,6 @@ export class MappedMap extends BaseObservableMap { this._mapper = mapper; this._updater = updater; this._mappedValues = new Map(); - this._config = config(); } _emitSpontaneousUpdate(key: K, params: any): void { @@ -115,18 +115,18 @@ export class MappedMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } } \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 950fb818..4b98c089 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {config, Mapper, Updater, Comparator, Filter} from "./config"; +import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; import {JoinedMap} from "./JoinedMap"; import {MappedMap} from "./MappedMap"; import {FilteredMap} from "./FilteredMap"; @@ -23,12 +23,11 @@ import {SortedMapList} from "../list/SortedMapList.js"; export class ObservableMap extends BaseObservableMap { - private _config: BaseObservableMapConfig + private _defaults = new BaseObservableMapDefaults(); private readonly _values: Map; constructor(initialValues?: (readonly [K, V])[]) { super(); - this._config = config(); this._values = new Map(initialValues); } @@ -101,19 +100,19 @@ export class ObservableMap extends BaseObservableMap { } join(...otherMaps: Array): JoinedMap { - return this._config.join(this, ...otherMaps); + return this._defaults.join(this, ...otherMaps); } mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._config.mapValues(this, mapper, updater); + return this._defaults.mapValues(this, mapper, updater); } sortValues(comparator: Comparator): SortedMapList { - return this._config.sortValues(this, comparator); + return this._defaults.sortValues(this, comparator); } filterValues(filter: Filter): FilteredMap { - return this._config.filterValues(this, filter); + return this._defaults.filterValues(this, filter); } }; diff --git a/src/observable/map/config.ts b/src/observable/map/config.ts deleted file mode 100644 index fab0bcf8..00000000 --- a/src/observable/map/config.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2022 Isaiah Becker-Mayer - -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 {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap"; -import {FilteredMap} from "./FilteredMap"; -import {MappedMap} from "./MappedMap"; -import {JoinedMap} from "./JoinedMap"; -import {SortedMapList} from "../list/SortedMapList.js"; - - -// This function is used as a default implementation of -// the respective abstract functions in BaseObservableMap. -// We implement it this way in order to avoid a circular -// dependency between the classes that are instantiated here -// (i.e. `new JoinedMap()`) and BaseObservableMap (as they extend it). -export function config(): BaseObservableMapConfig { - return { - join: (_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap => { - return new JoinedMap([_this].concat(otherMaps)); - }, - mapValues: (_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap => { - return new MappedMap(_this, mapper, updater); - }, - sortValues: (_this: BaseObservableMap, comparator: Comparator): SortedMapList => { - return new SortedMapList(_this, comparator); - }, - filterValues: (_this: BaseObservableMap, filter: Filter): FilteredMap => { - return new FilteredMap(_this, filter); - } - }; -} - -export type Mapper = ( - value: V, - emitSpontaneousUpdate: any, -) => V; - -export type Updater = (params: any, mappedValue?: V, value?: V) => void; - -export type Comparator = (a: V, b: V) => number; - -export type Filter = (v: V, k: K) => boolean; \ No newline at end of file From 70b68c5b162a9cd302154f99a5e4fe7174cc5a4e Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 26 Jul 2022 22:14:14 -0700 Subject: [PATCH 187/354] found a more clever way to do this which eliminates boilerplate --- src/observable/map/ApplyMap.ts | 26 +------------ src/observable/map/BaseObservableMap.ts | 37 +++++++++++++------ .../map/BaseObservableMapDefaults.ts | 5 +-- src/observable/map/FilteredMap.ts | 25 ++----------- src/observable/map/JoinedMap.ts | 25 +------------ src/observable/map/LogMap.ts | 27 ++------------ src/observable/map/MappedMap.ts | 25 ++----------- src/observable/map/ObservableMap.ts | 25 +------------ 8 files changed, 42 insertions(+), 153 deletions(-) diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index f8d09914..1786873a 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -16,22 +16,17 @@ limitations under the License. import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {JoinedMap} from "./JoinedMap"; -import {MappedMap} from "./MappedMap"; -import {FilteredMap} from "./FilteredMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; export class ApplyMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; private _apply?: Apply; constructor(source: BaseObservableMap, apply?: Apply) { - super(); + super(new BaseObservableMapDefaults()); this._source = source; this._apply = apply; } @@ -103,23 +98,6 @@ export class ApplyMap extends BaseObservableMap { get(key: K): V | undefined { return this._source.get(key); } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } - } type Apply = (key: K, value: V, params?: any) => void; \ No newline at end of file diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 02dedce5..86300439 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -19,7 +19,8 @@ import {JoinedMap} from "../map/JoinedMap"; import {MappedMap} from "../map/MappedMap"; import {FilteredMap} from "../map/FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; -import {Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; +import type {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; + export interface IMapObserver { onReset(): void; @@ -29,6 +30,13 @@ export interface IMapObserver { } export abstract class BaseObservableMap extends BaseObservable> { + private _defaults: BaseObservableMapDefaults; + + constructor(defaults: BaseObservableMapDefaults) { + super(); + this._defaults = defaults; + } + emitReset(): void { for(let h of this._handlers) { h.onReset(); @@ -54,17 +62,22 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap; - abstract mapValues(mapper: Mapper, updater?: Updater): MappedMap; - abstract sortValues(comparator: Comparator): SortedMapList; - abstract filterValues(filter: Filter): FilteredMap; + join(...otherMaps: Array): JoinedMap { + return this._defaults.join(this, ...otherMaps); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return this._defaults.mapValues(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return this._defaults.sortValues(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return this._defaults.filterValues(this, filter); + } + abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; diff --git a/src/observable/map/BaseObservableMapDefaults.ts b/src/observable/map/BaseObservableMapDefaults.ts index 46d9148e..444b8640 100644 --- a/src/observable/map/BaseObservableMapDefaults.ts +++ b/src/observable/map/BaseObservableMapDefaults.ts @@ -13,15 +13,14 @@ 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 {BaseObservableMap} from "./BaseObservableMap"; +import type {BaseObservableMap} from "./BaseObservableMap"; import {FilteredMap} from "./FilteredMap"; import {MappedMap} from "./MappedMap"; import {JoinedMap} from "./JoinedMap"; import {SortedMapList} from "../list/SortedMapList.js"; -// This class is used as a default implementation of -// the respective abstract functions in BaseObservableMap. +// This class provides implementations of functions that are part of BaseObservableMap. // It is kept as its own class in its own file in order to avoid a circular // dependency between the classes that extend BaseObservableMap which are // instantiated here (i.e. `new JoinedMap()`). diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index b0ed2e82..267d0d35 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -16,20 +16,17 @@ limitations under the License. import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {JoinedMap} from "./JoinedMap"; -import {MappedMap} from "./MappedMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults, Filter} from "./BaseObservableMapDefaults"; + export class FilteredMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _filter: Filter; private _included?: Map; private _subscription?: SubscriptionHandle; constructor(source: BaseObservableMap, filter: Filter) { - super(); + super(new BaseObservableMapDefaults()); this._source = source; this._filter = filter; } @@ -162,22 +159,6 @@ export class FilteredMap extends BaseObservableMap { return value; } } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } } class FilterIterator { diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index d4164f79..b343d22f 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -15,20 +15,16 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {FilteredMap} from "./FilteredMap"; -import {MappedMap} from "./MappedMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; import {SubscriptionHandle} from "../BaseObservable"; export class JoinedMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); protected _sources: BaseObservableMap[]; private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { - super(); + super(new BaseObservableMapDefaults()); this._sources = sources; } @@ -133,23 +129,6 @@ export class JoinedMap extends BaseObservableMap { } return undefined; } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } - } class JoinedIterator implements Iterator<[K, V]> { diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 7b857678..6db70f01 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -15,23 +15,19 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {FilteredMap} from "./FilteredMap"; -import {MappedMap} from "./MappedMap"; -import {JoinedMap} from "./JoinedMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; import {SubscriptionHandle} from "../BaseObservable"; import {ILogItem, LabelOrValues} from "../../logging/types"; import {LogLevel} from "../../logging/LogFilter"; + export class LogMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; private _log: ILogItem; constructor(source: BaseObservableMap, log: ILogItem) { - super(); + super(new BaseObservableMapDefaults()); this._source = source; this._log = log; } @@ -84,21 +80,4 @@ export class LogMap extends BaseObservableMap { get(key: K): V | undefined{ return this._source.get(key); } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } - } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 3d9e3a3c..72115d84 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -15,18 +15,15 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {JoinedMap} from "./JoinedMap"; -import {FilteredMap} from "./FilteredMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults, Mapper, Updater} from "./BaseObservableMapDefaults"; import {SubscriptionHandle} from "../BaseObservable"; + /* so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ export class MappedMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); private _source: BaseObservableMap; private _mapper: Mapper; private _updater?: Updater; @@ -39,7 +36,7 @@ export class MappedMap extends BaseObservableMap { mapper: Mapper, updater?: Updater ) { - super(); + super(new BaseObservableMapDefaults()); this._source = source; this._mapper = mapper; this._updater = updater; @@ -113,20 +110,4 @@ export class MappedMap extends BaseObservableMap { get(key: K): V | undefined { return this._mappedValues.get(key); } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap{ - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } } \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 4b98c089..9527ffdd 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -15,19 +15,14 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; -import {JoinedMap} from "./JoinedMap"; -import {MappedMap} from "./MappedMap"; -import {FilteredMap} from "./FilteredMap"; -import {SortedMapList} from "../list/SortedMapList.js"; +import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; export class ObservableMap extends BaseObservableMap { - private _defaults = new BaseObservableMapDefaults(); private readonly _values: Map; constructor(initialValues?: (readonly [K, V])[]) { - super(); + super(new BaseObservableMapDefaults()); this._values = new Map(initialValues); } @@ -98,22 +93,6 @@ export class ObservableMap extends BaseObservableMap { keys(): Iterator { return this._values.keys(); } - - join(...otherMaps: Array): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } - - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._defaults.mapValues(this, mapper, updater); - } - - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } }; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type From dd01e70b4a3a90ab4b7d9d1b8adb08d2635e69c0 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 31 Jul 2022 19:53:34 -0700 Subject: [PATCH 188/354] src/observable/map/BaseObservableMapDefaults.ts -> src/observable/map/BaseObservableMapTransformers.ts --- src/observable/map/ApplyMap.ts | 4 +-- src/observable/map/BaseObservableMap.ts | 6 ++--- ...ts.ts => BaseObservableMapTransformers.ts} | 25 +++++++++++++++---- src/observable/map/FilteredMap.ts | 4 +-- src/observable/map/JoinedMap.ts | 4 +-- src/observable/map/LogMap.ts | 4 +-- src/observable/map/MappedMap.ts | 4 +-- src/observable/map/ObservableMap.ts | 4 +-- 8 files changed, 35 insertions(+), 20 deletions(-) rename src/observable/map/{BaseObservableMapDefaults.ts => BaseObservableMapTransformers.ts} (63%) diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 1786873a..ee40c797 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -16,7 +16,7 @@ limitations under the License. import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; export class ApplyMap extends BaseObservableMap { @@ -26,7 +26,7 @@ export class ApplyMap extends BaseObservableMap { constructor(source: BaseObservableMap, apply?: Apply) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._source = source; this._apply = apply; } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 86300439..aa76f641 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -19,7 +19,7 @@ import {JoinedMap} from "../map/JoinedMap"; import {MappedMap} from "../map/MappedMap"; import {FilteredMap} from "../map/FilteredMap"; import {SortedMapList} from "../list/SortedMapList.js"; -import type {BaseObservableMapDefaults, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapDefaults"; +import type {BaseObservableMapTransformers, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapTransformers"; export interface IMapObserver { @@ -30,9 +30,9 @@ export interface IMapObserver { } export abstract class BaseObservableMap extends BaseObservable> { - private _defaults: BaseObservableMapDefaults; + private _defaults: BaseObservableMapTransformers; - constructor(defaults: BaseObservableMapDefaults) { + constructor(defaults: BaseObservableMapTransformers) { super(); this._defaults = defaults; } diff --git a/src/observable/map/BaseObservableMapDefaults.ts b/src/observable/map/BaseObservableMapTransformers.ts similarity index 63% rename from src/observable/map/BaseObservableMapDefaults.ts rename to src/observable/map/BaseObservableMapTransformers.ts index 444b8640..3b39587d 100644 --- a/src/observable/map/BaseObservableMapDefaults.ts +++ b/src/observable/map/BaseObservableMapTransformers.ts @@ -20,11 +20,26 @@ import {JoinedMap} from "./JoinedMap"; import {SortedMapList} from "../list/SortedMapList.js"; -// This class provides implementations of functions that are part of BaseObservableMap. -// It is kept as its own class in its own file in order to avoid a circular -// dependency between the classes that extend BaseObservableMap which are -// instantiated here (i.e. `new JoinedMap()`). -export class BaseObservableMapDefaults { +// This class provides implementations of functions that transform one BaseObservableMap +// to another type of Map. It's methods are effectively default implementations of the +// methods by the same name on BaseObservableMap. +// +// It is kept as its own class in its own file in order to avoid circular dependencies +// which would occur if these method implementations were defined on BaseObservableMap +// itself. For example, if we attmpted to do the following on BaseObservableMap: +// +// class BaseObservableMap extends BaseObservable> { +// join(...otherMaps: Array>): JoinedMap { +// return new JoinedMap(this.concat(otherMaps)); +// } +// } +// +// we would end up with a circular dependency between BaseObservableMap and JoinedMap, +// since BaseObservableMap would need to import JoinedMap for the +// `return new JoinedMap(this.concat(otherMaps))`, and +// JoinedMap would need to import BaseObservableMap to do +// `JoinedMap extends BaseObservableMap`. +export class BaseObservableMapTransformers { join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap { return new JoinedMap([_this].concat(otherMaps)); } diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 267d0d35..2f3ba9fd 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -16,7 +16,7 @@ limitations under the License. import {BaseObservableMap} from "./BaseObservableMap"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapDefaults, Filter} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers, Filter} from "./BaseObservableMapTransformers"; export class FilteredMap extends BaseObservableMap { @@ -26,7 +26,7 @@ export class FilteredMap extends BaseObservableMap { private _subscription?: SubscriptionHandle; constructor(source: BaseObservableMap, filter: Filter) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._source = source; this._filter = filter; } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index b343d22f..f6cb074a 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; import {SubscriptionHandle} from "../BaseObservable"; @@ -24,7 +24,7 @@ export class JoinedMap extends BaseObservableMap { private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._sources = sources; } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 6db70f01..15f204d2 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; import {SubscriptionHandle} from "../BaseObservable"; import {ILogItem, LabelOrValues} from "../../logging/types"; import {LogLevel} from "../../logging/LogFilter"; @@ -27,7 +27,7 @@ export class LogMap extends BaseObservableMap { private _log: ILogItem; constructor(source: BaseObservableMap, log: ILogItem) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._source = source; this._log = log; } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 72115d84..bd091856 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults, Mapper, Updater} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers, Mapper, Updater} from "./BaseObservableMapTransformers"; import {SubscriptionHandle} from "../BaseObservable"; @@ -36,7 +36,7 @@ export class MappedMap extends BaseObservableMap { mapper: Mapper, updater?: Updater ) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._source = source; this._mapper = mapper; this._updater = updater; diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 9527ffdd..09a73d71 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -15,14 +15,14 @@ limitations under the License. */ import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapDefaults} from "./BaseObservableMapDefaults"; +import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; export class ObservableMap extends BaseObservableMap { private readonly _values: Map; constructor(initialValues?: (readonly [K, V])[]) { - super(new BaseObservableMapDefaults()); + super(new BaseObservableMapTransformers()); this._values = new Map(initialValues); } From bfd54f27644f5a1eb678e907be78fa1d471dd943 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 9 Aug 2022 23:11:02 +0530 Subject: [PATCH 189/354] Delete localstorage on logout --- src/matrix/Client.js | 6 ++++++ src/matrix/storage/idb/stores/SessionStore.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 44643cc1..438bf3ab 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -451,6 +451,12 @@ export class Client { async deleteSession(log) { if (this._sessionId) { + await log.wrap("sessionStore", async () => { + const storage = this._storage ?? await this._platform.storageFactory.create(this._sessionId, log); + const txn = await storage.readWriteTxn([storage.storeNames.session]); + txn.session.delete(); + storage.close(); + }); // need to dispose first, so the storage is closed, // and also first sync finishing won't call Session.start anymore, // which assumes that the storage works. diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index 7faedc41..a7aaf594 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -105,4 +105,17 @@ export class SessionStore { } this._sessionStore.delete(key); } + + delete(): void { + const keys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = this._localStorage.key(i); + if (key?.startsWith(this._localStorageKeyPrefix)) { + keys.push(key); + } + } + for (const key of keys) { + this._localStorage.removeItem(key); + } + } } From ff706e542d73743d053fbfa0a0cfc8925b41e5e9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 10 Aug 2022 22:23:51 +0530 Subject: [PATCH 190/354] Throw ConnectionError instead of swallowing error --- src/platform/web/dom/request/fetch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index eb4caab6..5f6dcb65 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -119,10 +119,10 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { body = await response.text(); } } catch (err) { - // some error pages return html instead of json, ignore error - if (!(err.name === "SyntaxError" && status >= 400)) { - throw err; + if (err.name === "SyntaxError" && status >= 400) { + throw new ConnectionError(`${method} ${url}: Failed to fetch JSON file!`); } + throw err; } return {status, body}; }, err => { From 27363b3f63082e44cdaaf0a86658f499d223bec1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 10 Aug 2022 22:25:56 +0530 Subject: [PATCH 191/354] Throw and log errors if manifests cannot be loaded --- src/platform/web/theming/ThemeLoader.ts | 29 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index be1bafc0..7519329d 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {ILogItem} from "../../../logging/types"; -import type {Platform} from "../Platform.js"; import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser"; -import type {Variant, ThemeInformation} from "./parsers/types"; import {ColorSchemePreference} from "./parsers/types"; import {BuiltThemeParser} from "./parsers/BuiltThemeParser"; +import type {Variant, ThemeInformation} from "./parsers/types"; +import type {ThemeManifest} from "../../types/theme"; +import type {ILogItem} from "../../../logging/types"; +import type {Platform} from "../Platform.js"; export class ThemeLoader { private _platform: Platform; @@ -32,21 +33,31 @@ export class ThemeLoader { async init(manifestLocations: string[], log?: ILogItem): Promise { await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { - const results = await Promise.all( + let noManifestsAvailable = true; + const failedManifestLoads: string[] = []; + const results = await Promise.allSettled( manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme); const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme); const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { - const { body } = results[i]; + const result = results[i]; + if (result.status === "rejected") { + console.error(`Failed to load manifest at ${manifestLocations[i]}, reason: ${result.reason}`); + log.log({ l: "Manifest fetch failed", location: manifestLocations[i], reason: result.reason }); + failedManifestLoads.push(manifestLocations[i]) + continue; + } + noManifestsAvailable = false; + const { body } = result.value; try { if (body.extends) { - const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends); + const indexOfBaseManifest = results.findIndex(result => "value" in result && result.value.body.id === body.extends); if (indexOfBaseManifest === -1) { throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`); } - const {body: baseManifest} = results[indexOfBaseManifest]; + const { body: baseManifest } = (results[indexOfBaseManifest] as PromiseFulfilledResult<{ body: ThemeManifest }>).value; const baseManifestLocation = manifestLocations[indexOfBaseManifest]; const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log); runtimeThemePromises.push(promise); @@ -59,6 +70,10 @@ export class ThemeLoader { console.error(e); } } + if (noManifestsAvailable) { + // We need at least one working theme manifest! + throw new Error(`All configured theme manifests failed to load, the following were tried: ${failedManifestLoads.join(", ")}`); + } await Promise.all(runtimeThemePromises); this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping }; Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping); From d1c7a792b8cbe34cf4bc77f842e2038615fffe0c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 14 Aug 2022 17:43:24 +0530 Subject: [PATCH 192/354] Await in fill method to prevent multiple errors --- .../session/room/timeline/tiles/GapTile.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 9bebb7b5..65a01988 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -39,6 +39,17 @@ export class GapTile extends SimpleTile { console.error(`room.fillGap(): ${err.message}:\n${err.stack}`); this._error = err; this.emitChange("error"); + if (err instanceof ConnectionError) { + /* + We need to wait for reconnection here rather than in + notifyVisible() because when we return/throw here + this._loading is set to false and other queued invocations of + this method will succeed and attempt further room.fillGap() calls - + resulting in multiple error entries in logs and elsewhere! + */ + await this._waitForReconnection(); + return true; + } // rethrow so caller of this method // knows not to keep calling this for now throw err; @@ -63,8 +74,8 @@ export class GapTile extends SimpleTile { } catch (e) { if (e instanceof ConnectionError) { - await this.options.client.reconnector.connectionStatus.waitFor(status => status === ConnectionStatus.Online).promise; - canFillMore = true; + // Don't increase depth because this gap fill was a noop + continue; } } depth = depth + 1; @@ -101,6 +112,10 @@ export class GapTile extends SimpleTile { } } + async _waitForReconnection() { + this.options.client.reconnector.connectionStatus.waitFor(status => status === ConnectionStatus.Online).promise; + } + get shape() { return "gap"; } From 4a62cdb8fbcd5ae3010cc6a485f6ea111af9eded Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 14 Aug 2022 17:52:19 +0530 Subject: [PATCH 193/354] Await the promise --- src/domain/session/room/timeline/tiles/GapTile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 65a01988..f41dd722 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -77,6 +77,9 @@ export class GapTile extends SimpleTile { // Don't increase depth because this gap fill was a noop continue; } + else { + canFillMore = false; + } } depth = depth + 1; } while (depth < 10 && !this._siblingChanged && canFillMore && !this.isDisposed); @@ -113,7 +116,7 @@ export class GapTile extends SimpleTile { } async _waitForReconnection() { - this.options.client.reconnector.connectionStatus.waitFor(status => status === ConnectionStatus.Online).promise; + await this.options.client.reconnector.connectionStatus.waitFor(status => status === ConnectionStatus.Online).promise; } get shape() { From 749e038a470b74d96b2f3a55b901dfe3370f84de Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 14 Aug 2022 11:05:13 -0700 Subject: [PATCH 194/354] typescript-ifying PasswordLoginViewModel --- src/domain/login/LoginViewModel.ts | 6 ++-- ...ViewModel.js => PasswordLoginViewModel.ts} | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) rename src/domain/login/{PasswordLoginViewModel.js => PasswordLoginViewModel.ts} (66%) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 8eb11a9e..40f4b5f7 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -16,7 +16,7 @@ limitations under the License. import {Client} from "../../matrix/Client.js"; import {Options as BaseOptions, ViewModel} from "../ViewModel"; -import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; +import {PasswordLoginViewModel} from "./PasswordLoginViewModel"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; @@ -60,7 +60,7 @@ export class LoginViewModel extends ViewModel { this._initViewModels(); } - get passwordLoginViewModel(): PasswordLoginViewModel { + get passwordLoginViewModel(): PasswordLoginViewModel | undefined { return this._passwordLoginViewModel; } @@ -285,7 +285,7 @@ export class LoginViewModel extends ViewModel { type ReadyFn = (client: Client) => void; // TODO: move to Client.js when its converted to typescript. -type LoginOptions = { +export type LoginOptions = { homeserver: string; password?: (username: string, password: string) => PasswordLoginMethod; sso?: SSOLoginHelper; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.ts similarity index 66% rename from src/domain/login/PasswordLoginViewModel.js rename to src/domain/login/PasswordLoginViewModel.ts index 7c4ff78a..6b4d9978 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.ts @@ -14,43 +14,53 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; +import type {PasswordLoginMethod} from "../../matrix/login"; +import {Options as BaseOptions, ViewModel} from "../ViewModel"; +import type {LoginOptions} from "./LoginViewModel"; + +type Options = { + loginOptions: LoginOptions | undefined; + attemptLogin: (loginMethod: PasswordLoginMethod) => Promise; +} & BaseOptions export class PasswordLoginViewModel extends ViewModel { - constructor(options) { + private _loginOptions?: LoginOptions; + private _attemptLogin: (loginMethod: PasswordLoginMethod) => Promise; + private _isBusy = false; + private _errorMessage = ""; + + constructor(options: Options) { super(options); const {loginOptions, attemptLogin} = options; this._loginOptions = loginOptions; this._attemptLogin = attemptLogin; - this._isBusy = false; - this._errorMessage = ""; } - get isBusy() { return this._isBusy; } - get errorMessage() { return this._errorMessage; } + get isBusy(): boolean { return this._isBusy; } + get errorMessage(): string { return this._errorMessage; } - setBusy(status) { + setBusy(status: boolean): void { this._isBusy = status; this.emitChange("isBusy"); } - _showError(message) { + _showError(message: string): void { this._errorMessage = message; this.emitChange("errorMessage"); } - async login(username, password) { + async login(username: string, password: string): Promise{ this._errorMessage = ""; this.emitChange("errorMessage"); - const status = await this._attemptLogin(this._loginOptions.password(username, password)); + const status = await this._attemptLogin(this._loginOptions!.password!(username, password)); let error = ""; switch (status) { case LoginFailure.Credentials: error = this.i18n`Your username and/or password don't seem to be correct.`; break; case LoginFailure.Connection: - error = this.i18n`Can't connect to ${this._loginOptions.homeserver}.`; + error = this.i18n`Can't connect to ${this._loginOptions!.homeserver}.`; break; case LoginFailure.Unknown: error = this.i18n`Something went wrong while checking your login and password.`; From 53c4ecb69b36fa6963bd9a538c7d986c64059596 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 14 Aug 2022 11:14:54 -0700 Subject: [PATCH 195/354] typescript-ifying StartSSOLoginViewModel --- src/domain/login/LoginViewModel.ts | 4 +-- ...ViewModel.js => StartSSOLoginViewModel.ts} | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) rename src/domain/login/{StartSSOLoginViewModel.js => StartSSOLoginViewModel.ts} (56%) diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 40f4b5f7..43f0334a 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -17,7 +17,7 @@ limitations under the License. import {Client} from "../../matrix/Client.js"; import {Options as BaseOptions, ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel"; -import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; +import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; @@ -64,7 +64,7 @@ export class LoginViewModel extends ViewModel { return this._passwordLoginViewModel; } - get startSSOLoginViewModel(): StartSSOLoginViewModel { + get startSSOLoginViewModel(): StartSSOLoginViewModel | undefined { return this._startSSOLoginViewModel; } diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.ts similarity index 56% rename from src/domain/login/StartSSOLoginViewModel.js rename to src/domain/login/StartSSOLoginViewModel.ts index dba0bcb5..b85bec71 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.ts @@ -14,25 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel"; +import type {SSOLoginHelper} from "../../matrix/login"; +import {Options as BaseOptions, ViewModel} from "../ViewModel"; +import type {LoginOptions} from "./LoginViewModel"; + + +type Options = { + loginOptions: LoginOptions | undefined; +} & BaseOptions; export class StartSSOLoginViewModel extends ViewModel{ - constructor(options) { + private _sso?: SSOLoginHelper; + private _isBusy = false; + + constructor(options: Options) { super(options); - this._sso = options.loginOptions.sso; + this._sso = options.loginOptions!.sso; this._isBusy = false; } - - get isBusy() { return this._isBusy; } - - setBusy(status) { + + get isBusy(): boolean { return this._isBusy; } + + setBusy(status: boolean): void { this._isBusy = status; this.emitChange("isBusy"); } - async startSSOLogin() { - await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso.homeserver); - const link = this._sso.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); + async startSSOLogin(): Promise { + await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso!.homeserver); + const link = this._sso!.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); this.platform.openUrl(link); } } From c1161e5332cc64ab3c407f27495149d84c8da272 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 14 Aug 2022 11:27:25 -0700 Subject: [PATCH 196/354] typescript-ifying CompleteSSOLoginViewModel --- ...wModel.js => CompleteSSOLoginViewModel.ts} | 27 ++++++++++++++----- src/domain/login/LoginViewModel.ts | 4 +-- 2 files changed, 22 insertions(+), 9 deletions(-) rename src/domain/login/{CompleteSSOLoginViewModel.js => CompleteSSOLoginViewModel.ts} (72%) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.ts similarity index 72% rename from src/domain/login/CompleteSSOLoginViewModel.js rename to src/domain/login/CompleteSSOLoginViewModel.ts index d41d53ec..b7be4642 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.ts @@ -14,11 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel"; +import {Options as BaseOptions, ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; +import type {TokenLoginMethod} from "../../matrix/login"; +import { Client } from "../../matrix/Client.js"; + +type Options = { + client: Client; + attemptLogin: (loginMethod: TokenLoginMethod) => Promise; + loginToken: string; +} & BaseOptions export class CompleteSSOLoginViewModel extends ViewModel { - constructor(options) { + private _loginToken: string; + private _client: Client; + private _attemptLogin: (loginMethod: TokenLoginMethod) => Promise; + private _errorMessage = ""; + + constructor(options: Options) { super(options); const { loginToken, @@ -29,22 +42,22 @@ export class CompleteSSOLoginViewModel extends ViewModel { this._client = client; this._attemptLogin = attemptLogin; this._errorMessage = ""; - this.performSSOLoginCompletion(); + void this.performSSOLoginCompletion(); } - get errorMessage() { return this._errorMessage; } + get errorMessage(): string { return this._errorMessage; } - _showError(message) { + _showError(message: string): void { this._errorMessage = message; this.emitChange("errorMessage"); } - async performSSOLoginCompletion() { + async performSSOLoginCompletion(): Promise { if (!this._loginToken) { return; } const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); - let loginOptions; + let loginOptions: { token?: (loginToken: string) => TokenLoginMethod; }; try { loginOptions = await this._client.queryLogin(homeserver).result; } diff --git a/src/domain/login/LoginViewModel.ts b/src/domain/login/LoginViewModel.ts index 43f0334a..75d57880 100644 --- a/src/domain/login/LoginViewModel.ts +++ b/src/domain/login/LoginViewModel.ts @@ -18,7 +18,7 @@ import {Client} from "../../matrix/Client.js"; import {Options as BaseOptions, ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel"; -import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; +import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; import {SegmentType} from "../navigation/index"; @@ -68,7 +68,7 @@ export class LoginViewModel extends ViewModel { return this._startSSOLoginViewModel; } - get completeSSOLoginViewModel(): CompleteSSOLoginViewModel { + get completeSSOLoginViewModel(): CompleteSSOLoginViewModel | undefined { return this._completeSSOLoginViewModel; } From 8e2838264f5fd0e8ba41a667c6da446de69bad97 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 15 Aug 2022 15:00:31 +0530 Subject: [PATCH 197/354] Run binding when isLoading changes --- src/domain/session/room/timeline/tiles/GapTile.js | 1 + src/platform/web/ui/session/room/timeline/GapView.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index f41dd722..3a7f7533 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -38,6 +38,7 @@ export class GapTile extends SimpleTile { } catch (err) { console.error(`room.fillGap(): ${err.message}:\n${err.stack}`); this._error = err; + this._loading = false; this.emitChange("error"); if (err instanceof ConnectionError) { /* diff --git a/src/platform/web/ui/session/room/timeline/GapView.js b/src/platform/web/ui/session/room/timeline/GapView.js index 03f87fcf..87afe503 100644 --- a/src/platform/web/ui/session/room/timeline/GapView.js +++ b/src/platform/web/ui/session/room/timeline/GapView.js @@ -30,16 +30,17 @@ export class GapView extends TemplateView { isAtTop: vm => vm.isAtTop, }; return t.li({ className }, [ - t.map(vm => vm.error, - (error, t, vm) => { + t.map(vm => vm.isLoading, + (isLoading, t, vm) => { let elements; + const error = vm.error; if (error) { elements = [t.strong(() => error.message)]; if (error.showSpinner) { elements.unshift(spinner(t)); } } - else if (vm.isLoading) { + else if (isLoading) { elements = [spinner(t), t.span(vm.i18n`Loading more messages …`)]; } else { From 2e12ce74b792e20b73d94915769aab6e3e6521d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 15 Aug 2022 17:23:27 +0530 Subject: [PATCH 198/354] Show parse errors in the UI as well --- src/platform/web/theming/ThemeLoader.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 7519329d..96293009 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -35,6 +35,7 @@ export class ThemeLoader { await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { let noManifestsAvailable = true; const failedManifestLoads: string[] = []; + const parseErrors: string[] = []; const results = await Promise.allSettled( manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); @@ -68,15 +69,19 @@ export class ThemeLoader { } catch(e) { console.error(e); + parseErrors.push(e.message); } } + await Promise.all(runtimeThemePromises); + this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping }; if (noManifestsAvailable) { // We need at least one working theme manifest! throw new Error(`All configured theme manifests failed to load, the following were tried: ${failedManifestLoads.join(", ")}`); } - await Promise.all(runtimeThemePromises); - this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping }; - Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping); + else if (Object.keys(this._themeMapping).length === 0 && parseErrors.length) { + // Something is wrong..., themeMapping is empty! + throw new Error(`Failed to parse theme manifests, the following errors were encountered: ${parseErrors.join(", ")}`); + } this._addDefaultThemeToMapping(log); log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); log.log({ l: "Result", themeMapping: this._themeMapping }); From 7590c5540475d7bf0455bfb57217c0697741c76a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 15 Aug 2022 22:28:40 +0530 Subject: [PATCH 199/354] Log error when loading css file fails --- src/platform/web/Platform.js | 38 +++++++++++++++++-------- src/platform/web/theming/ThemeLoader.ts | 6 ++-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 15923a86..df24418c 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -192,7 +192,7 @@ export class Platform { await this._themeLoader?.init(manifests, log); const { themeName, themeVariant } = await this._themeLoader.getActiveTheme(); log.log({ l: "Active theme", name: themeName, variant: themeVariant }); - this._themeLoader.setTheme(themeName, themeVariant, log); + await this._themeLoader.setTheme(themeName, themeVariant, log); } }); } catch (err) { @@ -332,17 +332,31 @@ export class Platform { return this._themeLoader; } - replaceStylesheet(newPath) { - const head = document.querySelector("head"); - // remove default theme - document.querySelectorAll(".theme").forEach(e => e.remove()); - // add new theme - const styleTag = document.createElement("link"); - styleTag.href = newPath; - styleTag.rel = "stylesheet"; - styleTag.type = "text/css"; - styleTag.className = "theme"; - head.appendChild(styleTag); + async replaceStylesheet(newPath, log) { + await this.logger.wrapOrRun(log, { l: "replaceStylesheet", location: newPath, }, async (l) => { + let resolve; + const promise = new Promise(r => resolve = r); + const head = document.querySelector("head"); + // remove default theme + document.querySelectorAll(".theme").forEach(e => e.remove()); + // add new theme + const styleTag = document.createElement("link"); + styleTag.href = newPath; + styleTag.rel = "stylesheet"; + styleTag.type = "text/css"; + styleTag.className = "theme"; + styleTag.onerror = () => { + const error = new Error(`Failed to load stylesheet at ${newPath}`); + l.catch(error); + resolve(); + throw error + }; + styleTag.onload = () => { + resolve(); + }; + head.appendChild(styleTag); + await promise; + }); } get description() { diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 96293009..d15108a2 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -88,8 +88,8 @@ export class ThemeLoader { }); } - setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { - this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => { + async setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { + await this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, async (l) => { let cssLocation: string, variables: Record; let themeDetails = this._themeMapping[themeName]; if ("id" in themeDetails) { @@ -103,7 +103,7 @@ export class ThemeLoader { cssLocation = themeDetails[themeVariant].cssLocation; variables = themeDetails[themeVariant].variables; } - this._platform.replaceStylesheet(cssLocation); + await this._platform.replaceStylesheet(cssLocation, l); if (variables) { log?.log({l: "Derived Theme", variables}); this._injectCSSVariables(variables); From 6335da09326d8ab6c95a6bad0a59048c98376bcf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 15 Aug 2022 22:52:02 +0530 Subject: [PATCH 200/354] Throw error from outside log method This will show the error in the UI --- src/platform/web/Platform.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index df24418c..3e6de0c3 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -333,8 +333,8 @@ export class Platform { } async replaceStylesheet(newPath, log) { - await this.logger.wrapOrRun(log, { l: "replaceStylesheet", location: newPath, }, async (l) => { - let resolve; + const error = await this.logger.wrapOrRun(log, { l: "replaceStylesheet", location: newPath, }, async (l) => { + let resolve, error; const promise = new Promise(r => resolve = r); const head = document.querySelector("head"); // remove default theme @@ -346,17 +346,20 @@ export class Platform { styleTag.type = "text/css"; styleTag.className = "theme"; styleTag.onerror = () => { - const error = new Error(`Failed to load stylesheet at ${newPath}`); + error = new Error(`Failed to load stylesheet from ${newPath}`); l.catch(error); resolve(); - throw error }; styleTag.onload = () => { resolve(); }; head.appendChild(styleTag); await promise; + return error; }); + if (error) { + throw error; + } } get description() { From 08f9edaf68ab15882f7a15cb2589e71695e86fd8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 15 Aug 2022 22:58:26 +0530 Subject: [PATCH 201/354] Use Error LogLevel --- src/platform/web/theming/ThemeLoader.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index d15108a2..6382dcdb 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -21,6 +21,7 @@ import type {Variant, ThemeInformation} from "./parsers/types"; import type {ThemeManifest} from "../../types/theme"; import type {ILogItem} from "../../../logging/types"; import type {Platform} from "../Platform.js"; +import {LogLevel} from "../../../logging/LogFilter"; export class ThemeLoader { private _platform: Platform; @@ -46,7 +47,7 @@ export class ThemeLoader { const result = results[i]; if (result.status === "rejected") { console.error(`Failed to load manifest at ${manifestLocations[i]}, reason: ${result.reason}`); - log.log({ l: "Manifest fetch failed", location: manifestLocations[i], reason: result.reason }); + log.log({ l: "Manifest fetch failed", location: manifestLocations[i], reason: result.reason }, LogLevel.Error); failedManifestLoads.push(manifestLocations[i]) continue; } From 5d63069f317fd3d553c7121e90537a8e88e2e88b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 16 Aug 2022 14:32:18 +0530 Subject: [PATCH 202/354] Check status code instead of throwing error --- src/platform/web/dom/request/fetch.js | 7 ++++--- src/platform/web/theming/ThemeLoader.ts | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index 5f6dcb65..c2e2d4b7 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -119,10 +119,11 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { body = await response.text(); } } catch (err) { - if (err.name === "SyntaxError" && status >= 400) { - throw new ConnectionError(`${method} ${url}: Failed to fetch JSON file!`); + // some error pages return html instead of json, ignore error + // detect these ignored errors from the response status + if (!(err.name === "SyntaxError" && status >= 400)) { + throw err; } - throw err; } return {status, body}; }, err => { diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 6382dcdb..665c3a17 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -37,7 +37,7 @@ export class ThemeLoader { let noManifestsAvailable = true; const failedManifestLoads: string[] = []; const parseErrors: string[] = []; - const results = await Promise.allSettled( + const results = await Promise.all( manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme); @@ -45,14 +45,14 @@ export class ThemeLoader { const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const result = results[i]; - if (result.status === "rejected") { - console.error(`Failed to load manifest at ${manifestLocations[i]}, reason: ${result.reason}`); - log.log({ l: "Manifest fetch failed", location: manifestLocations[i], reason: result.reason }, LogLevel.Error); + const { status, body } = result; + if (!(status >= 200 && status <= 299)) { + console.error(`Failed to load manifest at ${manifestLocations[i]}, status: ${status}`); + log.log({ l: "Manifest fetch failed", location: manifestLocations[i], status }, LogLevel.Error); failedManifestLoads.push(manifestLocations[i]) continue; } noManifestsAvailable = false; - const { body } = result.value; try { if (body.extends) { const indexOfBaseManifest = results.findIndex(result => "value" in result && result.value.body.id === body.extends); From 220144898be390bbeeec68bcb5037f706c63a520 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 17 Aug 2022 13:13:20 +0530 Subject: [PATCH 203/354] Fix errors and simplify code --- .../session/room/timeline/tiles/GapTile.js | 15 ++++++---- .../web/ui/session/room/timeline/GapView.js | 30 ++++++++----------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 3a7f7533..7aae124d 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -26,21 +26,22 @@ export class GapTile extends SimpleTile { this._error = null; this._isAtTop = true; this._siblingChanged = false; + this._showSpinner = false; } async fill() { if (!this._loading && !this._entry.edgeReached) { this._loading = true; this._error = null; + this._showSpinner = true; this.emitChange("isLoading"); try { await this._room.fillGap(this._entry, 10); } catch (err) { console.error(`room.fillGap(): ${err.message}:\n${err.stack}`); this._error = err; - this._loading = false; - this.emitChange("error"); if (err instanceof ConnectionError) { + this.emitChange("error"); /* We need to wait for reconnection here rather than in notifyVisible() because when we return/throw here @@ -49,13 +50,13 @@ export class GapTile extends SimpleTile { resulting in multiple error entries in logs and elsewhere! */ await this._waitForReconnection(); - return true; } // rethrow so caller of this method // knows not to keep calling this for now throw err; } finally { this._loading = false; + this._showSpinner = false; this.emitChange("isLoading"); } return true; @@ -128,13 +129,17 @@ export class GapTile extends SimpleTile { return this._loading; } + get showSpinner() { + return this._showSpinner; + } + get error() { if (this._error) { if (this._error instanceof ConnectionError) { - return { message: "Waiting for reconnection", showSpinner: true }; + return "Waiting for reconnection"; } const dir = this._entry.prev_batch ? "previous" : "next"; - return { message: `Could not load ${dir} messages: ${this._error.message}`, showSpinner: false }; + return `Could not load ${dir} messages: ${this._error.message}`; } return null; } diff --git a/src/platform/web/ui/session/room/timeline/GapView.js b/src/platform/web/ui/session/room/timeline/GapView.js index 87afe503..e80321be 100644 --- a/src/platform/web/ui/session/room/timeline/GapView.js +++ b/src/platform/web/ui/session/room/timeline/GapView.js @@ -30,24 +30,18 @@ export class GapView extends TemplateView { isAtTop: vm => vm.isAtTop, }; return t.li({ className }, [ - t.map(vm => vm.isLoading, - (isLoading, t, vm) => { - let elements; - const error = vm.error; - if (error) { - elements = [t.strong(() => error.message)]; - if (error.showSpinner) { - elements.unshift(spinner(t)); - } - } - else if (isLoading) { - elements = [spinner(t), t.span(vm.i18n`Loading more messages …`)]; - } - else { - elements = t.span(vm.i18n`Not loading!`); - } - return t.div({ className: "GapView__container" }, elements); - }) + t.if(vm => vm.showSpinner, (t) => spinner(t)), + t.span(vm => { + if (vm.error) { + return vm.error; + } + else if (vm.isLoading) { + return "Loading"; + } + else { + return "Not Loading"; + } + }) ]); } From 98bd8cd624063e2b3458af87425269819caeca93 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 17 Aug 2022 13:19:11 +0530 Subject: [PATCH 204/354] Remove unused css --- src/platform/web/ui/css/themes/element/timeline.css | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 47f9a365..43c57d19 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -422,12 +422,3 @@ only loads when the top comes into view*/ .GapView.isAtTop { padding: 52px 20px 12px 20px; } - -.GapView__container { - display: flex; - align-items: center; -} - -.GapView__container .spinner { - margin-right: 10px; -} From e6f43d6f4f32717153eda313a77a04ff970d2bbd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 19 Aug 2022 11:55:23 +0530 Subject: [PATCH 205/354] Put logic into VM --- src/domain/session/room/timeline/tiles/GapTile.js | 12 ++++++++++++ src/platform/web/ui/session/room/timeline/GapView.js | 12 +----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 7aae124d..56d05edd 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -143,6 +143,18 @@ export class GapTile extends SimpleTile { } return null; } + + get currentAction() { + if (this.error) { + return this.error; + } + else if (this.isLoading) { + return "Loading"; + } + else { + return "Not Loading"; + } + } } import {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry.js"; diff --git a/src/platform/web/ui/session/room/timeline/GapView.js b/src/platform/web/ui/session/room/timeline/GapView.js index e80321be..4fc0e3d6 100644 --- a/src/platform/web/ui/session/room/timeline/GapView.js +++ b/src/platform/web/ui/session/room/timeline/GapView.js @@ -31,17 +31,7 @@ export class GapView extends TemplateView { }; return t.li({ className }, [ t.if(vm => vm.showSpinner, (t) => spinner(t)), - t.span(vm => { - if (vm.error) { - return vm.error; - } - else if (vm.isLoading) { - return "Loading"; - } - else { - return "Not Loading"; - } - }) + t.span(vm => vm.currentAction) ]); } From fdd6eb8fdc21f47e2b64f2b24e1e63f91e4e5417 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 19 Aug 2022 11:55:44 +0530 Subject: [PATCH 206/354] Set boolean to true so that gapfill proceeds --- src/domain/session/room/timeline/tiles/GapTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 56d05edd..bb7d8086 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -76,6 +76,7 @@ export class GapTile extends SimpleTile { } catch (e) { if (e instanceof ConnectionError) { + canFillMore = true; // Don't increase depth because this gap fill was a noop continue; } From 34dd7e4fa5f931a4fba85835660ec44e3307fe45 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 19 Aug 2022 16:31:48 +0530 Subject: [PATCH 207/354] Remove code --- src/matrix/Client.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 438bf3ab..44643cc1 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -451,12 +451,6 @@ export class Client { async deleteSession(log) { if (this._sessionId) { - await log.wrap("sessionStore", async () => { - const storage = this._storage ?? await this._platform.storageFactory.create(this._sessionId, log); - const txn = await storage.readWriteTxn([storage.storeNames.session]); - txn.session.delete(); - storage.close(); - }); // need to dispose first, so the storage is closed, // and also first sync finishing won't call Session.start anymore, // which assumes that the storage works. From d6cea6fc5c8fc4fd27c3bb848c8c6164652299fe Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 19 Aug 2022 16:36:21 +0530 Subject: [PATCH 208/354] Extract method to function outside class --- src/matrix/storage/idb/StorageFactory.ts | 12 ++++--- src/matrix/storage/idb/stores/SessionStore.ts | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 1f64baf3..44d481eb 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -16,11 +16,12 @@ limitations under the License. import {IDOMStorage} from "./types"; import {Storage} from "./Storage"; -import { openDatabase, reqAsPromise } from "./utils"; -import { exportSession, importSession, Export } from "./export"; -import { schema } from "./schema"; -import { detectWebkitEarlyCloseTxnBug } from "./quirks"; -import { ILogItem } from "../../../logging/types"; +import {openDatabase, reqAsPromise} from "./utils"; +import {exportSession, importSession, Export} from "./export"; +import {schema} from "./schema"; +import {detectWebkitEarlyCloseTxnBug} from "./quirks"; +import {ILogItem} from "../../../logging/types"; +import {clearKeysFromLocalStorage} from "./stores/SessionStore"; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: ILogItem) { @@ -79,6 +80,7 @@ export class StorageFactory { delete(sessionId: string): Promise { const databaseName = sessionName(sessionId); + clearKeysFromLocalStorage(this._localStorage, databaseName); const req = this._idbFactory.deleteDatabase(databaseName); return reqAsPromise(req); } diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index a7aaf594..9ae9bb7e 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -24,6 +24,23 @@ export interface SessionEntry { value: any; } +function getLocalStorageKeyPrefix(databaseName: string): string { + return `${databaseName}.session.`; +} + +export function clearKeysFromLocalStorage(localStorage: IDOMStorage, databaseName: string): void { + const keys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.startsWith(getLocalStorageKeyPrefix(databaseName))) { + keys.push(key); + } + } + for (const key of keys) { + localStorage.removeItem(key); + } +} + export class SessionStore { private _sessionStore: Store private _localStorage: IDOMStorage; @@ -34,7 +51,7 @@ export class SessionStore { } private get _localStorageKeyPrefix(): string { - return `${this._sessionStore.databaseName}.session.`; + return getLocalStorageKeyPrefix(this._sessionStore.databaseName); } async get(key: string): Promise { @@ -105,17 +122,4 @@ export class SessionStore { } this._sessionStore.delete(key); } - - delete(): void { - const keys: string[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = this._localStorage.key(i); - if (key?.startsWith(this._localStorageKeyPrefix)) { - keys.push(key); - } - } - for (const key of keys) { - this._localStorage.removeItem(key); - } - } } From 86fec8bf0e0488c89825c81f7fd2a7b25cc5c80e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 19 Aug 2022 18:22:37 +0530 Subject: [PATCH 209/354] Make code more readable --- src/platform/web/Platform.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 3e6de0c3..29a83e1f 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -334,8 +334,7 @@ export class Platform { async replaceStylesheet(newPath, log) { const error = await this.logger.wrapOrRun(log, { l: "replaceStylesheet", location: newPath, }, async (l) => { - let resolve, error; - const promise = new Promise(r => resolve = r); + let error; const head = document.querySelector("head"); // remove default theme document.querySelectorAll(".theme").forEach(e => e.remove()); @@ -345,14 +344,16 @@ export class Platform { styleTag.rel = "stylesheet"; styleTag.type = "text/css"; styleTag.className = "theme"; - styleTag.onerror = () => { - error = new Error(`Failed to load stylesheet from ${newPath}`); - l.catch(error); - resolve(); - }; - styleTag.onload = () => { - resolve(); - }; + const promise = new Promise(resolve => { + styleTag.onerror = () => { + error = new Error(`Failed to load stylesheet from ${newPath}`); + l.catch(error); + resolve(); + }; + styleTag.onload = () => { + resolve(); + }; + }); head.appendChild(styleTag); await promise; return error; From 92ed503700e9c8350849e62067f295eead66865d Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Fri, 19 Aug 2022 19:02:06 -0400 Subject: [PATCH 210/354] Fixes MappedMap type system --- src/observable/map/BaseObservableMap.ts | 2 +- .../map/BaseObservableMapTransformers.ts | 8 ++++---- src/observable/map/MappedMap.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index aa76f641..807a11e7 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -66,7 +66,7 @@ export abstract class BaseObservableMap extends BaseObservable, updater?: Updater): MappedMap { + mapValues(mapper: Mapper, updater?: Updater): MappedMap { return this._defaults.mapValues(this, mapper, updater); } diff --git a/src/observable/map/BaseObservableMapTransformers.ts b/src/observable/map/BaseObservableMapTransformers.ts index 3b39587d..b81ffe33 100644 --- a/src/observable/map/BaseObservableMapTransformers.ts +++ b/src/observable/map/BaseObservableMapTransformers.ts @@ -44,7 +44,7 @@ export class BaseObservableMapTransformers { return new JoinedMap([_this].concat(otherMaps)); } - mapValues(_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap { + mapValues(_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap { return new MappedMap(_this, mapper, updater); } @@ -57,12 +57,12 @@ export class BaseObservableMapTransformers { } } -export type Mapper = ( +export type Mapper = ( value: V, emitSpontaneousUpdate: any, -) => V; +) => MappedV; -export type Updater = (params: any, mappedValue?: V, value?: V) => void; +export type Updater = (params: any, mappedValue?: MappedV, value?: V) => void; export type Comparator = (a: V, b: V) => number; diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index bd091856..e932e1b8 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -23,24 +23,24 @@ import {SubscriptionHandle} from "../BaseObservable"; so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ -export class MappedMap extends BaseObservableMap { +export class MappedMap extends BaseObservableMap { private _source: BaseObservableMap; - private _mapper: Mapper; - private _updater?: Updater; - private _mappedValues: Map; + private _mapper: Mapper; + private _updater?: Updater; + private _mappedValues: Map; private _subscription?: SubscriptionHandle; constructor( source: BaseObservableMap, - mapper: Mapper, - updater?: Updater + mapper: Mapper, + updater?: Updater ) { - super(new BaseObservableMapTransformers()); + super(new BaseObservableMapTransformers()); this._source = source; this._mapper = mapper; this._updater = updater; - this._mappedValues = new Map(); + this._mappedValues = new Map(); } _emitSpontaneousUpdate(key: K, params: any): void { @@ -107,7 +107,7 @@ export class MappedMap extends BaseObservableMap { return this._mappedValues.size; } - get(key: K): V | undefined { + get(key: K): MappedV | undefined { return this._mappedValues.get(key); } } \ No newline at end of file From 77f21f7a911a6d4e7e0878516ef922c39c28d252 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 20 Aug 2022 16:39:39 -0400 Subject: [PATCH 211/354] fixes import order according to https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de --- .../session/leftpanel/LeftPanelViewModel.js | 2 +- src/lib.ts | 3 +- src/observable/index.ts | 2 +- src/observable/map/ApplyMap.ts | 10 ++- src/observable/map/BaseObservableMap.ts | 50 +++++++++----- .../map/BaseObservableMapTransformers.ts | 69 ------------------- src/observable/map/FilteredMap.ts | 10 ++- src/observable/map/JoinedMap.ts | 10 ++- src/observable/map/LogMap.ts | 10 ++- src/observable/map/MappedMap.ts | 10 ++- src/observable/map/ObservableMap.ts | 10 ++- src/observable/map/index.ts | 16 +++++ 12 files changed, 92 insertions(+), 110 deletions(-) delete mode 100644 src/observable/map/BaseObservableMapTransformers.ts create mode 100644 src/observable/map/index.ts diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 8e814151..2c657201 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -20,7 +20,7 @@ import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; -import {ApplyMap} from "../../../observable/map/ApplyMap"; +import {ApplyMap} from "../../../observable"; import {addPanelIfNeeded} from "../../navigation"; export class LeftPanelViewModel extends ViewModel { diff --git a/src/lib.ts b/src/lib.ts index 4d1f906f..df96bfcd 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -78,8 +78,7 @@ export { MappedList, AsyncMappedList, ConcatList, - ObservableMap -} from "./observable/index"; +} from "./observable"; export { BaseObservableValue, ObservableValue, diff --git a/src/observable/index.ts b/src/observable/index.ts index dfd272fd..25af50b8 100644 --- a/src/observable/index.ts +++ b/src/observable/index.ts @@ -16,7 +16,7 @@ limitations under the License. // re-export "root" (of chain) collection -export { ObservableMap } from "./map/ObservableMap"; +export { ObservableMap, ApplyMap, FilteredMap, JoinedMap, LogMap, MappedMap } from "./map"; export { ObservableArray } from "./list/ObservableArray"; export { SortedArray } from "./list/SortedArray"; export { MappedList } from "./list/MappedList"; diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index ee40c797..44acd1fe 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -14,11 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap} from "./index"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class ApplyMap extends BaseObservableMap { private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; @@ -26,7 +30,7 @@ export class ApplyMap extends BaseObservableMap { constructor(source: BaseObservableMap, apply?: Apply) { - super(new BaseObservableMapTransformers()); + super(); this._source = source; this._apply = apply; } diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 807a11e7..9b501285 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -15,11 +15,10 @@ limitations under the License. */ import {BaseObservable} from "../BaseObservable"; -import {JoinedMap} from "../map/JoinedMap"; -import {MappedMap} from "../map/MappedMap"; -import {FilteredMap} from "../map/FilteredMap"; +import {JoinedMap} from "./index"; +import {MappedMap} from "./index"; +import {FilteredMap} from "./index"; import {SortedMapList} from "../list/SortedMapList.js"; -import type {BaseObservableMapTransformers, Mapper, Updater, Comparator, Filter} from "./BaseObservableMapTransformers"; export interface IMapObserver { @@ -29,12 +28,15 @@ export interface IMapObserver { onRemove(key: K, value: V): void } +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export abstract class BaseObservableMap extends BaseObservable> { - private _defaults: BaseObservableMapTransformers; - constructor(defaults: BaseObservableMapTransformers) { + constructor() { super(); - this._defaults = defaults; } emitReset(): void { @@ -63,23 +65,33 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap { - return this._defaults.join(this, ...otherMaps); - } + return new JoinedMap([this].concat(otherMaps)); + } - mapValues(mapper: Mapper, updater?: Updater): MappedMap { - return this._defaults.mapValues(this, mapper, updater); - } + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return new MappedMap(this, mapper, updater); + } - sortValues(comparator: Comparator): SortedMapList { - return this._defaults.sortValues(this, comparator); - } - - filterValues(filter: Filter): FilteredMap { - return this._defaults.filterValues(this, filter); - } + sortValues(comparator: Comparator): SortedMapList { + return new SortedMapList(this, comparator); + } + filterValues(filter: Filter): FilteredMap { + return new FilteredMap(this, filter); + } abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; abstract get(key: K): V | undefined; } + +export type Mapper = ( + value: V, + emitSpontaneousUpdate: any, +) => MappedV; + +export type Updater = (params: any, mappedValue?: MappedV, value?: V) => void; + +export type Comparator = (a: V, b: V) => number; + +export type Filter = (v: V, k: K) => boolean; \ No newline at end of file diff --git a/src/observable/map/BaseObservableMapTransformers.ts b/src/observable/map/BaseObservableMapTransformers.ts deleted file mode 100644 index b81ffe33..00000000 --- a/src/observable/map/BaseObservableMapTransformers.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2022 Isaiah Becker-Mayer - -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 type {BaseObservableMap} from "./BaseObservableMap"; -import {FilteredMap} from "./FilteredMap"; -import {MappedMap} from "./MappedMap"; -import {JoinedMap} from "./JoinedMap"; -import {SortedMapList} from "../list/SortedMapList.js"; - - -// This class provides implementations of functions that transform one BaseObservableMap -// to another type of Map. It's methods are effectively default implementations of the -// methods by the same name on BaseObservableMap. -// -// It is kept as its own class in its own file in order to avoid circular dependencies -// which would occur if these method implementations were defined on BaseObservableMap -// itself. For example, if we attmpted to do the following on BaseObservableMap: -// -// class BaseObservableMap extends BaseObservable> { -// join(...otherMaps: Array>): JoinedMap { -// return new JoinedMap(this.concat(otherMaps)); -// } -// } -// -// we would end up with a circular dependency between BaseObservableMap and JoinedMap, -// since BaseObservableMap would need to import JoinedMap for the -// `return new JoinedMap(this.concat(otherMaps))`, and -// JoinedMap would need to import BaseObservableMap to do -// `JoinedMap extends BaseObservableMap`. -export class BaseObservableMapTransformers { - join(_this: BaseObservableMap, ...otherMaps: Array>): JoinedMap { - return new JoinedMap([_this].concat(otherMaps)); - } - - mapValues(_this: BaseObservableMap, mapper: Mapper, updater?: Updater): MappedMap { - return new MappedMap(_this, mapper, updater); - } - - sortValues(_this: BaseObservableMap, comparator: Comparator): SortedMapList { - return new SortedMapList(_this, comparator); - } - - filterValues(_this: BaseObservableMap, filter: Filter): FilteredMap { - return new FilteredMap(_this, filter); - } -} - -export type Mapper = ( - value: V, - emitSpontaneousUpdate: any, -) => MappedV; - -export type Updater = (params: any, mappedValue?: MappedV, value?: V) => void; - -export type Comparator = (a: V, b: V) => number; - -export type Filter = (v: V, k: K) => boolean; \ No newline at end of file diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 2f3ba9fd..1fd7d89a 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -14,11 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; +import {BaseObservableMap, Filter} from "./index"; import {SubscriptionHandle} from "../BaseObservable"; -import {BaseObservableMapTransformers, Filter} from "./BaseObservableMapTransformers"; +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class FilteredMap extends BaseObservableMap { private _source: BaseObservableMap; private _filter: Filter; @@ -26,7 +30,7 @@ export class FilteredMap extends BaseObservableMap { private _subscription?: SubscriptionHandle; constructor(source: BaseObservableMap, filter: Filter) { - super(new BaseObservableMapTransformers()); + super(); this._source = source; this._filter = filter; } diff --git a/src/observable/map/JoinedMap.ts b/src/observable/map/JoinedMap.ts index f6cb074a..c125f4da 100644 --- a/src/observable/map/JoinedMap.ts +++ b/src/observable/map/JoinedMap.ts @@ -14,17 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; +import {BaseObservableMap} from "."; import {SubscriptionHandle} from "../BaseObservable"; +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class JoinedMap extends BaseObservableMap { protected _sources: BaseObservableMap[]; private _subscriptions?: SourceSubscriptionHandler[]; constructor(sources: BaseObservableMap[]) { - super(new BaseObservableMapTransformers()); + super(); this._sources = sources; } diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 15f204d2..60ac7721 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -14,20 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; +import {BaseObservableMap} from "./index"; import {SubscriptionHandle} from "../BaseObservable"; import {ILogItem, LabelOrValues} from "../../logging/types"; import {LogLevel} from "../../logging/LogFilter"; +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class LogMap extends BaseObservableMap { private _source: BaseObservableMap; private _subscription?: SubscriptionHandle; private _log: ILogItem; constructor(source: BaseObservableMap, log: ILogItem) { - super(new BaseObservableMapTransformers()); + super(); this._source = source; this._log = log; } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index e932e1b8..0d2c47c0 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapTransformers, Mapper, Updater} from "./BaseObservableMapTransformers"; +import {BaseObservableMap, Mapper, Updater} from "./index"; import {SubscriptionHandle} from "../BaseObservable"; @@ -23,6 +22,11 @@ import {SubscriptionHandle} from "../BaseObservable"; so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class MappedMap extends BaseObservableMap { private _source: BaseObservableMap; private _mapper: Mapper; @@ -36,7 +40,7 @@ export class MappedMap extends BaseObservableMap { mapper: Mapper, updater?: Updater ) { - super(new BaseObservableMapTransformers()); + super(); this._source = source; this._mapper = mapper; this._updater = updater; diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 09a73d71..c5ffe397 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -14,15 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap"; -import {BaseObservableMapTransformers} from "./BaseObservableMapTransformers"; +import {BaseObservableMap} from "./index"; +/* +This class MUST never be imported directly from here. +Instead, it MUST be imported from index.ts. See the +top level comment in index.ts for details. +*/ export class ObservableMap extends BaseObservableMap { private readonly _values: Map; constructor(initialValues?: (readonly [K, V])[]) { - super(new BaseObservableMapTransformers()); + super(); this._values = new Map(initialValues); } diff --git a/src/observable/map/index.ts b/src/observable/map/index.ts new file mode 100644 index 00000000..1f4afc3e --- /dev/null +++ b/src/observable/map/index.ts @@ -0,0 +1,16 @@ +// In order to avoid a circular dependency problem at runtime between BaseObservableMap +// and the classes that extend it, it's important that: +// +// 1) It always remain the first module exported below. +// 2) Anything that imports any of the classes in this module +// ONLY import them from this index.ts file. +// +// See https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de +// for more on why this discipline is necessary. +export {BaseObservableMap, Mapper, Updater, Comparator, Filter} from './BaseObservableMap'; +export {ApplyMap} from './ApplyMap'; +export {FilteredMap} from './FilteredMap'; +export {JoinedMap} from './JoinedMap'; +export {LogMap} from './LogMap'; +export {MappedMap} from './MappedMap'; +export {ObservableMap} from './ObservableMap'; \ No newline at end of file From ebd8c0751a11e63e877d74a68dd836cac4fcfa5c Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 20 Aug 2022 17:04:13 -0400 Subject: [PATCH 212/354] fixes AsyncMappedList --- src/observable/list/AsyncMappedList.ts | 24 ++++++++++++------------ src/observable/map/ApplyMap.ts | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/observable/list/AsyncMappedList.ts b/src/observable/list/AsyncMappedList.ts index f1785c13..2c5ef63f 100644 --- a/src/observable/list/AsyncMappedList.ts +++ b/src/observable/list/AsyncMappedList.ts @@ -22,7 +22,7 @@ export class AsyncMappedList extends BaseMappedList> impleme private _eventQueue: AsyncEvent[] | null = null; private _flushing: boolean = false; - async onSubscribeFirst(): Promise { + onSubscribeFirst(): void { this._sourceUnsubscribe = this._sourceList.subscribe(this); this._eventQueue = []; this._mappedValues = []; @@ -31,7 +31,7 @@ export class AsyncMappedList extends BaseMappedList> impleme this._eventQueue.push(new AddEvent(idx, item)); idx += 1; } - await this._flush(); + void this._flush(); } async _flush(): Promise { @@ -49,38 +49,38 @@ export class AsyncMappedList extends BaseMappedList> impleme } } - async onReset(): Promise { + onReset(): void { if (this._eventQueue) { this._eventQueue.push(new ResetEvent()); - await this._flush(); + void this._flush(); } } - async onAdd(index: number, value: F): Promise { + onAdd(index: number, value: F): void { if (this._eventQueue) { this._eventQueue.push(new AddEvent(index, value)); - await this._flush(); + void this._flush(); } } - async onUpdate(index: number, value: F, params: any): Promise { + onUpdate(index: number, value: F, params: any): void { if (this._eventQueue) { this._eventQueue.push(new UpdateEvent(index, value, params)); - await this._flush(); + void this._flush(); } } - async onRemove(index: number): Promise { + onRemove(index: number): void { if (this._eventQueue) { this._eventQueue.push(new RemoveEvent(index)); - await this._flush(); + void this._flush(); } } - async onMove(fromIdx: number, toIdx: number): Promise { + onMove(fromIdx: number, toIdx: number): void { if (this._eventQueue) { this._eventQueue.push(new MoveEvent(fromIdx, toIdx)); - await this._flush(); + void this._flush(); } } diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index 44acd1fe..a13cf757 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -28,7 +28,6 @@ export class ApplyMap extends BaseObservableMap { private _subscription?: SubscriptionHandle; private _apply?: Apply; - constructor(source: BaseObservableMap, apply?: Apply) { super(); this._source = source; From 1e6d5ca42fc361bed81648292c0957773e05ab3d Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 21 Aug 2022 07:42:06 -0400 Subject: [PATCH 213/354] exports types instead of objects --- src/observable/map/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/observable/map/index.ts b/src/observable/map/index.ts index 1f4afc3e..a78446c4 100644 --- a/src/observable/map/index.ts +++ b/src/observable/map/index.ts @@ -7,7 +7,8 @@ // // See https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de // for more on why this discipline is necessary. -export {BaseObservableMap, Mapper, Updater, Comparator, Filter} from './BaseObservableMap'; +export {BaseObservableMap} from './BaseObservableMap'; +export type {Mapper, Updater, Comparator, Filter} from './BaseObservableMap'; export {ApplyMap} from './ApplyMap'; export {FilteredMap} from './FilteredMap'; export {JoinedMap} from './JoinedMap'; From 216c5da3865a6e1fc68219d7b915a8c7e93c8751 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 22 Aug 2022 21:59:33 +0530 Subject: [PATCH 214/354] Implement forced logout --- src/domain/ForcedLogoutViewModel.ts | 55 +++++++++++++++++++++++++ src/domain/RootViewModel.js | 19 +++++++++ src/domain/navigation/index.ts | 5 ++- src/matrix/Client.js | 8 ++++ src/platform/web/ui/ForcedLogoutView.js | 45 ++++++++++++++++++++ src/platform/web/ui/RootView.js | 3 ++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/domain/ForcedLogoutViewModel.ts create mode 100644 src/platform/web/ui/ForcedLogoutView.js diff --git a/src/domain/ForcedLogoutViewModel.ts b/src/domain/ForcedLogoutViewModel.ts new file mode 100644 index 00000000..87c71418 --- /dev/null +++ b/src/domain/ForcedLogoutViewModel.ts @@ -0,0 +1,55 @@ +/* +Copyright 2021 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 {Options as BaseOptions, ViewModel} from "./ViewModel"; +import {Client} from "../matrix/Client.js"; +import {SegmentType} from "./navigation/index"; + +type Options = { sessionId: string; } & BaseOptions; + +export class ForcedLogoutViewModel extends ViewModel { + private _sessionId: string; + private _error?: Error; + private _logoutPromise: Promise; + + constructor(options: Options) { + super(options); + this._sessionId = options.sessionId; + this._logoutPromise = this.forceLogout(); + } + + async forceLogout(): Promise { + try { + const client = new Client(this.platform); + await client.startForcedLogout(this._sessionId); + } + catch (err) { + this._error = err; + this.emitChange("error"); + } + } + + async proceed(): Promise { + await this._logoutPromise; + this.navigation.push("session", true); + } + + get error(): string | undefined { + if (this._error) { + return this.i18n`Could not log out of device: ${this._error.message}`; + } + } +} diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 4094d864..c13f7ce6 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -19,6 +19,7 @@ import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel"; import {LogoutViewModel} from "./LogoutViewModel"; +import {ForcedLogoutViewModel} from "./ForcedLogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel"; @@ -30,6 +31,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel = null; this._loginViewModel = null; this._logoutViewModel = null; + this._forcedLogoutViewModel = null; this._sessionViewModel = null; this._pendingClient = null; } @@ -38,12 +40,14 @@ export class RootViewModel extends ViewModel { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); + this.track(this.navigation.observe("forced-logout").subscribe(() => this._applyNavigation())); this._applyNavigation(true); } async _applyNavigation(shouldRestoreLastUrl) { const isLogin = this.navigation.path.get("login"); const logoutSessionId = this.navigation.path.get("logout")?.value; + const forcedLogoutSessionId = this.navigation.path.get("forced-logout")?.value; const sessionId = this.navigation.path.get("session")?.value; const loginToken = this.navigation.path.get("sso")?.value; if (isLogin) { @@ -54,6 +58,10 @@ export class RootViewModel extends ViewModel { if (this.activeSection !== "logout") { this._showLogout(logoutSessionId); } + } else if (forcedLogoutSessionId) { + if (this.activeSection !== "forced-logout") { + this._showForcedLogout(forcedLogoutSessionId); + } } else if (sessionId === true) { if (this.activeSection !== "picker") { this._showPicker(); @@ -136,6 +144,12 @@ export class RootViewModel extends ViewModel { }); } + _showForcedLogout(sessionId) { + this._setSection(() => { + this._forcedLogoutViewModel = new ForcedLogoutViewModel(this.childOptions({sessionId})); + }); + } + _showSession(client) { this._setSection(() => { this._sessionViewModel = new SessionViewModel(this.childOptions({client})); @@ -164,6 +178,8 @@ export class RootViewModel extends ViewModel { return "login"; } else if (this._logoutViewModel) { return "logout"; + } else if (this._forcedLogoutViewModel) { + return "forced-logout"; } else if (this._sessionPickerViewModel) { return "picker"; } else if (this._sessionLoadViewModel) { @@ -180,6 +196,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel); this._loginViewModel = this.disposeTracked(this._loginViewModel); this._logoutViewModel = this.disposeTracked(this._logoutViewModel); + this._forcedLogoutViewModel = this.disposeTracked(this._forcedLogoutViewModel); this._sessionViewModel = this.disposeTracked(this._sessionViewModel); // now set it again setter(); @@ -187,6 +204,7 @@ export class RootViewModel extends ViewModel { this._sessionLoadViewModel && this.track(this._sessionLoadViewModel); this._loginViewModel && this.track(this._loginViewModel); this._logoutViewModel && this.track(this._logoutViewModel); + this._forcedLogoutViewModel && this.track(this._forcedLogoutViewModel); this._sessionViewModel && this.track(this._sessionViewModel); this.emitChange("activeSection"); } @@ -195,6 +213,7 @@ export class RootViewModel extends ViewModel { get sessionViewModel() { return this._sessionViewModel; } get loginViewModel() { return this._loginViewModel; } get logoutViewModel() { return this._logoutViewModel; } + get forcedLogoutViewModel() { return this._forcedLogoutViewModel; } get sessionPickerViewModel() { return this._sessionPickerViewModel; } get sessionLoadViewModel() { return this._sessionLoadViewModel; } } diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index afba0d86..c1fbee13 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -23,6 +23,7 @@ export type SegmentType = { "session": string | boolean; "sso": string; "logout": true; + "forced-logout": true; "room": string; "rooms": string[]; "settings": true; @@ -48,7 +49,9 @@ function allowsChild(parent: Segment | undefined, child: Segment { + this._sessionId = sessionId; + log.set("id", this._sessionId); + await this.deleteSession(log); + }); + } + dispose() { if (this._reconnectSubscription) { this._reconnectSubscription(); diff --git a/src/platform/web/ui/ForcedLogoutView.js b/src/platform/web/ui/ForcedLogoutView.js new file mode 100644 index 00000000..fe18dac3 --- /dev/null +++ b/src/platform/web/ui/ForcedLogoutView.js @@ -0,0 +1,45 @@ +/* +Copyright 2020 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 {TemplateView, InlineTemplateView} from "./general/TemplateView"; + +export class ForcedLogoutView extends TemplateView { + render(t, vm) { + const proceedView = new InlineTemplateView(vm, t => { + return t.div([ + t.p("Your access token is no longer valid! You can reauthenticate in the next screen."), + t.div({ className: "button-row" }, [ + t.button({ + className: "button-action primary", + type: "submit", + onClick: () => vm.proceed(), + }, vm.i18n`Proceed`) + ]), + ]); + }); + const progressView = new InlineTemplateView(vm, t => { + return t.p({className: "status"}, [ t.span(vm => vm.error) ]); + }); + + return t.div({className: "LogoutScreen"}, [ + t.div({className: "content"}, + t.mapView(vm => vm.error, error => { + return error? progressView: proceedView; + }) + ), + ]); + } +} diff --git a/src/platform/web/ui/RootView.js b/src/platform/web/ui/RootView.js index 69b327a5..1db5c334 100644 --- a/src/platform/web/ui/RootView.js +++ b/src/platform/web/ui/RootView.js @@ -17,6 +17,7 @@ limitations under the License. import {SessionView} from "./session/SessionView.js"; import {LoginView} from "./login/LoginView"; import {LogoutView} from "./LogoutView.js"; +import {ForcedLogoutView} from "./ForcedLogoutView.js"; import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionPickerView} from "./login/SessionPickerView.js"; import {TemplateView} from "./general/TemplateView"; @@ -39,6 +40,8 @@ export class RootView extends TemplateView { return new LoginView(vm.loginViewModel); case "logout": return new LogoutView(vm.logoutViewModel); + case "forced-logout": + return new ForcedLogoutView(vm.forcedLogoutViewModel); case "picker": return new SessionPickerView(vm.sessionPickerViewModel); case "redirecting": From 0f2b7a1ce9111ff29acc46f788dbfb874b5ac21a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 23 Aug 2022 21:58:35 +0530 Subject: [PATCH 215/354] Navigate to UI when sync fails --- src/domain/session/SessionViewModel.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index a67df3a7..3bbcc08a 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -28,6 +28,7 @@ import {CreateRoomViewModel} from "./CreateRoomViewModel.js"; import {ViewModel} from "../ViewModel"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; +import {SyncStatus} from "../../matrix/Sync.js"; export class SessionViewModel extends ViewModel { constructor(options) { @@ -45,6 +46,7 @@ export class SessionViewModel extends ViewModel { this._gridViewModel = null; this._createRoomViewModel = null; this._setupNavigation(); + this._setupForcedLogoutOnAccessTokenInvalidation(); } _setupNavigation() { @@ -93,6 +95,18 @@ export class SessionViewModel extends ViewModel { this._updateRightPanel(); } + _setupForcedLogoutOnAccessTokenInvalidation() { + this._client.sync.status.subscribe(status => { + if (status === SyncStatus.Stopped) { + const error = this._client.sync.error; + if (error.errcode === "M_UNKNOWN_TOKEN") { + // Access token is no longer valid, so force the user to log out + this.navigation.push("forced-logout", this.id); + } + } + }); + } + get id() { return this._client.sessionId; } From 55218624984956ff8f72a2c764e9ca0ac1b27872 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 24 Aug 2022 16:03:00 +0530 Subject: [PATCH 216/354] Use forced segment inside logout --- src/domain/RootViewModel.js | 12 ++++++------ src/domain/navigation/index.ts | 8 ++++---- src/domain/session/SessionViewModel.js | 7 ++++++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index c13f7ce6..8e74244b 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -40,28 +40,28 @@ export class RootViewModel extends ViewModel { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); - this.track(this.navigation.observe("forced-logout").subscribe(() => this._applyNavigation())); + this.track(this.navigation.observe("logout").subscribe(() => this._applyNavigation())); this._applyNavigation(true); } async _applyNavigation(shouldRestoreLastUrl) { const isLogin = this.navigation.path.get("login"); const logoutSessionId = this.navigation.path.get("logout")?.value; - const forcedLogoutSessionId = this.navigation.path.get("forced-logout")?.value; + const isForcedLogout = this.navigation.path.get("forced")?.value; const sessionId = this.navigation.path.get("session")?.value; const loginToken = this.navigation.path.get("sso")?.value; if (isLogin) { if (this.activeSection !== "login") { this._showLogin(); } + } else if (logoutSessionId && isForcedLogout) { + if (this.activeSection !== "forced-logout") { + this._showForcedLogout(logoutSessionId); + } } else if (logoutSessionId) { if (this.activeSection !== "logout") { this._showLogout(logoutSessionId); } - } else if (forcedLogoutSessionId) { - if (this.activeSection !== "forced-logout") { - this._showForcedLogout(forcedLogoutSessionId); - } } else if (sessionId === true) { if (this.activeSection !== "picker") { this._showPicker(); diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index c1fbee13..849d870a 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -23,7 +23,7 @@ export type SegmentType = { "session": string | boolean; "sso": string; "logout": true; - "forced-logout": true; + "forced": true; "room": string; "rooms": string[]; "settings": true; @@ -49,9 +49,7 @@ function allowsChild(parent: Segment | undefined, child: Segment | undefined, child: Segment Date: Wed, 24 Aug 2022 16:03:21 +0530 Subject: [PATCH 217/354] error prop may not always exist --- src/domain/session/SessionViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index bfd0251a..3373dbab 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -99,7 +99,7 @@ export class SessionViewModel extends ViewModel { this._client.sync.status.subscribe(status => { if (status === SyncStatus.Stopped) { const error = this._client.sync.error; - if (error.errcode === "M_UNKNOWN_TOKEN") { + if (error?.errcode === "M_UNKNOWN_TOKEN") { // Access token is no longer valid, so force the user to log out const segments = [ this.navigation.segment("logout", this.id), From 205ecdc52eb6a427b437a5893a8abbf64d3fbc2a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 24 Aug 2022 19:31:38 +0530 Subject: [PATCH 218/354] Show error, progress in view --- src/domain/ForcedLogoutViewModel.ts | 31 ++++++++++++++++++++----- src/platform/web/ui/ForcedLogoutView.js | 7 +++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/domain/ForcedLogoutViewModel.ts b/src/domain/ForcedLogoutViewModel.ts index 87c71418..5b7f84ef 100644 --- a/src/domain/ForcedLogoutViewModel.ts +++ b/src/domain/ForcedLogoutViewModel.ts @@ -24,6 +24,8 @@ export class ForcedLogoutViewModel extends ViewModel { private _sessionId: string; private _error?: Error; private _logoutPromise: Promise; + private _showStatus: boolean = false; + private _showSpinner: boolean = false; constructor(options: Options) { super(options); @@ -38,18 +40,35 @@ export class ForcedLogoutViewModel extends ViewModel { } catch (err) { this._error = err; + this._showSpinner = false; + this._showStatus = true; this.emitChange("error"); } } async proceed(): Promise { + this._showSpinner = true; + this._showStatus = true; + this.emitChange("showStatus"); await this._logoutPromise; - this.navigation.push("session", true); - } - - get error(): string | undefined { - if (this._error) { - return this.i18n`Could not log out of device: ${this._error.message}`; + if (!this._error) { + this.navigation.push("session", true); } } + + get status(): string { + if (this._error) { + return this.i18n`Could not log out of device: ${this._error.message}`; + } else { + return this.i18n`Logging out… Please don't close the app.`; + } + } + + get showStatus(): boolean { + return this._showStatus; + } + + get showSpinner(): boolean { + return this._showSpinner; + } } diff --git a/src/platform/web/ui/ForcedLogoutView.js b/src/platform/web/ui/ForcedLogoutView.js index fe18dac3..9add5504 100644 --- a/src/platform/web/ui/ForcedLogoutView.js +++ b/src/platform/web/ui/ForcedLogoutView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {TemplateView, InlineTemplateView} from "./general/TemplateView"; +import {spinner} from "./common.js"; export class ForcedLogoutView extends TemplateView { render(t, vm) { @@ -31,13 +32,13 @@ export class ForcedLogoutView extends TemplateView { ]); }); const progressView = new InlineTemplateView(vm, t => { - return t.p({className: "status"}, [ t.span(vm => vm.error) ]); + return t.p({className: "status"}, [spinner(t, {hidden: vm => !vm.showSpinner}), t.span(vm => vm.status)]); }); return t.div({className: "LogoutScreen"}, [ t.div({className: "content"}, - t.mapView(vm => vm.error, error => { - return error? progressView: proceedView; + t.mapView(vm => vm.showStatus, showStatus => { + return showStatus? progressView: proceedView; }) ), ]); From f718034f2b495bdd217e06a639c007bbbc9bbc30 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 24 Aug 2022 19:40:11 +0530 Subject: [PATCH 219/354] Track the subscription --- src/domain/session/SessionViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 3373dbab..7226dbdf 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -96,7 +96,7 @@ export class SessionViewModel extends ViewModel { } _setupForcedLogoutOnAccessTokenInvalidation() { - this._client.sync.status.subscribe(status => { + this.track(this._client.sync.status.subscribe(status => { if (status === SyncStatus.Stopped) { const error = this._client.sync.error; if (error?.errcode === "M_UNKNOWN_TOKEN") { @@ -109,7 +109,7 @@ export class SessionViewModel extends ViewModel { this.navigation.applyPath(path); } } - }); + })); } get id() { From d025c1111e98ac481dfdf685ed54b2ca9a30e09c Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 25 Aug 2022 22:03:46 -0400 Subject: [PATCH 220/354] fixes [Symbol.iterator] typing --- src/observable/list/BaseMappedList.ts | 3 +-- src/observable/list/ConcatList.ts | 6 ++--- src/observable/list/ObservableArray.ts | 3 +-- src/observable/list/SortedArray.ts | 31 +++++++++----------------- src/observable/map/ApplyMap.ts | 7 +++--- src/observable/map/FilteredMap.ts | 8 +++---- src/observable/map/LogMap.ts | 3 +-- src/observable/map/MappedMap.ts | 11 +++++---- src/observable/map/ObservableMap.ts | 2 +- 9 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/observable/list/BaseMappedList.ts b/src/observable/list/BaseMappedList.ts index 0435a760..8646153e 100644 --- a/src/observable/list/BaseMappedList.ts +++ b/src/observable/list/BaseMappedList.ts @@ -45,8 +45,7 @@ export class BaseMappedList extends BaseObservableList { return this._mappedValues!.length; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): IterableIterator { return this._mappedValues!.values(); } } diff --git a/src/observable/list/ConcatList.ts b/src/observable/list/ConcatList.ts index aaad5a6a..80accb81 100644 --- a/src/observable/list/ConcatList.ts +++ b/src/observable/list/ConcatList.ts @@ -86,13 +86,11 @@ export class ConcatList extends BaseObservableList implements IListObserve return len; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): Iterator { let sourceListIdx = 0; let it = this._sourceLists[0][Symbol.iterator](); return { - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - next: () => { + next: (): IteratorResult => { let result = it.next(); while (result.done) { sourceListIdx += 1; diff --git a/src/observable/list/ObservableArray.ts b/src/observable/list/ObservableArray.ts index 662f715e..1b962e81 100644 --- a/src/observable/list/ObservableArray.ts +++ b/src/observable/list/ObservableArray.ts @@ -75,8 +75,7 @@ export class ObservableArray extends BaseObservableList { return this._items.length; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): IterableIterator { return this._items.values(); } } diff --git a/src/observable/list/SortedArray.ts b/src/observable/list/SortedArray.ts index c956f7b8..e4723db1 100644 --- a/src/observable/list/SortedArray.ts +++ b/src/observable/list/SortedArray.ts @@ -112,40 +112,31 @@ export class SortedArray extends BaseObservableList { return this._items.length; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): Iterator { return new Iterator(this); } } // iterator that works even if the current value is removed while iterating class Iterator { - private _sortedArray: SortedArray | null - private _current: T | null | undefined + private _sortedArray: SortedArray; + private _current: T | null | undefined; + private _consumed: boolean = false; constructor(sortedArray: SortedArray) { this._sortedArray = sortedArray; this._current = null; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - next() { - if (this._sortedArray) { - if (this._current) { - this._current = this._sortedArray._getNext(this._current); - } else { - this._current = this._sortedArray.get(0); - } - if (this._current) { - return {value: this._current}; - } else { - // cause done below - this._sortedArray = null; - } + next(): IteratorResult { + if (this._consumed) { + return {value: undefined, done: true}; } - if (!this._sortedArray) { - return {done: true}; + this._current = this._current? this._sortedArray._getNext(this._current): this._sortedArray.get(0); + if (!this._current) { + this._consumed = true; } + return { value: this._current, done: this._consumed } as IteratorResult; } } diff --git a/src/observable/map/ApplyMap.ts b/src/observable/map/ApplyMap.ts index a13cf757..0c4962c8 100644 --- a/src/observable/map/ApplyMap.ts +++ b/src/observable/map/ApplyMap.ts @@ -79,7 +79,9 @@ export class ApplyMap extends BaseObservableMap { onUnsubscribeLast(): void { super.onUnsubscribeLast(); - if (this._subscription) this._subscription = this._subscription(); + if (this._subscription) { + this._subscription = this._subscription(); + } } onReset(): void { @@ -89,8 +91,7 @@ export class ApplyMap extends BaseObservableMap { this.emitReset(); } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): Iterator<[K, V]> { return this._source[Symbol.iterator](); } diff --git a/src/observable/map/FilteredMap.ts b/src/observable/map/FilteredMap.ts index 1fd7d89a..c97bc48a 100644 --- a/src/observable/map/FilteredMap.ts +++ b/src/observable/map/FilteredMap.ts @@ -142,8 +142,7 @@ export class FilteredMap extends BaseObservableMap { this.emitReset(); } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): FilterIterator { return new FilterIterator(this._source, this._included); } @@ -157,7 +156,7 @@ export class FilteredMap extends BaseObservableMap { return count; } - get(key): V | undefined{ + get(key: K): V | undefined { const value = this._source.get(key); if (value && this._filter(value, key)) { return value; @@ -173,8 +172,7 @@ class FilterIterator { this._sourceIterator = map[Symbol.iterator](); } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - next() { + next(): IteratorResult<[K, V]> { // eslint-disable-next-line no-constant-condition while (true) { const sourceResult = this._sourceIterator.next(); diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts index 60ac7721..ce9d343e 100644 --- a/src/observable/map/LogMap.ts +++ b/src/observable/map/LogMap.ts @@ -72,8 +72,7 @@ export class LogMap extends BaseObservableMap { this.emitReset(); } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): Iterator<[K, V]> { return this._source[Symbol.iterator](); } diff --git a/src/observable/map/MappedMap.ts b/src/observable/map/MappedMap.ts index 0d2c47c0..6ada079f 100644 --- a/src/observable/map/MappedMap.ts +++ b/src/observable/map/MappedMap.ts @@ -64,7 +64,9 @@ export class MappedMap extends BaseObservableMap { onRemove(key: K/*, _value*/): void { const mappedValue = this._mappedValues.get(key); if (this._mappedValues.delete(key)) { - if (mappedValue) this.emitRemove(key, mappedValue); + if (mappedValue) { + this.emitRemove(key, mappedValue); + } } } @@ -93,7 +95,9 @@ export class MappedMap extends BaseObservableMap { onUnsubscribeLast(): void { super.onUnsubscribeLast(); - if (this._subscription) this._subscription = this._subscription(); + if (this._subscription) { + this._subscription = this._subscription(); + } this._mappedValues.clear(); } @@ -102,8 +106,7 @@ export class MappedMap extends BaseObservableMap { this.emitReset(); } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - [Symbol.iterator]() { + [Symbol.iterator](): IterableIterator<[K, MappedV]> { return this._mappedValues.entries(); } diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index c5ffe397..f0d4c77a 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -97,7 +97,7 @@ export class ObservableMap extends BaseObservableMap { keys(): Iterator { return this._values.keys(); } -}; +} // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { From 1f139f17e99153c36affea9acc548ecf7fe21a1c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 26 Aug 2022 14:43:41 +0530 Subject: [PATCH 221/354] Add explaining comments --- src/domain/ForcedLogoutViewModel.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/domain/ForcedLogoutViewModel.ts b/src/domain/ForcedLogoutViewModel.ts index 5b7f84ef..0659f803 100644 --- a/src/domain/ForcedLogoutViewModel.ts +++ b/src/domain/ForcedLogoutViewModel.ts @@ -30,6 +30,7 @@ export class ForcedLogoutViewModel extends ViewModel { constructor(options: Options) { super(options); this._sessionId = options.sessionId; + // Start the logout process immediately without any user interaction this._logoutPromise = this.forceLogout(); } @@ -40,6 +41,7 @@ export class ForcedLogoutViewModel extends ViewModel { } catch (err) { this._error = err; + // Show the error in the UI this._showSpinner = false; this._showStatus = true; this.emitChange("error"); @@ -47,10 +49,15 @@ export class ForcedLogoutViewModel extends ViewModel { } async proceed(): Promise { + /** + * The logout should already be completed because we started it from the ctor. + * In case the logout is still proceeding, we will show a message with a spinner. + */ this._showSpinner = true; this._showStatus = true; this.emitChange("showStatus"); await this._logoutPromise; + // At this point, the logout is completed for sure. if (!this._error) { this.navigation.push("session", true); } From b440bdcff069ba5dd1dc1c24be506914dbc448af Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 26 Aug 2022 14:46:15 +0530 Subject: [PATCH 222/354] Redirect to login screen --- src/domain/ForcedLogoutViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/ForcedLogoutViewModel.ts b/src/domain/ForcedLogoutViewModel.ts index 0659f803..19b581ed 100644 --- a/src/domain/ForcedLogoutViewModel.ts +++ b/src/domain/ForcedLogoutViewModel.ts @@ -59,7 +59,7 @@ export class ForcedLogoutViewModel extends ViewModel { await this._logoutPromise; // At this point, the logout is completed for sure. if (!this._error) { - this.navigation.push("session", true); + this.navigation.push("login", true); } } From bba2d9f0a9314d0172f4d7202798502a5573b99f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 26 Aug 2022 14:55:01 +0530 Subject: [PATCH 223/354] Use t.map --- src/platform/web/ui/ForcedLogoutView.js | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/platform/web/ui/ForcedLogoutView.js b/src/platform/web/ui/ForcedLogoutView.js index 9add5504..2c2aab97 100644 --- a/src/platform/web/ui/ForcedLogoutView.js +++ b/src/platform/web/ui/ForcedLogoutView.js @@ -14,31 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView, InlineTemplateView} from "./general/TemplateView"; +import {TemplateView} from "./general/TemplateView"; import {spinner} from "./common.js"; export class ForcedLogoutView extends TemplateView { - render(t, vm) { - const proceedView = new InlineTemplateView(vm, t => { - return t.div([ - t.p("Your access token is no longer valid! You can reauthenticate in the next screen."), - t.div({ className: "button-row" }, [ - t.button({ - className: "button-action primary", - type: "submit", - onClick: () => vm.proceed(), - }, vm.i18n`Proceed`) - ]), - ]); - }); - const progressView = new InlineTemplateView(vm, t => { - return t.p({className: "status"}, [spinner(t, {hidden: vm => !vm.showSpinner}), t.span(vm => vm.status)]); - }); - - return t.div({className: "LogoutScreen"}, [ - t.div({className: "content"}, - t.mapView(vm => vm.showStatus, showStatus => { - return showStatus? progressView: proceedView; + render(t) { + return t.div({ className: "LogoutScreen" }, [ + t.div({ className: "content" }, + t.map(vm => vm.showStatus, (showStatus, t, vm) => { + if (showStatus) { + return t.p({ className: "status" }, [ + spinner(t, { hidden: vm => !vm.showSpinner }), + t.span(vm => vm.status) + ]); + } + else { + return t.div([ + t.p("Your access token is no longer valid! You can reauthenticate in the next screen."), + t.div({ className: "button-row" }, [ + t.button({ + className: "button-action primary", + type: "submit", + onClick: () => vm.proceed(), + }, vm.i18n`Proceed`) + ]), + ]); + } }) ), ]); From 5f9cfffa3b547991b665f57a8bf715270a1b2ef1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 26 Aug 2022 20:08:44 +0200 Subject: [PATCH 224/354] release v0.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f49ee21b..2143ebd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.1", + "version": "0.3.2", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From b6f718d4a28e8c3e1ec3b72dc2d046f4ba6120ef Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 29 Aug 2022 11:19:57 +0530 Subject: [PATCH 225/354] Install cypress as dev dependency --- package.json | 1 + yarn.lock | 913 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 906 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 2143ebd0..f7fb040f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "aes-js": "^3.1.2", "bs58": "^4.0.1", "core-js": "^3.6.5", + "cypress": "^10.6.0", "es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush", "escodegen": "^2.0.0", "eslint": "^7.32.0", diff --git a/yarn.lock b/yarn.lock index c48d2719..994657f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,43 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cypress/request@^2.88.10": + version "2.88.10" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" + integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + http-signature "~1.3.6" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -87,6 +124,33 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/node@*": + version "18.7.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.13.tgz#23e6c5168333480d454243378b69e861ab5c011a" + integrity sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw== + +"@types/node@^14.14.31": + version "14.18.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.26.tgz#239e19f8b4ea1a9eb710528061c1d733dc561996" + integrity sha512-0b+utRBSYj8L7XAp0d+DX7lI4cSmowNaaTkk6/1SKzbKkG+doLuPusB9EOvzLJ8ahJSk03bTLIL6cWaEd4dBKA== + +"@types/sinonjs__fake-timers@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== + +"@types/sizzle@^2.3.2": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" @@ -181,6 +245,14 @@ aes-js@^3.1.2: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -211,11 +283,23 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -230,6 +314,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -242,11 +331,48 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -269,6 +395,28 @@ base64-arraybuffer@^0.2.0: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -296,11 +444,34 @@ bs58@^4.0.1: dependencies: base-x "^3.0.2" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +cachedir@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -318,6 +489,53 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + +ci-info@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" + integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@~0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -342,11 +560,28 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.16: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + colors@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + commander@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -357,6 +592,11 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -372,7 +612,12 @@ core-js@^3.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.3.tgz#6df8142a996337503019ff3235a7022d7cdf4559" integrity sha512-LeLBMgEGSsG7giquSzvgBrTS7V5UL6ks3eQlUSbN8dJStlLFiRzUm5iqsRyzUB8carhfKjkJ2vzKqE6z1Vga9g== -cross-spawn@^7.0.2: +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -417,6 +662,73 @@ cuint@^0.2.2: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= +cypress@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.6.0.tgz#13f46867febf2c3715874ed5dce9c2e946b175fe" + integrity sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA== + dependencies: + "@cypress/request" "^2.88.10" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "8.1.1" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + buffer "^5.6.0" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.1" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.2" + enquirer "^2.3.6" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.6" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + proxy-from-env "1.0.0" + request-progress "^3.0.0" + semver "^7.3.2" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +dayjs@^1.10.4: + version "1.11.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" + integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -431,6 +743,13 @@ debug@^4.3.1: dependencies: ms "2.1.2" +debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -441,6 +760,11 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -502,12 +826,27 @@ domutils@^2.6.0: domelementtype "^2.2.0" domhandler "^4.2.0" +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -enquirer@^2.3.5: +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -907,11 +1246,59 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -extend@^3.0.1: +eventemitter2@^6.4.3: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== + +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +extend@^3.0.1, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + fake-indexeddb@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.2.tgz#8073a12ed3b254f7afc064f3cc2629f0110a5303" @@ -953,6 +1340,20 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -980,6 +1381,30 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1000,6 +1425,27 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1019,6 +1465,13 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + globals@^13.6.0: version "13.8.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" @@ -1045,6 +1498,11 @@ globby@^11.0.3: merge2 "^1.3.0" slash "^3.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1067,6 +1525,25 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -1099,6 +1576,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1112,6 +1594,18 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +is-ci@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -1136,21 +1630,54 @@ is-glob@^4.0.0, is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1164,6 +1691,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1174,11 +1706,45 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1195,6 +1761,20 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -1205,16 +1785,39 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.7.0: +lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1239,6 +1842,11 @@ merge-options@^3.0.4: dependencies: is-plain-obj "^2.1.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1252,6 +1860,23 @@ micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -1259,11 +1884,21 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanoid@^3.3.3: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -1282,6 +1917,13 @@ node-html-parser@^4.0.0: css-select "^4.1.3" he "1.2.0" +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nth-check@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" @@ -1296,13 +1938,20 @@ off-color@^2.0.0: dependencies: core-js "^3.6.5" -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -1327,6 +1976,18 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1339,7 +2000,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -1354,6 +2015,16 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -1364,6 +2035,11 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + postcss-css-variables@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93" @@ -1402,16 +2078,44 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -1437,6 +2141,13 @@ regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== + dependencies: + throttleit "^1.0.0" + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -1456,12 +2167,25 @@ resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -1482,11 +2206,23 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.0.1: +rxjs@^7.5.1: + version "7.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" + integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + dependencies: + tslib "^2.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^7.2.1, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -1494,6 +2230,13 @@ semver@^7.2.1, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.3.2: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -1511,11 +2254,25 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -1540,11 +2297,35 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sshpk@^1.14.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1561,6 +2342,18 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -1580,6 +2373,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -1620,6 +2420,23 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -1627,6 +2444,14 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tr46@^2.0.2: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -1639,6 +2464,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -1646,6 +2476,18 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1665,6 +2507,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + typescript@^4.7.0: version "4.7.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" @@ -1689,6 +2536,16 @@ typeson@^6.0.0: resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -1696,11 +2553,25 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + vite@^2.9.8: version "2.9.8" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.8.tgz#2c2cb0790beb0fbe4b8c0995b80fe691a91c2545" @@ -1744,6 +2615,24 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -1760,3 +2649,11 @@ yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" From badfc84eb8abaf17de51c69ea832e77a2a1ef71a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 29 Aug 2022 14:26:13 +0530 Subject: [PATCH 226/354] Write initial passing test --- cypress.config.ts | 8 ++++++++ cypress/e2e/startup.cy.ts | 22 ++++++++++++++++++++++ cypress/tsconfig.json | 8 ++++++++ package.json | 3 ++- 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 cypress.config.ts create mode 100644 cypress/e2e/startup.cy.ts create mode 100644 cypress/tsconfig.json diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000..fe5b673f --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + baseUrl: "http://localhost:3000", + supportFile: false, + }, +}); diff --git a/cypress/e2e/startup.cy.ts b/cypress/e2e/startup.cy.ts new file mode 100644 index 00000000..e7bf9c2c --- /dev/null +++ b/cypress/e2e/startup.cy.ts @@ -0,0 +1,22 @@ +/* +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. +*/ + +describe("App has no startup errors that prevent UI render", () => { + it("passes", () => { + cy.visit("/"); + cy.contains("Log In"); + }) +}) diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000..18edb199 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/package.json b/package.json index f7fb040f..fbba3b2e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "start": "vite --port 3000", "build": "vite build && ./scripts/cleanup.sh", "build:sdk": "./scripts/sdk/build.sh", - "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch" + "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch", + "test:app": "vite --port 3000 & yarn run cypress open" }, "repository": { "type": "git", From 9f53962f552099ffa17583a779aa7a170b095abb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 30 Aug 2022 11:39:39 +0530 Subject: [PATCH 227/354] Port plugin from eleweb + write test for login --- .gitignore | 1 + cypress.config.ts | 11 +- cypress/e2e/login.cy.ts | 39 ++++ cypress/plugins/docker/index.ts | 138 +++++++++++++ cypress/plugins/index.ts | 36 ++++ cypress/plugins/log.ts | 35 ++++ cypress/plugins/performance.ts | 47 +++++ cypress/plugins/synapsedocker/index.ts | 187 ++++++++++++++++++ .../synapsedocker/templates/COPYME/README.md | 3 + .../templates/COPYME/homeserver.yaml | 72 +++++++ .../synapsedocker/templates/COPYME/log.config | 50 +++++ .../synapsedocker/templates/consent/README.md | 1 + .../templates/consent/homeserver.yaml | 84 ++++++++ .../templates/consent/log.config | 50 +++++ .../consent/res/templates/privacy/en/1.0.html | 23 +++ .../res/templates/privacy/en/success.html | 9 + .../synapsedocker/templates/default/README.md | 1 + .../templates/default/homeserver.yaml | 76 +++++++ .../templates/default/log.config | 50 +++++ cypress/plugins/utils/port.ts | 27 +++ cypress/plugins/webserver.ts | 52 +++++ cypress/support/e2e.ts | 19 ++ cypress/support/synapse.ts | 122 ++++++++++++ cypress/tsconfig.json | 1 + 24 files changed, 1130 insertions(+), 4 deletions(-) create mode 100644 cypress/e2e/login.cy.ts create mode 100644 cypress/plugins/docker/index.ts create mode 100644 cypress/plugins/index.ts create mode 100644 cypress/plugins/log.ts create mode 100644 cypress/plugins/performance.ts create mode 100644 cypress/plugins/synapsedocker/index.ts create mode 100644 cypress/plugins/synapsedocker/templates/COPYME/README.md create mode 100644 cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/COPYME/log.config create mode 100644 cypress/plugins/synapsedocker/templates/consent/README.md create mode 100644 cypress/plugins/synapsedocker/templates/consent/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/consent/log.config create mode 100644 cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html create mode 100644 cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html create mode 100644 cypress/plugins/synapsedocker/templates/default/README.md create mode 100644 cypress/plugins/synapsedocker/templates/default/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/default/log.config create mode 100644 cypress/plugins/utils/port.ts create mode 100644 cypress/plugins/webserver.ts create mode 100644 cypress/support/e2e.ts create mode 100644 cypress/support/synapse.ts diff --git a/.gitignore b/.gitignore index 78f9f348..b0714d24 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ lib *.tar.gz .eslintcache .tmp +cypress/videos diff --git a/cypress.config.ts b/cypress.config.ts index fe5b673f..6761d05d 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,8 +1,11 @@ import { defineConfig } from "cypress"; export default defineConfig({ - e2e: { - baseUrl: "http://localhost:3000", - supportFile: false, - }, + e2e: { + setupNodeEvents(on, config) { + require("./cypress/plugins/index.ts").default(on, config); + return config; + }, + baseUrl: "http://localhost:3000", + }, }); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts new file mode 100644 index 00000000..36badebf --- /dev/null +++ b/cypress/e2e/login.cy.ts @@ -0,0 +1,39 @@ +/* +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 type { SynapseInstance } from "../plugins/synapsedocker"; + +describe("Login", () => { + let synapse: SynapseInstance; + + beforeEach(() => { + cy.startSynapse("consent").then((data) => { + synapse = data; + }); + }); + + it("Login using username/password", () => { + const username = "foobaraccount"; + const password = "password123"; + cy.registerUser(synapse, username, password); + cy.visit("/"); + cy.get("#homeserver").clear().type(synapse.baseUrl); + cy.get("#username").clear().type(username); + cy.get("#password").clear().type(password); + cy.contains("Log In").click(); + cy.get(".SessionView"); + }); +}); + diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts new file mode 100644 index 00000000..024cb9d4 --- /dev/null +++ b/cypress/plugins/docker/index.ts @@ -0,0 +1,138 @@ +/* +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 * as os from "os"; +import * as childProcess from "child_process"; +import * as fse from "fs-extra"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; + +// A cypress plugin to run docker commands + +export function dockerRun(args: { + image: string; + containerName: string; + params?: string[]; +}): Promise { + const userInfo = os.userInfo(); + const params = args.params ?? []; + + if (userInfo.uid >= 0) { + // On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult + params.push("-u", `${userInfo.uid}:${userInfo.gid}`); + } + + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "run", + "--name", args.containerName, + "-d", + ...params, + args.image, + "run", + ], (err, stdout) => { + console.log("error", err, "stdout", stdout); + if (err) reject(err); + resolve(stdout.trim()); + }); + }); +} + +export function dockerExec(args: { + containerId: string; + params: string[]; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", [ + "exec", args.containerId, + ...args.params, + ], { encoding: 'utf8' }, (err, stdout, stderr) => { + if (err) { + console.log(stdout); + console.log(stderr); + reject(err); + return; + } + resolve(); + }); + }); +} + +export async function dockerLogs(args: { + containerId: string; + stdoutFile?: string; + stderrFile?: string; +}): Promise { + const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore"; + const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore"; + + await new Promise((resolve) => { + childProcess.spawn("docker", [ + "logs", + args.containerId, + ], { + stdio: ["ignore", stdoutFile, stderrFile], + }).once('close', resolve); + }); + + if (args.stdoutFile) await fse.close(stdoutFile); + if (args.stderrFile) await fse.close(stderrFile); +} + +export function dockerStop(args: { + containerId: string; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "stop", + args.containerId, + ], err => { + if (err) reject(err); + resolve(); + }); + }); +} + +export function dockerRm(args: { + containerId: string; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "rm", + args.containerId, + ], err => { + if (err) reject(err); + resolve(); + }); + }); +} + +/** + * @type {Cypress.PluginConfig} + */ +export function docker(on: PluginEvents, config: PluginConfigOptions) { + console.log("Code gets to here!"); + on("task", { + dockerRun, + dockerExec, + dockerLogs, + dockerStop, + dockerRm, + }); +} diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts new file mode 100644 index 00000000..44dd93b8 --- /dev/null +++ b/cypress/plugins/index.ts @@ -0,0 +1,36 @@ +/* +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 PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { performance } from "./performance"; +import { synapseDocker } from "./synapsedocker"; +import { webserver } from "./webserver"; +import { docker } from "./docker"; +import { log } from "./log"; + +/** + * @type {Cypress.PluginConfig} + */ +export default function(on: PluginEvents, config: PluginConfigOptions) { + docker(on, config); + performance(on, config); + synapseDocker(on, config); + webserver(on, config); + log(on, config); +} diff --git a/cypress/plugins/log.ts b/cypress/plugins/log.ts new file mode 100644 index 00000000..4b16c9b8 --- /dev/null +++ b/cypress/plugins/log.ts @@ -0,0 +1,35 @@ +/* +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 PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; + +export function log(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + log(message: string) { + console.log(message); + + return null; + }, + table(message: string) { + console.table(message); + + return null; + }, + }); +} diff --git a/cypress/plugins/performance.ts b/cypress/plugins/performance.ts new file mode 100644 index 00000000..c6bd3e4c --- /dev/null +++ b/cypress/plugins/performance.ts @@ -0,0 +1,47 @@ +/* +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 * as path from "path"; +import * as fse from "fs-extra"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; + +// This holds all the performance measurements throughout the run +let bufferedMeasurements: PerformanceEntry[] = []; + +function addMeasurements(measurements: PerformanceEntry[]): void { + bufferedMeasurements = bufferedMeasurements.concat(measurements); + return null; +} + +async function writeMeasurementsFile() { + try { + const measurementsPath = path.join("cypress", "performance", "measurements.json"); + await fse.outputJSON(measurementsPath, bufferedMeasurements, { + spaces: 4, + }); + } finally { + bufferedMeasurements = []; + } +} + +export function performance(on: PluginEvents, config: PluginConfigOptions) { + on("task", { addMeasurements }); + on("after:run", writeMeasurementsFile); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts new file mode 100644 index 00000000..5227b5e4 --- /dev/null +++ b/cypress/plugins/synapsedocker/index.ts @@ -0,0 +1,187 @@ +/* +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 * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; +import * as fse from "fs-extra"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to start & stop synapses in +// docker with preset templates. + +interface SynapseConfig { + configDir: string; + registrationSecret: string; + // Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage + baseUrl: string; + port: number; +} + +export interface SynapseInstance extends SynapseConfig { + synapseId: string; +} + +const synapses = new Map(); + +function randB64Bytes(numBytes: number): string { + return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); +} + +async function cfgDirFromTemplate(template: string): Promise { + const templateDir = path.join(__dirname, "templates", template); + + const stats = await fse.stat(templateDir); + if (!stats?.isDirectory) { + throw new Error(`No such template: ${template}`); + } + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-')); + + // copy the contents of the template dir, omitting homeserver.yaml as we'll template that + console.log(`Copy ${templateDir} -> ${tempDir}`); + await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' }); + + const registrationSecret = randB64Bytes(16); + const macaroonSecret = randB64Bytes(16); + const formSecret = randB64Bytes(16); + + const port = await getFreePort(); + const baseUrl = `http://localhost:${port}`; + + // now copy homeserver.yaml, applying substitutions + console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`); + let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8"); + hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); + hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); + hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); + hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); + await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); + + // now generate a signing key (we could use synapse's config generation for + // this, or we could just do this...) + // NB. This assumes the homeserver.yaml specifies the key in this location + const signingKey = randB64Bytes(32); + console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`); + await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); + + return { + port, + baseUrl, + configDir: tempDir, + registrationSecret, + }; +} + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function synapseStart(template: string): Promise { + const synCfg = await cfgDirFromTemplate(template); + + console.log(`Starting synapse with config dir ${synCfg.configDir}...`); + + const synapseId = await dockerRun({ + image: "matrixdotorg/synapse:develop", + containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`, + params: [ + "--rm", + "-v", `${synCfg.configDir}:/data`, + "-p", `${synCfg.port}:8008/tcp`, + ], + }); + + console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); + + // Await Synapse healthcheck + await dockerExec({ + containerId: synapseId, + params: [ + "curl", + "--connect-timeout", "30", + "--retry", "30", + "--retry-delay", "1", + "--retry-all-errors", + "--silent", + "http://localhost:8008/health", + ], + }); + + const synapse: SynapseInstance = { synapseId, ...synCfg }; + synapses.set(synapseId, synapse); + return synapse; +} + +async function synapseStop(id: string): Promise { + const synCfg = synapses.get(id); + + if (!synCfg) throw new Error("Unknown synapse ID"); + + const synapseLogsPath = path.join("cypress", "synapselogs", id); + await fse.ensureDir(synapseLogsPath); + + await dockerLogs({ + containerId: id, + stdoutFile: path.join(synapseLogsPath, "stdout.log"), + stderrFile: path.join(synapseLogsPath, "stderr.log"), + }); + + await dockerStop({ + containerId: id, + }); + + await fse.remove(synCfg.configDir); + + synapses.delete(id); + + console.log(`Stopped synapse id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + synapseStart, + synapseStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining synapse instances after a spec run + // This is on the theory that we should avoid re-using synapse + // instances between spec runs: they should be cheap enough to + // start that we can have a separate one for each spec run or even + // test. If we accidentally re-use synapses, we could inadvertently + // make our tests depend on each other. + for (const synId of synapses.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await synapseStop(synId); + } + }); + + on("before:run", async () => { + // tidy up old synapse log files before each run + await fse.emptyDir(path.join("cypress", "synapselogs")); + }); +} diff --git a/cypress/plugins/synapsedocker/templates/COPYME/README.md b/cypress/plugins/synapsedocker/templates/COPYME/README.md new file mode 100644 index 00000000..df1ed89e --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/README.md @@ -0,0 +1,3 @@ +# Meta-template for synapse templates + +To make another template, you can copy this directory diff --git a/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml b/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml new file mode 100644 index 00000000..fab1bc1c --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml @@ -0,0 +1,72 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +# XXX: This won't actually be right: it lets docker allocate an ephemeral port, +# so we have a chicken-and-egg problem +public_baseurl: http://localhost:8008/ +# Listener is always port 8008 (configured in the container) +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +# An sqlite in-memory database is fast & automatically wipes each time +database: + name: "sqlite3" + args: + database: ":memory:" + +# Needs to be configured to log to the console like a good docker process +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +# These placeholders will be be replaced with values generated at start +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +# Signing key must be here: it will be generated to this file +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/cypress/plugins/synapsedocker/templates/COPYME/log.config b/cypress/plugins/synapsedocker/templates/COPYME/log.config new file mode 100644 index 00000000..ac232762 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/plugins/synapsedocker/templates/consent/README.md b/cypress/plugins/synapsedocker/templates/consent/README.md new file mode 100644 index 00000000..713e55f9 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/README.md @@ -0,0 +1 @@ +A synapse configured with user privacy consent enabled diff --git a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml new file mode 100644 index 00000000..6decaeb5 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml @@ -0,0 +1,84 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +user_consent: + template_dir: /data/res/templates/privacy + version: 1.0 + server_notice_content: + msgtype: m.text + body: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + send_server_notice_to_guests: True + block_events_error: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + require_at_registration: true + +server_notices: + system_mxid_localpart: notices + system_mxid_display_name: "Server Notices" + system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ" + room_name: "Server Notices" +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/cypress/plugins/synapsedocker/templates/consent/log.config b/cypress/plugins/synapsedocker/templates/consent/log.config new file mode 100644 index 00000000..b9123d0f --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html new file mode 100644 index 00000000..d4959b4b --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html @@ -0,0 +1,23 @@ + + + + Test Privacy policy + + + {% if has_consented %} +

+ Thank you, you've already accepted the license. +

+ {% else %} +

+ Please accept the license! +

+
+ + + + +
+ {% endif %} + + \ No newline at end of file diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 00000000..abe27d87 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + + Test Privacy policy + + +

Danke schon

+ + \ No newline at end of file diff --git a/cypress/plugins/synapsedocker/templates/default/README.md b/cypress/plugins/synapsedocker/templates/default/README.md new file mode 100644 index 00000000..8f6b11f9 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/default/README.md @@ -0,0 +1 @@ +A synapse configured with user privacy consent disabled diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml new file mode 100644 index 00000000..347dadc8 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -0,0 +1,76 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/cypress/plugins/synapsedocker/templates/default/log.config new file mode 100644 index 00000000..b9123d0f --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/default/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/plugins/utils/port.ts b/cypress/plugins/utils/port.ts new file mode 100644 index 00000000..064ccc7c --- /dev/null +++ b/cypress/plugins/utils/port.ts @@ -0,0 +1,27 @@ +/* +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 * as net from "net"; + +export async function getFreePort(): Promise { + return new Promise(resolve => { + const srv = net.createServer(); + srv.listen(0, () => { + const port = (srv.address()).port; + srv.close(() => resolve(port)); + }); + }); +} diff --git a/cypress/plugins/webserver.ts b/cypress/plugins/webserver.ts new file mode 100644 index 00000000..55a25a31 --- /dev/null +++ b/cypress/plugins/webserver.ts @@ -0,0 +1,52 @@ +/* +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 * as http from "http"; +import { AddressInfo } from "net"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; + +const servers: http.Server[] = []; + +function serveHtmlFile(html: string): string { + const server = http.createServer((req, res) => { + res.writeHead(200, { + "Content-Type": "text/html", + }); + res.end(html); + }); + server.listen(); + servers.push(server); + + return `http://localhost:${(server.address() as AddressInfo).port}/`; +} + +function stopWebServers(): null { + for (const server of servers) { + server.close(); + } + servers.splice(0, servers.length); // clear + + return null; // tell cypress we did the task successfully (doesn't allow undefined) +} + +export function webserver(on: PluginEvents, config: PluginConfigOptions) { + on("task", { serveHtmlFile, stopWebServers }); + on("after:run", stopWebServers); +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 00000000..289319fe --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,19 @@ +/* +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 "./synapse"; diff --git a/cypress/support/synapse.ts b/cypress/support/synapse.ts new file mode 100644 index 00000000..5696e8c0 --- /dev/null +++ b/cypress/support/synapse.ts @@ -0,0 +1,122 @@ +/* +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 * as crypto from 'crypto'; + +import Chainable = Cypress.Chainable; +import AUTWindow = Cypress.AUTWindow; +import { SynapseInstance } from "../plugins/synapsedocker"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Start a synapse instance with a given config template. + * @param template path to template within cypress/plugins/synapsedocker/template/ directory. + */ + startSynapse(template: string): Chainable; + + /** + * Custom command wrapping task:synapseStop whilst preventing uncaught exceptions + * for if Synapse stopping races with the app's background sync loop. + * @param synapse the synapse instance returned by startSynapse + */ + stopSynapse(synapse: SynapseInstance): Chainable; + + /** + * Register a user on the given Synapse using the shared registration secret. + * @param synapse the synapse instance returned by startSynapse + * @param username the username of the user to register + * @param password the password of the user to register + * @param displayName optional display name to set on the newly registered user + */ + registerUser( + synapse: SynapseInstance, + username: string, + password: string, + displayName?: string, + ): Chainable; + } + } +} + +function startSynapse(template: string): Chainable { + return cy.task("synapseStart", template); +} + +function stopSynapse(synapse?: SynapseInstance): Chainable { + if (!synapse) return; + // Navigate away from app to stop the background network requests which will race with Synapse shutting down + return cy.window({ log: false }).then((win) => { + win.location.href = 'about:blank'; + cy.task("synapseStop", synapse.synapseId); + }); +} + +interface Credentials { + accessToken: string; + userId: string; + deviceId: string; + homeServer: string; +} + +function registerUser( + synapse: SynapseInstance, + username: string, + password: string, + displayName?: string, +): Chainable { + const url = `${synapse.baseUrl}/_synapse/admin/v1/register`; + return cy.then(() => { + // get a nonce + return cy.request<{ nonce: string }>({ url }); + }).then(response => { + const { nonce } = response.body; + const mac = crypto.createHmac('sha1', synapse.registrationSecret).update( + `${nonce}\0${username}\0${password}\0notadmin`, + ).digest('hex'); + + return cy.request<{ + access_token: string; + user_id: string; + home_server: string; + device_id: string; + }>({ + url, + method: "POST", + body: { + nonce, + username, + password, + mac, + admin: false, + displayname: displayName, + }, + }); + }).then(response => ({ + homeServer: response.body.home_server, + accessToken: response.body.access_token, + userId: response.body.user_id, + deviceId: response.body.device_id, + })); +} + +Cypress.Commands.add("startSynapse", startSynapse); +Cypress.Commands.add("stopSynapse", stopSynapse); +Cypress.Commands.add("registerUser", registerUser); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 18edb199..6bd44918 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "downlevelIteration": true, "target": "es5", "lib": ["es5", "dom"], "types": ["cypress", "node"] From 23912b2c5af172d8fc8fe6d3b977411d114d0da9 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 16:49:23 -0500 Subject: [PATCH 228/354] Remove unused conflicting generic styles Split off from https://github.com/vector-im/hydrogen-web/pull/653 These styles were conflicting with some in the Matrix public archive and instead of adjusting in that project, they seem unused here anyway so we can simplify and remove here. Conflict with the `.room-header-change-dates-button` styles in https://github.com/matrix-org/matrix-public-archive/pull/53 --- src/platform/web/ui/css/room.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/platform/web/ui/css/room.css b/src/platform/web/ui/css/room.css index 746349c8..7dd4fd81 100644 --- a/src/platform/web/ui/css/room.css +++ b/src/platform/web/ui/css/room.css @@ -23,10 +23,6 @@ limitations under the License. flex: 1; } -.middle-header button { - display: block; -} - .middle-header .room-description { flex: 1; min-width: 0; From d98a3e43bf30aba0dbed66b27b33c843987f6ff7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:24:57 +0530 Subject: [PATCH 229/354] Ignore synapse logs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b0714d24..20f09f19 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ lib .eslintcache .tmp cypress/videos +cypress/synapselogs From 193415ac1d4d93875f19a5640d758a140011b266 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:25:28 +0530 Subject: [PATCH 230/354] Add configurable variables --- cypress.config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index 6761d05d..02f39392 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -6,6 +6,12 @@ export default defineConfig({ require("./cypress/plugins/index.ts").default(on, config); return config; }, - baseUrl: "http://localhost:3000", + baseUrl: "http://127.0.0.1:3000", + }, + env: { + SYNAPSE_IP_ADDRESS: "172.18.0.5", + SYNAPSE_PORT: "8008", + DEX_IP_ADDRESS: "172.18.0.4", + DEX_PORT: "5556", }, }); From 2376ef8b8ce9f62ff97bbfd0439beac1158b8508 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:26:45 +0530 Subject: [PATCH 231/354] Write plugin for Dex --- cypress/plugins/dex/index.ts | 132 +++++++++++++++++++++++ cypress/plugins/dex/template/config.yaml | 56 ++++++++++ cypress/plugins/dex/template/dev.db | Bin 0 -> 110592 bytes cypress/plugins/index.ts | 2 + cypress/support/dex.ts | 51 +++++++++ cypress/support/e2e.ts | 1 + 6 files changed, 242 insertions(+) create mode 100644 cypress/plugins/dex/index.ts create mode 100755 cypress/plugins/dex/template/config.yaml create mode 100755 cypress/plugins/dex/template/dev.db create mode 100644 cypress/support/dex.ts diff --git a/cypress/plugins/dex/index.ts b/cypress/plugins/dex/index.ts new file mode 100644 index 00000000..b07a78e8 --- /dev/null +++ b/cypress/plugins/dex/index.ts @@ -0,0 +1,132 @@ +/* +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 * as path from "path"; +import * as os from "os"; +import * as fse from "fs-extra"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import {dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to start & stop dex instances + +interface DexConfig { + configDir: string; + baseUrl: string; + port: number; + host: string; +} + +export interface DexInstance extends DexConfig { + dexId: string; +} + +const dexConfigs = new Map(); +let env; + +async function produceConfigWithSynapseURLAdded(): Promise { + const templateDir = path.join(__dirname, "template"); + + const stats = await fse.stat(templateDir); + if (!stats?.isDirectory) { + throw new Error(`Template directory at ${templateDir} not found!`); + } + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'hydrogen-testing-dex-')); + + // copy the contents of the template dir, omitting config.yaml as we'll template that + console.log(`Copy ${templateDir} -> ${tempDir}`); + await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'config.yaml' }); + + // now copy config.yaml, applying substitutions + console.log(`Gen ${path.join(templateDir, "config.yaml")}`); + let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8"); + const synapseAddress = `${env.SYNAPSE_IP_ADDRESS}:${env.SYNAPSE_PORT}`; + hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress); + const host = env.DEX_IP_ADDRESS; + const port = env.DEX_PORT; + const dexAddress = `${host}:${port}`; + hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress); + await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml); + + const baseUrl = `http://${host}:${port}`; + return { + host, + port, + baseUrl, + configDir: tempDir, + }; +} + +async function dexStart(): Promise { + const dexCfg = await produceConfigWithSynapseURLAdded(); + console.log(`Starting dex with config dir ${dexCfg.configDir}...`); + const dexId = await dockerRun({ + image: "bitnami/dex:latest", + containerName: "dex", + dockerParams: [ + "--rm", + "-v", `${dexCfg.configDir}:/data`, + `--ip=${dexCfg.host}`, + "-p", `${dexCfg.port}:5556/tcp`, + "--network=hydrogen" + ], + applicationParams: [ + "serve", + "data/config.yaml", + ] + }); + + console.log(`Started dex with id ${dexId} on port ${dexCfg.port}.`); + + const dex: DexInstance = { dexId, ...dexCfg }; + dexConfigs.set(dexId, dex); + return dex; +} + +async function dexStop(id: string): Promise { + const dexCfg = dexConfigs.get(id); + if (!dexCfg) throw new Error("Unknown dex ID"); + await dockerStop({ containerId: id, }); + await fse.remove(dexCfg.configDir); + dexConfigs.delete(id); + console.log(`Stopped dex id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function dexDocker(on: PluginEvents, config: PluginConfigOptions) { + env = config.env; + + on("task", { + dexStart, + dexStop, + }); + + on("after:spec", async (spec) => { + for (const dexId of dexConfigs.keys()) { + console.warn(`Cleaning up dex ID ${dexId} after ${spec.name}`); + await dexStop(dexId); + } + }); +} + diff --git a/cypress/plugins/dex/template/config.yaml b/cypress/plugins/dex/template/config.yaml new file mode 100755 index 00000000..f773eb76 --- /dev/null +++ b/cypress/plugins/dex/template/config.yaml @@ -0,0 +1,56 @@ +issuer: http://{{DEX_ADDRESS}}/dex + +storage: + type: sqlite3 + config: + file: data/dev.db + + +# Configuration for the HTTP endpoints. +web: + http: 0.0.0.0:5556 + # Uncomment for HTTPS options. + # https: 127.0.0.1:5554 + # tlsCert: /etc/dex/tls.crt + # tlsKey: /etc/dex/tls.key + +# Configuration for telemetry +telemetry: + http: 0.0.0.0:5558 + # enableProfiling: true + +staticClients: +- id: synapse + secret: secret + redirectURIs: + - 'http://{{SYNAPSE_ADDRESS}}/_synapse/client/oidc/callback' + name: 'Synapse' +connectors: +- type: mockCallback + id: mock + name: Example +# - type: google +# id: google +# name: Google +# config: +# issuer: https://accounts.google.com +# # Connector config values starting with a "$" will read from the environment. +# clientID: $GOOGLE_CLIENT_ID +# clientSecret: $GOOGLE_CLIENT_SECRET +# redirectURI: http://127.0.0.1:5556/dex/callback +# hostedDomains: +# - $GOOGLE_HOSTED_DOMAIN + +# Let dex keep a list of passwords which can be used to login to dex. +enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its underlying storage for passwords. +# +# If this option isn't chosen users may be added through the gRPC API. +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/cypress/plugins/dex/template/dev.db b/cypress/plugins/dex/template/dev.db new file mode 100755 index 0000000000000000000000000000000000000000..95a42aedb495111cfc33e6e614237d869a7373da GIT binary patch literal 110592 zcmeI5S&Ssfd6>IrcDm>4=_R?t<}S5jb3kgi-l?9vk0#`0on7~RHbE5f&aAxe%AyHq z#-tC*upz+$EI-kJ0K=9a>?c181}s<)228`?iva^N1U-lk1}sB_VF89Ct9xd;x~o=0 zYTDXeMWL%JBO@a-|L_0fk3S;v=ymE^ARWrKXL7+IzV-Ij_V(7d4iC4s9{;DUt*tMg zf49N!yWsm4_`dk0>;8fd*vHYRd<$%~z5nOwwqM-;^S8*oKi&Nsd%w5+gT3$H|H=K| zx%Zp*w5{L0E8Y3g9rNDT?tN|JE}xAa>>hmh;r4SW;CMr_W!cayY3fV9ui4h6H@Fz$f%2Z>ouhfiw>eEgO78!+5gH5^T%bkmX6TC;93jZ`k~+ zQio{?NeFCjDsln0%styxyIW~wyOYCOadP;L^FuyaZVnFcL3*?gevrAnd+_-2_7C4( zN_KvPX<+M;wUW5ABGpRPxlEVEIdnYDcQan3!USG(wqA%5ZneZ!vxupKI+>^tl5?4?BP?TgYG=js& zkAWe05*J9~6x6;zGbKOZOy}ikzuFQ(5Hz#=J4BG<<=^3;p9ecII6p`k13UhO%AW&v zfHg^;2hJqb*Hvx2I+e#C{J?u__u%Va-~RQ-OMB(QK%IJ$8>VM}VfogIwO-75GsX(4 zp<`RVG!2rU*J#(<`QiEJW9=Ov(mbH*Y3OOIdOfp*we=yjQ~(Ef+BMDwIA1}Ua*pHK z5ob)nkLd(!On7OChOH<-Nt(53gL8fUb**(z?k+J&b4LSS?`oAetChci(SQG2JG%#8 z`O5a+YrQgh!4{>ZU8f7XSIs?L{bp=^y}^F0p>8z9kF($$eOZ@B2s($;8*u|4muxhlKWC{HbpP3c>5l=^=r$( z@0&{om$EMzO{2u`}(G%Ew_7V%NJ8SAKu(K z_~_kD1%}=C_u)XL&bQ@Sm-FZCmyw5HN#K`D&N#VypK0 z!F}rJ126Q`5ZsY~OS^2gFN}6=-v^kvp$Tguv2?eaJSd3ZY!EzN>dh=I!P(wD`07`; zpPkvQsVUyFV)(C?Zmht2wf+pC6`Jc$W?k0O=dA(tF!C z?|k(_GL_=(*AoS91qna`kN_kA2|xmn03-kjKmw2eBmfCO0-sI-chl2`{r^v=^59uR z0+0YC00}?>kN_kA2|xmn03-kjKmxBx;KIH6@9qEj{?G2+xclJFd$)gO7u)%px2kWw zcjKqqKij^s^~2ZP-Cu6A`A6@(v$wZ*`*!;8oxQCd_~k4u#^qQvXTn?;93`GPb`%Ak zXC<~L85E8gCPj)OiN#(VSUg6ick#dYzP!+ZJVeQ3lsd-gBNSy>h9VL4!{u$%&)ofo zAAR|kh_F&QKk&o!zy~3x#+V_fiELu_EG7vxNW5^)8-8MOjxT-tQF?dhqwjqC5x8aE z(!@tk9)UY|Wz7KV)F5zsawUI|?VCbQmh_;O4Ev3OgZcSnoGav0aZOv$ zUy4gD#U+jj@`z#xilAuxTwDj-Pj5YQAFP*IkK8#e`kp25K}aQbv4?-+A=#OR1h0$=m_=#_7C4TdG9P10mEy6{F*8l9|it zDx)eDRkNJFE(Lhe5L<)!xYVd?f@g|Zqku^9L^-kRBR)Wc+&C{`<&hId4F<{J44J_g zgaMZl)igr^Rp77>9%m5fXNhl}{~iVD%5OhPZ{V^$|Iv3+>yT}m=e$ccf*hmh5rd;R zieT(2uls9x&0~@Zj6@SnmA9?AyYQ_LGl-bHCSJ*)B={52E}_P_S*hacXsngdcCk(M zlJ;;xAjJ|-dOkbJH;@9UBJ-S)E9Lv8Iy$brd0q&9Owva*3H%O&U#e60Uh+C~U}t{B zvGh3=OIG6ZZeU3g?cr1$NsfF?yyg`)eMs$?pm2GFA8NvAZMORi#RU0 z0jYzp9DS-}rjJ;Lph=RVSAFN5wKC7VFjit(z!lNPF-IZv#e(#8Uc4rm(N=xP*p4VQ z`$GF9=;R7La4WfTI`$aUY3aF+%KuiKaSYL!2AZw>U7-NMwwCa2qPH^r&$_bk$HEm%#LEY7Vahs zPtuLpbVx_=6j=khzMi-M=}&B-K5+77nI9!Gyw6eRxO!shnQrQ$}D8Z zVy`^RRV%@`6u9}kBNcMvg-~J8x`FE|Q5#m*)#s(m2y;YI7=cmDYEyOdipXjBX9R&C zMYv4qxDxTc==+8>6MT=jCXp>n>CgODU+-lXlWd{}sy6Ic+)cnP-{;n zg_$vLa%Ns*X+Na$1$vRIl;_Qm7HngN2=Qqgpg6mzh2DZ8=Q6>1-lD6#;iwe>_5mIl z20T7VvM5PY=!c-2e(OE(@7)LK{QrCR|7>fowTJF~VPD_-7Z3i|p1JqO`~Q6Za}R#9 z_aFA3?l&I%+x_2vaPz@O55BYiM|;11|IZ%$_Ws|0@YM(3eem7CTz4Oqh6Er1NB|Om z1Rw!O01|)%AOT3=%@Fuy&vAGjBSK9Tq_npy=z>PdqG5gc{hJ?c&$T&~=yTe_cvaCn zC!shYmQ*u&FU=JNBN24b5Ct&ShjV%y%6Q;eR`hO~E6fs`jttXxd{T8>QzBHy5J(^U z(lVEiL*0vnNXKzgb`70~DZ}yz{UFWNT|@Q=X^tm~4rVY3krZPROeBh5OmpXCY`BUL zBq|BUoy1U2LXPG+S$~-3iU}Vjp-pKxg@uMEONK{S9&f1cq`6jzo0K5a_`6Q%ph+)Y!850-o0H2Jod=5Cy7x}WB5oLIS+ z=5CyVxSQr~9IU^yEW2q~`}Xpo2h$nFOUEv00}?>kN_kA2|xmn03`5N zOyI`7kG@P)!V8x^MvEYld2pf9+;Sz0BF!j{0xf+U`io0hu3p+&*Gd{MQ+=h#t1We$ zjU}yq!cd&6b%sIr7rF5DjZSxxhhVxBT<43@M_@V}yful%E?oGqwRQ9JmmxT2LM9lg z(7YfUxEfQAX-8sczM2`n3ZakiN?gsC<_1!Z#aYf6b6Le_8Y7jeS?x~-1QJ~3!yGXv zjWP&M(ii5)fgNt|T*d(2!9WXosM+8$022!=N|wMi2&SNYj4gNqLed6Hw`+@T*PLjl zvJ}Rl)h(&x8pC`7Fr2VE!sNr1o{WHOq0>g0f;#Iyc?*HHa zqy69C|M&a)U$KvZhld0p0Z0H6fCL}`NB|Om1Rw!O01|)%J_7_M8%NyI`|!UR7{;6k zOfk@CkCQYm)&hD)<8uRKo^BkcTE1TNmp6{z+yI5&-#D=GV*dX#02hh?2|xmn03-kj zKmw2eBmfCO0+0YC00}?>8wk9Z|KE@eS3?4j03-kjKmw2eBmfCO0+0YC00}?>kichv z0G$8-3@8zb00}?>kN_kA2|xmn03-kjKmw2eBmfEg1q9&y|6c$IWp8*2t{QvgNU)utI@B;}z0{@>0{FCo(-+B0tynp(wzk9!@ONsyO z?>=%u-q3`alsr2A?xWC`9vwgOHRaKhM|uEOJ?i$d>9Qt*Wro9eoZ)eqA;J4@DT+f$ ziDw1y#u$`K-+ja37*8=G3kqc9vb78+t(Xjt`{;^&;u`2pY{tf zn!!}b?&wOrH*<;dOdYCGYwpb|{oF({Mq1chEa*~&822kwcoI=mDNMrl$!Q0`O*NM7 zWOI+c2bSlK=A~-G5Dd#8YzCqF2zV+jd5qvkEI}|Rf};2-*!1U9JSq}0PBW||2pEbI zJc$UngdiNv2^>OmqzE4MDk9uk#b-)gr+$(j_3?J7lzNt&ZR<@9&B{{C)`WT8b34sx z#;g=(beySqi`l>yJ*?vv>kO48nhio~*M{wwMJEX>X3Il#L`p<34amf>reiyk_rp+AfA zUAMzj*mlcLk7Y)aBk)zB?#>(Z4nQG@)VM-v4 zQuhQr{CE=4j%15dci=Hv4>c<7oGIEpJXEn$Dk|cQ9ML4vEJ2Q6NAV~Q9>aS~V&F}@ zG)|EWo+|!3e_in^jj9RejfN$PtPkt8Zkr${tXH3uD>$tLBwA#eL!^@< zCq=Uj9&SF;xrlYC_FN9FsEX^;pvo+;B7#~T+A_dHu!C_kL-09nPS#>Xq1q)LThaBCN2w<^a;L?GF$V8i&*-M>r-R|R+-OE&Juj9`al{}CgqY`NqB_jxhYZb? zd*N&_tg_*(SY#NiD}^P;|7bZk~?CRxSUYUyHk)xF@W_mh{hOK--b<|Q6m20!u zsvE*_@Dc>qFZp%6cx>9I8RCTFwc2R2d0D zbJ3_+N@vtN@dYMd=mb(7jjOoUpPErrJfZpv;6oZjzbslj)gVZW1P|hJ4cr~|2E`;H zL^YcfHH+iA0jg8^Sy{-@Xwfkm;EY{x#sr!lb=7dllunG8<>Q{}*_~MlP1F)=3{HxT zZcr;^D-Ew+p69)KU{?EqFrzC|uiMJx%ucadLpxH#?HV*Sqx)J-*J^>rFm6FjdR)t~ zavWooGD6_@JasXVQOadb?XGSQa`ka{%E@DmrX6g`RPzkq6v=upv_&pz4YAXTnrSlm zn&d`qsXY|aGZs_G)(PvloF9URx%bNzx`eO=TF5mf0WeoD)51JNH>AvBG9~B3;k-Z> z!29#MW+T&fdktn-ZAid{bK0zlxWh4m+w)GN?2;4OAC&V}4xR&wkbc^Y-PfkDRxX5x6v zQwya(SX6@YtUZyZ)54gR{LX2O)WqS`7LwU)zR;L?&rj__1E*|g)l$3F8RR|7>|iCN z%1a$H8;?Cv*Yrg^pHeL^b4qqpDscT$E~jR(QVE?;#9Gs&`4&@ZWuq)JD@}4t!8#$h zo~NK)r&j1#+5YKxUQUYiv^H4Oi6Jer&AL|+aZ;2!rZh6+%mCVW$u2U+R zUboXXCS}>2(QQs7o#~9jQCVe!LH5!2V9WHWZkfLKGUg-?QTAf!L7~j~3n9|^|Ly(% zy#@Z@2NHk;AOT1K5`Y9C0Z0H6fCL}`NB|Om1U_Q~c5gnslg>9T=l}Qra0~pw4kN_kA34BTk{POm##}D8B5|Y6gGJ`P)1Fk?dZ9)I;_hj4V zIqz)#|EF8~KmC*n2u~XlfCL}`NB|Om1Rw!O01|)%AOT1K5`Y9Q5P103u z|6f1^7a;*i01|)%AOT1K5`Y9C0Z0H6fCM0c&nN--{Qu9WVxd@&03-kjKmw2eBmfCO z0+0YC00}?>kiZ23aQ^=S47dmhKmw2eBmfCO0+0YC00}?>kN_kA34BHg!1@2rsA8d5 zkN_kA2|xmn03-kjKmw2eBmfCO0+7H30_pt!?OV6D_W$97@q-WVx_f`R`;)!jyLD^t z`}cox|99^F<~?oeckfDfesssY_qBUpyHId-@!9CX?!kv2Za{3oc>9O%F6o^gVH()FWUVCbtdLvDI+y9PIERj>nVgp#f|os< zi)Lt&6>JdB7i{o)<;${VGKbY(t9i(I9+v=x9r}T%S&F|zW!aV>U1oR&(cm=GpMD(X zS6f(|d1WuDl7zEOB$LyO%SXAKKaC_$lQl^^)T}^KB=41eE}oF$*`f3LLY@_Usou+k zUm46L^{3^Y94?i(dg6zoBy*t=96o*w48fDQKoX~1a2RN&@O_eTCvuPId8^TK{a%2%a^7>^79()s-6F!aeH@J|0b~hUGapl{-rhb zlT`nn9NN5Z8&Uu)KW!q;yvr-6^1%<@-Pt|(%2&33xc#br3brUMt6;jYd$ne!tKUqu zT&-K@t?J>=--e{#9rz>71AcWHdfKWJeRB7rOaJTI#+*}-=8gv1_^S=TSsV28yZAIf z5v8f1a)u#U%B7b1LR)Zo!>K6+s(mS@y0HBRf2a1=ZrXPIgEL=yu~oRZ{HbX>UfB2H z`R8Np{Nv0yHTSi#RGI-A*5Hy7&f*T)_5^9lIgV#XoG}GIrW33Qf0mWSuoVR~@S3%0 zgL6ZHwdvDRo6y~zgW^}Vw=_$X=DurapiKbP$X=$?C3U*2$mq+(+w`rSgWA_Ww#a!b zH$5wLp4$2F=FY)K?`|q6oOLVEv){L#<2QB>-hFrb``Kj^bhh^F|IL-g=WN|;w>-WC z0%i~@U+uD8tXr=i+^3Z*@IpTg!5vBRe!h}U$QzolrZ%O!y&6NGRasCAZP1=w>dmAT z^aE#m_u#8v-G27r5}K(g9v6WAv;S)8#tOVw>(2mMp}GEK*7h#IAwSr=OKkN_kA30x0> z=khztKH?AdmvN9NMH=YjEXO)7E+4GKLKkz+!^!hi%b|^xi1F&Ec-p}V{dIBW*$_)Q z+J6?lZR~5GM|bIP?0Lw%rbGVZ?u(A%i(kh!4A3PDBoB1pu0(I=9r$bNjMEQ4t}k$r z=oGz}`V!Fl?Ns;Q{KC#b>w`^wg7YFTyA!MFcb>oZ`Q3vLKG^=-_C+U@e(lazI?leXifroXfB||tOvfj{yi7U|xUMWX3t3Z^ zGfqpne|3N7p!4A-i&~v5w5I5#6>q<>iO5gfB;n(|or7;a+yo|lm-`e=|37%Xe}DJj z;lu6kpN;;e>(cMuit^JnudVm9X*FTl7o9FkUYh(|8T3lkc*oZi3rrEFTVI|{Jac?6 zirD-NjPG79GX*{CtL4^|vA{sU)B_GA?I*ACw@aIPdT-|-|NbU)4(I!^?TJ(L#nSy} zPwws>y#N08^Rtfli}f#l@2#MGvGyG2nwiS0&5g=knL%D6SU>l6=3>^4$}Qz%@9Z2j z9&eIwWtw=3t|afm`~R;egn@g71Rw!O01|)%AOT1K5`Y9C0Z0H6fCN5C1mOMupCoO- zbAbdP0Z0H6fCL}`NB|Om1Rw!O01|)%u7^N6|Nmh7XIl?`y!YSk|H-|d-Mex3!JYSR z|H>}5^EYo*-+J%HPq%-zedBtL{YlyD4)W2Le~AbymD{)X_V#YS^Kh%Tw|5Y7YK$3z zn#d+*&tj5LgTxExyx}Jn=lIgMAEj52J^IeKAAwuuElqs%4<4?SS&!T~E&84%@Igo=cI0q^MquhS$vo_g^GaRpU~}?B znJpSkvbh+~TxMptbSp1w;Sg`}2Gu0Hn8CPXl@J*=9Em>PiJT2G?*JOYkAFM zk_wDO6HS%3t+~7Ktq?Pan7k%l$)F_o6VWcA#<*Fj;_7IumC<&wP4$xYa6ur&5>I+Q zJIObY0;wYNoRKT#`=vTMuDp3(2!2e`M>I*{7=vG`Q}ZUoc8udkIEhjO$)N0to^P*}*-|`0 z@H|%nkNi<|pRiTmlmnT+E??lEXsRPN#0hhn?NY`(>zf`W8O4%i=v<>mjE6XKitvl1 z8U&?r!kXi#IxLT^PRFfnlo=(DFp{Bgnx*j-nRnO9>?oFN;cl|xm1H{=^pQ11EoKlQOBvYBU@=t)Lo@aSNX-&RC7evhUiQ*NRQbJ7$!kJ9l8o9VGA127XdOLBsNxQ{FC9Q-~ zg-Od1Xsg*C5A#}stR+>y;bjAr8=-3LG(TpN9$GFme92g+XX2P-j|t`oG;;{B?v=RU znR{!k%;J2m`GKWVI*9)SnF!~Co?w!wUz48m4%cq8&Da!*jN0qjy}_)`*D1vD9d4#J z`X`cEZ^VLOYdJP4XXR10p6@0a-p@|f>Uk+MsLv=%)1YOiSL(C#))kpMX)L4)vLb2} z&AGU!nJTA<#N459<(g!cRAIo5(a`|Jvs$%ex*cMYRVuTP9gDs4FjuVv<5J+}^Nv)= zjTb_NLF)#tt3+*B-Kb}TIU*^Hz$j+5sk(VZ z$QGvbXMU@%_p*ygHc>%e=dPbeW==s9-Y_4pm5msjVg;|w~&2WRr z^f5*q5j2JqD9)@jRnCnoGN)>GJku4#kcbG82-R62Xu5>qoXB63%wo3I8MXUjiz3Ta z!ECjodTlTv+)@(j1K;XZxJc}}`4Za*<1$MxYWbKOt0IZmlhy{AmrWIlV=RTS*affs zJhS~$$JhyCI)T0@Az*!I_vf~OCEjz|_c`#0>wI`Fp%$PShGq16LG_4ds_yz#HMe{HM1_oMsHy>H$9gS&t5^Yqi7XAai#7x*~Ty-0|3 z95-dx(21BbERVP@Z_2ukJ8lN9&X+}!>Ch@4+fGz7nNBAy@!gy?I_3dyLYcrDstVClPvu z*E?%@i3uMip-pKxg@uMEONK{S9&cRNNKd0m={hnui+P%j^5(!e85Qy&GYOhX&KQMF zk(f;+Tqzbpk&ioc4UvtY)~x%9R(R9?5Y$HuKStOi1cXK)1g0)Vz|S*ZSj)=_ag!2c z8ebR|70O{W4+)-M#Md<%ICesZbx!$e*UHzathG3CBa5#)QGS?qZhC&ZHqgopoim3e zMeepFu4DV<`Ki{|l54Op(A-i-EQWz04}@6tU!Pyg%it{6}i5nU~*E=*W0*O=k20TXL+YF=%6jHJ&*Y|-b4GfC>&l3FAVg;Q7~W*I;9u= z{WEv}C9jL4YM2I{*CnlP|vn{vv=*`wV&%aSJL8X6l^R@wrNS#zFz3 zzt}YyXuImg9FselSe=1t%LdgLx5z@RGRY3QPQLGwq9Am+q^5OncPttaIO~tzc_*E} zIWv>JbP{LJa~z(>h)`1nDedtHx}cG=XjnE7M^Lp;!xYaYXN#Hdk}4HAblTr|@qIZ| zP9CDnF$Ur|`UqhN2E%FU+(@5i-n)V;?N-jUIhE*h+QN8M(L5)iI3bo)Gr1;Q%~5mS z6KBm+X2GkGQZ3p}MYnRu#I3eR)FPA?+$bk>ftQKOgqCmWC4T0L0Y^3o?9JC_VmYfo zG9*nP47u9AzPpx}C>V*Li-stG!8M%I<50!}&$6y-#Mo~$&A~j(&iuy0=-V}x?~1jt zjrzrKPIytX%DM){8(pGbOz2ko)FeX}>EL{2Wqd}%E& zVV2l*WSG9=ld9vI5}`VVK>F8J9gCdXCd`o|;p&9J6ZbS4S)EgDfi~zdkD>TUcY(HA z#z}D!w=ucX%uaI1Jj>R2J-CK=7blLvC=WuS3}`c$iz8dlGjFfuwXhT|o{PfVQx + +import Chainable = Cypress.Chainable; +import AUTWindow = Cypress.AUTWindow; +import { DexInstance } from "../plugins/dex"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Start the dex server + */ + startDex(): Chainable; + + /** + * Stop the dex server + * @param dex the dex instance returned by startSynapse + */ + stopDex(dex: DexInstance): Chainable; + } + } +} + +function startDex(): Chainable { + return cy.task("dexStart"); +} + +function stopDex(dex?: DexInstance): Chainable { + if (!dex) return; + cy.task("dexStop", dex.dexId); +} + +Cypress.Commands.add("startDex", startDex); +Cypress.Commands.add("stopDex", stopDex); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 289319fe..d186c5a0 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -17,3 +17,4 @@ limitations under the License. /// import "./synapse"; +import "./dex"; From 2d1e8f10b76eb9159126e1060999b4c1f5eef38c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:27:47 +0530 Subject: [PATCH 232/354] Support app params and docker params separately --- cypress/plugins/docker/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index 024cb9d4..8a429b67 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -28,10 +28,12 @@ import PluginConfigOptions = Cypress.PluginConfigOptions; export function dockerRun(args: { image: string; containerName: string; - params?: string[]; + dockerParams?: string[]; + applicationParams?: string[]; }): Promise { const userInfo = os.userInfo(); - const params = args.params ?? []; + const params = args.dockerParams ?? []; + const appParams = args.applicationParams ?? []; if (userInfo.uid >= 0) { // On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult @@ -45,10 +47,12 @@ export function dockerRun(args: { "-d", ...params, args.image, - "run", - ], (err, stdout) => { - console.log("error", err, "stdout", stdout); - if (err) reject(err); + ... appParams + ], (err, stdout, stderr) => { + console.log("error", err, "stdout", stdout, "stderr", stderr); + if (err) { + reject(err); + } resolve(stdout.trim()); }); }); From 3ff83f3d3dc21fbddb01174a6b74bf1fe3481480 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:28:36 +0530 Subject: [PATCH 233/354] Add braces for if-else --- cypress/plugins/docker/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index 8a429b67..fe9c24ae 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -107,7 +107,9 @@ export function dockerStop(args: { "stop", args.containerId, ], err => { - if (err) reject(err); + if (err) { + reject(err); + } resolve(); }); }); @@ -121,7 +123,9 @@ export function dockerRm(args: { "rm", args.containerId, ], err => { - if (err) reject(err); + if (err) { + reject(err); + } resolve(); }); }); From 888a509c372d9a173148d2776191edd5d1fe761c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:29:37 +0530 Subject: [PATCH 234/354] Support creating docker networks from the plugin --- cypress/plugins/docker/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index fe9c24ae..a55f341e 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -78,6 +78,31 @@ export function dockerExec(args: { }); } +/** + * Create a docker network; does not fail if network already exists + */ +export function dockerCreateNetwork(args: { + networkName: string; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", [ + "network", + "create", + args.networkName + ], { encoding: 'utf8' }, (err, stdout, stderr) => { + if(err) { + if (stderr.includes(`network with name ${args.networkName} already exists`)) { + // Don't consider this as error + resolve(); + } + reject(err); + return; + } + resolve(); + }) + }); +} + export async function dockerLogs(args: { containerId: string; stdoutFile?: string; @@ -142,5 +167,6 @@ export function docker(on: PluginEvents, config: PluginConfigOptions) { dockerLogs, dockerStop, dockerRm, + dockerCreateNetwork }); } From ca350470b307724e89952f6af3eea555a752c22d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:31:09 +0530 Subject: [PATCH 235/354] Remove unused code --- cypress/plugins/utils/port.ts | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 cypress/plugins/utils/port.ts diff --git a/cypress/plugins/utils/port.ts b/cypress/plugins/utils/port.ts deleted file mode 100644 index 064ccc7c..00000000 --- a/cypress/plugins/utils/port.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -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 * as net from "net"; - -export async function getFreePort(): Promise { - return new Promise(resolve => { - const srv = net.createServer(); - srv.listen(0, () => { - const port = (srv.address()).port; - srv.close(() => resolve(port)); - }); - }); -} From 096c42198dcecf352a32d8a6c29a0fc7342abf25 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:38:02 +0530 Subject: [PATCH 236/354] Make synapse plugin modifications --- cypress/plugins/synapsedocker/index.ts | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 5227b5e4..268f7320 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -23,8 +23,8 @@ import * as fse from "fs-extra"; import PluginEvents = Cypress.PluginEvents; import PluginConfigOptions = Cypress.PluginConfigOptions; -import { getFreePort } from "../utils/port"; -import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; +import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; + // A cypress plugins to add command to start & stop synapses in // docker with preset templates. @@ -35,6 +35,7 @@ interface SynapseConfig { // Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage baseUrl: string; port: number; + host: string; } export interface SynapseInstance extends SynapseConfig { @@ -42,6 +43,7 @@ export interface SynapseInstance extends SynapseConfig { } const synapses = new Map(); +let env; function randB64Bytes(numBytes: number): string { return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); @@ -64,8 +66,9 @@ async function cfgDirFromTemplate(template: string): Promise { const macaroonSecret = randB64Bytes(16); const formSecret = randB64Bytes(16); - const port = await getFreePort(); - const baseUrl = `http://localhost:${port}`; + const host = env["SYNAPSE_IP_ADDRESS"]; + const port = parseInt(env["SYNAPSE_PORT"], 10); + const baseUrl = `http://${host}:${port}`; // now copy homeserver.yaml, applying substitutions console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`); @@ -74,6 +77,8 @@ async function cfgDirFromTemplate(template: string): Promise { hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); + const dexUrl = `http://${env["DEX_IP_ADDRESS"]}:${env["DEX_PORT"]}/dex`; + hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl); await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); // now generate a signing key (we could use synapse's config generation for @@ -85,6 +90,7 @@ async function cfgDirFromTemplate(template: string): Promise { return { port, + host, baseUrl, configDir: tempDir, registrationSecret, @@ -96,17 +102,24 @@ async function cfgDirFromTemplate(template: string): Promise { // directory async function synapseStart(template: string): Promise { const synCfg = await cfgDirFromTemplate(template); - console.log(`Starting synapse with config dir ${synCfg.configDir}...`); - + await dockerCreateNetwork({ networkName: "hydrogen" }); const synapseId = await dockerRun({ image: "matrixdotorg/synapse:develop", containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`, - params: [ + dockerParams: [ "--rm", "-v", `${synCfg.configDir}:/data`, + `--ip=${synCfg.host}`, + /** + * When using -p flag with --ip, the docker internal port must be used to access from the host + */ "-p", `${synCfg.port}:8008/tcp`, + "--network=hydrogen", ], + applicationParams: [ + "run" + ] }); console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); @@ -162,6 +175,9 @@ async function synapseStop(id: string): Promise { * @type {Cypress.PluginConfig} */ export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) { + env = config.env; + + on("task", { synapseStart, synapseStop, From 66532a9693110fc4596787fc293129dbcefd3770 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:38:46 +0530 Subject: [PATCH 237/354] Add sso template --- .../templates/sso/homeserver.yaml | 89 +++++++++++++++++++ .../synapsedocker/templates/sso/log.config | 50 +++++++++++ 2 files changed, 139 insertions(+) create mode 100644 cypress/plugins/synapsedocker/templates/sso/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/sso/log.config diff --git a/cypress/plugins/synapsedocker/templates/sso/homeserver.yaml b/cypress/plugins/synapsedocker/templates/sso/homeserver.yaml new file mode 100644 index 00000000..10246f5c --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/sso/homeserver.yaml @@ -0,0 +1,89 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +oidc_providers: + - idp_id: dex + idp_name: "My Dex server" + skip_verification: true # This is needed as Dex is served on an insecure endpoint + issuer: "{{OIDC_ISSUER}}" + client_id: "synapse" + client_secret: "secret" + scopes: ["openid", "profile"] + user_mapping_provider: + config: + localpart_template: "{{ user.name }}" + display_name_template: "{{ user.name|capitalize }}" diff --git a/cypress/plugins/synapsedocker/templates/sso/log.config b/cypress/plugins/synapsedocker/templates/sso/log.config new file mode 100644 index 00000000..b9123d0f --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/sso/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false From 9be9d85d5d48df586398c4efcdc1ce42756e6af9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:39:17 +0530 Subject: [PATCH 238/354] Write test for SSO --- cypress/e2e/login.cy.ts | 52 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 36badebf..5ec34749 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -13,17 +13,27 @@ 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 type { DexInstance } from "../plugins/dex"; import type { SynapseInstance } from "../plugins/synapsedocker"; describe("Login", () => { let synapse: SynapseInstance; + let dex: DexInstance; beforeEach(() => { - cy.startSynapse("consent").then((data) => { - synapse = data; + cy.startDex().then((data) => { + dex = data; + cy.startSynapse("sso").then((data) => { + synapse = data; + }); }); }); + afterEach(() => { + cy.stopSynapse(synapse); + cy.stopDex(dex); + }) + it("Login using username/password", () => { const username = "foobaraccount"; const password = "password123"; @@ -35,5 +45,43 @@ describe("Login", () => { cy.contains("Log In").click(); cy.get(".SessionView"); }); + + it.only("Login using SSO", () => { + /** + * Add the homeserver to the localStorage manually; clicking on the start sso button would normally do this but we can't + * use two different origins in a single cypress test! + */ + cy.visit("/"); + cy.window().then(win => win.localStorage.setItem("hydrogen_setting_v1_sso_ongoing_login_homeserver", synapse.baseUrl)); + // Perform the SSO login manually using requests + const synapseAddress = synapse.baseUrl; + const dexAddress = dex.baseUrl; + // const dexAddress = `${Cypress.env("DEX_IP_ADDRESS")}:${Cypress.env("DEX_PORT")}`; + const redirectAddress = Cypress.config().baseUrl; + const ssoLoginUrl = `${synapseAddress}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(redirectAddress)}`; + cy.request(ssoLoginUrl).then(response => { + // Request the Dex page + const dexPageHtml = response.body; + const loginWithExampleLink = Cypress.$(dexPageHtml).find(`a:contains("Log in with Example")`).attr("href"); + cy.log("Login with example link", loginWithExampleLink); + + // Proceed to next page + cy.request(`${dexAddress}${loginWithExampleLink}`).then(response => { + const secondDexPageHtml = response.body; + // This req token is used to approve this login in Dex + const req = Cypress.$(secondDexPageHtml).find(`input[name=req]`).attr("value"); + cy.log("req for sso login", req); + + // Next request will redirect us back to Synapse page with "Continue" link + cy.request("POST", `${dexAddress}/dex/approval?req=${req}&approval=approve`).then(response => { + const synapseHtml = response.body; + const hydrogenLinkWithToken = Cypress.$(synapseHtml).find(`a:contains("Continue")`).attr("href"); + cy.log("SSO redirect link", hydrogenLinkWithToken); + cy.visit(hydrogenLinkWithToken); + cy.get(".SessionView"); + }); + }); + }); + }) }); From a10025ddb2a023143d319dd660089f163861947d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 01:41:39 +0530 Subject: [PATCH 239/354] Run both tests --- cypress/e2e/login.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 5ec34749..969c1884 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -46,7 +46,7 @@ describe("Login", () => { cy.get(".SessionView"); }); - it.only("Login using SSO", () => { + it("Login using SSO", () => { /** * Add the homeserver to the localStorage manually; clicking on the start sso button would normally do this but we can't * use two different origins in a single cypress test! From cfbd0672c5329955f91b392c416c0d9758c73c8e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 6 Sep 2022 15:42:41 +0530 Subject: [PATCH 240/354] Refactor left panel to use menu --- .../session/leftpanel/LeftPanelViewModel.js | 5 ++-- .../web/ui/session/leftpanel/LeftPanelView.js | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 8c8d71a2..fc783cfa 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -34,7 +34,6 @@ export class LeftPanelViewModel extends ViewModel { this._setupNavigation(); this._closeUrl = this.urlCreator.urlForSegment("session"); this._settingsUrl = this.urlCreator.urlForSegment("settings"); - this._createRoomUrl = this.urlCreator.urlForSegment("create-room"); } _mapTileViewModels(roomsBeingCreated, invites, rooms) { @@ -74,7 +73,9 @@ export class LeftPanelViewModel extends ViewModel { return this._settingsUrl; } - get createRoomUrl() { return this._createRoomUrl; } + showCreateRoomView() { + this.navigation.push("create-room"); + } _setupNavigation() { const roomObservable = this.navigation.observe("room"); diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index c79192be..ac0fbb81 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -17,6 +17,8 @@ limitations under the License. import {ListView} from "../../general/ListView"; import {TemplateView} from "../../general/TemplateView"; import {RoomTileView} from "./RoomTileView.js"; +import {Menu} from "../../general/Menu.js"; +import {Popup} from "../../general/Popup.js"; class FilterField extends TemplateView { render(t, options) { @@ -51,6 +53,11 @@ class FilterField extends TemplateView { } export class LeftPanelView extends TemplateView { + constructor(vm) { + super(vm); + this._createMenuPopup = null; + } + render(t, vm) { const gridButtonLabel = vm => { return vm.gridEnabled ? @@ -90,7 +97,11 @@ export class LeftPanelView extends TemplateView { "aria-label": gridButtonLabel }), t.a({className: "button-utility settings", href: vm.settingsUrl, "aria-label": vm.i18n`Settings`, title: vm.i18n`Settings`}), - t.a({className: "button-utility create", href: vm.createRoomUrl, "aria-label": vm.i18n`Create room`, title: vm.i18n`Create room`}), + t.button({ + className: "button-utility create", + "aria-label": vm.i18n`Create room`, + onClick: evt => this._toggleCreateMenu(evt) + }), ]); return t.div({className: "LeftPanel"}, [ @@ -98,4 +109,18 @@ export class LeftPanelView extends TemplateView { roomList ]); } + + _toggleCreateMenu(evt) { + if (this._createMenuPopup && this._createMenuPopup.isOpen) { + this._createMenuPopup.close(); + } else { + const vm = this.value; + const options = []; + options.push(Menu.option(vm.i18n`Create Room`, () => { vm.showCreateRoomView(); })) + options.push(Menu.option(vm.i18n`Join Room`, () => {})) + this._createMenuPopup = new Popup(new Menu(options)); + this._createMenuPopup.trackInTemplateView(this); + this._createMenuPopup.showRelativeTo(evt.target, 10); + } + } } From 413ba3279f6ad15621de48ae55ac8a0e2066f8f2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 14:49:05 +0530 Subject: [PATCH 241/354] add "join-room" segment --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 849d870a..af3c35bd 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -51,7 +51,7 @@ function allowsChild(parent: Segment | undefined, child: Segment Date: Mon, 12 Sep 2022 17:08:09 +0530 Subject: [PATCH 242/354] Add vm/view --- src/domain/session/JoinRoomViewModel.ts | 63 +++++++++++++++++++++ src/platform/web/ui/session/JoinRoomView.ts | 63 +++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/domain/session/JoinRoomViewModel.ts create mode 100644 src/platform/web/ui/session/JoinRoomView.ts diff --git a/src/domain/session/JoinRoomViewModel.ts b/src/domain/session/JoinRoomViewModel.ts new file mode 100644 index 00000000..a4be548b --- /dev/null +++ b/src/domain/session/JoinRoomViewModel.ts @@ -0,0 +1,63 @@ +/* +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 {ViewModel, Options as BaseOptions} from "../ViewModel"; +import {SegmentType} from "../navigation/index"; +import type {Session} from "../../matrix/Session.js"; +import {joinRoom} from "../../matrix/room/joinRoom"; + +type Options = BaseOptions & { + session: Session; +}; + +export class JoinRoomViewModel extends ViewModel { + private _session: Session; + private _joinInProgress: boolean = false; + private _error: Error | undefined; + + constructor(options: Readonly) { + super(options); + this._session = options.session; + } + + async join(roomId: string) { + this._error = undefined; + this._joinInProgress = true; + this.emitChange("joinInProgress"); + try { + const id = await joinRoom(roomId, this._session); + this.navigation.push("room", id); + } + catch (e) { + this._error = e; + this._joinInProgress = false; + this.emitChange("error"); + } + } + + get joinInProgress(): boolean { + return this._joinInProgress; + } + + get status(): string | undefined { + if (this._error) { + return this._error.message; + } + else if(this._joinInProgress){ + return "Joining room"; + } + } +} diff --git a/src/platform/web/ui/session/JoinRoomView.ts b/src/platform/web/ui/session/JoinRoomView.ts new file mode 100644 index 00000000..bc6d751f --- /dev/null +++ b/src/platform/web/ui/session/JoinRoomView.ts @@ -0,0 +1,63 @@ +/* +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 {TemplateView} from "../general/TemplateView"; +import type {JoinRoomViewModel} from "../../../../domain/session/JoinRoomViewModel"; +import {spinner} from "../common.js"; + +export class JoinRoomView extends TemplateView { + render(t, vm) { + const input = t.input({ + type: "text", + name: "id", + id: "id", + placeholder: vm.i18n`Enter a room id`, + disabled: vm => vm.joinInProgress, + }); + return t.main({className: "middle"}, + t.div({className: "JoinRoomView centered-column"}, [ + t.h2("Join room"), + t.form({className: "JoinRoomView_detailsForm form", onSubmit: evt => this.onSubmit(evt, input.value)}, [ + t.div({className: "vertical-layout"}, [ + t.div({className: "stretch form-row text"}, [ + t.label({for: "id"}, vm.i18n`Room id`), + input, + ]), + ]), + t.div({className: "button-row"}, [ + t.button({ + className: "button-action primary", + type: "submit", + disabled: vm => vm.joinInProgress + }, vm.i18n`Join`), + ]), + t.map(vm => vm.status, (status, t) => { + return t.div({ className: "JoinRoomView_status" }, [ + spinner(t, { hidden: vm => !vm.joinInProgress }), + t.span(status), + ]); + }) + ]) + ]) + ); + } + + onSubmit(evt, id) { + evt.preventDefault(); + this.value.join(id); + } +} + From 84920622e302e3d71a69e80e113e18d63e0d0e8c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:18:57 +0530 Subject: [PATCH 243/354] Add join vm to session vm --- src/domain/session/SessionViewModel.js | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 7226dbdf..ecb2b759 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -25,6 +25,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {CreateRoomViewModel} from "./CreateRoomViewModel.js"; +import {JoinRoomViewModel} from "./JoinRoomViewModel"; import {ViewModel} from "../ViewModel"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; @@ -45,6 +46,7 @@ export class SessionViewModel extends ViewModel { this._roomViewModelObservable = null; this._gridViewModel = null; this._createRoomViewModel = null; + this._joinRoomViewModel = null; this._setupNavigation(); this._setupForcedLogoutOnAccessTokenInvalidation(); } @@ -83,6 +85,12 @@ export class SessionViewModel extends ViewModel { })); this._updateCreateRoom(createRoom.get()); + const joinRoom = this.navigation.observe("join-room"); + this.track(joinRoom.subscribe((joinRoomOpen) => { + this._updateJoinRoom(joinRoomOpen); + })); + this._updateJoinRoom(joinRoom.get()); + const lightbox = this.navigation.observe("lightbox"); this.track(lightbox.subscribe(eventId => { this._updateLightbox(eventId); @@ -121,7 +129,13 @@ export class SessionViewModel extends ViewModel { } get activeMiddleViewModel() { - return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel || this._createRoomViewModel; + return ( + this._roomViewModelObservable?.get() || + this._gridViewModel || + this._settingsViewModel || + this._createRoomViewModel || + this._joinRoomViewModel + ); } get roomGridViewModel() { @@ -152,6 +166,10 @@ export class SessionViewModel extends ViewModel { return this._createRoomViewModel; } + get joinRoomViewModel() { + return this._joinRoomViewModel; + } + _updateGrid(roomIds) { const changed = !(this._gridViewModel && roomIds); const currentRoomId = this.navigation.path.get("room"); @@ -286,6 +304,16 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } + _updateJoinRoom(joinRoomOpen) { + if (this._joinRoomViewModel) { + this._joinRoomViewModel = this.disposeTracked(this._joinRoomViewModel); + } + if (joinRoomOpen) { + this._joinRoomViewModel = this.track(new JoinRoomViewModel(this.childOptions({session: this._client.session}))); + } + this.emitChange("activeMiddleViewModel"); + } + _updateLightbox(eventId) { if (this._lightboxViewModel) { this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); From f1b86e35324fdb16929f7b645fd8de948d5282b9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:20:30 +0530 Subject: [PATCH 244/354] Add method to show join room view --- src/domain/session/leftpanel/LeftPanelViewModel.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index fc783cfa..ce9aa0ee 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -77,6 +77,10 @@ export class LeftPanelViewModel extends ViewModel { this.navigation.push("create-room"); } + showJoinRoomView() { + this.navigation.push("join-room"); + } + _setupNavigation() { const roomObservable = this.navigation.observe("room"); this.track(roomObservable.subscribe(roomId => this._open(roomId))); From 2d4b6b03413e5978e9206b3fef43894d4e34e294 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:22:10 +0530 Subject: [PATCH 245/354] Exctract into function --- src/domain/session/room/RoomViewModel.js | 19 +++-------- src/matrix/room/joinRoom.ts | 42 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 src/matrix/room/joinRoom.ts diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 75f90730..2328a6be 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -23,7 +23,7 @@ import {imageToInfo} from "../common.js"; // TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry // this is a breaking SDK change though to make this option mandatory import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; -import {RoomStatus} from "../../../matrix/room/common"; +import {joinRoom} from "../../../matrix/room/joinRoom"; export class RoomViewModel extends ViewModel { constructor(options) { @@ -200,22 +200,11 @@ export class RoomViewModel extends ViewModel { async _processCommandJoin(roomName) { try { - const roomId = await this._options.client.session.joinRoom(roomName); - const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId); - await roomStatusObserver.waitFor(status => status === RoomStatus.Joined); + const session = this._options.client.session; + const roomId = await joinRoom(roomName, session); this.navigation.push("room", roomId); } catch (err) { - let exc; - if ((err.statusCode ?? err.status) === 400) { - exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`); - } else if ((err.statusCode ?? err.status) === 404 || (err.statusCode ?? err.status) === 502 || err.message == "Internal Server Error") { - exc = new Error(`/join : room '${roomName}' not found`); - } else if ((err.statusCode ?? err.status) === 403) { - exc = new Error(`/join : you're not invited to join '${roomName}'`); - } else { - exc = err; - } - this._sendError = exc; + this._sendError = err; this._timelineError = null; this.emitChange("error"); } diff --git a/src/matrix/room/joinRoom.ts b/src/matrix/room/joinRoom.ts new file mode 100644 index 00000000..cd2d1611 --- /dev/null +++ b/src/matrix/room/joinRoom.ts @@ -0,0 +1,42 @@ +/* +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 type {Session} from "../Session.js"; +import {RoomStatus} from "./common"; + +/** + * Join a room and wait for it to arrive in the next sync + * @param roomId The id of the room to join + * @param session A session instance + */ +export async function joinRoom(roomId: string, session: Session) { + try { + const internalRoomId = await session.joinRoom(roomId); + const roomStatusObserver = await session.observeRoomStatus(internalRoomId); + await roomStatusObserver.waitFor((status: RoomStatus) => status === RoomStatus.Joined); + return internalRoomId; + } + catch (e) { + if ((e.statusCode ?? e.status) === 400) { + throw new Error(`'${roomId}' is not a legal room ID or alias`); + } else if ((e.statusCode ?? e.status) === 404 || (e.statusCode ?? e.status) === 502 || e.message == "Internal Server eor") { + throw new Error(`Room '${roomId}' could not be found`); + } else if ((e.statusCode ?? e.status) === 403) { + throw new Error(`You are not invited to join '${roomId}'`); + } else { + throw e; + } + } +} From d6ba218c08e01455d6e12cbef70cc90941645bc4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:22:53 +0530 Subject: [PATCH 246/354] Return view from SessionView --- src/platform/web/ui/session/SessionView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index ef63b29b..7bcd8c0f 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -29,6 +29,7 @@ import {SettingsView} from "./settings/SettingsView.js"; import {CreateRoomView} from "./CreateRoomView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; import {viewClassForTile} from "./room/common"; +import {JoinRoomView} from "./JoinRoomView"; export class SessionView extends TemplateView { render(t, vm) { @@ -48,6 +49,8 @@ export class SessionView extends TemplateView { return new SettingsView(vm.settingsViewModel); } else if (vm.createRoomViewModel) { return new CreateRoomView(vm.createRoomViewModel); + } else if (vm.joinRoomViewModel) { + return new JoinRoomView(vm.joinRoomViewModel); } else if (vm.currentRoomViewModel) { if (vm.currentRoomViewModel.kind === "invite") { return new InviteView(vm.currentRoomViewModel); From 3426240ef4e27ac476ea88036b1fd18bc3384c4f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:23:44 +0530 Subject: [PATCH 247/354] Style JoinRoom View --- src/platform/web/ui/css/themes/element/theme.css | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 05681cbb..e6f5b7a1 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1180,7 +1180,7 @@ button.RoomDetailsView_row::after { gap: 12px; } -.CreateRoomView, .RoomBeingCreated_error { +.CreateRoomView, .JoinRoomView, .RoomBeingCreated_error { max-width: 400px; } @@ -1211,3 +1211,14 @@ button.RoomDetailsView_row::after { background-position: center; background-size: 36px; } + +.JoinRoomView_status { + display: flex; + align-items: center; + justify-content: center; + margin-top: 10px; +} + +.JoinRoomView_status .spinner { + margin-right: 5px; +} From 44eddd0d6b720296b8f6bd335905edc2c44a8f40 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 17:24:58 +0530 Subject: [PATCH 248/354] Call method from menu --- src/platform/web/ui/session/leftpanel/LeftPanelView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index ac0fbb81..fb2feb57 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -116,8 +116,8 @@ export class LeftPanelView extends TemplateView { } else { const vm = this.value; const options = []; - options.push(Menu.option(vm.i18n`Create Room`, () => { vm.showCreateRoomView(); })) - options.push(Menu.option(vm.i18n`Join Room`, () => {})) + options.push(Menu.option(vm.i18n`Create Room`, () => vm.showCreateRoomView())); + options.push(Menu.option(vm.i18n`Join Room`, () => vm.showJoinRoomView())); this._createMenuPopup = new Popup(new Menu(options)); this._createMenuPopup.trackInTemplateView(this); this._createMenuPopup.showRelativeTo(evt.target, 10); From 88091622dbf52841640ad9d5040f076cd9531c18 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 23:21:55 +0530 Subject: [PATCH 249/354] Rename variable --- src/matrix/room/joinRoom.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/joinRoom.ts b/src/matrix/room/joinRoom.ts index cd2d1611..a4f3550e 100644 --- a/src/matrix/room/joinRoom.ts +++ b/src/matrix/room/joinRoom.ts @@ -24,8 +24,8 @@ import {RoomStatus} from "./common"; export async function joinRoom(roomId: string, session: Session) { try { const internalRoomId = await session.joinRoom(roomId); - const roomStatusObserver = await session.observeRoomStatus(internalRoomId); - await roomStatusObserver.waitFor((status: RoomStatus) => status === RoomStatus.Joined); + const roomStatusObservable = await session.observeRoomStatus(internalRoomId); + await roomStatusObservable.waitFor((status: RoomStatus) => status === RoomStatus.Joined); return internalRoomId; } catch (e) { From 1898c4892e5a6c28061f5b4139f5903af6727886 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 23:44:51 +0530 Subject: [PATCH 250/354] Specify return type --- src/matrix/room/joinRoom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/joinRoom.ts b/src/matrix/room/joinRoom.ts index a4f3550e..7f2ed19e 100644 --- a/src/matrix/room/joinRoom.ts +++ b/src/matrix/room/joinRoom.ts @@ -21,7 +21,7 @@ import {RoomStatus} from "./common"; * @param roomId The id of the room to join * @param session A session instance */ -export async function joinRoom(roomId: string, session: Session) { +export async function joinRoom(roomId: string, session: Session): Promise { try { const internalRoomId = await session.joinRoom(roomId); const roomStatusObservable = await session.observeRoomStatus(internalRoomId); From 67dfbc5e28f51d68939f97e6895b90261d4b32cb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Sep 2022 23:48:01 +0530 Subject: [PATCH 251/354] Add return type --- src/domain/session/JoinRoomViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/JoinRoomViewModel.ts b/src/domain/session/JoinRoomViewModel.ts index a4be548b..1b7148f9 100644 --- a/src/domain/session/JoinRoomViewModel.ts +++ b/src/domain/session/JoinRoomViewModel.ts @@ -33,7 +33,7 @@ export class JoinRoomViewModel extends ViewModel { this._session = options.session; } - async join(roomId: string) { + async join(roomId: string): Promise { this._error = undefined; this._joinInProgress = true; this.emitChange("joinInProgress"); From 8f414f4cf45cf432a59783aa6e5e4032db50dc2f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 16 Sep 2022 13:50:29 -0500 Subject: [PATCH 252/354] Log errors when mounting views to the console for easier reference From the console, I can click the source references in the stack trace to jump to the spot in the code where things are going wrong. It also helps with the problem of the error not having enough space to be read in some components. Split off from https://github.com/vector-im/hydrogen-web/pull/653 --- src/platform/web/ui/general/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/web/ui/general/utils.ts b/src/platform/web/ui/general/utils.ts index b310571f..3f470048 100644 --- a/src/platform/web/ui/general/utils.ts +++ b/src/platform/web/ui/general/utils.ts @@ -22,6 +22,9 @@ export function mountView(view: IView, mountArgs?: IMountArgs): ViewNode { try { node = view.mount(mountArgs); } catch (err) { + // Log it to the console so it's easy to reference + console.error(err); + // Then render our error boundary to the DOM node = errorToDOM(err); } return node; From b096ac18ca1ed29cfc6a46daf3634c15e1a20ceb Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 16 Sep 2022 13:57:15 -0500 Subject: [PATCH 253/354] Add header, footer, style HTML tags to template with Split off from https://github.com/vector-im/hydrogen-web/pull/653 --- src/platform/web/ui/general/html.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 44f7476a..4e1fe78d 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -102,10 +102,11 @@ export const SVG_NS: string = "http://www.w3.org/2000/svg"; export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", + "p", "strong", "em", "span", "img", "section", "header", "main", "footer", + "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", - "progress", "output", "video"], + "progress", "output", "video", "style"], [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; From 90ac16854b98dac6c8a41fed8bff010dcd505e1b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 16 Sep 2022 13:59:50 -0500 Subject: [PATCH 254/354] Add `data-event-id` to `AnnouncementView` so it appears for state events in the timeline Follow-up to https://github.com/vector-im/hydrogen-web/pull/690 Split off from https://github.com/vector-im/hydrogen-web/pull/653 --- .../web/ui/session/room/timeline/AnnouncementView.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/room/timeline/AnnouncementView.js b/src/platform/web/ui/session/room/timeline/AnnouncementView.js index 5ae92daa..8b68d33b 100644 --- a/src/platform/web/ui/session/room/timeline/AnnouncementView.js +++ b/src/platform/web/ui/session/room/timeline/AnnouncementView.js @@ -22,8 +22,11 @@ export class AnnouncementView extends TemplateView { super(vm); } - render(t) { - return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement)); + render(t, vm) { + return t.li({ + className: "AnnouncementView", + 'data-event-id': vm.eventId + }, t.div(vm => vm.announcement)); } /* This is called by the parent ListView, which just has 1 listener for the whole list */ From 0e40258404f09221a04b23537b1a7b8a258321f7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 19 Sep 2022 12:20:50 -0500 Subject: [PATCH 255/354] Fix SDK asset build failing on Windows (#859) Fix: ```sh $ yarn run vite build -c vite.sdk-assets-config.js yarn run v1.22.18 $ C:\Users\MLM\Documents\GitHub\element\hydrogen-web\node_modules\.bin\vite build -c vite.sdk-assets-config.js locally linked postcss cleanUrl(id) C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?type=runtime [build-themes] Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error during build: Error: Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. ``` Regressed in: https://github.com/vector-im/hydrogen-web/pull/769/files#diff-5432b565e86d2514c825ed9972c37ea19820bf12b5d8d3203fc9d4ea4654bd34L20 where the `const path = require('path');` was removed but we also started using `path` in more places which needed the same treatment. When making the fix, we also have to make sure we don't also regress: https://github.com/vector-im/hydrogen-web/pull/750 --- scripts/build-plugins/rollup-plugin-build-themes.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index c8c73220..d159f1db 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -13,7 +13,13 @@ 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. */ -const path = require('path').posix; +// Use the path implementation native to the platform so paths from disk play +// well with resolving against the relative location (think Windows `C:\` and +// backslashes). +const path = require('path'); +// Use the posix (forward slash) implementation when working with `import` paths +// to reference resources +const posixPath = require('path').posix; const {optimize} = require('svgo'); async function readCSSSource(location) { @@ -238,7 +244,7 @@ module.exports = function buildThemes(options) { switch (file) { case "index.js": { const isDark = variants[variant].dark; - return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + + return `import "${posixPath.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + `import "@theme/${theme}/${variant}/variables.css"`; } case "variables.css": { From 2e9470027bd76556c74730ffdcf5f493d035bd07 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:50:37 +0000 Subject: [PATCH 256/354] Update src/platform/web/ui/session/JoinRoomView.ts --- src/platform/web/ui/session/JoinRoomView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/JoinRoomView.ts b/src/platform/web/ui/session/JoinRoomView.ts index bc6d751f..90194317 100644 --- a/src/platform/web/ui/session/JoinRoomView.ts +++ b/src/platform/web/ui/session/JoinRoomView.ts @@ -24,7 +24,7 @@ export class JoinRoomView extends TemplateView { type: "text", name: "id", id: "id", - placeholder: vm.i18n`Enter a room id`, + placeholder: vm.i18n`Enter a room id or alias`, disabled: vm => vm.joinInProgress, }); return t.main({className: "middle"}, From c8e6ca9a83cdd9a90b9f39207ff050613909c390 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sun, 18 Sep 2022 21:56:05 -0400 Subject: [PATCH 257/354] typescriptifying linkify and regex --- .../session/room/timeline/MessageBody.js | 2 +- .../session/room/timeline/deserialize.js | 2 +- .../linkify/{linkify.js => linkify.ts} | 46 +++++++++---------- .../timeline/linkify/{regex.js => regex.ts} | 0 4 files changed, 25 insertions(+), 25 deletions(-) rename src/domain/session/room/timeline/linkify/{linkify.js => linkify.ts} (77%) rename src/domain/session/room/timeline/linkify/{regex.js => regex.ts} (100%) diff --git a/src/domain/session/room/timeline/MessageBody.js b/src/domain/session/room/timeline/MessageBody.js index 65b487a9..f2df1166 100644 --- a/src/domain/session/room/timeline/MessageBody.js +++ b/src/domain/session/room/timeline/MessageBody.js @@ -1,4 +1,4 @@ -import { linkify } from "./linkify/linkify.js"; +import { linkify } from "./linkify/linkify"; import { getIdentifierColorNumber, avatarInitials } from "../../../avatar"; /** diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js index b59c2e59..2e2a30f2 100644 --- a/src/domain/session/room/timeline/deserialize.js +++ b/src/domain/session/room/timeline/deserialize.js @@ -15,7 +15,7 @@ limitations under the License. */ import { MessageBody, HeaderBlock, TableBlock, ListBlock, CodeBlock, PillPart, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js" -import { linkify } from "./linkify/linkify.js"; +import {linkify} from "./linkify/linkify"; /* At the time of writing (Jul 1 2021), Matrix Spec recommends * allowing the following HTML tags: diff --git a/src/domain/session/room/timeline/linkify/linkify.js b/src/domain/session/room/timeline/linkify/linkify.ts similarity index 77% rename from src/domain/session/room/timeline/linkify/linkify.js rename to src/domain/session/room/timeline/linkify/linkify.ts index 6c285d79..6e5f90d1 100644 --- a/src/domain/session/room/timeline/linkify/linkify.js +++ b/src/domain/session/room/timeline/linkify/linkify.ts @@ -21,10 +21,8 @@ import { regex } from "./regex.js"; * For each such separated token, callback is called * with the token and a boolean passed as argument. * The boolean indicates whether the token is a link or not. - * @param {string} text Text to split - * @param {function(string, boolean)} callback A function to call with split tokens */ -export function linkify(text, callback) { +export function linkify(text: string, callback: (token: string, isLink: boolean) => void): void { const matches = text.matchAll(regex); let curr = 0; for (let match of matches) { @@ -32,16 +30,18 @@ export function linkify(text, callback) { callback(precedingText, false); callback(match[0], true); const len = match[0].length; - curr = match.index + len; + curr = match.index! + len; } const remainingText = text.slice(curr); callback(remainingText, false); } -export function tests() { +export function tests(): any { class MockCallback { - mockCallback(text, isLink) { + result: { type: "link" | "text", text: string }[]; + + mockCallback(text: string, isLink: boolean): void { if (!text.length) { return; } @@ -53,13 +53,13 @@ export function tests() { } } - function test(assert, input, output) { + function test(assert, input, output): void { const m = new MockCallback; linkify(input, m.mockCallback.bind(m)); assert.deepEqual(output, m.result); } - function testLink(assert, link, expectFail = false) { + function testLink(assert, link, expectFail = false): void { const input = link; const output = expectFail ? [{ type: "text", text: input }] : [{ type: "link", text: input }]; @@ -67,23 +67,23 @@ export function tests() { } return { - "Link with host": assert => { + "Link with host": (assert): void => { testLink(assert, "https://matrix.org"); }, - "Link with host & path": assert => { + "Link with host & path": (assert): void => { testLink(assert, "https://matrix.org/docs/develop"); }, - "Link with host & fragment": assert => { + "Link with host & fragment": (assert): void => { testLink(assert, "https://matrix.org#test"); }, - "Link with host & query": assert => { + "Link with host & query": (assert): void => { testLink(assert, "https://matrix.org/?foo=bar"); }, - "Complex link": assert => { + "Complex link": (assert): void => { const link = "https://www.foobar.com/url?sa=t&rct=j&q=&esrc=s&source" + "=web&cd=&cad=rja&uact=8&ved=2ahUKEwjyu7DJ-LHwAhUQyzgGHc" + "OKA70QFjAAegQIBBAD&url=https%3A%2F%2Fmatrix.org%2Fdocs%" + @@ -92,26 +92,26 @@ export function tests() { testLink(assert, link); }, - "Localhost link": assert => { + "Localhost link": (assert): void => { testLink(assert, "http://localhost"); testLink(assert, "http://localhost:3000"); }, - "IPV4 link": assert => { + "IPV4 link": (assert): void => { testLink(assert, "https://192.0.0.1"); testLink(assert, "https://250.123.67.23:5924"); }, - "IPV6 link": assert => { + "IPV6 link": (assert): void => { testLink(assert, "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"); testLink(assert, "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:7000"); }, - "Missing scheme must not linkify": assert => { + "Missing scheme must not linkify": (assert): void => { testLink(assert, "matrix.org/foo/bar", true); }, - "Punctuation at end of link must not linkify": assert => { + "Punctuation at end of link must not linkify": (assert): void => { const link = "https://foo.bar/?nenjil=lal810"; const end = ".,? "; for (const char of end) { @@ -120,28 +120,28 @@ export function tests() { } }, - "Link doesn't adopt closing parenthesis": assert => { + "Link doesn't adopt closing parenthesis": (assert): void => { const link = "(https://matrix.org)"; const out = [{ type: "text", text: "(" }, { type: "link", text: "https://matrix.org" }, { type: "text", text: ")" }]; test(assert, link, out); }, - "Unicode in hostname must not linkify": assert => { + "Unicode in hostname must not linkify": (assert): void => { const link = "https://foo.bar\uD83D\uDE03.com"; const out = [{ type: "link", text: "https://foo.bar" }, { type: "text", text: "\uD83D\uDE03.com" }]; test(assert, link, out); }, - "Link with unicode only after / must linkify": assert => { + "Link with unicode only after / must linkify": (assert): void => { testLink(assert, "https://foo.bar.com/\uD83D\uDE03"); }, - "Link with unicode after fragment without path must linkify": assert => { + "Link with unicode after fragment without path must linkify": (assert): void => { testLink(assert, "https://foo.bar.com#\uD83D\uDE03"); }, - "Link ends with <": assert => { + "Link ends with <": (assert): void => { const link = "https://matrix.org<"; const out = [{ type: "link", text: "https://matrix.org" }, { type: "text", text: "<" }]; test(assert, link, out); diff --git a/src/domain/session/room/timeline/linkify/regex.js b/src/domain/session/room/timeline/linkify/regex.ts similarity index 100% rename from src/domain/session/room/timeline/linkify/regex.js rename to src/domain/session/room/timeline/linkify/regex.ts From 92ae3a3236e63c601b7d9e07ae79a5fb8e2b159a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Oct 2022 11:41:31 +0530 Subject: [PATCH 258/354] vm may not have created when this is called --- src/domain/session/room/RoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 2328a6be..be2e7832 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -128,7 +128,7 @@ export class RoomViewModel extends ViewModel { // so emit all fields originating from summary _onRoomChange() { // propagate the update to the child view models so it's bindings can update based on room changes - this._composerVM.emitChange(); + this._composerVM?.emitChange(); this.emitChange(); } From 85a9a019c8d164ca9a2fe2eb237699bac4366a4e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Oct 2022 17:58:07 +0530 Subject: [PATCH 259/354] Don't inhibit login --- src/matrix/net/HomeServerApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index e9902ef8..bb791577 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -164,7 +164,7 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: BaseRequestOptions = {}): IHomeServerRequest { + register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = false , options: BaseRequestOptions = {}): IHomeServerRequest { options.allowedStatusCodes = [401]; const body: any = { auth, From 1f764664c9c08bc42da36a9168f0e84bf707f744 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Oct 2022 17:58:33 +0530 Subject: [PATCH 260/354] Expose homeserver from Registration object --- src/matrix/registration/Registration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index ded66719..e47bc2db 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -35,8 +35,10 @@ export class Registration { private readonly _accountDetails: AccountDetails; private readonly _flowSelector: FlowSelector; private _sessionInfo?: RegistrationResponseSuccess + public readonly homeserver: string; - constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { + constructor(homeserver: string, hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { + this.homeserver = homeserver; this._hsApi = hsApi; this._accountDetails = accountDetails; this._flowSelector = flowSelector ?? (flows => flows[0]); From a448c0218defbc104e2116d7f76d92c194e2826a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Oct 2022 18:01:54 +0530 Subject: [PATCH 261/354] Fix missing semicolon --- src/matrix/registration/Registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index e47bc2db..82e3ade5 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -34,7 +34,7 @@ export class Registration { private readonly _hsApi: HomeServerApi; private readonly _accountDetails: AccountDetails; private readonly _flowSelector: FlowSelector; - private _sessionInfo?: RegistrationResponseSuccess + private _sessionInfo?: RegistrationResponseSuccess; public readonly homeserver: string; constructor(homeserver: string, hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { From dcba6d1500e13492240c90d35eada669210544d4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Oct 2022 18:05:54 +0530 Subject: [PATCH 262/354] Split method --- src/matrix/Client.js | 48 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index f81b9085..2670ce07 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -137,7 +137,7 @@ export class Client { async startRegistration(homeserver, username, password, initialDeviceDisplayName, flowSelector) { const request = this._platform.request; const hsApi = new HomeServerApi({homeserver, request}); - const registration = new Registration(hsApi, { + const registration = new Registration(homeserver, hsApi, { username, password, initialDeviceDisplayName, @@ -146,6 +146,21 @@ export class Client { return registration; } + async startWithFinishedRegistration(registration) { + this._platform.logger.run("startWithFinishedRegistration", async (log) => { + const sessionInfo = registration.sessionInfo; + if (!sessionInfo) { + throw new Error("Registration.sessionInfo is not available; are you sure that registration is finished?"); + } + await this.startWithSessionInfo({ + accessToken: sessionInfo.access_token, + deviceId: sessionInfo.device_id, + userId: sessionInfo.user_id, + homeserver: registration.homeserver, + }, true, log); + }); + } + async startWithLogin(loginMethod, {inspectAccountSetup} = {}) { const currentStatus = this._status.get(); if (currentStatus !== LoadStatus.LoginFailed && @@ -156,23 +171,17 @@ export class Client { this._resetStatus(); await this._platform.logger.run("login", async log => { this._status.set(LoadStatus.Login); - const clock = this._platform.clock; let sessionInfo; try { const request = this._platform.request; const hsApi = new HomeServerApi({homeserver: loginMethod.homeserver, request}); const loginData = await loginMethod.login(hsApi, "Hydrogen", log); - const sessionId = this.createNewSessionId(); sessionInfo = { - id: sessionId, deviceId: loginData.device_id, userId: loginData.user_id, - homeServer: loginMethod.homeserver, // deprecate this over time homeserver: loginMethod.homeserver, accessToken: loginData.access_token, - lastUsed: clock.now() }; - log.set("id", sessionId); } catch (err) { this._error = err; if (err.name === "HomeServerError") { @@ -191,9 +200,26 @@ export class Client { } return; } + await this.startWithSessionInfo(sessionInfo, inspectAccountSetup, log); + }); + } + + async startWithSessionInfo({deviceId, userId, accessToken, homeserver}, inspectAccountSetup, log) { + await log.wrap("startWithSessionInfo", async (l) => { + const id = this.createNewSessionId(); + const lastUsed = this._platform.clock.now(); + const sessionInfo = { + id, + deviceId, + userId, + homeServer: homeserver, // deprecate this over time + homeserver, + accessToken, + lastUsed, + }; let dehydratedDevice; if (inspectAccountSetup) { - dehydratedDevice = await this._inspectAccountAfterLogin(sessionInfo, log); + dehydratedDevice = await this._inspectAccountAfterLogin(sessionInfo, l); if (dehydratedDevice) { sessionInfo.deviceId = dehydratedDevice.deviceId; } @@ -203,10 +229,10 @@ export class Client { // LoadStatus.Error in case of an error, // so separate try/catch try { - await this._loadSessionInfo(sessionInfo, dehydratedDevice, log); - log.set("status", this._status.get()); + await this._loadSessionInfo(sessionInfo, dehydratedDevice, l); + l.set("status", this._status.get()); } catch (err) { - log.catch(err); + l.catch(err); // free olm Account that might be contained dehydratedDevice?.dispose(); this._error = err; From f46d2c1bf5ee01432bc2ef09296d72c194534ecf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:17:32 +0200 Subject: [PATCH 263/354] make startWithFinishedRegistration more broadly useful --- src/matrix/Client.js | 25 ++++++++++--------------- src/matrix/registration/Registration.ts | 16 ++++++++++++---- src/matrix/registration/types.ts | 7 +++++++ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 2670ce07..4d6f7bb5 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -146,18 +146,13 @@ export class Client { return registration; } - async startWithFinishedRegistration(registration) { - this._platform.logger.run("startWithFinishedRegistration", async (log) => { - const sessionInfo = registration.sessionInfo; - if (!sessionInfo) { - throw new Error("Registration.sessionInfo is not available; are you sure that registration is finished?"); - } - await this.startWithSessionInfo({ - accessToken: sessionInfo.access_token, - deviceId: sessionInfo.device_id, - userId: sessionInfo.user_id, - homeserver: registration.homeserver, - }, true, log); + /** Method to start client after registration or with given access token. + * To start the client after registering, use `startWithAuthData(registration.authData)`. + * `homeserver` won't be resolved or normalized using this method, + * use `lookupHomeserver` first if needed (not needed after registration) */ + async startWithAuthData({accessToken, deviceId, userId, homeserver}) { + this._platform.logger.run("startWithAuthData", async (log) => { + await this._createSessionAfterAuth({accessToken, deviceId, userId, homeserver}, true, log); }); } @@ -200,12 +195,12 @@ export class Client { } return; } - await this.startWithSessionInfo(sessionInfo, inspectAccountSetup, log); + await this._createSessionAfterAuth(sessionInfo, inspectAccountSetup, log); }); } - async startWithSessionInfo({deviceId, userId, accessToken, homeserver}, inspectAccountSetup, log) { - await log.wrap("startWithSessionInfo", async (l) => { + async _createSessionAfterAuth({deviceId, userId, accessToken, homeserver}, inspectAccountSetup, log) { + await log.wrap("_createSessionAfterAuth", async (l) => { const id = this.createNewSessionId(); const lastUsed = this._platform.clock.now(); const sessionInfo = { diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 82e3ade5..5440d66d 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -25,6 +25,7 @@ import type { RegistrationResponseMoreDataNeeded, RegistrationResponse, RegistrationResponseSuccess, + AuthData, RegistrationParams, } from "./types"; @@ -34,7 +35,7 @@ export class Registration { private readonly _hsApi: HomeServerApi; private readonly _accountDetails: AccountDetails; private readonly _flowSelector: FlowSelector; - private _sessionInfo?: RegistrationResponseSuccess; + private _registerResponse?: RegistrationResponseSuccess; public readonly homeserver: string; constructor(homeserver: string, hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { @@ -93,7 +94,7 @@ export class Registration { private async parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { switch (response.status) { case 200: - this._sessionInfo = response; + this._registerResponse = response; return undefined; case 401: if (response.completed?.includes(currentStage.type)) { @@ -119,7 +120,14 @@ export class Registration { } } - get sessionInfo(): RegistrationResponseSuccess | undefined { - return this._sessionInfo; + get authData(): AuthData | undefined { + if (this._registerResponse) { + return { + accessToken: this._registerResponse.access_token, + homeserver: this.homeserver, + userId: this._registerResponse.user_id, + deviceId: this._registerResponse.device_id, + }; + } } } diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts index f1ddbe98..7e0d01ff 100644 --- a/src/matrix/registration/types.ts +++ b/src/matrix/registration/types.ts @@ -38,6 +38,13 @@ export type RegistrationResponseSuccess = { status: 200; } +export type AuthData = { + userId: string; + deviceId: string; + homeserver: string; + accessToken?: string; +} + export type RegistrationFlow = { stages: string[]; } From 1f8a591cf64d9793f45295d52d8717b52ce9c327 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:28:42 +0200 Subject: [PATCH 264/354] dont need extra log depth --- src/matrix/Client.js | 64 +++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 4d6f7bb5..68eeed27 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -200,40 +200,38 @@ export class Client { } async _createSessionAfterAuth({deviceId, userId, accessToken, homeserver}, inspectAccountSetup, log) { - await log.wrap("_createSessionAfterAuth", async (l) => { - const id = this.createNewSessionId(); - const lastUsed = this._platform.clock.now(); - const sessionInfo = { - id, - deviceId, - userId, - homeServer: homeserver, // deprecate this over time - homeserver, - accessToken, - lastUsed, - }; - let dehydratedDevice; - if (inspectAccountSetup) { - dehydratedDevice = await this._inspectAccountAfterLogin(sessionInfo, l); - if (dehydratedDevice) { - sessionInfo.deviceId = dehydratedDevice.deviceId; - } + const id = this.createNewSessionId(); + const lastUsed = this._platform.clock.now(); + const sessionInfo = { + id, + deviceId, + userId, + homeServer: homeserver, // deprecate this over time + homeserver, + accessToken, + lastUsed, + }; + let dehydratedDevice; + if (inspectAccountSetup) { + dehydratedDevice = await this._inspectAccountAfterLogin(sessionInfo, log); + if (dehydratedDevice) { + sessionInfo.deviceId = dehydratedDevice.deviceId; } - await this._platform.sessionInfoStorage.add(sessionInfo); - // loading the session can only lead to - // LoadStatus.Error in case of an error, - // so separate try/catch - try { - await this._loadSessionInfo(sessionInfo, dehydratedDevice, l); - l.set("status", this._status.get()); - } catch (err) { - l.catch(err); - // free olm Account that might be contained - dehydratedDevice?.dispose(); - this._error = err; - this._status.set(LoadStatus.Error); - } - }); + } + await this._platform.sessionInfoStorage.add(sessionInfo); + // loading the session can only lead to + // LoadStatus.Error in case of an error, + // so separate try/catch + try { + await this._loadSessionInfo(sessionInfo, dehydratedDevice, log); + log.set("status", this._status.get()); + } catch (err) { + log.catch(err); + // free olm Account that might be contained + dehydratedDevice?.dispose(); + this._error = err; + this._status.set(LoadStatus.Error); + } } async _loadSessionInfo(sessionInfo, dehydratedDevice, log) { From d37965ef91496d3407c5dae3d80a54f0f9c0b99e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 14 Oct 2022 20:08:26 +0200 Subject: [PATCH 265/354] return promise from _createSessionAfterAuth --- src/matrix/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 68eeed27..ce0c44d7 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -151,7 +151,7 @@ export class Client { * `homeserver` won't be resolved or normalized using this method, * use `lookupHomeserver` first if needed (not needed after registration) */ async startWithAuthData({accessToken, deviceId, userId, homeserver}) { - this._platform.logger.run("startWithAuthData", async (log) => { + await this._platform.logger.run("startWithAuthData", async (log) => { await this._createSessionAfterAuth({accessToken, deviceId, userId, homeserver}, true, log); }); } From 2d526bc08829d77b864c52229f997bae4d8d2712 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 14 Oct 2022 20:13:23 +0200 Subject: [PATCH 266/354] fix sdk build failing on tsc error --- src/observable/map/BaseObservableMap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 9b501285..f81a94f8 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -64,8 +64,8 @@ export abstract class BaseObservableMap extends BaseObservable): JoinedMap { - return new JoinedMap([this].concat(otherMaps)); + join>(...otherMaps: Array): JoinedMap { + return new JoinedMap([this as BaseObservableMap].concat(otherMaps)); } mapValues(mapper: Mapper, updater?: Updater): MappedMap { From b9ed70e453e03db338919bc0fc47fccc152f81df Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 14 Oct 2022 20:16:56 +0200 Subject: [PATCH 267/354] release sdk 0.1.1 --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index b00b0b62..92df93c5 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.1.0", + "version": "0.1.1", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From b4f4636555e4790c36dd9976e6a297293a6bb668 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 17 Oct 2022 15:56:34 -0500 Subject: [PATCH 268/354] Clarify that we no-op because if it's already aborted, that's what we wanted to do anyway (#861) Split off from https://github.com/vector-im/hydrogen-web/pull/653 --- src/matrix/storage/idb/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 4ac373d2..44149e12 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -83,7 +83,11 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore, // try aborting on error, if that hasn't been done already try { txn.abort(); - } catch (err) {} + } catch (err) { + // No-op: `InvalidStateError` is only thrown if the transaction has + // already been committed or aborted. Since we wanted the txn to + // be aborted anyway, it doesn't matter if this fails. + } } }; return reqAsPromise(req); From 9ad5dd752ca8c4dc7924d83462a0788c5ba5b465 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 17 Oct 2022 15:59:24 -0500 Subject: [PATCH 269/354] Make no content PL fallback to `0` (#863) Make no `content` PL fallback to `0`. A valid PL event will have `content` but when you're just stubbing a PL event with an empty object `{}`, it doesn't need to exist and we can just as easily fallback to `0` PL level. Split off from https://github.com/vector-im/hydrogen-web/pull/653 Changes necessary for https://github.com/matrix-org/matrix-public-archive/pull/57 --- src/lib.ts | 1 + src/matrix/room/PowerLevels.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index df96bfcd..cbb943e0 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -17,6 +17,7 @@ limitations under the License. export {Platform} from "./platform/web/Platform.js"; export {Client, LoadStatus} from "./matrix/Client.js"; export {RoomStatus} from "./matrix/room/common"; +export {PowerLevels} from "./matrix/room/PowerLevels.js"; // export main view & view models export {createNavigation, createRouter} from "./domain/navigation/index"; export {RootViewModel} from "./domain/RootViewModel.js"; diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index bb723c34..76e062ef 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -75,11 +75,11 @@ export class PowerLevels { } _getEventTypeLevel(eventType) { - const level = this._plEvent?.content.events?.[eventType]; + const level = this._plEvent?.content?.events?.[eventType]; if (typeof level === "number") { return level; } else { - const level = this._plEvent?.content.events_default; + const level = this._plEvent?.content?.events_default; if (typeof level === "number") { return level; } else { From 3ee26e14d74a915578048b233368a056cef84477 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 18 Oct 2022 23:44:58 +0530 Subject: [PATCH 270/354] Convert Cypress --> Playwright --- .gitignore | 3 +- cypress.config.ts | 17 --- cypress/e2e/login.cy.ts | 87 ------------- cypress/plugins/index.ts | 38 ------ cypress/plugins/log.ts | 35 ----- cypress/plugins/performance.ts | 47 ------- cypress/plugins/webserver.ts | 52 -------- cypress/support/dex.ts | 51 -------- cypress/support/synapse.ts | 122 ------------------ cypress/tsconfig.json | 9 -- package.json | 3 +- playwright.config.ts | 16 +++ .../global-setup.ts | 18 ++- {cypress => playwright}/plugins/dex/index.ts | 46 ++----- .../plugins/dex/template/config.yaml | 0 .../plugins/dex/template/dev.db | Bin .../plugins/docker/index.ts | 23 +--- .../plugins/synapsedocker/index.ts | 88 ++++++------- .../synapsedocker/templates/COPYME/README.md | 0 .../templates/COPYME/homeserver.yaml | 0 .../synapsedocker/templates/COPYME/log.config | 0 .../synapsedocker/templates/consent/README.md | 0 .../templates/consent/homeserver.yaml | 0 .../templates/consent/log.config | 0 .../consent/res/templates/privacy/en/1.0.html | 0 .../res/templates/privacy/en/success.html | 0 .../synapsedocker/templates/default/README.md | 0 .../templates/default/homeserver.yaml | 0 .../templates/default/log.config | 0 .../templates/sso/homeserver.yaml | 0 .../synapsedocker/templates/sso/log.config | 0 playwright/tests/login.spec.ts | 59 +++++++++ .../tests/startup.spec.ts | 9 +- yarn.lock | 13 ++ 34 files changed, 164 insertions(+), 572 deletions(-) delete mode 100644 cypress.config.ts delete mode 100644 cypress/e2e/login.cy.ts delete mode 100644 cypress/plugins/index.ts delete mode 100644 cypress/plugins/log.ts delete mode 100644 cypress/plugins/performance.ts delete mode 100644 cypress/plugins/webserver.ts delete mode 100644 cypress/support/dex.ts delete mode 100644 cypress/support/synapse.ts delete mode 100644 cypress/tsconfig.json create mode 100644 playwright.config.ts rename cypress/e2e/startup.cy.ts => playwright/global-setup.ts (66%) rename {cypress => playwright}/plugins/dex/index.ts (74%) rename {cypress => playwright}/plugins/dex/template/config.yaml (100%) rename {cypress => playwright}/plugins/dex/template/dev.db (100%) rename {cypress => playwright}/plugins/docker/index.ts (88%) rename {cypress => playwright}/plugins/synapsedocker/index.ts (74%) rename {cypress => playwright}/plugins/synapsedocker/templates/COPYME/README.md (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/COPYME/homeserver.yaml (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/COPYME/log.config (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/consent/README.md (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/consent/homeserver.yaml (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/consent/log.config (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/default/README.md (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/default/homeserver.yaml (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/default/log.config (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/sso/homeserver.yaml (100%) rename {cypress => playwright}/plugins/synapsedocker/templates/sso/log.config (100%) create mode 100644 playwright/tests/login.spec.ts rename cypress/support/e2e.ts => playwright/tests/startup.spec.ts (73%) diff --git a/.gitignore b/.gitignore index 20f09f19..ef4f67ca 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ lib *.tar.gz .eslintcache .tmp -cypress/videos -cypress/synapselogs +playwright/synapselogs diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index 02f39392..00000000 --- a/cypress.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from "cypress"; - -export default defineConfig({ - e2e: { - setupNodeEvents(on, config) { - require("./cypress/plugins/index.ts").default(on, config); - return config; - }, - baseUrl: "http://127.0.0.1:3000", - }, - env: { - SYNAPSE_IP_ADDRESS: "172.18.0.5", - SYNAPSE_PORT: "8008", - DEX_IP_ADDRESS: "172.18.0.4", - DEX_PORT: "5556", - }, -}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts deleted file mode 100644 index 969c1884..00000000 --- a/cypress/e2e/login.cy.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* -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 type { DexInstance } from "../plugins/dex"; -import type { SynapseInstance } from "../plugins/synapsedocker"; - -describe("Login", () => { - let synapse: SynapseInstance; - let dex: DexInstance; - - beforeEach(() => { - cy.startDex().then((data) => { - dex = data; - cy.startSynapse("sso").then((data) => { - synapse = data; - }); - }); - }); - - afterEach(() => { - cy.stopSynapse(synapse); - cy.stopDex(dex); - }) - - it("Login using username/password", () => { - const username = "foobaraccount"; - const password = "password123"; - cy.registerUser(synapse, username, password); - cy.visit("/"); - cy.get("#homeserver").clear().type(synapse.baseUrl); - cy.get("#username").clear().type(username); - cy.get("#password").clear().type(password); - cy.contains("Log In").click(); - cy.get(".SessionView"); - }); - - it("Login using SSO", () => { - /** - * Add the homeserver to the localStorage manually; clicking on the start sso button would normally do this but we can't - * use two different origins in a single cypress test! - */ - cy.visit("/"); - cy.window().then(win => win.localStorage.setItem("hydrogen_setting_v1_sso_ongoing_login_homeserver", synapse.baseUrl)); - // Perform the SSO login manually using requests - const synapseAddress = synapse.baseUrl; - const dexAddress = dex.baseUrl; - // const dexAddress = `${Cypress.env("DEX_IP_ADDRESS")}:${Cypress.env("DEX_PORT")}`; - const redirectAddress = Cypress.config().baseUrl; - const ssoLoginUrl = `${synapseAddress}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(redirectAddress)}`; - cy.request(ssoLoginUrl).then(response => { - // Request the Dex page - const dexPageHtml = response.body; - const loginWithExampleLink = Cypress.$(dexPageHtml).find(`a:contains("Log in with Example")`).attr("href"); - cy.log("Login with example link", loginWithExampleLink); - - // Proceed to next page - cy.request(`${dexAddress}${loginWithExampleLink}`).then(response => { - const secondDexPageHtml = response.body; - // This req token is used to approve this login in Dex - const req = Cypress.$(secondDexPageHtml).find(`input[name=req]`).attr("value"); - cy.log("req for sso login", req); - - // Next request will redirect us back to Synapse page with "Continue" link - cy.request("POST", `${dexAddress}/dex/approval?req=${req}&approval=approve`).then(response => { - const synapseHtml = response.body; - const hydrogenLinkWithToken = Cypress.$(synapseHtml).find(`a:contains("Continue")`).attr("href"); - cy.log("SSO redirect link", hydrogenLinkWithToken); - cy.visit(hydrogenLinkWithToken); - cy.get(".SessionView"); - }); - }); - }); - }) -}); - diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts deleted file mode 100644 index 27c8b9a3..00000000 --- a/cypress/plugins/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -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 PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; -import { performance } from "./performance"; -import { synapseDocker } from "./synapsedocker"; -import { dexDocker } from "./dex"; -import { webserver } from "./webserver"; -import { docker } from "./docker"; -import { log } from "./log"; - -/** - * @type {Cypress.PluginConfig} - */ -export default function(on: PluginEvents, config: PluginConfigOptions) { - docker(on, config); - performance(on, config); - synapseDocker(on, config); - dexDocker(on, config); - webserver(on, config); - log(on, config); -} diff --git a/cypress/plugins/log.ts b/cypress/plugins/log.ts deleted file mode 100644 index 4b16c9b8..00000000 --- a/cypress/plugins/log.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* -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 PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; - -export function log(on: PluginEvents, config: PluginConfigOptions) { - on("task", { - log(message: string) { - console.log(message); - - return null; - }, - table(message: string) { - console.table(message); - - return null; - }, - }); -} diff --git a/cypress/plugins/performance.ts b/cypress/plugins/performance.ts deleted file mode 100644 index c6bd3e4c..00000000 --- a/cypress/plugins/performance.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -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 * as path from "path"; -import * as fse from "fs-extra"; - -import PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; - -// This holds all the performance measurements throughout the run -let bufferedMeasurements: PerformanceEntry[] = []; - -function addMeasurements(measurements: PerformanceEntry[]): void { - bufferedMeasurements = bufferedMeasurements.concat(measurements); - return null; -} - -async function writeMeasurementsFile() { - try { - const measurementsPath = path.join("cypress", "performance", "measurements.json"); - await fse.outputJSON(measurementsPath, bufferedMeasurements, { - spaces: 4, - }); - } finally { - bufferedMeasurements = []; - } -} - -export function performance(on: PluginEvents, config: PluginConfigOptions) { - on("task", { addMeasurements }); - on("after:run", writeMeasurementsFile); -} diff --git a/cypress/plugins/webserver.ts b/cypress/plugins/webserver.ts deleted file mode 100644 index 55a25a31..00000000 --- a/cypress/plugins/webserver.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* -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 * as http from "http"; -import { AddressInfo } from "net"; - -import PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; - -const servers: http.Server[] = []; - -function serveHtmlFile(html: string): string { - const server = http.createServer((req, res) => { - res.writeHead(200, { - "Content-Type": "text/html", - }); - res.end(html); - }); - server.listen(); - servers.push(server); - - return `http://localhost:${(server.address() as AddressInfo).port}/`; -} - -function stopWebServers(): null { - for (const server of servers) { - server.close(); - } - servers.splice(0, servers.length); // clear - - return null; // tell cypress we did the task successfully (doesn't allow undefined) -} - -export function webserver(on: PluginEvents, config: PluginConfigOptions) { - on("task", { serveHtmlFile, stopWebServers }); - on("after:run", stopWebServers); -} diff --git a/cypress/support/dex.ts b/cypress/support/dex.ts deleted file mode 100644 index 599eee26..00000000 --- a/cypress/support/dex.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -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 Chainable = Cypress.Chainable; -import AUTWindow = Cypress.AUTWindow; -import { DexInstance } from "../plugins/dex"; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - interface Chainable { - /** - * Start the dex server - */ - startDex(): Chainable; - - /** - * Stop the dex server - * @param dex the dex instance returned by startSynapse - */ - stopDex(dex: DexInstance): Chainable; - } - } -} - -function startDex(): Chainable { - return cy.task("dexStart"); -} - -function stopDex(dex?: DexInstance): Chainable { - if (!dex) return; - cy.task("dexStop", dex.dexId); -} - -Cypress.Commands.add("startDex", startDex); -Cypress.Commands.add("stopDex", stopDex); diff --git a/cypress/support/synapse.ts b/cypress/support/synapse.ts deleted file mode 100644 index 5696e8c0..00000000 --- a/cypress/support/synapse.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* -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 * as crypto from 'crypto'; - -import Chainable = Cypress.Chainable; -import AUTWindow = Cypress.AUTWindow; -import { SynapseInstance } from "../plugins/synapsedocker"; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - interface Chainable { - /** - * Start a synapse instance with a given config template. - * @param template path to template within cypress/plugins/synapsedocker/template/ directory. - */ - startSynapse(template: string): Chainable; - - /** - * Custom command wrapping task:synapseStop whilst preventing uncaught exceptions - * for if Synapse stopping races with the app's background sync loop. - * @param synapse the synapse instance returned by startSynapse - */ - stopSynapse(synapse: SynapseInstance): Chainable; - - /** - * Register a user on the given Synapse using the shared registration secret. - * @param synapse the synapse instance returned by startSynapse - * @param username the username of the user to register - * @param password the password of the user to register - * @param displayName optional display name to set on the newly registered user - */ - registerUser( - synapse: SynapseInstance, - username: string, - password: string, - displayName?: string, - ): Chainable; - } - } -} - -function startSynapse(template: string): Chainable { - return cy.task("synapseStart", template); -} - -function stopSynapse(synapse?: SynapseInstance): Chainable { - if (!synapse) return; - // Navigate away from app to stop the background network requests which will race with Synapse shutting down - return cy.window({ log: false }).then((win) => { - win.location.href = 'about:blank'; - cy.task("synapseStop", synapse.synapseId); - }); -} - -interface Credentials { - accessToken: string; - userId: string; - deviceId: string; - homeServer: string; -} - -function registerUser( - synapse: SynapseInstance, - username: string, - password: string, - displayName?: string, -): Chainable { - const url = `${synapse.baseUrl}/_synapse/admin/v1/register`; - return cy.then(() => { - // get a nonce - return cy.request<{ nonce: string }>({ url }); - }).then(response => { - const { nonce } = response.body; - const mac = crypto.createHmac('sha1', synapse.registrationSecret).update( - `${nonce}\0${username}\0${password}\0notadmin`, - ).digest('hex'); - - return cy.request<{ - access_token: string; - user_id: string; - home_server: string; - device_id: string; - }>({ - url, - method: "POST", - body: { - nonce, - username, - password, - mac, - admin: false, - displayname: displayName, - }, - }); - }).then(response => ({ - homeServer: response.body.home_server, - accessToken: response.body.access_token, - userId: response.body.user_id, - deviceId: response.body.device_id, - })); -} - -Cypress.Commands.add("startSynapse", startSynapse); -Cypress.Commands.add("stopSynapse", stopSynapse); -Cypress.Commands.add("registerUser", registerUser); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index 6bd44918..00000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "downlevelIteration": true, - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress", "node"] - }, - "include": ["**/*.ts"] -} diff --git a/package.json b/package.json index fbba3b2e..569354f0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build": "vite build && ./scripts/cleanup.sh", "build:sdk": "./scripts/sdk/build.sh", "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch", - "test:app": "vite --port 3000 & yarn run cypress open" + "test:app": "vite --port 3000 & playwright test" }, "repository": { "type": "git", @@ -32,6 +32,7 @@ }, "homepage": "https://github.com/vector-im/hydrogen-web/#readme", "devDependencies": { + "@playwright/test": "^1.27.1", "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.2", "acorn": "^8.6.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..901e48d9 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,16 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const BASE_URL = process.env["BASE_URL"] ?? "http://127.0.0.1:3000"; + +const config: PlaywrightTestConfig = { + use: { + headless: false, + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + video: 'on-first-retry', + baseURL: BASE_URL, + }, + testDir: "./playwright/tests", + globalSetup: require.resolve("./playwright/global-setup"), +}; +export default config; diff --git a/cypress/e2e/startup.cy.ts b/playwright/global-setup.ts similarity index 66% rename from cypress/e2e/startup.cy.ts rename to playwright/global-setup.ts index e7bf9c2c..5944e55c 100644 --- a/cypress/e2e/startup.cy.ts +++ b/playwright/global-setup.ts @@ -14,9 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -describe("App has no startup errors that prevent UI render", () => { - it("passes", () => { - cy.visit("/"); - cy.contains("Log In"); - }) -}) +const env = { + SYNAPSE_IP_ADDRESS: "172.18.0.5", + SYNAPSE_PORT: "8008", + DEX_IP_ADDRESS: "172.18.0.4", + DEX_PORT: "5556", +} + +export default function setupEnvironmentVariables() { + for (const [key, value] of Object.entries(env)) { + process.env[key] = value; + } +} diff --git a/cypress/plugins/dex/index.ts b/playwright/plugins/dex/index.ts similarity index 74% rename from cypress/plugins/dex/index.ts rename to playwright/plugins/dex/index.ts index b07a78e8..56605cf5 100644 --- a/cypress/plugins/dex/index.ts +++ b/playwright/plugins/dex/index.ts @@ -20,8 +20,6 @@ import * as path from "path"; import * as os from "os"; import * as fse from "fs-extra"; -import PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; import {dockerRun, dockerStop } from "../docker"; // A cypress plugins to add command to start & stop dex instances @@ -38,7 +36,6 @@ export interface DexInstance extends DexConfig { } const dexConfigs = new Map(); -let env; async function produceConfigWithSynapseURLAdded(): Promise { const templateDir = path.join(__dirname, "template"); @@ -56,24 +53,26 @@ async function produceConfigWithSynapseURLAdded(): Promise { // now copy config.yaml, applying substitutions console.log(`Gen ${path.join(templateDir, "config.yaml")}`); let hsYaml = await fse.readFile(path.join(templateDir, "config.yaml"), "utf8"); - const synapseAddress = `${env.SYNAPSE_IP_ADDRESS}:${env.SYNAPSE_PORT}`; + const synapseHost = process.env.SYNAPSE_IP_ADDRESS; + const synapsePort = process.env.SYNAPSE_PORT; + const synapseAddress = `${synapseHost}:${synapsePort}`; hsYaml = hsYaml.replace(/{{SYNAPSE_ADDRESS}}/g, synapseAddress); - const host = env.DEX_IP_ADDRESS; - const port = env.DEX_PORT; - const dexAddress = `${host}:${port}`; + const dexHost = process.env.DEX_IP_ADDRESS!; + const dexPort = parseInt(process.env.DEX_PORT!, 10); + const dexAddress = `${dexHost}:${dexPort}`; hsYaml = hsYaml.replace(/{{DEX_ADDRESS}}/g, dexAddress); await fse.writeFile(path.join(tempDir, "config.yaml"), hsYaml); - const baseUrl = `http://${host}:${port}`; + const baseUrl = `http://${dexHost}:${dexPort}`; return { - host, - port, + host: dexHost, + port: dexPort, baseUrl, configDir: tempDir, }; } -async function dexStart(): Promise { +export async function dexStart(): Promise { const dexCfg = await produceConfigWithSynapseURLAdded(); console.log(`Starting dex with config dir ${dexCfg.configDir}...`); const dexId = await dockerRun({ @@ -99,34 +98,11 @@ async function dexStart(): Promise { return dex; } -async function dexStop(id: string): Promise { +export async function dexStop(id: string): Promise { const dexCfg = dexConfigs.get(id); if (!dexCfg) throw new Error("Unknown dex ID"); await dockerStop({ containerId: id, }); await fse.remove(dexCfg.configDir); dexConfigs.delete(id); console.log(`Stopped dex id ${id}.`); - // cypress deliberately fails if you return 'undefined', so - // return null to signal all is well, and we've handled the task. - return null; } - -/** - * @type {Cypress.PluginConfig} - */ -export function dexDocker(on: PluginEvents, config: PluginConfigOptions) { - env = config.env; - - on("task", { - dexStart, - dexStop, - }); - - on("after:spec", async (spec) => { - for (const dexId of dexConfigs.keys()) { - console.warn(`Cleaning up dex ID ${dexId} after ${spec.name}`); - await dexStop(dexId); - } - }); -} - diff --git a/cypress/plugins/dex/template/config.yaml b/playwright/plugins/dex/template/config.yaml similarity index 100% rename from cypress/plugins/dex/template/config.yaml rename to playwright/plugins/dex/template/config.yaml diff --git a/cypress/plugins/dex/template/dev.db b/playwright/plugins/dex/template/dev.db similarity index 100% rename from cypress/plugins/dex/template/dev.db rename to playwright/plugins/dex/template/dev.db diff --git a/cypress/plugins/docker/index.ts b/playwright/plugins/docker/index.ts similarity index 88% rename from cypress/plugins/docker/index.ts rename to playwright/plugins/docker/index.ts index a55f341e..b0080d6c 100644 --- a/cypress/plugins/docker/index.ts +++ b/playwright/plugins/docker/index.ts @@ -20,11 +20,6 @@ import * as os from "os"; import * as childProcess from "child_process"; import * as fse from "fs-extra"; -import PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; - -// A cypress plugin to run docker commands - export function dockerRun(args: { image: string; containerName: string; @@ -48,8 +43,7 @@ export function dockerRun(args: { ...params, args.image, ... appParams - ], (err, stdout, stderr) => { - console.log("error", err, "stdout", stdout, "stderr", stderr); + ], (err, stdout) => { if (err) { reject(err); } @@ -155,18 +149,3 @@ export function dockerRm(args: { }); }); } - -/** - * @type {Cypress.PluginConfig} - */ -export function docker(on: PluginEvents, config: PluginConfigOptions) { - console.log("Code gets to here!"); - on("task", { - dockerRun, - dockerExec, - dockerLogs, - dockerStop, - dockerRm, - dockerCreateNetwork - }); -} diff --git a/cypress/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts similarity index 74% rename from cypress/plugins/synapsedocker/index.ts rename to playwright/plugins/synapsedocker/index.ts index 268f7320..1917aa85 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/playwright/plugins/synapsedocker/index.ts @@ -21,9 +21,8 @@ import * as os from "os"; import * as crypto from "crypto"; import * as fse from "fs-extra"; -import PluginEvents = Cypress.PluginEvents; -import PluginConfigOptions = Cypress.PluginConfigOptions; import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; +import { request } from "@playwright/test"; // A cypress plugins to add command to start & stop synapses in @@ -43,7 +42,6 @@ export interface SynapseInstance extends SynapseConfig { } const synapses = new Map(); -let env; function randB64Bytes(numBytes: number): string { return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); @@ -56,7 +54,7 @@ async function cfgDirFromTemplate(template: string): Promise { if (!stats?.isDirectory) { throw new Error(`No such template: ${template}`); } - const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-')); + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'synapsedocker-')); // copy the contents of the template dir, omitting homeserver.yaml as we'll template that console.log(`Copy ${templateDir} -> ${tempDir}`); @@ -66,9 +64,10 @@ async function cfgDirFromTemplate(template: string): Promise { const macaroonSecret = randB64Bytes(16); const formSecret = randB64Bytes(16); - const host = env["SYNAPSE_IP_ADDRESS"]; - const port = parseInt(env["SYNAPSE_PORT"], 10); - const baseUrl = `http://${host}:${port}`; + const synapseHost = process.env["SYNAPSE_IP_ADDRESS"]!!; + const synapsePort = parseInt(process.env["SYNAPSE_PORT"]!, 10); + const baseUrl = `http://${synapseHost}:${synapsePort}`; + // now copy homeserver.yaml, applying substitutions console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`); @@ -77,7 +76,10 @@ async function cfgDirFromTemplate(template: string): Promise { hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); - const dexUrl = `http://${env["DEX_IP_ADDRESS"]}:${env["DEX_PORT"]}/dex`; + + const dexHost = process.env["DEX_IP_ADDRESS"]; + const dexPort = process.env["DEX_PORT"]; + const dexUrl = `http://${dexHost}:${dexPort}/dex`; hsYaml = hsYaml.replace(/{{OIDC_ISSUER}}/g, dexUrl); await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); @@ -89,8 +91,8 @@ async function cfgDirFromTemplate(template: string): Promise { await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); return { - port, - host, + port: synapsePort, + host: synapseHost, baseUrl, configDir: tempDir, registrationSecret, @@ -100,7 +102,7 @@ async function cfgDirFromTemplate(template: string): Promise { // Start a synapse instance: the template must be the name of // one of the templates in the cypress/plugins/synapsedocker/templates // directory -async function synapseStart(template: string): Promise { +export async function synapseStart(template: string): Promise { const synCfg = await cfgDirFromTemplate(template); console.log(`Starting synapse with config dir ${synCfg.configDir}...`); await dockerCreateNetwork({ networkName: "hydrogen" }); @@ -143,12 +145,12 @@ async function synapseStart(template: string): Promise { return synapse; } -async function synapseStop(id: string): Promise { +export async function synapseStop(id: string): Promise { const synCfg = synapses.get(id); if (!synCfg) throw new Error("Unknown synapse ID"); - const synapseLogsPath = path.join("cypress", "synapselogs", id); + const synapseLogsPath = path.join("playwright", "synapselogs", id); await fse.ensureDir(synapseLogsPath); await dockerLogs({ @@ -162,42 +164,40 @@ async function synapseStop(id: string): Promise { }); await fse.remove(synCfg.configDir); - synapses.delete(id); - console.log(`Stopped synapse id ${id}.`); - // cypress deliberately fails if you return 'undefined', so - // return null to signal all is well, and we've handled the task. - return null; } -/** - * @type {Cypress.PluginConfig} - */ -export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) { - env = config.env; - - on("task", { - synapseStart, - synapseStop, - }); - on("after:spec", async (spec) => { - // Cleans up any remaining synapse instances after a spec run - // This is on the theory that we should avoid re-using synapse - // instances between spec runs: they should be cheap enough to - // start that we can have a separate one for each spec run or even - // test. If we accidentally re-use synapses, we could inadvertently - // make our tests depend on each other. - for (const synId of synapses.keys()) { - console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); - await synapseStop(synId); +interface Credentials { + accessToken: string; + userId: string; + deviceId: string; + homeServer: string; +} + +export async function registerUser(synapse: SynapseInstance, username: string, password: string, displayName?: string,): Promise { + const url = `${synapse.baseUrl}/_synapse/admin/v1/register`; + const context = await request.newContext({ baseURL: url }); + const { nonce } = await (await context.get(url)).json(); + const mac = crypto.createHmac('sha1', synapse.registrationSecret).update( + `${nonce}\0${username}\0${password}\0notadmin`, + ).digest('hex'); + const response = await (await context.post(url, { + data: { + nonce, + username, + password, + mac, + admin: false, + displayname: displayName, } - }); - - on("before:run", async () => { - // tidy up old synapse log files before each run - await fse.emptyDir(path.join("cypress", "synapselogs")); - }); + })).json(); + return { + homeServer: response.home_server, + accessToken: response.access_token, + userId: response.user_id, + deviceId: response.device_id, + }; } diff --git a/cypress/plugins/synapsedocker/templates/COPYME/README.md b/playwright/plugins/synapsedocker/templates/COPYME/README.md similarity index 100% rename from cypress/plugins/synapsedocker/templates/COPYME/README.md rename to playwright/plugins/synapsedocker/templates/COPYME/README.md diff --git a/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml b/playwright/plugins/synapsedocker/templates/COPYME/homeserver.yaml similarity index 100% rename from cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml rename to playwright/plugins/synapsedocker/templates/COPYME/homeserver.yaml diff --git a/cypress/plugins/synapsedocker/templates/COPYME/log.config b/playwright/plugins/synapsedocker/templates/COPYME/log.config similarity index 100% rename from cypress/plugins/synapsedocker/templates/COPYME/log.config rename to playwright/plugins/synapsedocker/templates/COPYME/log.config diff --git a/cypress/plugins/synapsedocker/templates/consent/README.md b/playwright/plugins/synapsedocker/templates/consent/README.md similarity index 100% rename from cypress/plugins/synapsedocker/templates/consent/README.md rename to playwright/plugins/synapsedocker/templates/consent/README.md diff --git a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml b/playwright/plugins/synapsedocker/templates/consent/homeserver.yaml similarity index 100% rename from cypress/plugins/synapsedocker/templates/consent/homeserver.yaml rename to playwright/plugins/synapsedocker/templates/consent/homeserver.yaml diff --git a/cypress/plugins/synapsedocker/templates/consent/log.config b/playwright/plugins/synapsedocker/templates/consent/log.config similarity index 100% rename from cypress/plugins/synapsedocker/templates/consent/log.config rename to playwright/plugins/synapsedocker/templates/consent/log.config diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html b/playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html similarity index 100% rename from cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html rename to playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html b/playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html similarity index 100% rename from cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html rename to playwright/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html diff --git a/cypress/plugins/synapsedocker/templates/default/README.md b/playwright/plugins/synapsedocker/templates/default/README.md similarity index 100% rename from cypress/plugins/synapsedocker/templates/default/README.md rename to playwright/plugins/synapsedocker/templates/default/README.md diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/playwright/plugins/synapsedocker/templates/default/homeserver.yaml similarity index 100% rename from cypress/plugins/synapsedocker/templates/default/homeserver.yaml rename to playwright/plugins/synapsedocker/templates/default/homeserver.yaml diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/playwright/plugins/synapsedocker/templates/default/log.config similarity index 100% rename from cypress/plugins/synapsedocker/templates/default/log.config rename to playwright/plugins/synapsedocker/templates/default/log.config diff --git a/cypress/plugins/synapsedocker/templates/sso/homeserver.yaml b/playwright/plugins/synapsedocker/templates/sso/homeserver.yaml similarity index 100% rename from cypress/plugins/synapsedocker/templates/sso/homeserver.yaml rename to playwright/plugins/synapsedocker/templates/sso/homeserver.yaml diff --git a/cypress/plugins/synapsedocker/templates/sso/log.config b/playwright/plugins/synapsedocker/templates/sso/log.config similarity index 100% rename from cypress/plugins/synapsedocker/templates/sso/log.config rename to playwright/plugins/synapsedocker/templates/sso/log.config diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts new file mode 100644 index 00000000..a7684eae --- /dev/null +++ b/playwright/tests/login.spec.ts @@ -0,0 +1,59 @@ +/* +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 { test } from '@playwright/test'; +import { synapseStart, synapseStop, registerUser } from "../plugins/synapsedocker"; +import { dexStart, dexStop } from "../plugins/dex"; +import type { DexInstance } from "../plugins/dex"; +import type { SynapseInstance } from "../plugins/synapsedocker"; + +test.describe("Login", () => { + let synapse: SynapseInstance; + let dex: DexInstance; + + test.beforeEach(async () => { + dex = await dexStart(); + synapse = await synapseStart("sso"); + }); + + test.afterEach(async () => { + await synapseStop(synapse.synapseId); + await dexStop(dex.dexId); + }); + + test("Login using username/password", async ({ page }) => { + const username = "foobaraccount"; + const password = "password123"; + await registerUser(synapse, username, password); + await page.goto("/"); + await page.locator("#homeserver").fill(""); + await page.locator("#homeserver").type(synapse.baseUrl); + await page.locator("#username").type(username); + await page.locator("#password").type(password); + await page.getByText('Log In', { exact: true }).click(); + await page.locator(".SessionView").waitFor(); + }); + + test("Login using SSO", async ({ page }) => { + await page.goto("/"); + await page.locator("#homeserver").fill(""); + await page.locator("#homeserver").type(synapse.baseUrl); + await page.locator(".StartSSOLoginView_button").click(); + await page.getByText("Log in with Example").click(); + await page.locator(".dex-btn-text", {hasText: "Grant Access"}).click(); + await page.locator(".primary-button", {hasText: "Continue"}).click(); + await page.locator(".SessionView").waitFor(); + }); +}); diff --git a/cypress/support/e2e.ts b/playwright/tests/startup.spec.ts similarity index 73% rename from cypress/support/e2e.ts rename to playwright/tests/startup.spec.ts index d186c5a0..0d38218a 100644 --- a/cypress/support/e2e.ts +++ b/playwright/tests/startup.spec.ts @@ -13,8 +13,9 @@ 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 { test } from '@playwright/test'; -/// - -import "./synapse"; -import "./dex"; +test("App has no startup errors that prevent UI render", async ({ page }) => { + await page.goto('/'); + await page.getByText('Log In', { exact: true }).waitFor(); +}); diff --git a/yarn.lock b/yarn.lock index 994657f3..188a5fe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -114,6 +114,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.27.1": + version "1.27.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.27.1.tgz#9364d1e02021261211c8ff586d903faa79ce95c4" + integrity sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A== + dependencies: + "@types/node" "*" + playwright-core "1.27.1" + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -2040,6 +2048,11 @@ pify@^2.2.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +playwright-core@1.27.1: + version "1.27.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4" + integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q== + postcss-css-variables@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz#d97b6da19e86245eb817006e11117382f997bb93" From 097d2296e01d6b1055f1d9a1a2c43c93addb0d02 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 25 Oct 2022 17:02:17 +0530 Subject: [PATCH 271/354] Make testing more reliable - Stop any running containers - Start dev server from playwright so that we don't keep spawning new node instances --- package.json | 2 +- playwright.config.ts | 24 +++++++++++++---------- playwright/plugins/dex/index.ts | 2 +- playwright/plugins/synapsedocker/index.ts | 2 +- scripts/test-app.sh | 19 ++++++++++++++++++ 5 files changed, 36 insertions(+), 13 deletions(-) create mode 100755 scripts/test-app.sh diff --git a/package.json b/package.json index 569354f0..002cc24a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build": "vite build && ./scripts/cleanup.sh", "build:sdk": "./scripts/sdk/build.sh", "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch", - "test:app": "vite --port 3000 & playwright test" + "test:app": "./scripts/test-app.sh" }, "repository": { "type": "git", diff --git a/playwright.config.ts b/playwright.config.ts index 901e48d9..6fbb499b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,16 +1,20 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; +import type { PlaywrightTestConfig } from "@playwright/test"; const BASE_URL = process.env["BASE_URL"] ?? "http://127.0.0.1:3000"; const config: PlaywrightTestConfig = { - use: { - headless: false, - viewport: { width: 1280, height: 720 }, - ignoreHTTPSErrors: true, - video: 'on-first-retry', - baseURL: BASE_URL, - }, - testDir: "./playwright/tests", - globalSetup: require.resolve("./playwright/global-setup"), + use: { + headless: false, + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + video: "on-first-retry", + baseURL: BASE_URL, + }, + testDir: "./playwright/tests", + globalSetup: require.resolve("./playwright/global-setup"), + webServer: { + command: "yarn start", + url: `${BASE_URL}/#/login`, + }, }; export default config; diff --git a/playwright/plugins/dex/index.ts b/playwright/plugins/dex/index.ts index 56605cf5..f45257fc 100644 --- a/playwright/plugins/dex/index.ts +++ b/playwright/plugins/dex/index.ts @@ -77,7 +77,7 @@ export async function dexStart(): Promise { console.log(`Starting dex with config dir ${dexCfg.configDir}...`); const dexId = await dockerRun({ image: "bitnami/dex:latest", - containerName: "dex", + containerName: "hydrogen-dex", dockerParams: [ "--rm", "-v", `${dexCfg.configDir}:/data`, diff --git a/playwright/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts index 1917aa85..56ffa214 100644 --- a/playwright/plugins/synapsedocker/index.ts +++ b/playwright/plugins/synapsedocker/index.ts @@ -108,7 +108,7 @@ export async function synapseStart(template: string): Promise { await dockerCreateNetwork({ networkName: "hydrogen" }); const synapseId = await dockerRun({ image: "matrixdotorg/synapse:develop", - containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`, + containerName: `hydrogen-synapse`, dockerParams: [ "--rm", "-v", `${synCfg.configDir}:/data`, diff --git a/scripts/test-app.sh b/scripts/test-app.sh new file mode 100755 index 00000000..cfbd37a3 --- /dev/null +++ b/scripts/test-app.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Make sure docker is available +if ! docker info > /dev/null 2>&1; then + echo "You need to intall docker before you can run the tests!" + exit 1 +fi + +# Stop running containers +if docker stop hydrogen-synapse > /dev/null 2>&1; then + echo "Existing 'hydrogen-synapse' container stopped ✔" +fi + +if docker stop hydrogen-dex > /dev/null 2>&1; then + echo "Existing 'hydrogen-dex' container stopped ✔" +fi + +# Run playwright +yarn playwright test From d810f22237c1d645f01e153c9f926b61406fb43e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 25 Oct 2022 17:06:39 +0530 Subject: [PATCH 272/354] Remove cypress as dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 002cc24a..20a16a4d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "aes-js": "^3.1.2", "bs58": "^4.0.1", "core-js": "^3.6.5", - "cypress": "^10.6.0", "es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush", "escodegen": "^2.0.0", "eslint": "^7.32.0", From a3a5d278a043b957ce04f134a3658e0622dcd211 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 25 Oct 2022 21:18:56 +0530 Subject: [PATCH 273/354] Limit workers to 1 for now --- playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright.config.ts b/playwright.config.ts index 6fbb499b..0a00a88f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,5 +16,6 @@ const config: PlaywrightTestConfig = { command: "yarn start", url: `${BASE_URL}/#/login`, }, + workers: 1 }; export default config; From 53c3bf0ab382650f12690653bdaa63f715eba6d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 25 Oct 2022 21:20:20 +0530 Subject: [PATCH 274/354] Change quotes --- playwright/tests/startup.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/tests/startup.spec.ts b/playwright/tests/startup.spec.ts index 0d38218a..5bb7abf0 100644 --- a/playwright/tests/startup.spec.ts +++ b/playwright/tests/startup.spec.ts @@ -16,6 +16,6 @@ limitations under the License. import { test } from '@playwright/test'; test("App has no startup errors that prevent UI render", async ({ page }) => { - await page.goto('/'); - await page.getByText('Log In', { exact: true }).waitFor(); + await page.goto("/"); + await page.getByText("Log In", { exact: true }).waitFor(); }); From a3f70fd29be37f3c5d00174d2dbada5507c6315d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 25 Oct 2022 21:29:28 +0530 Subject: [PATCH 275/354] Fix formatting --- playwright/plugins/synapsedocker/index.ts | 4 ++-- playwright/tests/login.spec.ts | 10 +++++----- playwright/tests/startup.spec.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/playwright/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts index 56ffa214..390888cd 100644 --- a/playwright/plugins/synapsedocker/index.ts +++ b/playwright/plugins/synapsedocker/index.ts @@ -21,8 +21,8 @@ import * as os from "os"; import * as crypto from "crypto"; import * as fse from "fs-extra"; -import { dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; -import { request } from "@playwright/test"; +import {dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop} from "../docker"; +import {request} from "@playwright/test"; // A cypress plugins to add command to start & stop synapses in diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts index a7684eae..a5f30476 100644 --- a/playwright/tests/login.spec.ts +++ b/playwright/tests/login.spec.ts @@ -13,11 +13,11 @@ 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 { test } from '@playwright/test'; -import { synapseStart, synapseStop, registerUser } from "../plugins/synapsedocker"; -import { dexStart, dexStop } from "../plugins/dex"; -import type { DexInstance } from "../plugins/dex"; -import type { SynapseInstance } from "../plugins/synapsedocker"; +import {test} from '@playwright/test'; +import {synapseStart, synapseStop, registerUser} from "../plugins/synapsedocker"; +import {dexStart, dexStop} from "../plugins/dex"; +import type {DexInstance} from "../plugins/dex"; +import type {SynapseInstance} from "../plugins/synapsedocker"; test.describe("Login", () => { let synapse: SynapseInstance; diff --git a/playwright/tests/startup.spec.ts b/playwright/tests/startup.spec.ts index 5bb7abf0..562859d7 100644 --- a/playwright/tests/startup.spec.ts +++ b/playwright/tests/startup.spec.ts @@ -13,7 +13,7 @@ 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 { test } from '@playwright/test'; +import {test} from '@playwright/test'; test("App has no startup errors that prevent UI render", async ({ page }) => { await page.goto("/"); From ad02c1625f6ab9bfd09269281332f6af32727917 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Thu, 27 Oct 2022 15:42:23 +0100 Subject: [PATCH 276/354] Encode SSO redirect URL as it may contain multiple query parameters If the returnURL contains multiple query parameters (e.g. http://localhost:3000?foo=bar&bar=baz), the homeserver would fail to correctly parse the URL, and only the first query parameter would be kept. This is not an issue with the homeserver since the URL cannot be parsed in an unambiguous way, as the resulting URL would be: https://example.com/_matrix/client/r0/login/sso/redirect?redirectUrl=http://localhost:3000?foo=bar&bar=baz It's not possible to know whether the bar parameter is part of the "parent" URL, or part of the redirectUrl parameter. ---- To fix this, we now encode the redirectUrl parameter, which results in: https://example.com/_matrix/client/r0/login/sso/redirect?redirectUrl=http%3A%2F%2Flocalhost%3A3000%2Fparent.html%3Ffoo%3Dbar%26bar%3Dbaz This URL is correctly parsed by synapse. --- src/matrix/login/SSOLoginHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/login/SSOLoginHelper.ts b/src/matrix/login/SSOLoginHelper.ts index 0fe3d6b8..1d2dc7d0 100644 --- a/src/matrix/login/SSOLoginHelper.ts +++ b/src/matrix/login/SSOLoginHelper.ts @@ -24,6 +24,6 @@ export class SSOLoginHelper{ get homeserver(): string { return this._homeserver; } createSSORedirectURL(returnURL: string): string { - return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`; + return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(returnURL)}`; } } From c544819b42fe0f85235985a89605f1c091141bc5 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:33:46 +0200 Subject: [PATCH 277/354] rename and refactor method to verify locally known senders a bit to differentiate with method to come for verifying senders that need to be fetched still, and also allow reuse of verification logic by said new method. Also get rid of roomTracked flag in DecryptionResult as once we fetch unknown senders (in commit to come), we should now always have a device, unless the device isn't known on the server, in which case we should shout. --- src/matrix/e2ee/DecryptionResult.ts | 9 +-------- src/matrix/e2ee/RoomEncryption.js | 31 ++++++++++++++++------------- src/matrix/room/BaseRoom.js | 2 +- src/matrix/room/Room.js | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/matrix/e2ee/DecryptionResult.ts b/src/matrix/e2ee/DecryptionResult.ts index 7735856a..0bb2e926 100644 --- a/src/matrix/e2ee/DecryptionResult.ts +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -47,10 +47,6 @@ export class DecryptionResult { this.device = device; } - setRoomNotTrackedYet(): void { - this.roomTracked = false; - } - get isVerified(): boolean { if (this.device) { const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key; @@ -62,15 +58,12 @@ export class DecryptionResult { get isUnverified(): boolean { if (this.device) { return !this.isVerified; - } else if (this.isVerificationUnknown) { - return false; } else { return true; } } get isVerificationUnknown(): boolean { - // verification is unknown if we haven't yet fetched the devices for the room - return !this.device && !this.roomTracked; + return !this.device; } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 36424b02..dbd8fd43 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -208,16 +208,19 @@ export class RoomEncryption { }); } - async _verifyDecryptionResult(result, txn) { - let device = this._senderDeviceCache.get(result.senderCurve25519Key); - if (!device) { - device = await this._deviceTracker.getDeviceByCurve25519Key(result.senderCurve25519Key, txn); - this._senderDeviceCache.set(result.senderCurve25519Key, device); - } - if (device) { - result.setDevice(device); - } else if (!this._room.isTrackingMembers) { - result.setRoomNotTrackedYet(); + async _verifyDecryptionResults(results, txn) { + await Promise.all(results.map(async result => { + let device = this._senderDeviceCache.get(result.senderCurve25519Key); + if (!device) { + device = await this._deviceTracker.getDeviceByCurve25519Key(result.senderCurve25519Key, txn); + this._senderDeviceCache.set(result.senderCurve25519Key, device); + } + if (device) { + result.setDevice(device); + } + })); + } + } } @@ -545,10 +548,10 @@ class BatchDecryptionResult { } } - verifySenders(txn) { - return Promise.all(Array.from(this.results.values()).map(result => { - return this._roomEncryption._verifyDecryptionResult(result, txn); - })); + /** Verify the decryption results by looking for the corresponding device in local persistance + * @returns {BatchDecryptionResult} a new batch result with the results for which we now found a device */ + verifyKnownSenders(txn) { + return this._roomEncryption._verifyDecryptionResults(Array.from(this.results.values()), txn); } } diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 57d2a7b2..6f14b7f9 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -154,7 +154,7 @@ export class BaseRoom extends EventEmitter { try { decryption = await changes.write(writeTxn, log); if (isTimelineOpen) { - await decryption.verifySenders(writeTxn); + await decryption.verifyKnownSenders(writeTxn); } } catch (err) { writeTxn.abort(); diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 8cc87845..2ff17d42 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -129,7 +129,7 @@ export class Room extends BaseRoom { log.set("decryptionResults", decryption.results.size); log.set("decryptionErrors", decryption.errors.size); if (this._isTimelineOpen) { - await decryption.verifySenders(txn); + await decryption.verifyKnownSenders(txn); } decryption.applyToEntries(newEntries); if (retryEntries?.length) { From 9c13b2b4a43b71311261d820e933bcd995590458 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:41:18 +0200 Subject: [PATCH 278/354] add method to fetch missing sender keys --- src/matrix/e2ee/DecryptionResult.ts | 5 ++-- src/matrix/e2ee/RoomEncryption.js | 30 +++++++++++++++++++ .../megolm/decryption/SessionDecryption.ts | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/matrix/e2ee/DecryptionResult.ts b/src/matrix/e2ee/DecryptionResult.ts index 0bb2e926..8846616c 100644 --- a/src/matrix/e2ee/DecryptionResult.ts +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -27,6 +27,7 @@ limitations under the License. */ import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore"; +import type {TimelineEvent} from "../storage/types"; type DecryptedEvent = { type?: string, @@ -35,12 +36,12 @@ type DecryptedEvent = { export class DecryptionResult { private device?: DeviceIdentity; - private roomTracked: boolean = true; constructor( public readonly event: DecryptedEvent, public readonly senderCurve25519Key: string, - public readonly claimedEd25519Key: string + public readonly claimedEd25519Key: string, + public readonly encryptedEvent?: TimelineEvent ) {} setDevice(device: DeviceIdentity): void { diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index dbd8fd43..0e17ad6f 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -221,7 +221,30 @@ export class RoomEncryption { })); } + /** fetches the devices that are not yet known locally from the homeserver to verify the sender of this message. */ + _fetchKeyAndVerifyDecryptionResults(results, hsApi, log) { + const resultsWithoutDevice = results.filter(r => r.isVerificationUnknown); + if (resultsWithoutDevice.length) { + return log.wrap("fetch unverified senders", async log => { + const sendersWithoutDevice = Array.from(resultsWithoutDevice.reduce((senders, r) => { + return senders.add(r.encryptedEvent.sender); + }, new Set())); + log.set("senders", sendersWithoutDevice); + // fetch the devices, ignore return value, + // and just reuse _verifyDecryptionResults method so we only have one impl how to verify + await this._deviceTracker.devicesForRoomMembers(this._room.id, sendersWithoutDevice, hsApi, log); + // now that we've fetched the missing devices, try verifying the results again + const txn = await this._storage.readTxn([this._storage.storeNames.deviceIdentities]); + return this._verifyDecryptionResults(resultsWithoutDevice, txn); + const resultsWithFoundDevice = resultsWithoutDevice.filter(r => !r.isVerificationUnknown); + const resultsToEventIdMap = resultsWithFoundDevice.reduce((map, r) => { + map.set(r.encryptedEvent.event_id, r); + return map; + }, new Map()); + return new BatchDecryptionResult(resultsToEventIdMap, new Map(), this); + }); } + return new BatchDecryptionResult(new Map(), new Map(), this); } async _requestMissingSessionFromBackup(senderKey, sessionId, log) { @@ -553,6 +576,13 @@ class BatchDecryptionResult { verifyKnownSenders(txn) { return this._roomEncryption._verifyDecryptionResults(Array.from(this.results.values()), txn); } + + /** Verify any decryption results for which we could not find a device when + * calling `verifyKnownSenders` prior, by fetching them from the homeserver. + * @returns {Promise} the results for which we found a device */ + fetchAndVerifyRemainingSenders(hsApi, log) { + return this._roomEncryption._fetchKeyAndVerifyDecryptionResults(Array.from(this.results.values()), hsApi, log); + } } import {createMockStorage} from "../../mocks/Storage"; diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index 57ef9a96..36224fc5 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -75,7 +75,7 @@ export class SessionDecryption { {encryptedRoomId: payload.room_id, eventRoomId: this.key.roomId}); } replayEntries.push(new ReplayDetectionEntry(this.key.sessionId, decryptionResult!.message_index, event)); - const result = new DecryptionResult(payload, this.key.senderKey, this.key.claimedEd25519Key); + const result = new DecryptionResult(payload, this.key.senderKey, this.key.claimedEd25519Key, event); results.set(event.event_id, result); } catch (err) { // ignore AbortError from cancelling decryption requests in dispose method From fcb1546fba74c711d4299b30af62f4da11c05f0f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:25:31 +0200 Subject: [PATCH 279/354] fetch keys as part of afterSyncCompleted step remove needsAfterSyncCompleted step as well, performance difference should be neglible --- src/matrix/Sync.js | 14 ++++--------- src/matrix/e2ee/RoomEncryption.js | 13 +++++++++++- src/matrix/room/Room.js | 33 +++++++++++++++++++++++-------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 3574213e..c995e3e2 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -47,10 +47,8 @@ function timelineIsEmpty(roomResponse) { * const changes = await room.writeSync(roomResponse, isInitialSync, preparation, syncTxn); * // applies and emits changes once syncTxn is committed * room.afterSync(changes); - * if (room.needsAfterSyncCompleted(changes)) { - * // can do network requests - * await room.afterSyncCompleted(changes); - * } + * // can do network requests + * await room.afterSyncCompleted(changes); * ``` */ export class Sync { @@ -163,13 +161,9 @@ export class Sync { await log.wrap("session", log => this._session.afterSyncCompleted(sessionChanges, isCatchupSync, log), log.level.Detail); } catch (err) {} // error is logged, but don't fail sessionPromise })(); - - const roomsNeedingAfterSyncCompleted = roomStates.filter(rs => { - return rs.room.needsAfterSyncCompleted(rs.changes); - }); - const roomsPromises = roomsNeedingAfterSyncCompleted.map(async rs => { + const roomsPromises = roomStates.map(async rs => { try { - await log.wrap("room", log => rs.room.afterSyncCompleted(rs.changes, log), log.level.Detail); + await rs.room.afterSyncCompleted(rs.changes, log); } catch (err) {} // error is logged, but don't fail roomsPromises }); // run everything in parallel, diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 0e17ad6f..080aa2eb 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -557,15 +557,17 @@ class BatchDecryptionResult { this._roomEncryption = roomEncryption; } - applyToEntries(entries) { + applyToEntries(entries, callback = undefined) { for (const entry of entries) { const result = this.results.get(entry.id); if (result) { entry.setDecryptionResult(result); + callback?.(entry); } else { const error = this.errors.get(entry.id); if (error) { entry.setDecryptionError(error); + callback?.(entry); } } } @@ -577,6 +579,15 @@ class BatchDecryptionResult { return this._roomEncryption._verifyDecryptionResults(Array.from(this.results.values()), txn); } + get hasUnverifiedSenders() { + for (const r of this.results.values()) { + if (r.isVerificationUnknown) { + return true; + } + } + return false; + } + /** Verify any decryption results for which we could not find a device when * calling `verifyKnownSenders` prior, by fetching them from the homeserver. * @returns {Promise} the results for which we found a device */ diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 2ff17d42..ad8ec110 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -189,6 +189,7 @@ export class Room extends BaseRoom { heroChanges, powerLevelsEvent, encryptionChanges, + decryption }; } @@ -291,19 +292,35 @@ export class Room extends BaseRoom { } } - needsAfterSyncCompleted({encryptionChanges}) { - return encryptionChanges?.shouldFlush; - } - /** * Only called if the result of writeSync had `needsAfterSyncCompleted` set. * Can be used to do longer running operations that resulted from the last sync, * like network operations. */ - async afterSyncCompleted(changes, log) { - log.set("id", this.id); - if (this._roomEncryption) { - await this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log); + async afterSyncCompleted({encryptionChanges, decryption, newEntries, updatedEntries}, log) { + const shouldFlushKeys = encryptionChanges?.shouldFlush; + const shouldFetchUnverifiedSenders = this._isTimelineOpen && decryption?.hasUnverifiedSenders; + // only log rooms where we actually do something + if (shouldFlushKeys || shouldFetchUnverifiedSenders) { + log.wrap({l: "room", id: this.id}, async log => { + const promises = []; + if (shouldFlushKeys) { + promises.push(this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log)); + } + if (shouldFetchUnverifiedSenders) { + const promise = (async () => { + const newlyVerifiedDecryption = await decryption.fetchAndVerifyRemainingSenders(this._hsApi, log); + const verifiedEntries = []; + const updateCallback = entry => verifiedEntries.push(entry); + newlyVerifiedDecryption.applyToEntries(newEntries, updateCallback); + newlyVerifiedDecryption.applyToEntries(updated, updateCallback); + // TODO: update _observedEvents here as well? + this._timeline.replaceEntries(verifiedEntries); + }); + promises.push(promise); + } + await Promise.all(promises); + }); } } From 6dbcd46d80a56a4cda2f9f62472f10d6f82f9db9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:31:00 +0200 Subject: [PATCH 280/354] cleanup of ctor --- .../e2ee/megolm/decryption/SessionDecryption.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index 36224fc5..ca294460 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -31,17 +31,14 @@ interface DecryptAllResult { * Does the actual decryption of all events for a given megolm session in a batch */ export class SessionDecryption { - private key: RoomKey; - private events: TimelineEvent[]; - private keyLoader: KeyLoader; - private olmWorker?: OlmWorker; private decryptionRequests?: any[]; - constructor(key: RoomKey, events: TimelineEvent[], olmWorker: OlmWorker | undefined, keyLoader: KeyLoader) { - this.key = key; - this.events = events; - this.olmWorker = olmWorker; - this.keyLoader = keyLoader; + constructor( + private readonly key: RoomKey, + private readonly events: TimelineEvent[], + private readonly olmWorker: OlmWorker | undefined, + private readonly keyLoader: KeyLoader + ) { this.decryptionRequests = olmWorker ? [] : undefined; } From d0122d17c0ceffe4cc5d4a0938d3e968ad83537e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:49:33 +0200 Subject: [PATCH 281/354] don't assume timeline is open as have an await since last check --- src/matrix/room/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index ad8ec110..90a6ec26 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -315,7 +315,7 @@ export class Room extends BaseRoom { newlyVerifiedDecryption.applyToEntries(newEntries, updateCallback); newlyVerifiedDecryption.applyToEntries(updated, updateCallback); // TODO: update _observedEvents here as well? - this._timeline.replaceEntries(verifiedEntries); + this._timeline?.replaceEntries(verifiedEntries); }); promises.push(promise); } From 218d25d973e69a3b2bbca61ccdc5c50606021750 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:50:01 +0200 Subject: [PATCH 282/354] fix typo --- src/matrix/room/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 90a6ec26..e03f4cd1 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -313,7 +313,7 @@ export class Room extends BaseRoom { const verifiedEntries = []; const updateCallback = entry => verifiedEntries.push(entry); newlyVerifiedDecryption.applyToEntries(newEntries, updateCallback); - newlyVerifiedDecryption.applyToEntries(updated, updateCallback); + newlyVerifiedDecryption.applyToEntries(updatedEntries, updateCallback); // TODO: update _observedEvents here as well? this._timeline?.replaceEntries(verifiedEntries); }); From dc25f96e73d3a1aa11eb455e50ddc999826a9f05 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:55:20 +0200 Subject: [PATCH 283/354] update observed events as well --- src/matrix/room/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index e03f4cd1..ab336ea1 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -314,8 +314,8 @@ export class Room extends BaseRoom { const updateCallback = entry => verifiedEntries.push(entry); newlyVerifiedDecryption.applyToEntries(newEntries, updateCallback); newlyVerifiedDecryption.applyToEntries(updatedEntries, updateCallback); - // TODO: update _observedEvents here as well? this._timeline?.replaceEntries(verifiedEntries); + this._observedEvents?.updateEvents(verifiedEntries); }); promises.push(promise); } From 6123d794da31302c5b2382446ac321c7a1e78f90 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:55:36 +0200 Subject: [PATCH 284/354] also fetch unknown sender keys after decrypting entries outside of sync --- src/matrix/room/BaseRoom.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 6f14b7f9..851e75c9 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -166,6 +166,16 @@ export class BaseRoom extends EventEmitter { if (this._observedEvents) { this._observedEvents.updateEvents(entries); } + if (isTimelineOpen && decryption.hasUnverifiedSenders) { + // verify missing senders async and update timeline once done so we don't delay rendering with network requests + log.wrapDetached("fetch unknown senders keys", async () => { + const newlyVerifiedDecryption = await decryption.fetchAndVerifyRemainingSenders(this._hsApi, log); + const verifiedEntries = []; + newlyVerifiedDecryption.applyToEntries(entries, entry => verifiedEntries.push(entry)); + this._timeline?.replaceEntries(verifiedEntries); + this._observedEvents?.updateEvents(verifiedEntries); + }); + } }, ensureLogItem(log)); return request; } From cd5343414ab0003999972a54d50617d6d4c0f418 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 28 Oct 2022 17:16:32 +0200 Subject: [PATCH 285/354] fix lint --- src/matrix/e2ee/RoomEncryption.js | 5 ++--- src/matrix/room/Room.js | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 080aa2eb..6d42c873 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -235,7 +235,7 @@ export class RoomEncryption { await this._deviceTracker.devicesForRoomMembers(this._room.id, sendersWithoutDevice, hsApi, log); // now that we've fetched the missing devices, try verifying the results again const txn = await this._storage.readTxn([this._storage.storeNames.deviceIdentities]); - return this._verifyDecryptionResults(resultsWithoutDevice, txn); + await this._verifyDecryptionResults(resultsWithoutDevice, txn); const resultsWithFoundDevice = resultsWithoutDevice.filter(r => !r.isVerificationUnknown); const resultsToEventIdMap = resultsWithFoundDevice.reduce((map, r) => { map.set(r.encryptedEvent.event_id, r); @@ -600,7 +600,6 @@ import {createMockStorage} from "../../mocks/Storage"; import {Clock as MockClock} from "../../mocks/Clock"; import {poll} from "../../mocks/poll"; import {Instance as NullLoggerInstance} from "../../logging/NullLogger"; -import {ConsoleLogger} from "../../logging/ConsoleLogger"; import {HomeServer as MockHomeServer} from "../../mocks/HomeServer.js"; export function tests() { @@ -709,7 +708,7 @@ export function tests() { const storage = await createMockStorage(); let isMemberChangesCalled = false; const deviceTracker = { - async writeMemberChanges(room, memberChanges, historyVisibility, txn) { + async writeMemberChanges(room, memberChanges, historyVisibility) { assert.equal(historyVisibility, "invited"); isMemberChangesCalled = true; return {removed: [], added: []}; diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index ab336ea1..aad0310a 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -124,8 +124,9 @@ export class Room extends BaseRoom { const {entries: newEntries, updatedEntries, newLiveKey, memberChanges} = await log.wrap("syncWriter", log => this._syncWriter.writeSync( roomResponse, isRejoin, summaryChanges.hasFetchedMembers, txn, log), log.level.Detail); + let decryption; if (decryptChanges) { - const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log)); + decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log)); log.set("decryptionResults", decryption.results.size); log.set("decryptionErrors", decryption.errors.size); if (this._isTimelineOpen) { From 25c8f1cf04beacd7db56d44afb7bd9a8dbd176bf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:36:31 +0100 Subject: [PATCH 286/354] always return a promise from this method, even if nothing to verify --- src/matrix/e2ee/RoomEncryption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 6d42c873..4dd6cd0b 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -222,7 +222,7 @@ export class RoomEncryption { } /** fetches the devices that are not yet known locally from the homeserver to verify the sender of this message. */ - _fetchKeyAndVerifyDecryptionResults(results, hsApi, log) { + async _fetchKeyAndVerifyDecryptionResults(results, hsApi, log) { const resultsWithoutDevice = results.filter(r => r.isVerificationUnknown); if (resultsWithoutDevice.length) { return log.wrap("fetch unverified senders", async log => { From 258260024989013523b81f0f8e98480d2af6e319 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:37:20 +0100 Subject: [PATCH 287/354] await operation, otherwise it keeps running during next sync --- src/matrix/room/Room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index aad0310a..fab3bf15 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -303,7 +303,7 @@ export class Room extends BaseRoom { const shouldFetchUnverifiedSenders = this._isTimelineOpen && decryption?.hasUnverifiedSenders; // only log rooms where we actually do something if (shouldFlushKeys || shouldFetchUnverifiedSenders) { - log.wrap({l: "room", id: this.id}, async log => { + await log.wrap({l: "room", id: this.id}, async log => { const promises = []; if (shouldFlushKeys) { promises.push(this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log)); From dfede83c0bc194be7ca3327f6552ec07d51ea7f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:37:38 +0100 Subject: [PATCH 288/354] log verifying senders in own item --- src/matrix/room/Room.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index fab3bf15..d3d1a3b0 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -309,15 +309,16 @@ export class Room extends BaseRoom { promises.push(this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log)); } if (shouldFetchUnverifiedSenders) { - const promise = (async () => { + const promise = log.wrap("verify senders", (async log => { const newlyVerifiedDecryption = await decryption.fetchAndVerifyRemainingSenders(this._hsApi, log); const verifiedEntries = []; const updateCallback = entry => verifiedEntries.push(entry); newlyVerifiedDecryption.applyToEntries(newEntries, updateCallback); newlyVerifiedDecryption.applyToEntries(updatedEntries, updateCallback); + log.set("verifiedEntries", verifiedEntries.length); this._timeline?.replaceEntries(verifiedEntries); this._observedEvents?.updateEvents(verifiedEntries); - }); + })); promises.push(promise); } await Promise.all(promises); From dd7bbe89ac6bc4abf13d98f73afc480912126fe4 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:37:53 +0100 Subject: [PATCH 289/354] put detached logs in correct log item --- src/matrix/room/BaseRoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 851e75c9..9e12f257 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -168,7 +168,7 @@ export class BaseRoom extends EventEmitter { } if (isTimelineOpen && decryption.hasUnverifiedSenders) { // verify missing senders async and update timeline once done so we don't delay rendering with network requests - log.wrapDetached("fetch unknown senders keys", async () => { + log.wrapDetached("fetch unknown senders keys", async log => { const newlyVerifiedDecryption = await decryption.fetchAndVerifyRemainingSenders(this._hsApi, log); const verifiedEntries = []; newlyVerifiedDecryption.applyToEntries(entries, entry => verifiedEntries.push(entry)); From fa21ac021c014c8d0e0b9096f3bf1932cb78e9de Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:38:08 +0100 Subject: [PATCH 290/354] use binding to update marking a message as (un)verified --- src/platform/web/ui/session/room/timeline/BaseMessageView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 74b96ecf..ee0a37db 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -46,7 +46,7 @@ export class BaseMessageView extends TemplateView { "Timeline_message": true, own: vm.isOwn, unsent: vm.isUnsent, - unverified: vm.isUnverified, + unverified: vm => vm.isUnverified, disabled: !this._interactive, continuation: vm => vm.isContinuation, }, From 6c73c317351f3e28c914b115c60f7d385346323d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 3 Nov 2022 17:32:01 +0100 Subject: [PATCH 291/354] take any decryption result, as it might now have a device to verify with --- src/matrix/room/timeline/entries/EventEntry.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index d218a598..cf56cbf9 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -33,10 +33,11 @@ export class EventEntry extends BaseEventEntry { } updateFrom(other) { - if (other._decryptionResult && !this._decryptionResult) { + // only update these when we attempted decryption, as some updates (like reactions) don't. + if (other._decryptionResult) { this._decryptionResult = other._decryptionResult; } - if (other._decryptionError && !this._decryptionError) { + if (other._decryptionError) { this._decryptionError = other._decryptionError; } this._contextForEntries = other.contextForEntries; From 0c0e164f13159e910afde3e38bc11bdd338a4459 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 4 Nov 2022 11:04:53 +0100 Subject: [PATCH 292/354] release v0.3.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20a16a4d..163f93d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.2", + "version": "0.3.3", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From ec4413550b48ab1e44c0f912b2e3de3b26525884 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 8 Nov 2022 07:59:26 +0100 Subject: [PATCH 293/354] apply method rename (and catch errors) --- src/domain/SessionLoadViewModel.js | 2 +- src/matrix/storage/idb/StorageFactory.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index abc16299..e86d718f 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -154,7 +154,7 @@ export class SessionLoadViewModel extends ViewModel { } async logout() { - await this._client.logout(); + await this._client.startLogout(this.navigation.path.get("session").value); this.navigation.push("session", true); } diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 44d481eb..722cf0a6 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -78,11 +78,15 @@ export class StorageFactory { return new Storage(db, this._idbFactory, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug, this._localStorage, log.logger); } - delete(sessionId: string): Promise { + async delete(sessionId: string): Promise { const databaseName = sessionName(sessionId); - clearKeysFromLocalStorage(this._localStorage, databaseName); - const req = this._idbFactory.deleteDatabase(databaseName); - return reqAsPromise(req); + try { + clearKeysFromLocalStorage(this._localStorage, databaseName); + } catch (e) {} + try { + const req = this._idbFactory.deleteDatabase(databaseName); + await reqAsPromise(req); + } catch (e) {} } async export(sessionId: string, log: ILogItem): Promise { From 308a2153499856a604043545e4a5f1cc718f586e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 8 Nov 2022 22:11:38 -0600 Subject: [PATCH 294/354] Add full MXID to message avatar/sender --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 2 +- src/platform/web/ui/avatar.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 03cc16ba..8fec8572 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -75,7 +75,7 @@ export class BaseMessageTile extends SimpleTile { } get avatarTitle() { - return this.displayName; + return this.sender; } get date() { diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 547d4b51..ce2dfdab 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -31,7 +31,11 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - const avatar = tag.div({className: avatarClasses, "data-testid": "avatar"}, [avatarContent]); + const avatar = tag.div({ + className: avatarClasses, + title: vm.avatarTitle, + "data-testid": "avatar", + }, [avatarContent]); if (hasAvatar) { setAttribute(avatar, "data-avatar-letter", vm.avatarLetter); setAttribute(avatar, "data-avatar-color", vm.avatarColorNumber); From a9fac140ac2024e3c8406a159bf72cfebb67f75b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 8 Nov 2022 23:44:22 -0600 Subject: [PATCH 295/354] Rename urlRouter usage to urlRouter --- doc/SDK.md | 2 +- doc/impl-thoughts/SSO.md | 2 +- src/domain/LogoutViewModel.ts | 2 +- src/domain/RootViewModel.js | 4 ++-- src/domain/SessionLoadViewModel.js | 2 +- src/domain/SessionPickerViewModel.js | 4 ++-- src/domain/ViewModel.ts | 6 +++--- src/domain/login/StartSSOLoginViewModel.ts | 2 +- src/domain/session/SessionStatusViewModel.js | 2 +- src/domain/session/leftpanel/InviteTileViewModel.js | 8 ++++---- src/domain/session/leftpanel/LeftPanelViewModel.js | 4 ++-- .../session/leftpanel/RoomBeingCreatedTileViewModel.js | 8 ++++---- src/domain/session/leftpanel/RoomTileViewModel.js | 2 +- src/domain/session/rightpanel/MemberTileViewModel.js | 2 +- src/domain/session/rightpanel/RightPanelViewModel.js | 4 ++-- src/domain/session/room/InviteViewModel.js | 2 +- src/domain/session/room/LightboxViewModel.js | 3 +-- src/domain/session/room/RoomBeingCreatedViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 2 +- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 2 +- src/domain/session/room/timeline/tiles/ImageTile.js | 2 +- src/domain/session/settings/SettingsViewModel.js | 2 +- src/platform/web/main.js | 2 +- 23 files changed, 35 insertions(+), 36 deletions(-) diff --git a/doc/SDK.md b/doc/SDK.md index c8f5197f..ba021ad1 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -85,7 +85,7 @@ async function main() { room, ownUserId: session.userId, platform, - urlCreator: urlRouter, + urlRouter: urlRouter, navigation, }); await vm.load(); diff --git a/doc/impl-thoughts/SSO.md b/doc/impl-thoughts/SSO.md index 2c84cd2c..51b87077 100644 --- a/doc/impl-thoughts/SSO.md +++ b/doc/impl-thoughts/SSO.md @@ -30,7 +30,7 @@ if (loginOptions.sso) { // store the homeserver for when we get redirected back after the sso flow platform.settingsStorage.setString("sso_homeserver", loginOptions.homeserver); // create the redirect url - const callbackUrl = urlCreator.createSSOCallbackURL(); // will just return the document url without any fragment + const callbackUrl = urlRouter.createSSOCallbackURL(); // will just return the document url without any fragment const redirectUrl = sso.createRedirectUrl(callbackUrl, provider); // and open it platform.openURL(redirectUrl); diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index b7aecc2c..49933f21 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -43,7 +43,7 @@ export class LogoutViewModel extends ViewModel { } get cancelUrl(): string | undefined { - return this.urlCreator.urlForSegment("session", true); + return this.urlRouter.urlForSegment("session", true); } async logout(): Promise { diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 8e74244b..524dfe13 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -83,14 +83,14 @@ export class RootViewModel extends ViewModel { } } } else if (loginToken) { - this.urlCreator.normalizeUrl(); + this.urlRouter.normalizeUrl(); if (this.activeSection !== "login") { this._showLogin(loginToken); } } else { try { - if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) { + if (!(shouldRestoreLastUrl && this.urlRouter.tryRestoreLastUrl())) { const sessionInfos = await this.platform.sessionInfoStorage.getAll(); if (sessionInfos.length === 0) { this.navigation.push("login"); diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index abc16299..8f34b798 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -29,7 +29,7 @@ export class SessionLoadViewModel extends ViewModel { this._deleteSessionOnCancel = deleteSessionOnCancel; this._loading = false; this._error = null; - this.backUrl = this.urlCreator.urlForSegment("session", true); + this.backUrl = this.urlRouter.urlForSegment("session", true); this._accountSetupViewModel = undefined; } diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index f4a16f1c..97b29f39 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -38,7 +38,7 @@ class SessionItemViewModel extends ViewModel { } get openUrl() { - return this.urlCreator.urlForSegment("session", this.id); + return this.urlRouter.urlForSegment("session", this.id); } get label() { @@ -94,6 +94,6 @@ export class SessionPickerViewModel extends ViewModel { } get cancelUrl() { - return this.urlCreator.urlForSegment("login"); + return this.urlRouter.urlForSegment("login"); } } diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 62da159f..8dbc37ea 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -33,7 +33,7 @@ import type {IURLRouter} from "./navigation/URLRouter"; export type Options = { platform: Platform; logger: ILogger; - urlCreator: IURLRouter; + urlRouter: IURLRouter; navigation: Navigation; emitChange?: (params: any) => void; } @@ -137,8 +137,8 @@ export class ViewModel = Op return this.platform.logger; } - get urlCreator(): IURLRouter { - return this._options.urlCreator; + get urlRouter(): IURLRouter { + return this._options.urlRouter; } get navigation(): Navigation { diff --git a/src/domain/login/StartSSOLoginViewModel.ts b/src/domain/login/StartSSOLoginViewModel.ts index b85bec71..0a20249b 100644 --- a/src/domain/login/StartSSOLoginViewModel.ts +++ b/src/domain/login/StartSSOLoginViewModel.ts @@ -42,7 +42,7 @@ export class StartSSOLoginViewModel extends ViewModel{ async startSSOLogin(): Promise { await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso!.homeserver); - const link = this._sso!.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); + const link = this._sso!.createSSORedirectURL(this.urlRouter.createSSOCallbackURL()); this.platform.openUrl(link); } } diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 8f1d0748..16332477 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -36,7 +36,7 @@ export class SessionStatusViewModel extends ViewModel { this._reconnector = reconnector; this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); this._session = session; - this._setupKeyBackupUrl = this.urlCreator.urlForSegment("settings"); + this._setupKeyBackupUrl = this.urlRouter.urlForSegment("settings"); this._dismissSecretStorage = false; } diff --git a/src/domain/session/leftpanel/InviteTileViewModel.js b/src/domain/session/leftpanel/InviteTileViewModel.js index cdd955b1..26c8ec76 100644 --- a/src/domain/session/leftpanel/InviteTileViewModel.js +++ b/src/domain/session/leftpanel/InviteTileViewModel.js @@ -22,7 +22,7 @@ export class InviteTileViewModel extends BaseTileViewModel { super(options); const {invite} = options; this._invite = invite; - this._url = this.urlCreator.openRoomActionUrl(this._invite.id); + this._url = this.urlRouter.openRoomActionUrl(this._invite.id); } get busy() { return this._invite.accepting || this._invite.rejecting; } @@ -53,9 +53,9 @@ export class InviteTileViewModel extends BaseTileViewModel { export function tests() { return { "test compare with timestamp": assert => { - const urlCreator = {openRoomActionUrl() { return "";}} - const vm1 = new InviteTileViewModel({invite: {timestamp: 500, id: "1"}, urlCreator}); - const vm2 = new InviteTileViewModel({invite: {timestamp: 250, id: "2"}, urlCreator}); + const urlRouter = {openRoomActionUrl() { return "";}} + const vm1 = new InviteTileViewModel({invite: {timestamp: 500, id: "1"}, urlRouter}); + const vm2 = new InviteTileViewModel({invite: {timestamp: 250, id: "2"}, urlRouter}); assert(vm1.compare(vm2) < 0); assert(vm2.compare(vm1) > 0); assert.equal(vm1.compare(vm1), 0); diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 652aa57c..23dca0b2 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -32,8 +32,8 @@ export class LeftPanelViewModel extends ViewModel { this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b)); this._currentTileVM = null; this._setupNavigation(); - this._closeUrl = this.urlCreator.urlForSegment("session"); - this._settingsUrl = this.urlCreator.urlForSegment("settings"); + this._closeUrl = this.urlRouter.urlForSegment("session"); + this._settingsUrl = this.urlRouter.urlForSegment("settings"); } _mapTileViewModels(roomsBeingCreated, invites, rooms) { diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index 81785bd9..b6c8c976 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -23,7 +23,7 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { super(options); const {roomBeingCreated} = options; this._roomBeingCreated = roomBeingCreated; - this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.id); + this._url = this.urlRouter.openRoomActionUrl(this._roomBeingCreated.id); } get busy() { return !this._roomBeingCreated.error; } @@ -59,9 +59,9 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { export function tests() { return { "test compare with names": assert => { - const urlCreator = {openRoomActionUrl() { return "";}} - const vm1 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "A", id: "1"}, urlCreator}); - const vm2 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "B", id: "2"}, urlCreator}); + const urlRouter = {openRoomActionUrl() { return "";}} + const vm1 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "A", id: "1"}, urlRouter}); + const vm2 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "B", id: "2"}, urlRouter}); assert(vm1.compare(vm2) < 0); assert(vm2.compare(vm1) > 0); assert.equal(vm1.compare(vm1), 0); diff --git a/src/domain/session/leftpanel/RoomTileViewModel.js b/src/domain/session/leftpanel/RoomTileViewModel.js index 8c38bb3e..ece57eaf 100644 --- a/src/domain/session/leftpanel/RoomTileViewModel.js +++ b/src/domain/session/leftpanel/RoomTileViewModel.js @@ -22,7 +22,7 @@ export class RoomTileViewModel extends BaseTileViewModel { super(options); const {room} = options; this._room = room; - this._url = this.urlCreator.openRoomActionUrl(this._room.id); + this._url = this.urlRouter.openRoomActionUrl(this._room.id); } get kind() { diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index 153c70c8..c8dcf63a 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -48,7 +48,7 @@ export class MemberTileViewModel extends ViewModel { get detailsUrl() { const roomId = this.navigation.path.get("room").value; - return `${this.urlCreator.openRoomActionUrl(roomId)}/member/${this._member.userId}`; + return `${this.urlRouter.openRoomActionUrl(roomId)}/member/${this._member.userId}`; } _updatePreviousName(newName) { diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index b4b6b4eb..7a13c7b1 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -64,8 +64,8 @@ export class RightPanelViewModel extends ViewModel { this._hookUpdaterToSegment("member", MemberDetailsViewModel, () => this._getMemberDetailsArguments(), () => { // If we fail to create the member details panel, fallback to memberlist - const url = `${this.urlCreator.urlUntilSegment("room")}/members`; - this.urlCreator.pushUrl(url); + const url = `${this.urlRouter.urlUntilSegment("room")}/members`; + this.urlRouter.pushUrl(url); } ); } diff --git a/src/domain/session/room/InviteViewModel.js b/src/domain/session/room/InviteViewModel.js index 00697642..7a6c2a6a 100644 --- a/src/domain/session/room/InviteViewModel.js +++ b/src/domain/session/room/InviteViewModel.js @@ -26,7 +26,7 @@ export class InviteViewModel extends ViewModel { this._mediaRepository = mediaRepository; this._onInviteChange = this._onInviteChange.bind(this); this._error = null; - this._closeUrl = this.urlCreator.urlUntilSegment("session"); + this._closeUrl = this.urlRouter.urlUntilSegment("session"); this._invite.on("change", this._onInviteChange); this._inviter = null; if (this._invite.inviter) { diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 8ce8757a..a14eef89 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -22,8 +22,7 @@ export class LightboxViewModel extends ViewModel { this._eventId = options.eventId; this._unencryptedImageUrl = null; this._decryptedImage = null; - this._closeUrl = this.urlCreator.urlUntilSegment("room"); - this._eventEntry = null; + this._closeUrl = this.urlRouter.urlUntilSegment("room"); this._date = null; this._subscribeToEvent(options.room, options.eventId); } diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index b503af73..9e230308 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -25,7 +25,7 @@ export class RoomBeingCreatedViewModel extends ViewModel { this._roomBeingCreated = roomBeingCreated; this._mediaRepository = mediaRepository; this._onRoomChange = this._onRoomChange.bind(this); - this._closeUrl = this.urlCreator.urlUntilSegment("session"); + this._closeUrl = this.urlRouter.urlUntilSegment("session"); this._roomBeingCreated.on("change", this._onRoomChange); } diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index be2e7832..ac169795 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -43,7 +43,7 @@ export class RoomViewModel extends ViewModel { this._recreateComposerOnPowerLevelChange(); } this._clearUnreadTimout = null; - this._closeUrl = this.urlCreator.urlUntilSegment("session"); + this._closeUrl = this.urlRouter.urlUntilSegment("session"); } async load() { diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 03cc16ba..31f28637 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -58,7 +58,7 @@ export class BaseMessageTile extends SimpleTile { } get memberPanelLink() { - return `${this.urlCreator.urlUntilSegment("room")}/member/${this.sender}`; + return `${this.urlRouter.urlUntilSegment("room")}/member/${this.sender}`; } // Avatar view model contract diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js index dd959b28..8ae55ca1 100644 --- a/src/domain/session/room/timeline/tiles/ImageTile.js +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -20,7 +20,7 @@ import {BaseMediaTile} from "./BaseMediaTile.js"; export class ImageTile extends BaseMediaTile { constructor(entry, options) { super(entry, options); - this._lightboxUrl = this.urlCreator.urlForSegments([ + this._lightboxUrl = this.urlRouter.urlForSegments([ // ensure the right room is active if in grid view this.navigation.segment("room", this._room.id), this.navigation.segment("lightbox", this._entry.id) diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 4dcdb111..ad01c64e 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -45,7 +45,7 @@ export class SettingsViewModel extends ViewModel { const {client} = options; this._client = client; this._keyBackupViewModel = this.track(new KeyBackupViewModel(this.childOptions({session: this._session}))); - this._closeUrl = this.urlCreator.urlUntilSegment("session"); + this._closeUrl = this.urlRouter.urlUntilSegment("session"); this._estimate = null; this.sentImageSizeLimit = null; this.minSentImageSizeLimit = 400; diff --git a/src/platform/web/main.js b/src/platform/web/main.js index 83644456..2b28187e 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -41,7 +41,7 @@ export async function main(platform) { platform, // the only public interface of the router is to create urls, // so we call it that in the view models - urlCreator: urlRouter, + urlRouter: urlRouter, navigation, }); await vm.load(); From 47d557b38dab64b310c5531fc23d49c277da72c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:20:59 +0100 Subject: [PATCH 296/354] expand comment how to handle race here --- src/matrix/e2ee/DeviceTracker.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 484a6d0b..55f666e4 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -202,6 +202,9 @@ export class DeviceTracker { async _queryKeys(userIds, hsApi, log) { // TODO: we need to handle the race here between /sync and /keys/query just like we need to do for the member list ... // there are multiple requests going out for /keys/query though and only one for /members + // So, while doing /keys/query, writeDeviceChanges should add userIds marked as outdated to a list + // when /keys/query returns, we should check that list and requery if we queried for a given user. + // and then remove the list. const deviceKeyResponse = await hsApi.queryKeys({ "timeout": 10000, From 860f435855611c0191ed55dcd382bdbcf59efd8d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:26:21 +0100 Subject: [PATCH 297/354] log session afterSyncCompleted with normal log level --- src/matrix/Sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index c995e3e2..4c414149 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -158,7 +158,7 @@ export class Sync { const isCatchupSync = this._status.get() === SyncStatus.CatchupSync; const sessionPromise = (async () => { try { - await log.wrap("session", log => this._session.afterSyncCompleted(sessionChanges, isCatchupSync, log), log.level.Detail); + await log.wrap("session", log => this._session.afterSyncCompleted(sessionChanges, isCatchupSync, log)); } catch (err) {} // error is logged, but don't fail sessionPromise })(); const roomsPromises = roomStates.map(async rs => { From 31579b4945352c3622d16e09e037629eff916e5b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:26:50 +0100 Subject: [PATCH 298/354] when tracking room, check roomId isn't on user we shouldn't share with --- src/matrix/e2ee/DeviceTracker.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 55f666e4..edfdbd31 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -120,6 +120,7 @@ export class DeviceTracker { const txn = await this._storage.readWriteTxn([ this._storage.storeNames.roomSummary, this._storage.storeNames.userIdentities, + this._storage.storeNames.deviceIdentities, // to remove all devices in _removeRoomFromUserIdentity ]); try { let isTrackingChanges; @@ -127,9 +128,13 @@ export class DeviceTracker { isTrackingChanges = room.writeIsTrackingMembers(true, txn); const members = Array.from(memberList.members.values()); log.set("members", members.length); + // TODO: should we remove any userIdentities we should not share the key with?? + // e.g. as an extra security measure if we had a mistake in other code? await Promise.all(members.map(async member => { if (shouldShareKey(member.membership, historyVisibility)) { await this._addRoomToUserIdentity(member.roomId, member.userId, txn); + } else { + await this._removeRoomFromUserIdentity(member.roomId, member.userId, txn); } })); } catch (err) { From c78bed846ec1719c93c1966d588175bc7b8c4932 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:28:18 +0100 Subject: [PATCH 299/354] create unknown userIdentity when processing /keys/query response this can happen when the room isn't tracked yet, which is a use case we add support for in the next commit to verify senders that we don't know about yet (e.g. when the room isn't tracked). --- src/matrix/e2ee/DeviceTracker.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index edfdbd31..410dfe85 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -21,13 +21,17 @@ import {RoomMember} from "../room/members/RoomMember.js"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; +function createUserIdentity(userId, initialRoomId = undefined) { + return { + userId: userId, + roomIds: initialRoomId ? [initialRoomId] : [], + deviceTrackingStatus: TRACKING_STATUS_OUTDATED, + }; +} + function addRoomToIdentity(identity, userId, roomId) { if (!identity) { - identity = { - userId: userId, - roomIds: [roomId], - deviceTrackingStatus: TRACKING_STATUS_OUTDATED, - }; + identity = createUserIdentity(userId, roomId); return identity; } else { if (!identity.roomIds.includes(roomId)) { @@ -272,7 +276,15 @@ export class DeviceTracker { txn.deviceIdentities.set(deviceIdentity); } // mark user identities as up to date - const identity = await txn.userIdentities.get(userId); + let identity = await txn.userIdentities.get(userId); + if (!identity) { + // create the identity if it doesn't exist, which can happen if + // we request devices before tracking the room. + // IMPORTANT here that the identity gets created without any roomId! + // if we claim that we share and e2ee room with the user without having + // checked, we could share keys with that user without them being in the room + identity = createUserIdentity(userId); + } identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; txn.userIdentities.set(identity); From 3d5a733267742a1a730f72ff160e058909d0bd08 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:51:40 +0100 Subject: [PATCH 300/354] split up _devicesForUserIds to reuse with different outdated criteria --- src/matrix/e2ee/DeviceTracker.js | 60 ++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 410dfe85..d200850b 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -343,6 +343,7 @@ export class DeviceTracker { /** * Gives all the device identities for a room that is already tracked. + * Can be used to decide which users to share keys with. * Assumes room is already tracked. Call `trackRoom` first if unsure. * @param {String} roomId [description] * @return {[type]} [description] @@ -359,41 +360,67 @@ export class DeviceTracker { // So, this will also contain non-joined memberships const userIds = await txn.roomMembers.getAllUserIds(roomId); - - return await this._devicesForUserIds(roomId, userIds, txn, hsApi, log); + // TODO: check here if userIds is safe? yes it is + return await this._devicesForUserIdsInTrackedRoom(roomId, userIds, txn, hsApi, log); } + /** + * Can be used to decide which users to share keys with. + * Assumes room is already tracked. Call `trackRoom` first if unsure. + */ async devicesForRoomMembers(roomId, userIds, hsApi, log) { const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, ]); - return await this._devicesForUserIds(roomId, userIds, txn, hsApi, log); + return await this._devicesForUserIdsInTrackedRoom(roomId, userIds, txn, hsApi, log); + } } /** - * @param {string} roomId [description] - * @param {Array} userIds a set of user ids to try and find the identity for. Will be check to belong to roomId. + * Gets all the device identities with which keys should be shared for a set of users in a tracked room. + * If any userIdentities are outdated, it will fetch them from the homeserver. + * @param {string} roomId the id of the tracked room to filter users by. + * @param {Array} userIds a set of user ids to try and find the identity for. * @param {Transaction} userIdentityTxn to read the user identities * @param {HomeServerApi} hsApi - * @return {Array} + * @return {Array} all devices identities for the given users we should share keys with. */ - async _devicesForUserIds(roomId, userIds, userIdentityTxn, hsApi, log) { + async _devicesForUserIdsInTrackedRoom(roomId, userIds, userIdentityTxn, hsApi, log) { const allMemberIdentities = await Promise.all(userIds.map(userId => userIdentityTxn.userIdentities.get(userId))); const identities = allMemberIdentities.filter(identity => { - // identity will be missing for any userIds that don't have - // membership join in any of your encrypted rooms + // we use roomIds to decide with whom we should share keys for a given room, + // taking into account the membership and room history visibility. + // so filter out anyone who we shouldn't share keys with. + // Given we assume the room is tracked, + // also exclude any userId which doesn't have a userIdentity yet. return identity && identity.roomIds.includes(roomId); }); const upToDateIdentities = identities.filter(i => i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE); - const outdatedIdentities = identities.filter(i => i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED); + const outdatedUserIds = identities + .filter(i => i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED) + .map(i => i.userId); + let devices = await this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); + // filter out our own device as we should never share keys with it. + devices = devices.filter(device => { + const isOwnDevice = device.userId === this._ownUserId && device.deviceId === this._ownDeviceId; + return !isOwnDevice; + }); + return devices; + } + + /** Gets the device identites for a set of user identities that + * are known to be up to date, and a set of userIds that are known + * to be absent from our store our outdated. The outdated user ids + * will have their keys fetched from the homeserver. */ + async _devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log) { log.set("uptodate", upToDateIdentities.length); - log.set("outdated", outdatedIdentities.length); + log.set("outdated", outdatedUserIds.length); let queriedDevices; - if (outdatedIdentities.length) { + if (outdatedUserIds.length) { // TODO: ignore the race between /sync and /keys/query for now, // where users could get marked as outdated or added/removed from the room while // querying keys - queriedDevices = await this._queryKeys(outdatedIdentities.map(i => i.userId), hsApi, log); + queriedDevices = await this._queryKeys(outdatedUserIds, hsApi, log); } const deviceTxn = await this._storage.readTxn([ @@ -406,12 +433,7 @@ export class DeviceTracker { if (queriedDevices && queriedDevices.length) { flattenedDevices = flattenedDevices.concat(queriedDevices); } - // filter out our own device - const devices = flattenedDevices.filter(device => { - const isOwnDevice = device.userId === this._ownUserId && device.deviceId === this._ownDeviceId; - return !isOwnDevice; - }); - return devices; + return flattenedDevices; } async getDeviceByCurve25519Key(curve25519Key, txn) { From 155f4beba87b46d24452705999b55076ae58eb8c Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:53:11 +0100 Subject: [PATCH 301/354] add devicesForUser to fetch devices for untracked room and use it when fetching senders to verify. --- src/matrix/e2ee/DeviceTracker.js | 53 +++++++++++++++++++++++++++++++ src/matrix/e2ee/RoomEncryption.js | 7 ++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index d200850b..592ad24e 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -374,6 +374,29 @@ export class DeviceTracker { ]); return await this._devicesForUserIdsInTrackedRoom(roomId, userIds, txn, hsApi, log); } + + /** + * Cannot be used to decide which users to share keys with. + * Does not assume membership to any room or whether any room is tracked. + */ + async devicesForUsers(userIds, hsApi, log) { + const txn = await this._storage.readTxn([ + this._storage.storeNames.userIdentities, + ]); + + const upToDateIdentities = []; + const outdatedUserIds = []; + await Promise.all(userIds.map(async userId => { + const i = await txn.userIdentities.get(userId); + if (i && i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE) { + upToDateIdentities.push(i); + } else if (!i || i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED) { + // allow fetching for userIdentities we don't know about yet, + // as we don't assume the room is tracked here. + outdatedUserIds.push(userId); + } + })); + return this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); } /** @@ -773,5 +796,35 @@ export function tests() { const txn2 = await storage.readTxn([storage.storeNames.userIdentities]); assert.deepEqual((await txn2.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld", "!def:hs.tld"]); }, + "devicesForUsers fetches users even though they aren't in any tracked room": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + const hsApi = createQueryKeysHSApiMock(); + const devices = await tracker.devicesForUsers(["@bob:hs.tld"], hsApi, NullLoggerInstance.item); + assert.equal(devices.length, 1); + assert.equal(devices[0].curve25519Key, "curve25519:@bob:hs.tld:device1:key"); + const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, []); + }, + "devicesForUsers doesn't add any roomId when creating userIdentity": async assert => { + const storage = await createMockStorage(); + const tracker = new DeviceTracker({ + storage, + getSyncToken: () => "token", + olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + ownUserId: "@alice:hs.tld", + ownDeviceId: "ABCD", + }); + const hsApi = createQueryKeysHSApiMock(); + await tracker.devicesForUsers(["@bob:hs.tld"], hsApi, NullLoggerInstance.item); + const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, []); + } } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 4dd6cd0b..27388e1c 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -230,9 +230,10 @@ export class RoomEncryption { return senders.add(r.encryptedEvent.sender); }, new Set())); log.set("senders", sendersWithoutDevice); - // fetch the devices, ignore return value, - // and just reuse _verifyDecryptionResults method so we only have one impl how to verify - await this._deviceTracker.devicesForRoomMembers(this._room.id, sendersWithoutDevice, hsApi, log); + // Fetch the devices, ignore return value, and just reuse + // _verifyDecryptionResults method so we only have one impl how to verify. + // Use devicesForUsers rather than devicesForRoomMembers as the room might not be tracked yet + await this._deviceTracker.devicesForUsers(sendersWithoutDevice, hsApi, log); // now that we've fetched the missing devices, try verifying the results again const txn = await this._storage.readTxn([this._storage.storeNames.deviceIdentities]); await this._verifyDecryptionResults(resultsWithoutDevice, txn); From dbac61f78f45f135c4f9100a824c97fc7019bc7d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 10 Nov 2022 18:29:30 +0100 Subject: [PATCH 302/354] release v0.3.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 163f93d5..5429e0cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.3", + "version": "0.3.4", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 4125165760b866e7aa4e7f18c36e4fe05c025eb8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:59:27 -0600 Subject: [PATCH 303/354] Add full MXID tooltip to message sender display name Follow-up to https://github.com/vector-im/hydrogen-web/pull/917 --- .../web/ui/session/room/timeline/BaseMessageView.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index ee0a37db..d998e826 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -63,7 +63,13 @@ export class BaseMessageView extends TemplateView { li.removeChild(li.querySelector(".Timeline_messageSender")); } else if (!isContinuation && !this._isReplyPreview) { const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); - const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName); + const sender = tag.div( + { + className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`, + title: vm.sender, + }, + vm.displayName, + ); li.insertBefore(avatar, li.firstChild); li.insertBefore(sender, li.firstChild); } From 1aba18ca1db687d78cc86d2062cf3154ead443da Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 21:14:57 -0600 Subject: [PATCH 304/354] Add ,
, and HTML elements to templating --- src/platform/web/ui/general/html.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 4e1fe78d..93512897 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -102,10 +102,10 @@ export const SVG_NS: string = "http://www.w3.org/2000/svg"; export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "strong", "em", "span", "img", "section", "header", "main", "footer", - "article", "aside", "del", "blockquote", + "p", "strong", "em", "span", "img", "section", "header", "main", "footer", "dialog", + "article", "aside", "del", "blockquote", "details", "summary", "table", "thead", "tbody", "tr", "th", "td", "hr", - "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", + "pre", "code", "button", "time", "input", "textarea", "select", "option", "optgroup", "label", "form", "progress", "output", "video", "style"], [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; From 40f16a40fc04471bc1183fb96d7abcebe6802d7a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 15 Nov 2022 09:52:06 +0000 Subject: [PATCH 305/354] Update README.md Fix #909 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6c447024..336b5b53 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ You can run Hydrogen locally by the following commands in the terminal: Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md). +PS: You need nodejs, running yarn on top of any other js platform is not support. + # FAQ Some frequently asked questions are answered [here](doc/FAQ.md). From 2b218abda38cded949fa8c6d8701b0c1296227bc Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 15 Nov 2022 09:52:34 +0000 Subject: [PATCH 306/354] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 336b5b53..a4c1529f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ You can run Hydrogen locally by the following commands in the terminal: Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md). -PS: You need nodejs, running yarn on top of any other js platform is not support. +PS: You need nodejs, running yarn on top of any other js platform is not supported. # FAQ From 12e378eb62cb1cef52639c15ff2eab7cb22f760f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 18 Nov 2022 09:52:57 +0100 Subject: [PATCH 307/354] some thoughts on how different room types could be implemented --- doc/impl-thoughts/room-types.ts | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 doc/impl-thoughts/room-types.ts diff --git a/doc/impl-thoughts/room-types.ts b/doc/impl-thoughts/room-types.ts new file mode 100644 index 00000000..f683e927 --- /dev/null +++ b/doc/impl-thoughts/room-types.ts @@ -0,0 +1,55 @@ +/* +different room types create different kind of "sync listeners", who implement the sync lifecycle handlers + +they would each have a factory, +*/ + +interface IRoomSyncHandler { + prepareSync() + afterPrepareSync() + writeSync() + afterSync() + afterSyncCompleted() +} + +interface IRoom extends IRoomSyncHandler { + start(): void; + load(): void; + get id(): string; +} + +interface IRoomFactory { + createRoom(type, roomId, syncResponse): T + createSchema(db, txn, oldVersion, version, log) + get storesForSync(): string[]; + get rooms(): ObservableMap +} + +class InstantMessageRoom implements IRoom { +} + +class InstantMessageRoomFactory implements IRoomFactory{ + loadLastMessages(): Promise + /* + get all room ids and sort them according to idb sorting order + open cursor 'f' on `timelineFragments` + open a cursor 'e' on `timelineEvents` + for each room: + with cursor 'f', go to last fragment id and go up from there to find live fragment + with cursor 'e', go to last event index for fragment id and room id and go up until we have acceptable event type + for encrypted rooms: + decrypt message if needed (m.room.encrypted is likely something we want to display) + */ +} + +class SpaceRoom implements IRoom {} + +class SpaceRoomFactory implements IRoomFactory { + createRoom(type, roomId, syncResponse): IRoomSyncHandler +} + +class Session { + constructor(roomFactoriesByType: Map) { + + } +} From 3f7c1577e0fdd9d22682b9bd7e4578c0c0174db1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 18 Nov 2022 23:26:59 +0100 Subject: [PATCH 308/354] WIP --- .../session/room/timeline/TilesCollection.js | 76 ++++++++-- .../room/timeline/tiles/BaseMessageTile.js | 3 +- .../session/room/timeline/tiles/DateTile.ts | 141 ++++++++++++++++++ .../session/room/timeline/tiles/GapTile.js | 5 + .../session/room/timeline/tiles/ITile.ts | 44 ++++++ .../session/room/timeline/tiles/SimpleTile.js | 32 +++- .../session/room/timeline/tiles/index.ts | 4 +- src/platform/web/ui/session/room/common.ts | 27 ++-- .../session/room/timeline/DateHeaderView.ts | 33 ++++ 9 files changed, 332 insertions(+), 33 deletions(-) create mode 100644 src/domain/session/room/timeline/tiles/DateTile.ts create mode 100644 src/domain/session/room/timeline/tiles/ITile.ts create mode 100644 src/platform/web/ui/session/room/timeline/DateHeaderView.ts diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 173b0cf6..8dfbf2a1 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -16,6 +16,7 @@ limitations under the License. import {BaseObservableList} from "../../../../observable/list/BaseObservableList"; import {sortedIndex} from "../../../../utils/sortedIndex"; +import {TileShape} from "./tiles/ITile"; // maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary // for now, tileClassForEntry should be stable in whether it returns a tile or not. @@ -51,12 +52,14 @@ export class TilesCollection extends BaseObservableList { } _populateTiles() { + this._silent = true; this._tiles = []; let currentTile = null; for (let entry of this._entries) { if (!currentTile || !currentTile.tryIncludeEntry(entry)) { currentTile = this._createTile(entry); if (currentTile) { + console.log("adding initial tile", currentTile.shape, currentTile.eventId, "at", this._tiles.length); this._tiles.push(currentTile); } } @@ -72,11 +75,20 @@ export class TilesCollection extends BaseObservableList { if (prevTile) { prevTile.updateNextSibling(null); } + // add date headers here + for (let idx = 0; idx < this._tiles.length; idx += 1) { + const tile = this._tiles[idx]; + if (tile.needsDateSeparator) { + this._addTileAt(idx, tile.createDateSeparator(), true); + idx += 1; // tile's index moved one up, don't process it again + } + } // now everything is wired up, // allow tiles to emit updates for (const tile of this._tiles) { tile.setUpdateEmit(this._emitSpontanousUpdate); } + this._silent = false; } _findTileIdx(entry) { @@ -130,25 +142,61 @@ export class TilesCollection extends BaseObservableList { const newTile = this._createTile(entry); if (newTile) { - if (prevTile) { - prevTile.updateNextSibling(newTile); - // this emits an update while the add hasn't been emitted yet - newTile.updatePreviousSibling(prevTile); - } - if (nextTile) { - newTile.updateNextSibling(nextTile); - nextTile.updatePreviousSibling(newTile); - } - this._tiles.splice(tileIdx, 0, newTile); - this.emitAdd(tileIdx, newTile); - // add event is emitted, now the tile - // can emit updates - newTile.setUpdateEmit(this._emitSpontanousUpdate); + console.log("adding tile", newTile.shape, newTile.eventId, "at", tileIdx); + this._addTileAt(tileIdx, newTile); + this._evaluateDateHeaderAtIdx(tileIdx); } // find position by sort key // ask siblings to be included? both? yes, twice: a (insert c here) b, ask a(c), if yes ask b(a), else ask b(c)? if yes then b(a)? } + _evaluateDateHeaderAtIdx(tileIdx) { + //console.log("_evaluateDateHeaderAtIdx", tileIdx); + // consider the two adjacent tiles where the previous sibling changed: + // the new tile and the next tile + for (let i = 0; i < 5; i += 1) { + const idx = Math.max(tileIdx + i - 2, 0); + const tile = this._tiles[idx]; + const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined; + const hasDateSeparator = prevTile?.shape === TileShape.DateHeader; + if (tile.needsDateSeparator) { + if (hasDateSeparator) { + // TODO: replace this by return UpdateAction from updateNextSibling + // and do this in onAdd + //console.log(" update", idx - 1, prevTile?.shape, prevTile?.eventId); + this.emitUpdate(idx - 1, prevTile, "date"); + } else { + //console.log(" add", idx, tile.shape, tile.eventId); + tileIdx += 1; + this._addTileAt(idx, tile.createDateSeparator()); + } + // TODO must be looking at the wrong index to find the old date separator?? + } else if (hasDateSeparator) { + // this is never triggered because needsDateSeparator is not cleared + // when loading more items because we don't do anything once the + // direct sibling is a DateTile + //console.log(" remove", idx -1, prevTile?.shape, prevTile?.eventId); + this._removeTile(idx - 1, prevTile); + } + } + } + + _addTileAt(idx, newTile, silent = false) { + const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined; + const nextTile = this._tiles[idx]; + prevTile?.updateNextSibling(newTile); + newTile.updatePreviousSibling(prevTile); + newTile.updateNextSibling(nextTile); + nextTile?.updatePreviousSibling(newTile); + this._tiles.splice(idx, 0, newTile); + if (!silent) { + this.emitAdd(idx, newTile); + } + // add event is emitted, now the tile + // can emit updates + newTile.setUpdateEmit(this._emitSpontanousUpdate); + } + onUpdate(index, entry, params) { // if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it if (!this._tiles) { diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index cfa27a94..1ad1ba44 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -15,13 +15,13 @@ limitations under the License. */ import {SimpleTile} from "./SimpleTile.js"; +import {TileShape} from "./ITile"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; export class BaseMessageTile extends SimpleTile { constructor(entry, options) { super(entry, options); - this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._isContinuation = false; this._reactions = null; this._replyTile = null; @@ -78,6 +78,7 @@ export class BaseMessageTile extends SimpleTile { return this.sender; } + // TODO: remove? get date() { return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"}); } diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts new file mode 100644 index 00000000..413ec2b2 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -0,0 +1,141 @@ +import {ITile, TileShape, EmitUpdateFn} from "./ITile"; +import {UpdateAction} from "../UpdateAction"; +import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry"; +import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry"; +import {ViewModel} from "../../../../ViewModel"; +import type {Options} from "../../../../ViewModel"; + +/** + * edge cases: + * - be able to remove the tile in response to the sibling changing, + * probably by letting updateNextSibling/updatePreviousSibling + * return an UpdateAction and change TilesCollection accordingly. + * this is relevant when next becomes undefined there when + * a pending event is removed on remote echo. + * */ + +export class DateTile extends ViewModel implements ITile { + private _emitUpdate?: EmitUpdateFn; + private _dateString?: string; + + constructor(private _firstTileInDay: ITile, options: Options) { + super(options); + } + + setUpdateEmit(emitUpdate: EmitUpdateFn): void { + this._emitUpdate = emitUpdate; + } + + get upperEntry(): BaseEventEntry { + return this.refEntry; + } + + get lowerEntry(): BaseEventEntry { + return this.refEntry; + } + + /** the entry reference by this datetile, e.g. the entry of the first tile for this day */ + private get refEntry(): BaseEventEntry { + // lowerEntry is the first entry... i think? + // so given the date header always comes before, + // this is our closest entry. + return this._firstTileInDay.lowerEntry; + } + + compare(tile: ITile): number { + return this.compareEntry(tile.upperEntry); + } + + get date(): string { + if (!this._dateString) { + const date = new Date(this.refEntry.timestamp); + this._dateString = date.toLocaleDateString({}, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + return this._dateString; + } + + get shape(): TileShape { + return TileShape.DateHeader; + } + + get needsDateSeparator(): boolean { + return false; + } + + createDateSeparator(): undefined { + return undefined; + } + +/** + * _findTileIdx in TilesCollection should never return + * the index of a DateTile as that is mainly used + * for mapping incoming event indices coming from the Timeline + * to the tile index to propage the event. + * This is not a path that is relevant to date headers as they + * are added as a side-effect of adding other tiles and are generally + * not updated (only removed in some cases). _findTileIdx is also + * used for emitting spontanous updates, but that should also not be + * needed for a DateTile. + * The problem is basically that _findTileIdx maps an entry to + * a tile, and DateTile adopts the entry of it's sibling tile (_firstTileInDay) + * so now we have the entry pointing to two tiles. So we should avoid + * returning the DateTile itself from the compare method. + * We will always return -1 or 1 from here to signal an entry comes before or after us, + * never 0 + * */ + compareEntry(entry: BaseEntry): number { + const result = this.refEntry.compare(entry); + if (result === 0) { + // if it's a match for the reference entry (e.g. _firstTileInDay), + // say it comes after us as the date tile always comes at the top + // of the day. + return -1; + } + // otherwise, assume the given entry is never for ourselves + // as we don't have our own entry, we only borrow one from _firstTileInDay + return result; + } + + // update received for already included (falls within sort keys) entry + updateEntry(entry, param): UpdateAction { + return UpdateAction.Nothing(); + } + + // return whether the tile should be removed + // as SimpleTile only has one entry, the tile should be removed + removeEntry(entry: BaseEntry): boolean { + return false; + } + + // SimpleTile can only contain 1 entry + tryIncludeEntry(): boolean { + return false; + } + + // let item know it has a new sibling + updatePreviousSibling(prev: ITile | undefined): void { + this._firstTileInDay.updatePreviousSibling(prev); + } + + // let item know it has a new sibling + updateNextSibling(next: ITile | undefined): UpdateAction { + // TODO: next can be undefined when a pending event is removed + // TODO: we need a way to remove this date header + this._firstTileInDay = next!; + this._dateString = undefined; + // TODO: do we need to reevaluate our date here and emit an update? + } + + notifyVisible(): void { + // trigger sticky logic here? + } + + dispose(): void { + + } +} diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index bb7d8086..1e6bdd08 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -19,6 +19,7 @@ import {UpdateAction} from "../UpdateAction.js"; import {ConnectionError} from "../../../../../matrix/error.js"; import {ConnectionStatus} from "../../../../../matrix/net/Reconnector"; +// TODO: should this become an ITile and SimpleTile become EventTile? export class GapTile extends SimpleTile { constructor(entry, options) { super(entry, options); @@ -29,6 +30,10 @@ export class GapTile extends SimpleTile { this._showSpinner = false; } + get needsDateSeparator() { + return false; + } + async fill() { if (!this._loading && !this._entry.edgeReached) { this._loading = true; diff --git a/src/domain/session/room/timeline/tiles/ITile.ts b/src/domain/session/room/timeline/tiles/ITile.ts new file mode 100644 index 00000000..dd6c0f81 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/ITile.ts @@ -0,0 +1,44 @@ +import {UpdateAction} from "../UpdateAction.js"; +import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry"; +import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry"; +import {IDisposable} from "../../../../../utils/Disposables"; + +export type EmitUpdateFn = (tile: ITile, props: any) => void + +export enum TileShape { + Message = "message", + MessageStatus = "message-status", + Announcement = "announcement", + File = "file", + Gap = "gap", + Image = "image", + Location = "location", + MissingAttachment = "missing-attachment", + Redacted = "redacted", + Video = "video", + DateHeader = "date-header" +} + +// TODO: should we imply inheriting from view model here? +export interface ITile extends IDisposable { + setUpdateEmit(emitUpdate: EmitUpdateFn): void; + get upperEntry(): E; + get lowerEntry(): E; + compare(tile: ITile): number; + compareEntry(entry: BaseEntry): number; + // update received for already included (falls within sort keys) entry + updateEntry(entry: BaseEntry, param: any): UpdateAction; + // return whether the tile should be removed + // as SimpleTile only has one entry, the tile should be removed + removeEntry(entry: BaseEntry): boolean + // SimpleTile can only contain 1 entry + tryIncludeEntry(): boolean; + // let item know it has a new sibling + updatePreviousSibling(prev: ITile | undefined): void; + // let item know it has a new sibling + updateNextSibling(next: ITile | undefined): void; + notifyVisible(): void; + get needsDateSeparator(): boolean; + createDateSeparator(): ITile | undefined; + get shape(): TileShape; +} diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 04141576..7e394d25 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -15,13 +15,17 @@ limitations under the License. */ import {UpdateAction} from "../UpdateAction.js"; +import {TileShape} from "./ITile"; import {ViewModel} from "../../../../ViewModel"; import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js"; +import {DateTile} from "./DateTile"; export class SimpleTile extends ViewModel { constructor(entry, options) { super(options); this._entry = entry; + this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : undefined; + this._needsDateSeparator = false; this._emitUpdate = undefined; } // view model props for all subclasses @@ -37,8 +41,26 @@ export class SimpleTile extends ViewModel { return false; } - get hasDateSeparator() { - return false; + get needsDateSeparator() { + return this._needsDateSeparator; + } + + createDateSeparator() { + return new DateTile(this, this.childOptions({})); + } + + _updateDateSeparator(prev) { + if (prev && prev._date && this._date) { + const neededDateSeparator = this._needsDateSeparator; + this._needsDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() || + prev._date.getMonth() !== this._date.getMonth() || + prev._date.getDate() !== this._date.getDate(); + if (neededDateSeparator && !this._needsDateSeparator) { + console.log("clearing needsDateSeparator", {this: this._entry.content, prev: prev.content}); + } + } else { + this._needsDateSeparator = !!this._date; + } } get id() { @@ -123,8 +145,10 @@ export class SimpleTile extends ViewModel { return false; } // let item know it has a new sibling - updatePreviousSibling(/*prev*/) { - + updatePreviousSibling(prev) { + if (prev?.shape !== TileShape.DateHeader) { + this._updateDateSeparator(prev); + } } // let item know it has a new sibling diff --git a/src/domain/session/room/timeline/tiles/index.ts b/src/domain/session/room/timeline/tiles/index.ts index 242bea2f..6822a91d 100644 --- a/src/domain/session/room/timeline/tiles/index.ts +++ b/src/domain/session/room/timeline/tiles/index.ts @@ -27,7 +27,7 @@ import {EncryptedEventTile} from "./EncryptedEventTile.js"; import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js"; import {MissingAttachmentTile} from "./MissingAttachmentTile.js"; -import type {SimpleTile} from "./SimpleTile.js"; +import type {ITile, TileShape} from "./ITile"; import type {Room} from "../../../../../matrix/room/Room"; import type {Timeline} from "../../../../../matrix/room/timeline/Timeline"; import type {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry"; @@ -42,7 +42,7 @@ export type Options = ViewModelOptions & { timeline: Timeline tileClassForEntry: TileClassForEntryFn; }; -export type TileConstructor = new (entry: TimelineEntry, options: Options) => SimpleTile; +export type TileConstructor = new (entry: TimelineEntry, options: Options) => ITile; export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined { if (entry.isGap) { diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 7b62630f..6a47ff90 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -22,31 +22,34 @@ import {LocationView} from "./timeline/LocationView.js"; import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js"; import {AnnouncementView} from "./timeline/AnnouncementView.js"; import {RedactedView} from "./timeline/RedactedView.js"; -import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js"; +import {ITile, TileShape} from "../../../../../domain/session/room/timeline/tiles/ITile.js"; import {GapView} from "./timeline/GapView.js"; +import {DateHeaderView} from "./timeline/DateHeaderView"; import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView"; -export function viewClassForTile(vm: SimpleTile): TileViewConstructor { +export function viewClassForTile(vm: ITile): TileViewConstructor { switch (vm.shape) { - case "gap": + case TileShape.Gap: return GapView; - case "announcement": + case TileShape.Announcement: return AnnouncementView; - case "message": - case "message-status": + case TileShape.Message: + case TileShape.MessageStatus: return TextMessageView; - case "image": + case TileShape.Image: return ImageView; - case "video": + case TileShape.Video: return VideoView; - case "file": + case TileShape.File: return FileView; - case "location": + case TileShape.Location: return LocationView; - case "missing-attachment": + case TileShape.MissingAttachment: return MissingAttachmentView; - case "redacted": + case TileShape.Redacted: return RedactedView; + case TileShape.DateHeader: + return DateHeaderView; default: throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`); } diff --git a/src/platform/web/ui/session/room/timeline/DateHeaderView.ts b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts new file mode 100644 index 00000000..63003e91 --- /dev/null +++ b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts @@ -0,0 +1,33 @@ +/* +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"; +import {spinner} from "../../../common.js"; +import type {DateTile} from "../../../../../../domain/session/room/timeline/tiles/DateTile"; + +export class DateHeaderView extends TemplateView { + // ignore other argument + constructor(vm) { + super(vm); + } + + render(t, vm) { + return t.div({className: "DateHeader"}, t.div(vm.date)); + } + + /* This is called by the parent ListView, which just has 1 listener for the whole list */ + onClick() {} +} From e9053372d64fd380970f7282610878ad6af0df03 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Nov 2022 11:13:47 +0530 Subject: [PATCH 309/354] Encode ids as URI component --- src/domain/navigation/URLRouter.ts | 2 +- src/domain/navigation/index.ts | 30 ++++++++++++++----- .../session/rightpanel/MemberTileViewModel.js | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/domain/navigation/URLRouter.ts b/src/domain/navigation/URLRouter.ts index bf1c218d..23503530 100644 --- a/src/domain/navigation/URLRouter.ts +++ b/src/domain/navigation/URLRouter.ts @@ -144,7 +144,7 @@ export class URLRouter implements IURLRou 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}`; + const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${encodeURIComponent(roomId)}`; return this._history.pathAsUrl(urlPath); } diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index af3c35bd..f580206e 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -147,7 +147,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, segments.push(new Segment("empty-grid-tile", selectedIndex)); } } else if (type === "open-room") { - const roomId = iterator.next().value; + const roomId = decodeURIComponent(iterator.next().value); if (!roomId) { break; } const rooms = currentNavPath.get("rooms"); if (rooms) { @@ -176,7 +176,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, } else if (type === "details" || type === "members") { pushRightPanelSegment(segments, type); } else if (type === "member") { - const userId = iterator.next().value; + const userId = decodeURIComponent(iterator.next().value); if (!userId) { break; } pushRightPanelSegment(segments, type, userId); } else if (type.includes("loginToken")) { @@ -185,7 +185,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, segments.push(new Segment("sso", loginToken)); } else { // might be undefined, which will be turned into true by Segment - const value = iterator.next().value; + const value = decodeURIComponent(iterator.next().value); segments.push(new Segment(type, value)); } } @@ -196,19 +196,20 @@ export function stringifyPath(path: Path): string { let urlPath = ""; let prevSegment: Segment | undefined; for (const segment of path.segments) { + const encodedSegmentValue = encodeSegmentValue(segment.value); switch (segment.type) { case "rooms": - urlPath += `/rooms/${segment.value.join(",")}`; + urlPath += `/rooms/${(encodedSegmentValue as string[]).join(",")}`; break; case "empty-grid-tile": - urlPath += `/${segment.value}`; + urlPath += `/${encodedSegmentValue}`; break; case "room": if (prevSegment?.type === "rooms") { const index = prevSegment.value.indexOf(segment.value); urlPath += `/${index}`; } else { - urlPath += `/${segment.type}/${segment.value}`; + urlPath += `/${segment.type}/${encodedSegmentValue}`; } break; case "right-panel": @@ -217,8 +218,8 @@ export function stringifyPath(path: Path): string { continue; default: urlPath += `/${segment.type}`; - if (segment.value && segment.value !== true) { - urlPath += `/${segment.value}`; + if (encodedSegmentValue && encodedSegmentValue !== true) { + urlPath += `/${encodedSegmentValue}`; } } prevSegment = segment; @@ -226,6 +227,19 @@ export function stringifyPath(path: Path): string { return urlPath; } +function encodeSegmentValue(value: SegmentType[keyof SegmentType]) { + if (typeof value === "boolean") { + // Nothing to encode for boolean + return value; + } + else if (Array.isArray(value)) { + return value.map(v => encodeURIComponent(v)); + } + else { + return encodeURIComponent(value); + } +} + export function tests() { function createEmptyPath() { const nav: Navigation = new Navigation(allowsChild); diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index c8dcf63a..9f9a5483 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -48,7 +48,7 @@ export class MemberTileViewModel extends ViewModel { get detailsUrl() { const roomId = this.navigation.path.get("room").value; - return `${this.urlRouter.openRoomActionUrl(roomId)}/member/${this._member.userId}`; + return `${this.urlRouter.openRoomActionUrl(roomId)}/member/${encodeURIComponent(this._member.userId)}`; } _updatePreviousName(newName) { From 80080074fa668c4e52c23517e8807a2e2b9880d7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Nov 2022 11:20:47 +0530 Subject: [PATCH 310/354] Don't encode unknown segment values --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index f580206e..60da2435 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -185,7 +185,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, segments.push(new Segment("sso", loginToken)); } else { // might be undefined, which will be turned into true by Segment - const value = decodeURIComponent(iterator.next().value); + const value = iterator.next().value; segments.push(new Segment(type, value)); } } From 7bfadf37b42e953b3e12051c970fcc7894d89233 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Nov 2022 11:33:02 +0530 Subject: [PATCH 311/354] Decode all segment values that aren't undefined --- src/domain/navigation/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 60da2435..935f7138 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -185,7 +185,11 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, segments.push(new Segment("sso", loginToken)); } else { // might be undefined, which will be turned into true by Segment - const value = iterator.next().value; + let value = iterator.next().value; + if (value) { + // decode only if value isn't undefined! + value = decodeURIComponent(value) + } segments.push(new Segment(type, value)); } } From d889c7deeb1d2a87b618a2823b0e3fd71d373007 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 10:49:26 +0100 Subject: [PATCH 312/354] bound checks in date header loop and cleanup Co-authored-by: R Midhun Suresh --- src/domain/session/room/timeline/TilesCollection.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 8dfbf2a1..29e0bdfc 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -154,8 +154,14 @@ export class TilesCollection extends BaseObservableList { //console.log("_evaluateDateHeaderAtIdx", tileIdx); // consider the two adjacent tiles where the previous sibling changed: // the new tile and the next tile - for (let i = 0; i < 5; i += 1) { - const idx = Math.max(tileIdx + i - 2, 0); + for (let i = -2; i < 3; i += 1) { + const idx = tileIdx + i; + if (idx < 0) { + continue; + } + if (idx >= this._tiles.length) { + break; + } const tile = this._tiles[idx]; const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined; const hasDateSeparator = prevTile?.shape === TileShape.DateHeader; From 4abf18a5f11f4e70b8596fb81a0ed9c56b84472a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:26:53 +0100 Subject: [PATCH 313/354] don't look at tiles before the inserted tile, there is no need also clarify with comments how the current algorithm works --- src/domain/session/room/timeline/TilesCollection.js | 12 ++++++++---- src/domain/session/room/timeline/tiles/DateTile.ts | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 29e0bdfc..e4ed9d24 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -152,9 +152,11 @@ export class TilesCollection extends BaseObservableList { _evaluateDateHeaderAtIdx(tileIdx) { //console.log("_evaluateDateHeaderAtIdx", tileIdx); - // consider the two adjacent tiles where the previous sibling changed: - // the new tile and the next tile - for (let i = -2; i < 3; i += 1) { + // consider two tiles after the inserted tile, because + // the first of the two tiles may be a DateTile in which case, + // we remove it after looking at the needsDateSeparator prop of the + // next next tile + for (let i = 0; i < 2; i += 1) { const idx = tileIdx + i; if (idx < 0) { continue; @@ -173,10 +175,12 @@ export class TilesCollection extends BaseObservableList { this.emitUpdate(idx - 1, prevTile, "date"); } else { //console.log(" add", idx, tile.shape, tile.eventId); + // adding a tile shift all the indices we need to consider + // especially given we consider removals for the tile that + // comes after a datetile tileIdx += 1; this._addTileAt(idx, tile.createDateSeparator()); } - // TODO must be looking at the wrong index to find the old date separator?? } else if (hasDateSeparator) { // this is never triggered because needsDateSeparator is not cleared // when loading more items because we don't do anything once the diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 413ec2b2..92bd42e5 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -119,6 +119,9 @@ export class DateTile extends ViewModel implements ITile { // let item know it has a new sibling updatePreviousSibling(prev: ITile | undefined): void { + // forward the sibling update to our next tile, so it is informed + // about it's previous sibling beyond the date header (which is it's direct previous sibling) + // so it can recalculate whether it still needs a date header this._firstTileInDay.updatePreviousSibling(prev); } From 31f53d27c164347a48836f068625294e02a97ed9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:51:22 +0100 Subject: [PATCH 314/354] emit update from datetile when date might have changed --- .../session/room/timeline/TilesCollection.js | 14 ++------------ src/domain/session/room/timeline/tiles/DateTile.ts | 5 ++++- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index e4ed9d24..6652e89c 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -151,7 +151,6 @@ export class TilesCollection extends BaseObservableList { } _evaluateDateHeaderAtIdx(tileIdx) { - //console.log("_evaluateDateHeaderAtIdx", tileIdx); // consider two tiles after the inserted tile, because // the first of the two tiles may be a DateTile in which case, // we remove it after looking at the needsDateSeparator prop of the @@ -167,25 +166,16 @@ export class TilesCollection extends BaseObservableList { const tile = this._tiles[idx]; const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined; const hasDateSeparator = prevTile?.shape === TileShape.DateHeader; - if (tile.needsDateSeparator) { - if (hasDateSeparator) { - // TODO: replace this by return UpdateAction from updateNextSibling - // and do this in onAdd - //console.log(" update", idx - 1, prevTile?.shape, prevTile?.eventId); - this.emitUpdate(idx - 1, prevTile, "date"); - } else { - //console.log(" add", idx, tile.shape, tile.eventId); + if (tile.needsDateSeparator && !hasDateSeparator) { // adding a tile shift all the indices we need to consider // especially given we consider removals for the tile that // comes after a datetile tileIdx += 1; this._addTileAt(idx, tile.createDateSeparator()); - } - } else if (hasDateSeparator) { + } else if (!tile.needsDateSeparator && hasDateSeparator) { // this is never triggered because needsDateSeparator is not cleared // when loading more items because we don't do anything once the // direct sibling is a DateTile - //console.log(" remove", idx -1, prevTile?.shape, prevTile?.eventId); this._removeTile(idx - 1, prevTile); } } diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 92bd42e5..4e7cbda2 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -130,8 +130,11 @@ export class DateTile extends ViewModel implements ITile { // TODO: next can be undefined when a pending event is removed // TODO: we need a way to remove this date header this._firstTileInDay = next!; + const prevDateString = this._dateString; this._dateString = undefined; - // TODO: do we need to reevaluate our date here and emit an update? + if (prevDateString && prevDateString !== this.date) { + this._emitUpdate?.(this, "date"); + } } notifyVisible(): void { From cb0ab589de1d411f24006cce65fd0e3ecdada1c2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:51:38 +0100 Subject: [PATCH 315/354] remove lower bound check as we don't go negative anymore --- src/domain/session/room/timeline/TilesCollection.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 6652e89c..c9a9d6df 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -157,9 +157,6 @@ export class TilesCollection extends BaseObservableList { // next next tile for (let i = 0; i < 2; i += 1) { const idx = tileIdx + i; - if (idx < 0) { - continue; - } if (idx >= this._tiles.length) { break; } @@ -167,11 +164,11 @@ export class TilesCollection extends BaseObservableList { const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined; const hasDateSeparator = prevTile?.shape === TileShape.DateHeader; if (tile.needsDateSeparator && !hasDateSeparator) { - // adding a tile shift all the indices we need to consider - // especially given we consider removals for the tile that - // comes after a datetile - tileIdx += 1; - this._addTileAt(idx, tile.createDateSeparator()); + // adding a tile shift all the indices we need to consider + // especially given we consider removals for the tile that + // comes after a datetile + tileIdx += 1; + this._addTileAt(idx, tile.createDateSeparator()); } else if (!tile.needsDateSeparator && hasDateSeparator) { // this is never triggered because needsDateSeparator is not cleared // when loading more items because we don't do anything once the From 7c6d651b32a79159dae0c313d18606329f4f2118 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:09:28 +0100 Subject: [PATCH 316/354] remove date headers when removing pending tiles --- src/domain/session/room/timeline/TilesCollection.js | 4 ++++ src/domain/session/room/timeline/tiles/DateTile.ts | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index c9a9d6df..95eac7ac 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -255,6 +255,10 @@ export class TilesCollection extends BaseObservableList { this.emitRemove(tileIdx, tile); prevTile?.updateNextSibling(nextTile); nextTile?.updatePreviousSibling(prevTile); + + if (prevTile && prevTile.shape === TileShape.DateHeader && (!nextTile || !nextTile.needsDateSeparator)) { + this._removeTile(tileIdx - 1, prevTile); + } } // would also be called when unloading a part of the timeline diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 4e7cbda2..7e860511 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -127,9 +127,14 @@ export class DateTile extends ViewModel implements ITile { // let item know it has a new sibling updateNextSibling(next: ITile | undefined): UpdateAction { - // TODO: next can be undefined when a pending event is removed - // TODO: we need a way to remove this date header - this._firstTileInDay = next!; + if(!next) { + // If we are the DateTile for the last tile in the timeline, + // and that tile gets removed, next would be undefined + // and this DateTile would be removed as well, + // so do nothing + return; + } + this._firstTileInDay = next; const prevDateString = this._dateString; this._dateString = undefined; if (prevDateString && prevDateString !== this.date) { From b5d5adaa3638367e0dfb506752f5e9324d778fa6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 14:42:24 +0100 Subject: [PATCH 317/354] add tests Co-authored-by: R Midhun Suresh --- .../session/room/timeline/TilesCollection.js | 99 ++++++++++++++++++- .../session/room/timeline/tiles/DateTile.ts | 26 +++++ .../session/room/timeline/tiles/SimpleTile.js | 62 ++++++++++++ 3 files changed, 185 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 95eac7ac..4dc6e4aa 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -59,7 +59,6 @@ export class TilesCollection extends BaseObservableList { if (!currentTile || !currentTile.tryIncludeEntry(entry)) { currentTile = this._createTile(entry); if (currentTile) { - console.log("adding initial tile", currentTile.shape, currentTile.eventId, "at", this._tiles.length); this._tiles.push(currentTile); } } @@ -142,7 +141,6 @@ export class TilesCollection extends BaseObservableList { const newTile = this._createTile(entry); if (newTile) { - console.log("adding tile", newTile.shape, newTile.eventId, "at", tileIdx); this._addTileAt(tileIdx, newTile); this._evaluateDateHeaderAtIdx(tileIdx); } @@ -264,6 +262,7 @@ export class TilesCollection extends BaseObservableList { // would also be called when unloading a part of the timeline onRemove(index, entry) { const tileIdx = this._findTileIdx(entry); + const tile = this._findTileAtIdx(entry, tileIdx); if (tile) { const removeTile = tile.removeEntry(entry); @@ -317,6 +316,7 @@ export function tests() { constructor(entry) { this.entry = entry; this.update = null; + this.needsDateSeparator = false; } setUpdateEmit(update) { this.update = update; @@ -346,6 +346,34 @@ export function tests() { dispose() {} } + class DateHeaderTile extends TestTile { + get shape() { return TileShape.DateHeader; } + updateNextSibling(next) { + this.next = next; + } + updatePreviousSibling(prev) { + this.next?.updatePreviousSibling(prev); + } + compareEntry(b) { + // important that date tiles as sorted before their next item, but after their previous sibling + return this.next.compareEntry(b) - 0.5; + } + } + + class MessageNeedingDateHeaderTile extends TestTile { + get shape() { return TileShape.Message; } + + createDateSeparator() { + return new DateHeaderTile(this.entry); + } + updatePreviousSibling(prev) { + if (prev?.shape !== TileShape.DateHeader) { + // 1 day is 10 + this.needsDateSeparator = !prev || Math.floor(prev.entry.n / 10) !== Math.floor(this.entry.n / 10); + } + } + } + return { "don't emit update before add": assert => { class UpdateOnSiblingTile extends TestTile { @@ -404,6 +432,73 @@ export function tests() { }); entries.remove(1); assert.deepEqual(events, ["remove", "update"]); + }, + "date tile is added when needed when populating": assert => { + const entries = new ObservableArray([{n: 15}]); + const tileOptions = { + tileClassForEntry: () => MessageNeedingDateHeaderTile, + }; + const tiles = new TilesCollection(entries, tileOptions); + tiles.subscribe({}); + const tilesArray = Array.from(tiles); + assert.equal(tilesArray.length, 2); + assert.equal(tilesArray[0].shape, TileShape.DateHeader); + assert.equal(tilesArray[1].shape, TileShape.Message); + }, + "date header is added when receiving addition": assert => { + const entries = new ObservableArray([{n: 15}]); + const tileOptions = { + tileClassForEntry: () => MessageNeedingDateHeaderTile, + }; + const tiles = new TilesCollection(entries, tileOptions); + tiles.subscribe({ + onAdd() {}, + onRemove() {} + }); + entries.insert(0, {n: 5}); + const tilesArray = Array.from(tiles); + assert.equal(tilesArray[0].shape, TileShape.DateHeader); + assert.equal(tilesArray[1].shape, TileShape.Message); + assert.equal(tilesArray[2].shape, TileShape.DateHeader); + assert.equal(tilesArray[3].shape, TileShape.Message); + assert.equal(tilesArray.length, 4); + }, + "date header is removed and added when loading more messages for the same day": assert => { + const entries = new ObservableArray([{n: 15}]); + const tileOptions = { + tileClassForEntry: () => MessageNeedingDateHeaderTile, + }; + const tiles = new TilesCollection(entries, tileOptions); + tiles.subscribe({ + onAdd() {}, + onRemove() {} + }); + entries.insert(0, {n: 12}); + const tilesArray = Array.from(tiles); + assert.equal(tilesArray[0].shape, TileShape.DateHeader); + assert.equal(tilesArray[1].shape, TileShape.Message); + assert.equal(tilesArray[2].shape, TileShape.Message); + assert.equal(tilesArray.length, 3); + }, + "date header is removed at the end of the timeline": assert => { + const entries = new ObservableArray([{n: 5}, {n: 15}]); + const tileOptions = { + tileClassForEntry: () => MessageNeedingDateHeaderTile, + }; + const tiles = new TilesCollection(entries, tileOptions); + let removals = 0; + tiles.subscribe({ + onAdd() {}, + onRemove() { + removals += 1; + } + }); + entries.remove(1); + const tilesArray = Array.from(tiles); + assert.equal(tilesArray[0].shape, TileShape.DateHeader); + assert.equal(tilesArray[1].shape, TileShape.Message); + assert.equal(tilesArray.length, 2); + assert.equal(removals, 2); } } } diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 7e860511..36150a68 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -150,3 +150,29 @@ export class DateTile extends ViewModel implements ITile { } } + +import { EventEntry } from "../../../../../matrix/room/timeline/entries/EventEntry.js"; +import { SimpleTile } from "./SimpleTile"; + +export function tests() { + return { + "date tile sorts before reference tile": assert => { + const a = new SimpleTile(new EventEntry({ + event: {}, + eventIndex: 2, + fragmentId: 1 + }, undefined), {}); + const b = new SimpleTile(new EventEntry({ + event: {}, + eventIndex: 3, + fragmentId: 1 + }, undefined), {}); + const d = new DateTile(b, {} as any); + const tiles = [d, b, a]; + tiles.sort((a, b) => a.compare(b)); + assert.equal(tiles[0], a); + assert.equal(tiles[1], d); + assert.equal(tiles[2], b); + } + } +} \ No newline at end of file diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 7e394d25..f466e4d4 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -184,3 +184,65 @@ export class SimpleTile extends ViewModel { return this._options.timeline.me; } } + +import { EventEntry } from "../../../../../matrix/room/timeline/entries/EventEntry.js"; + +export function tests() { + return { + "needsDateSeparator is false when previous sibling is for same date": assert => { + const fridayEntry = new EventEntry({ + event: { + origin_server_ts: 1669376446222, + type: "m.room.message", + content: {} + } + }, undefined); + const thursdayEntry = new EventEntry({ + event: { + origin_server_ts: fridayEntry.timestamp - (60 * 60 * 8 * 1000), + type: "m.room.message", + content: {} + } + }, undefined); + const fridayTile = new SimpleTile(fridayEntry, {}); + const thursdayTile = new SimpleTile(thursdayEntry, {}); + assert.equal(fridayTile.needsDateSeparator, false); + fridayTile.updatePreviousSibling(thursdayTile); + assert.equal(fridayTile.needsDateSeparator, false); + }, + "needsDateSeparator is true when previous sibling is for different date": assert => { + const fridayEntry = new EventEntry({ + event: { + origin_server_ts: 1669376446222, + type: "m.room.message", + content: {} + } + }, undefined); + const thursdayEntry = new EventEntry({ + event: { + origin_server_ts: fridayEntry.timestamp - (60 * 60 * 24 * 1000), + type: "m.room.message", + content: {} + } + }, undefined); + const fridayTile = new SimpleTile(fridayEntry, {}); + const thursdayTile = new SimpleTile(thursdayEntry, {}); + assert.equal(fridayTile.needsDateSeparator, false); + fridayTile.updatePreviousSibling(thursdayTile); + assert.equal(fridayTile.needsDateSeparator, true); + }, + "needsDateSeparator is true when previous sibling is undefined": assert => { + const fridayEntry = new EventEntry({ + event: { + origin_server_ts: 1669376446222, + type: "m.room.message", + content: {} + } + }, undefined); + const fridayTile = new SimpleTile(fridayEntry, {}); + assert.equal(fridayTile.needsDateSeparator, false); + fridayTile.updatePreviousSibling(undefined); + assert.equal(fridayTile.needsDateSeparator, true); + }, + } +} \ No newline at end of file From 9b235f7c80b105ab84eb8e1321dd0ae4725c55a6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 14:42:54 +0100 Subject: [PATCH 318/354] also test next next tile, change this back after testing before --- src/domain/session/room/timeline/TilesCollection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 4dc6e4aa..458697ca 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -153,7 +153,7 @@ export class TilesCollection extends BaseObservableList { // the first of the two tiles may be a DateTile in which case, // we remove it after looking at the needsDateSeparator prop of the // next next tile - for (let i = 0; i < 2; i += 1) { + for (let i = 0; i < 3; i += 1) { const idx = tileIdx + i; if (idx >= this._tiles.length) { break; From 9c2c5af291b14c755e216ad978bcbe424a8cc1ec Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:12:35 +0100 Subject: [PATCH 319/354] small UI refinements for date header --- src/platform/web/ui/css/themes/element/timeline.css | 7 +++++++ .../web/ui/session/room/timeline/DateHeaderView.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 43c57d19..87b3e8c7 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -422,3 +422,10 @@ only loads when the top comes into view*/ .GapView.isAtTop { padding: 52px 20px 12px 20px; } + +.DateHeader { + color: var(--light-text-color); + font-weight: bold; + padding: 12px 4px; + text-align: center; +} \ No newline at end of file diff --git a/src/platform/web/ui/session/room/timeline/DateHeaderView.ts b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts index 63003e91..f9159c5e 100644 --- a/src/platform/web/ui/session/room/timeline/DateHeaderView.ts +++ b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts @@ -25,7 +25,7 @@ export class DateHeaderView extends TemplateView { } render(t, vm) { - return t.div({className: "DateHeader"}, t.div(vm.date)); + return t.div({className: "DateHeader"}, t.time(vm.date)); } /* This is called by the parent ListView, which just has 1 listener for the whole list */ From c4e239a4011805a68043194989195d0e358e7402 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:13:22 +0100 Subject: [PATCH 320/354] add timeformatter, shared between all view models --- src/domain/ViewModel.ts | 5 ++ .../session/room/timeline/tiles/DateTile.ts | 8 +- src/platform/types/types.ts | 6 +- src/platform/web/Platform.js | 2 + src/platform/web/dom/TimeFormatter.ts | 73 +++++++++++++++++++ 5 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 src/platform/web/dom/TimeFormatter.ts diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 8dbc37ea..878b43ba 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -29,6 +29,7 @@ import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {SegmentType} from "./navigation/index"; import type {IURLRouter} from "./navigation/URLRouter"; +import type { ITimeFormatter } from "../platform/types/types"; export type Options = { platform: Platform; @@ -145,4 +146,8 @@ export class ViewModel = Op // typescript needs a little help here return this._options.navigation as unknown as Navigation; } + + get timeFormatter(): ITimeFormatter { + return this._options.platform.timeFormatter; + } } diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 36150a68..d8dac808 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -48,13 +48,7 @@ export class DateTile extends ViewModel implements ITile { get date(): string { if (!this._dateString) { - const date = new Date(this.refEntry.timestamp); - this._dateString = date.toLocaleDateString({}, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }); + this._dateString = this.timeFormatter.formatRelativeDate(new Date(this.refEntry.timestamp)); } return this._dateString; } diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 1d359a09..5e982545 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -16,7 +16,7 @@ limitations under the License. import type {RequestResult} from "../web/dom/request/fetch.js"; import type {RequestBody} from "../../matrix/net/common"; -import type {ILogItem} from "../../logging/types"; +import type { BaseObservableValue } from "../../observable/ObservableValue"; export interface IRequestOptions { uploadProgress?: (loadedBytes: number) => void; @@ -43,3 +43,7 @@ export type File = { readonly name: string; readonly blob: IBlobHandle; } + +export interface ITimeFormatter { + formatRelativeDate(date: Date): string; +} \ No newline at end of file diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 29a83e1f..0d95e585 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -39,6 +39,7 @@ import {Disposables} from "../../utils/Disposables"; import {parseHTML} from "./parsehtml.js"; import {handleAvatarError} from "./ui/avatar"; import {ThemeLoader} from "./theming/ThemeLoader"; +import {TimeFormatter} from "./dom/TimeFormatter"; function addScript(src) { return new Promise(function (resolve, reject) { @@ -139,6 +140,7 @@ export class Platform { this._createLogger(options?.development); this.history = new History(); this.onlineStatus = new OnlineStatus(); + this.timeFormatter = new TimeFormatter(); this._serviceWorkerHandler = null; if (assetPaths.serviceWorker && "serviceWorker" in navigator) { this._serviceWorkerHandler = new ServiceWorkerHandler(); diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts new file mode 100644 index 00000000..ab9d454e --- /dev/null +++ b/src/platform/web/dom/TimeFormatter.ts @@ -0,0 +1,73 @@ +/* +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 type { ITimeFormatter } from "../../types/types"; +import {Clock} from "./Clock"; + +enum TimeScope { + Minute = 60 * 1000, + Day = 24 * 60 * 60 * 1000, +} + +export class TimeFormatter implements ITimeFormatter { + + private todayMidnight: Date; + private relativeDayFormatter: Intl.RelativeTimeFormat; + private weekdayFormatter: Intl.DateTimeFormat; + private currentYearFormatter: Intl.DateTimeFormat; + private otherYearFormatter: Intl.DateTimeFormat; + + constructor(private clock: Clock) { + // don't use the clock time here as the DOM relative formatters don't support setting the reference date anyway + this.todayMidnight = new Date(); + this.todayMidnight.setHours(0, 0, 0, 0); + this.relativeDayFormatter = new Intl.RelativeTimeFormat(undefined, {numeric: "auto"}); + this.weekdayFormatter = new Intl.DateTimeFormat(undefined, {weekday: 'long'}); + this.currentYearFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'long', + month: 'long', + day: 'numeric' + }); + this.otherYearFormatter = new Intl.DateTimeFormat(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + + formatRelativeDate(date: Date): string { + let daysDiff = Math.floor((date.getTime() - this.todayMidnight.getTime()) / TimeScope.Day); + console.log("formatRelativeDate daysDiff", daysDiff, date); + if (daysDiff >= -1 && daysDiff <= 1) { + // Tomorrow, Today, Yesterday + return capitalizeFirstLetter(this.relativeDayFormatter.format(daysDiff, "day")); + } else if (daysDiff > -7 && daysDiff < 0) { + // Wednesday + return this.weekdayFormatter.format(date); + } else if (this.todayMidnight.getFullYear() === date.getFullYear()) { + // Friday, November 6 + return this.currentYearFormatter.format(date); + } else { + // Friday, November 5, 2021 + return this.otherYearFormatter.format(date); + } + } +} + +function capitalizeFirstLetter(str: string) { + return str.slice(0, 1).toLocaleUpperCase() + str.slice(1); +} \ No newline at end of file From 2136b051a0ee8bf984df3eab38f4703253e384b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:13:31 +0100 Subject: [PATCH 321/354] cleanup --- src/domain/session/room/timeline/tiles/SimpleTile.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index f466e4d4..91e023cb 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -55,9 +55,6 @@ export class SimpleTile extends ViewModel { this._needsDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() || prev._date.getMonth() !== this._date.getMonth() || prev._date.getDate() !== this._date.getDate(); - if (neededDateSeparator && !this._needsDateSeparator) { - console.log("clearing needsDateSeparator", {this: this._entry.content, prev: prev.content}); - } } else { this._needsDateSeparator = !!this._date; } From c538f5dbb14e3a647be8da93eda28e6a2af054e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:31:44 +0100 Subject: [PATCH 322/354] make date header a bit more accessible --- .../session/room/timeline/tiles/DateTile.ts | 15 ++++++++++++--- src/platform/types/types.ts | 1 + src/platform/web/dom/TimeFormatter.ts | 3 +++ .../web/ui/css/themes/element/timeline.css | 1 + .../ui/session/room/timeline/DateHeaderView.ts | 2 +- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index d8dac808..590333b8 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -17,6 +17,7 @@ import type {Options} from "../../../../ViewModel"; export class DateTile extends ViewModel implements ITile { private _emitUpdate?: EmitUpdateFn; private _dateString?: string; + private _machineReadableString?: string; constructor(private _firstTileInDay: ITile, options: Options) { super(options); @@ -46,13 +47,20 @@ export class DateTile extends ViewModel implements ITile { return this.compareEntry(tile.upperEntry); } - get date(): string { + get relativeDate(): string { if (!this._dateString) { this._dateString = this.timeFormatter.formatRelativeDate(new Date(this.refEntry.timestamp)); } return this._dateString; } + get machineReadableDate(): string { + if (!this._machineReadableString) { + this._machineReadableString = this.timeFormatter.formatMachineReadableDate(new Date(this.refEntry.timestamp)); + } + return this._machineReadableString; + } + get shape(): TileShape { return TileShape.DateHeader; } @@ -131,8 +139,9 @@ export class DateTile extends ViewModel implements ITile { this._firstTileInDay = next; const prevDateString = this._dateString; this._dateString = undefined; - if (prevDateString && prevDateString !== this.date) { - this._emitUpdate?.(this, "date"); + this._machineReadableString = undefined; + if (prevDateString && prevDateString !== this.relativeDate) { + this._emitUpdate?.(this, "relativeDate"); } } diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 5e982545..9147f6b8 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -46,4 +46,5 @@ export type File = { export interface ITimeFormatter { formatRelativeDate(date: Date): string; + formatMachineReadableDate(date: Date): string; } \ No newline at end of file diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index ab9d454e..3f354713 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -48,6 +48,9 @@ export class TimeFormatter implements ITimeFormatter { day: 'numeric' }); } + formatMachineReadableDate(date: Date): string { + return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + } formatRelativeDate(date: Date): string { let daysDiff = Math.floor((date.getTime() - this.todayMidnight.getTime()) / TimeScope.Day); diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 87b3e8c7..16597a7b 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -428,4 +428,5 @@ only loads when the top comes into view*/ font-weight: bold; padding: 12px 4px; text-align: center; + font-size: 1.5rem; } \ No newline at end of file diff --git a/src/platform/web/ui/session/room/timeline/DateHeaderView.ts b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts index f9159c5e..3d640568 100644 --- a/src/platform/web/ui/session/room/timeline/DateHeaderView.ts +++ b/src/platform/web/ui/session/room/timeline/DateHeaderView.ts @@ -25,7 +25,7 @@ export class DateHeaderView extends TemplateView { } render(t, vm) { - return t.div({className: "DateHeader"}, t.time(vm.date)); + return t.h2({className: "DateHeader"}, t.time({dateTime: vm.machineReadableDate}, vm.relativeDate)); } /* This is called by the parent ListView, which just has 1 listener for the whole list */ From 85a06876cd8476ff70be8e42db4014da8885a512 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:47:00 +0100 Subject: [PATCH 323/354] make date header sticky with css this has the limitation that it needs to have a fixed with and an opaque background, but that's better than not making it sticky for now. --- .../web/ui/css/themes/element/timeline.css | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 16597a7b..8645bc3f 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -424,9 +424,20 @@ only loads when the top comes into view*/ } .DateHeader { - color: var(--light-text-color); font-weight: bold; - padding: 12px 4px; - text-align: center; font-size: 1.5rem; -} \ No newline at end of file + position: sticky; + top: 5px; +} + +.DateHeader time { + margin: 0 auto; + padding: 12px 4px; + width: 250px; + padding: 12px; + display: block; + color: var(--light-text-color); + background-color: var(--background-color-primary); + border-radius: 8px; + text-align: center; + } \ No newline at end of file From 77fd7e7aca680282a1d7ec64da189f0074f9431a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:47:54 +0100 Subject: [PATCH 324/354] format message time in timeFormatter as well --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 2 +- src/platform/types/types.ts | 1 + src/platform/web/dom/TimeFormatter.ts | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 1ad1ba44..05ff1867 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -84,7 +84,7 @@ export class BaseMessageTile extends SimpleTile { } get time() { - return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"}); + return this._date && this.timeFormatter.formatTime(this._date); } get isOwn() { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 9147f6b8..df7ce6ac 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -45,6 +45,7 @@ export type File = { } export interface ITimeFormatter { + formatTime(date: Date): string; formatRelativeDate(date: Date): string; formatMachineReadableDate(date: Date): string; } \ No newline at end of file diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index 3f354713..7db879ea 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -29,6 +29,7 @@ export class TimeFormatter implements ITimeFormatter { private weekdayFormatter: Intl.DateTimeFormat; private currentYearFormatter: Intl.DateTimeFormat; private otherYearFormatter: Intl.DateTimeFormat; + private timeFormatter: Intl.DateTimeFormat; constructor(private clock: Clock) { // don't use the clock time here as the DOM relative formatters don't support setting the reference date anyway @@ -47,7 +48,13 @@ export class TimeFormatter implements ITimeFormatter { month: 'long', day: 'numeric' }); + this.timeFormatter = new Intl.DateTimeFormat(undefined, {hour: "numeric", minute: "2-digit"}); } + + formatTime(date: Date): string { + return this.timeFormatter.format(date); + } + formatMachineReadableDate(date: Date): string { return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; } From efa45cade68a6f48edda48fef4cc1a7fc08d9924 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:48:46 +0100 Subject: [PATCH 325/354] remove date from message tiles --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 5 ----- src/platform/web/ui/session/room/timeline/BaseMediaView.js | 2 +- src/platform/web/ui/session/room/timeline/FileView.js | 2 +- src/platform/web/ui/session/room/timeline/LocationView.js | 2 +- src/platform/web/ui/session/room/timeline/TextMessageView.js | 2 +- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 05ff1867..45c2a05d 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -78,11 +78,6 @@ export class BaseMessageTile extends SimpleTile { return this.sender; } - // TODO: remove? - get date() { - return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"}); - } - get time() { return this._date && this.timeFormatter.formatTime(this._date); } diff --git a/src/platform/web/ui/session/room/timeline/BaseMediaView.js b/src/platform/web/ui/session/room/timeline/BaseMediaView.js index 9d534fd1..bc49b3f6 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMediaView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMediaView.js @@ -34,7 +34,7 @@ export class BaseMediaView extends BaseMessageView { const children = [ t.div({className: "spacer", style: spacerStyle}), this.renderMedia(t, vm), - t.time(vm.date + " " + vm.time), + t.time(vm.time), ]; const status = t.div({ className: { diff --git a/src/platform/web/ui/session/room/timeline/FileView.js b/src/platform/web/ui/session/room/timeline/FileView.js index 6a2d418e..ca0eb10e 100644 --- a/src/platform/web/ui/session/room/timeline/FileView.js +++ b/src/platform/web/ui/session/room/timeline/FileView.js @@ -24,7 +24,7 @@ export class FileView extends BaseMessageView { } else { children.push( t.button({className: "link", onClick: () => vm.download()}, vm => vm.label), - t.time(vm.date + " " + vm.time) + t.time(vm.time) ); } return t.p({className: "Timeline_messageBody statusMessage"}, children); diff --git a/src/platform/web/ui/session/room/timeline/LocationView.js b/src/platform/web/ui/session/room/timeline/LocationView.js index de605c6a..e0d2656c 100644 --- a/src/platform/web/ui/session/room/timeline/LocationView.js +++ b/src/platform/web/ui/session/room/timeline/LocationView.js @@ -21,7 +21,7 @@ export class LocationView extends BaseMessageView { return t.p({className: "Timeline_messageBody statusMessage"}, [ t.span(vm.label), t.a({className: "Timeline_locationLink", href: vm.mapsLink, target: "_blank", rel: "noopener"}, vm.i18n`Open in maps`), - t.time(vm.date + " " + vm.time) + t.time(vm.time) ]); } } diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 8d6cb4dc..a6741de7 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -20,7 +20,7 @@ import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js"; export class TextMessageView extends BaseMessageView { renderMessageBody(t, vm) { - const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time); + const time = t.time({className: {hidden: !vm.time}}, vm.time); const container = t.div({ className: { "Timeline_messageBody": true, From 195142c2492a5fae91b2d5c89cd11fad3d9b6730 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:56:01 +0100 Subject: [PATCH 326/354] fix lint warnings --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 1 - src/domain/session/room/timeline/tiles/DateTile.ts | 2 +- src/domain/session/room/timeline/tiles/SimpleTile.js | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 45c2a05d..a7dc82cd 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -15,7 +15,6 @@ limitations under the License. */ import {SimpleTile} from "./SimpleTile.js"; -import {TileShape} from "./ITile"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index 590333b8..ac4e6329 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -56,7 +56,7 @@ export class DateTile extends ViewModel implements ITile { get machineReadableDate(): string { if (!this._machineReadableString) { - this._machineReadableString = this.timeFormatter.formatMachineReadableDate(new Date(this.refEntry.timestamp)); + this._machineReadableString = this.timeFormatter.formatMachineReadableDate(new Date(this.refEntry.timestamp)); } return this._machineReadableString; } diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 91e023cb..7cb9617d 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -51,7 +51,6 @@ export class SimpleTile extends ViewModel { _updateDateSeparator(prev) { if (prev && prev._date && this._date) { - const neededDateSeparator = this._needsDateSeparator; this._needsDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() || prev._date.getMonth() !== this._date.getMonth() || prev._date.getDate() !== this._date.getDate(); From 7bdd23e7679ea4faf09d1bae3ac3dddd1bea6bab Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:21:58 +0100 Subject: [PATCH 327/354] restore active reaction bg color to be transparent version of accent color --- doc/THEMING.md | 1 + src/platform/web/theming/shared/color.mjs | 3 +++ src/platform/web/ui/css/themes/element/timeline.css | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index 599434bd..c00ab155 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -80,6 +80,7 @@ Currently supported operations are: | -------- | -------- | -------- | | darker | percentage | color | | lighter | percentage | color | +| alpha | alpha percentage | color | ## Aliases It is possible give aliases to variables in the `theme.css` file: diff --git a/src/platform/web/theming/shared/color.mjs b/src/platform/web/theming/shared/color.mjs index 8af76b6b..31d40185 100644 --- a/src/platform/web/theming/shared/color.mjs +++ b/src/platform/web/theming/shared/color.mjs @@ -36,5 +36,8 @@ export function derive(value, operation, argument, isDark) { const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); return newColorString; } + case "alpha": { + return offColor(value).rgba(argumentAsNumber / 100); + } } } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 8645bc3f..7ff35eb1 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -364,7 +364,7 @@ only loads when the top comes into view*/ } .Timeline_messageReactions button.active { - background-color: var(--background-color-secondary); + background-color: var(--accent-color--alpha-11); border-color: var(--accent-color); } @@ -440,4 +440,4 @@ only loads when the top comes into view*/ background-color: var(--background-color-primary); border-radius: 8px; text-align: center; - } \ No newline at end of file + } From 2d0122dda7781995d143d59812ed6d9667198bdf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:38:05 +0100 Subject: [PATCH 328/354] apply PR suggestions --- src/domain/navigation/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 935f7138..5a4b9abc 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -147,8 +147,9 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, segments.push(new Segment("empty-grid-tile", selectedIndex)); } } else if (type === "open-room") { - const roomId = decodeURIComponent(iterator.next().value); + let roomId = iterator.next().value; if (!roomId) { break; } + roomId = decodeURIComponent(roomId); const rooms = currentNavPath.get("rooms"); if (rooms) { segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath)); @@ -176,8 +177,9 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, } else if (type === "details" || type === "members") { pushRightPanelSegment(segments, type); } else if (type === "member") { - const userId = decodeURIComponent(iterator.next().value); + let userId = iterator.next().value; if (!userId) { break; } + userId = decodeURIComponent(userId); pushRightPanelSegment(segments, type, userId); } else if (type.includes("loginToken")) { // Special case for SSO-login with query parameter loginToken= From b8444a32c0f74cadabcc4db654d7378fe8b98fff Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:38:13 +0100 Subject: [PATCH 329/354] add missing decode --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 5a4b9abc..a75260d0 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -137,7 +137,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, if (type === "rooms") { const roomsValue = iterator.next().value; if (roomsValue === undefined) { break; } - const roomIds = roomsValue.split(","); + const roomIds = decodeURIComponent(roomsValue).split(","); segments.push(new Segment(type, roomIds)); const selectedIndex = parseInt(iterator.next().value || "0", 10); const roomId = roomIds[selectedIndex]; From 467a76c2231b9555a6d7deb4f19b4a5eddd6b3e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:45:06 +0100 Subject: [PATCH 330/354] decode ids separately, as we encode them separately as well --- src/domain/navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index a75260d0..3bbe4d3a 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -137,7 +137,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path, if (type === "rooms") { const roomsValue = iterator.next().value; if (roomsValue === undefined) { break; } - const roomIds = decodeURIComponent(roomsValue).split(","); + const roomIds = roomsValue.split(",").map(id => decodeURIComponent(id)); segments.push(new Segment(type, roomIds)); const selectedIndex = parseInt(iterator.next().value || "0", 10); const roomId = roomIds[selectedIndex]; From f7132a48d9a30fcb10ac58981cb5ee8dd5c2629f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:45:31 +0100 Subject: [PATCH 331/354] implement PR suggestion to do all encoding in fn, so return type is str --- src/domain/navigation/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 3bbe4d3a..a2705944 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -205,7 +205,7 @@ export function stringifyPath(path: Path): string { const encodedSegmentValue = encodeSegmentValue(segment.value); switch (segment.type) { case "rooms": - urlPath += `/rooms/${(encodedSegmentValue as string[]).join(",")}`; + urlPath += `/rooms/${encodedSegmentValue}`; break; case "empty-grid-tile": urlPath += `/${encodedSegmentValue}`; @@ -224,7 +224,7 @@ export function stringifyPath(path: Path): string { continue; default: urlPath += `/${segment.type}`; - if (encodedSegmentValue && encodedSegmentValue !== true) { + if (encodedSegmentValue) { urlPath += `/${encodedSegmentValue}`; } } @@ -233,13 +233,13 @@ export function stringifyPath(path: Path): string { return urlPath; } -function encodeSegmentValue(value: SegmentType[keyof SegmentType]) { - if (typeof value === "boolean") { +function encodeSegmentValue(value: SegmentType[keyof SegmentType]): string { + if (value === true) { // Nothing to encode for boolean - return value; + return ""; } else if (Array.isArray(value)) { - return value.map(v => encodeURIComponent(v)); + return value.map(v => encodeURIComponent(v)).join(","); } else { return encodeURIComponent(value); From 308bbee5ede6f4d21cd69e2f9239aec4e667b379 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 25 Nov 2022 17:47:54 +0100 Subject: [PATCH 332/354] release v0.3.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5429e0cf..1a21d58c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.4", + "version": "0.3.5", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From d1649ea4991fbe407277cd40ea6619b5a1a785f6 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Thu, 15 Dec 2022 15:22:55 +0000 Subject: [PATCH 333/354] Make sure fonts are cached by service worker --- .../web/ui/css/themes/element/inter.css | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/inter.css b/src/platform/web/ui/css/themes/element/inter.css index 5c69d7e6..d4cd1e8a 100644 --- a/src/platform/web/ui/css/themes/element/inter.css +++ b/src/platform/web/ui/css/themes/element/inter.css @@ -3,16 +3,16 @@ font-style: normal; font-weight: 100; font-display: swap; - src: url("inter/Inter-Thin.woff2?v=3.13") format("woff2"), - url("inter/Inter-Thin.woff?v=3.13") format("woff"); + src: url("inter/Inter-Thin.woff2") format("woff2"), + url("inter/Inter-Thin.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 100; font-display: swap; - src: url("inter/Inter-ThinItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-ThinItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-ThinItalic.woff2") format("woff2"), + url("inter/Inter-ThinItalic.woff") format("woff"); } @font-face { @@ -20,16 +20,16 @@ font-style: normal; font-weight: 200; font-display: swap; - src: url("inter/Inter-ExtraLight.woff2?v=3.13") format("woff2"), - url("inter/Inter-ExtraLight.woff?v=3.13") format("woff"); + src: url("inter/Inter-ExtraLight.woff2") format("woff2"), + url("inter/Inter-ExtraLight.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 200; font-display: swap; - src: url("inter/Inter-ExtraLightItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-ExtraLightItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-ExtraLightItalic.woff2") format("woff2"), + url("inter/Inter-ExtraLightItalic.woff") format("woff"); } @font-face { @@ -37,16 +37,16 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url("inter/Inter-Light.woff2?v=3.13") format("woff2"), - url("inter/Inter-Light.woff?v=3.13") format("woff"); + src: url("inter/Inter-Light.woff2") format("woff2"), + url("inter/Inter-Light.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 300; font-display: swap; - src: url("inter/Inter-LightItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-LightItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-LightItalic.woff2") format("woff2"), + url("inter/Inter-LightItalic.woff") format("woff"); } @font-face { @@ -54,16 +54,16 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url("inter/Inter-Regular.woff2?v=3.13") format("woff2"), - url("inter/Inter-Regular.woff?v=3.13") format("woff"); + src: url("inter/Inter-Regular.woff2") format("woff2"), + url("inter/Inter-Regular.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 400; font-display: swap; - src: url("inter/Inter-Italic.woff2?v=3.13") format("woff2"), - url("inter/Inter-Italic.woff?v=3.13") format("woff"); + src: url("inter/Inter-Italic.woff2") format("woff2"), + url("inter/Inter-Italic.woff") format("woff"); } @font-face { @@ -71,16 +71,16 @@ font-style: normal; font-weight: 500; font-display: swap; - src: url("inter/Inter-Medium.woff2?v=3.13") format("woff2"), - url("inter/Inter-Medium.woff?v=3.13") format("woff"); + src: url("inter/Inter-Medium.woff2") format("woff2"), + url("inter/Inter-Medium.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 500; font-display: swap; - src: url("inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-MediumItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-MediumItalic.woff2") format("woff2"), + url("inter/Inter-MediumItalic.woff") format("woff"); } @font-face { @@ -88,16 +88,16 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url("inter/Inter-SemiBold.woff2?v=3.13") format("woff2"), - url("inter/Inter-SemiBold.woff?v=3.13") format("woff"); + src: url("inter/Inter-SemiBold.woff2") format("woff2"), + url("inter/Inter-SemiBold.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 600; font-display: swap; - src: url("inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-SemiBoldItalic.woff2") format("woff2"), + url("inter/Inter-SemiBoldItalic.woff") format("woff"); } @font-face { @@ -105,16 +105,16 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url("inter/Inter-Bold.woff2?v=3.13") format("woff2"), - url("inter/Inter-Bold.woff?v=3.13") format("woff"); + src: url("inter/Inter-Bold.woff2") format("woff2"), + url("inter/Inter-Bold.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 700; font-display: swap; - src: url("inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-BoldItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-BoldItalic.woff2") format("woff2"), + url("inter/Inter-BoldItalic.woff") format("woff"); } @font-face { @@ -122,16 +122,16 @@ font-style: normal; font-weight: 800; font-display: swap; - src: url("inter/Inter-ExtraBold.woff2?v=3.13") format("woff2"), - url("inter/Inter-ExtraBold.woff?v=3.13") format("woff"); + src: url("inter/Inter-ExtraBold.woff2") format("woff2"), + url("inter/Inter-ExtraBold.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 800; font-display: swap; - src: url("inter/Inter-ExtraBoldItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-ExtraBoldItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-ExtraBoldItalic.woff2") format("woff2"), + url("inter/Inter-ExtraBoldItalic.woff") format("woff"); } @font-face { @@ -139,14 +139,14 @@ font-style: normal; font-weight: 900; font-display: swap; - src: url("inter/Inter-Black.woff2?v=3.13") format("woff2"), - url("inter/Inter-Black.woff?v=3.13") format("woff"); + src: url("inter/Inter-Black.woff2") format("woff2"), + url("inter/Inter-Black.woff") format("woff"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 900; font-display: swap; - src: url("inter/Inter-BlackItalic.woff2?v=3.13") format("woff2"), - url("inter/Inter-BlackItalic.woff?v=3.13") format("woff"); + src: url("inter/Inter-BlackItalic.woff2") format("woff2"), + url("inter/Inter-BlackItalic.woff") format("woff"); } From dbbbb1c29ababbcbe3f933b2a9424e3c0fc00ec5 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 16 Dec 2022 09:07:37 +0000 Subject: [PATCH 334/354] clarify ublock origin breaks the service worker --- doc/FAQ.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/FAQ.md b/doc/FAQ.md index ce372dc3..d445500e 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -10,6 +10,9 @@ TorBrowser ships a crippled IndexedDB implementation and will not work. At some It used work in pre-webkit Edge, to have it work on Windows Phone, but that support has probably bit-rotted as it isn't tested anymore. +The following browser extensions are known to break Hydrogen + - uBlock Origin (seems to block the service worker script) + ## Is there a way to run the app as a desktop app? You can install Hydrogen as a PWA using Chrome/Chromium on any platform or Edge on Windows. Gnome Web/Ephiphany also allows to "Install site as web application". There is no Electron build of Hydrogen, and there will likely be none in the near future, as Electron complicates the release process considerably. Once Hydrogen is more mature and feature complete, we might reconsider and use [Tauri](https://tauri.studio) if there are compelling use cases not possible with PWAs. For now though, we want to keep development and releasing fast and nimble ;) From 1473e5647d6b778bcb5979ce8273d7fe447df4d8 Mon Sep 17 00:00:00 2001 From: ElonSatoshi Date: Fri, 16 Dec 2022 03:21:22 -0600 Subject: [PATCH 335/354] Update FAQ.md Added some extra notes about uBlock Origin compatibility --- doc/FAQ.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index d445500e..3f195a0e 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -11,7 +11,8 @@ TorBrowser ships a crippled IndexedDB implementation and will not work. At some It used work in pre-webkit Edge, to have it work on Windows Phone, but that support has probably bit-rotted as it isn't tested anymore. The following browser extensions are known to break Hydrogen - - uBlock Origin (seems to block the service worker script) + - uBlock Origin (Some custom filters seem to block the service worker script) + - Try locating the filter that is blocking the service worker script in the uBlock Origin logger, and disabling that filter. Otherwise, the easiest solution is to disable uBlock Origin for the Hydrogen site. It is possible to re-enable it after logging in, but it may possibly break again when there is an update. ## Is there a way to run the app as a desktop app? From 03b0cfb47bda50ccf470b02954ef699fc1266ee7 Mon Sep 17 00:00:00 2001 From: ElonSatoshi Date: Fri, 16 Dec 2022 03:26:40 -0600 Subject: [PATCH 336/354] How to disable uBlock Origin Added a little note on how to disable uBlock Origin for a specific site --- doc/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index 3f195a0e..0823ee08 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -12,7 +12,7 @@ It used work in pre-webkit Edge, to have it work on Windows Phone, but that supp The following browser extensions are known to break Hydrogen - uBlock Origin (Some custom filters seem to block the service worker script) - - Try locating the filter that is blocking the service worker script in the uBlock Origin logger, and disabling that filter. Otherwise, the easiest solution is to disable uBlock Origin for the Hydrogen site. It is possible to re-enable it after logging in, but it may possibly break again when there is an update. + - Try locating the filter that is blocking the service worker script in the uBlock Origin logger, and disabling that filter. Otherwise, the easiest solution is to disable uBlock Origin for the Hydrogen site (by opening the uBlock Origin popup and clicking the large power button symbol). It is possible to re-enable it after logging in, but it may possibly break again when there is an update. ## Is there a way to run the app as a desktop app? From 6ecff485ecf74c0001b8b86ac55b8980c254330e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 20 Dec 2022 18:52:38 +0100 Subject: [PATCH 337/354] make tile comparison commutative again, allow DateTile to do comparison --- src/domain/session/room/timeline/tiles/DateTile.ts | 10 +++++++++- src/domain/session/room/timeline/tiles/ITile.ts | 13 +++++++++++++ .../session/room/timeline/tiles/SimpleTile.js | 12 ++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/DateTile.ts b/src/domain/session/room/timeline/tiles/DateTile.ts index ac4e6329..2174b9c5 100644 --- a/src/domain/session/room/timeline/tiles/DateTile.ts +++ b/src/domain/session/room/timeline/tiles/DateTile.ts @@ -119,6 +119,14 @@ export class DateTile extends ViewModel implements ITile { return false; } + /** + * This tile needs to do the comparison between tiles, as it uses the entry + * from another tile to determine its sorting order. + * */ + get comparisonIsNotCommutative(): boolean { + return true; + } + // let item know it has a new sibling updatePreviousSibling(prev: ITile | undefined): void { // forward the sibling update to our next tile, so it is informed @@ -178,4 +186,4 @@ export function tests() { assert.equal(tiles[2], b); } } -} \ No newline at end of file +} diff --git a/src/domain/session/room/timeline/tiles/ITile.ts b/src/domain/session/room/timeline/tiles/ITile.ts index dd6c0f81..f36c1c51 100644 --- a/src/domain/session/room/timeline/tiles/ITile.ts +++ b/src/domain/session/room/timeline/tiles/ITile.ts @@ -24,7 +24,20 @@ export interface ITile extends IDisposable { setUpdateEmit(emitUpdate: EmitUpdateFn): void; get upperEntry(): E; get lowerEntry(): E; + /** compare two tiles, returning: + * - 0 if both tiles are considered equal + * - a negative value if this tiles is sorted before the given tile + * - a positive value if this tiles is sorted after the given tile + **/ compare(tile: ITile): number; + /** Some tiles might need comparison mechanisms that are not commutative, + * (e.g. `tileA.compare(tileB)` not being the same as `tileB.compare(tileA)`), + * a property needed for reliably sorting the tiles in TilesCollection. + * To counteract this, tiles can indicate this is not the case for them and + * when any other tile is being compared to a tile where this flag is true, + * it should delegate the comparison to the given tile. + * E.g. one example where this flag is used is DateTile. */ + get comparisonIsNotCommutative(): boolean; compareEntry(entry: BaseEntry): number; // update received for already included (falls within sort keys) entry updateEntry(entry: BaseEntry, param: any): UpdateAction; diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 7cb9617d..93f89d66 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -110,8 +110,16 @@ export class SimpleTile extends ViewModel { return this._entry; } + get comparisonIsNotCommutative() { + return false; + } + compare(tile) { - return this.upperEntry.compare(tile.upperEntry); + if (tile.comparisonIsNotCommutative) { + return -tile.compare(this); + } else { + return this.upperEntry.compare(tile.upperEntry); + } } compareEntry(entry) { @@ -241,4 +249,4 @@ export function tests() { assert.equal(fridayTile.needsDateSeparator, true); }, } -} \ No newline at end of file +} From c38ee5239f68d0e945b70b6e54d1032dbc2ef5ac Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 20 Dec 2022 18:53:13 +0100 Subject: [PATCH 338/354] remove debug logging --- src/platform/web/dom/TimeFormatter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index 7db879ea..2a98a716 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -61,7 +61,6 @@ export class TimeFormatter implements ITimeFormatter { formatRelativeDate(date: Date): string { let daysDiff = Math.floor((date.getTime() - this.todayMidnight.getTime()) / TimeScope.Day); - console.log("formatRelativeDate daysDiff", daysDiff, date); if (daysDiff >= -1 && daysDiff <= 1) { // Tomorrow, Today, Yesterday return capitalizeFirstLetter(this.relativeDayFormatter.format(daysDiff, "day")); @@ -80,4 +79,4 @@ export class TimeFormatter implements ITimeFormatter { function capitalizeFirstLetter(str: string) { return str.slice(0, 1).toLocaleUpperCase() + str.slice(1); -} \ No newline at end of file +} From 3fecedfeb4cb23aea75c457e89c10eef9ccec2fc Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 20 Dec 2022 18:57:09 +0100 Subject: [PATCH 339/354] release v0.3.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1a21d58c..9bf21a7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.3.5", + "version": "0.3.6", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 3735e4420addf904dfe2623818a8c9162753e2f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:34:58 +0100 Subject: [PATCH 340/354] fix TS definition --- src/domain/session/room/timeline/tiles/ITile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/ITile.ts b/src/domain/session/room/timeline/tiles/ITile.ts index f36c1c51..24425a07 100644 --- a/src/domain/session/room/timeline/tiles/ITile.ts +++ b/src/domain/session/room/timeline/tiles/ITile.ts @@ -45,7 +45,7 @@ export interface ITile extends IDisposable { // as SimpleTile only has one entry, the tile should be removed removeEntry(entry: BaseEntry): boolean // SimpleTile can only contain 1 entry - tryIncludeEntry(): boolean; + tryIncludeEntry(entry: BaseEntry): boolean; // let item know it has a new sibling updatePreviousSibling(prev: ITile | undefined): void; // let item know it has a new sibling From 508d88edb54d308e27ecc7c0a3d0fd794ac24ba7 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:35:17 +0100 Subject: [PATCH 341/354] add some ideas to CSS doc how to document css classes --- doc/CSS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/CSS.md b/doc/CSS.md index 7365ec5b..36e02a3f 100644 --- a/doc/CSS.md +++ b/doc/CSS.md @@ -6,6 +6,10 @@ We could do top to bottom gradients in default avatars to make them look a bit c Can take ideas/adopt from OOCSS and SMACSS. +## Documentation + +Whether we use OOCSS, SMACSS or BEM, we should write a tool that uses a JS parser (acorn?) to find all css classes used in the view code by looking for a `{className: "..."}` pattern. E.g. if using BEM, use all the found classes to construct a doc with a section for every block, with therein all elements and modifiers. + ### Root - maybe we should not assume `body` is the root, but rather a `.brawl` class. The root is where we'd set root level css variables, fonts?, etc. Should we scope all css to this root class? That could get painful with just vanilla css. We could use something like https://github.com/domwashburn/postcss-parent-selector to only do this at build time. Other useful plugin for postcss: https://github.com/postcss/postcss-selector-parser From 014ae4185f4226b2ebc43f367cec2a0498fdc6cf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 6 Jan 2023 17:01:57 +0100 Subject: [PATCH 342/354] WIP --- src/utils/ErrorBoundary.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/utils/ErrorBoundary.ts diff --git a/src/utils/ErrorBoundary.ts b/src/utils/ErrorBoundary.ts new file mode 100644 index 00000000..e9f297c5 --- /dev/null +++ b/src/utils/ErrorBoundary.ts @@ -0,0 +1,38 @@ +/* +Copyright 2023 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 {ObservableValue, BaseObservableValue} from "../observable/ObservableValue"; + +export class ErrorBoundary { + constructor(private readonly errorCallback: (Error) => void) {} + + try(callback: () => T): T | undefined; + try(callback: () => Promise): Promise | undefined { + try { + let result: T | Promise = callback(); + if (result instanceof Promise) { + result = result.catch(err => { + this.errorCallback(err); + return undefined; + }); + } + return result; + } catch (err) { + this.errorCallback(err); + return undefined; + } + } +} From a9a72f88e5f5b44e148b104f630f4bc7f74445ad Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:36:28 +0100 Subject: [PATCH 343/354] finish ErrorBoundary --- src/utils/ErrorBoundary.ts | 56 ++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/utils/ErrorBoundary.ts b/src/utils/ErrorBoundary.ts index e9f297c5..520d2a35 100644 --- a/src/utils/ErrorBoundary.ts +++ b/src/utils/ErrorBoundary.ts @@ -14,25 +14,69 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ObservableValue, BaseObservableValue} from "../observable/ObservableValue"; +export const ErrorValue = Symbol("ErrorBoundary:Error"); export class ErrorBoundary { + private _error?: Error; + constructor(private readonly errorCallback: (Error) => void) {} - try(callback: () => T): T | undefined; - try(callback: () => Promise): Promise | undefined { + /** + * Executes callback() and then runs errorCallback() on error. + * This will never throw but instead return `errorValue` if an error occured. + */ + try(callback: () => T): T | typeof ErrorValue; + try(callback: () => Promise): Promise | typeof ErrorValue { try { - let result: T | Promise = callback(); + let result: T | Promise = callback(); if (result instanceof Promise) { result = result.catch(err => { + this._error = err; this.errorCallback(err); - return undefined; + return ErrorValue; }); } return result; } catch (err) { + this._error = err; this.errorCallback(err); - return undefined; + return ErrorValue; } } + + get error(): Error | undefined { + return this._error; + } } + +export function tests() { + return { + "catches sync error": assert => { + let emitted = false; + const boundary = new ErrorBoundary(() => emitted = true); + const result = boundary.try(() => { + throw new Error("fail!"); + }); + assert(emitted); + assert.strictEqual(result, ErrorValue); + }, + "return value of callback is forwarded": assert => { + let emitted = false; + const boundary = new ErrorBoundary(() => emitted = true); + const result = boundary.try(() => { + return "hello"; + }); + assert(!emitted); + assert.strictEqual(result, "hello"); + }, + "catches async error": async assert => { + let emitted = false; + const boundary = new ErrorBoundary(() => emitted = true); + const result = await boundary.try(async () => { + throw new Error("fail!"); + }); + assert(emitted); + assert.strictEqual(result, ErrorValue); + } + } +} \ No newline at end of file From 3706ff4f35cdc1c03dca826276be083b9e4a9d7b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Jan 2023 18:49:03 +0000 Subject: [PATCH 344/354] Docker builds are currently hosted on GHCR not GitLab --- doc/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/docker.md b/doc/docker.md index 910938f0..6b138dab 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -42,8 +42,8 @@ docker build -t hydrogen . Or, pull the docker image from GitLab: ``` -docker pull registry.gitlab.com/jcgruenhage/hydrogen-web -docker tag registry.gitlab.com/jcgruenhage/hydrogen-web hydrogen +docker pull ghcr.io/vector-im/hydrogen-web +docker tag ghcr.io/vector-im/hydrogen-web hydrogen ``` ### Start container image From 21d0c4d9b0e0baf0b78c0547cc99e58469851bf1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Jan 2023 18:56:18 +0000 Subject: [PATCH 345/354] Make consistent --- doc/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/docker.md b/doc/docker.md index 6b138dab..f1eeb596 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -39,7 +39,7 @@ In this repository, create a Docker image: docker build -t hydrogen . ``` -Or, pull the docker image from GitLab: +Or, pull the docker image from GitHub Container Registry: ``` docker pull ghcr.io/vector-im/hydrogen-web From df6474b63754ca6d579d41cb00d743481a87cdcc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 11 Jan 2023 18:06:25 +0000 Subject: [PATCH 346/354] Allow config override in docker image --- Dockerfile | 5 +++++ doc/docker.md | 24 ++++++++++++++++++++++++ docker/dynamic-config.sh | 8 ++++++++ 3 files changed, 37 insertions(+) create mode 100755 docker/dynamic-config.sh diff --git a/Dockerfile b/Dockerfile index f9e32313..07153148 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,9 @@ RUN yarn install \ && yarn build FROM docker.io/nginx:alpine + +# Copy the dynamic config script +COPY ./docker/dynamic-config.sh /docker-entrypoint.d/99-dynamic-config.sh + +# Copy the built app from the first build stage COPY --from=builder /app/target /usr/share/nginx/html diff --git a/doc/docker.md b/doc/docker.md index 910938f0..fc934999 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -56,3 +56,27 @@ docker run \ --publish 80:80 \ hydrogen ``` + +You can override the default `config.json` using the `CONFIG_OVERRIDE` environment variable. For example to specify a different Homeserver and : + +``` +docker run \ + --name hydrogen \ + --publish 80:80 \ + --env CONFIG_OVERRIDE='{ + "push": { + "appId": "io.element.hydrogen.web", + "gatewayUrl": "https://matrix.org", + "applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM" + }, + "defaultHomeServer": "https://fosdem.org", + "themeManifests": [ + "assets/theme-element.json" + ], + "defaultTheme": { + "light": "element-light", + "dark": "element-dark" + } +}' \ + hydrogen +``` diff --git a/docker/dynamic-config.sh b/docker/dynamic-config.sh new file mode 100755 index 00000000..952cb969 --- /dev/null +++ b/docker/dynamic-config.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -eux + +# Use config override environment variable if set +if [ -n "${CONFIG_OVERRIDE:-}" ]; then + echo "$CONFIG_OVERRIDE" > /usr/share/nginx/html/config.json +fi From 6fa73ec21496d6edee1189366f2365cc4cd1e1ec Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 11 Jan 2023 18:55:28 +0000 Subject: [PATCH 347/354] Install npm deps in separate docker layer --- Dockerfile | 11 ++++++++--- Dockerfile-dev | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index f9e32313..b0ec5128 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,14 @@ FROM docker.io/node:alpine as builder RUN apk add --no-cache git python3 build-base -COPY . /app + WORKDIR /app -RUN yarn install \ - && yarn build + +# Copy package.json and yarn.lock and install dependencies first to speed up subsequent builds +COPY package.json yarn.lock /app/ +RUN yarn install + +COPY . /app +RUN yarn build FROM docker.io/nginx:alpine COPY --from=builder /app/target /usr/share/nginx/html diff --git a/Dockerfile-dev b/Dockerfile-dev index 08dd9abd..7212a4ae 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,7 +1,12 @@ FROM docker.io/node:alpine RUN apk add --no-cache git python3 build-base -COPY . /code + WORKDIR /code + +# Copy package.json and yarn.lock and install dependencies first to speed up subsequent builds +COPY package.json yarn.lock /code/ RUN yarn install + +COPY . /code EXPOSE 3000 ENTRYPOINT ["yarn", "start"] From 7f004193d30de325e01d26047b91b7f6254e4199 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 18 Jan 2023 08:33:02 +0100 Subject: [PATCH 348/354] add user agent in log export --- src/logging/IDBLogger.ts | 1 + src/platform/web/Platform.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/logging/IDBLogger.ts b/src/logging/IDBLogger.ts index ab9474b0..863ff5a3 100644 --- a/src/logging/IDBLogger.ts +++ b/src/logging/IDBLogger.ts @@ -201,6 +201,7 @@ class IDBLogExport implements ILogExport { const log = { formatVersion: 1, appVersion: this._platform.updateService?.version, + platform: this._platform.description, items: this._items.map(i => JSON.parse(i.json)) }; const json = JSON.stringify(log); diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 0d95e585..a138e70c 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -366,7 +366,7 @@ export class Platform { } get description() { - return navigator.userAgent ?? ""; + return "web-" + (navigator.userAgent ?? ""); } dispose() { From 5a124809afec177b6828469f5b77c9fd9c254de3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 17 Jan 2023 17:19:12 +0100 Subject: [PATCH 349/354] write docs how updates work --- doc/updates.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 doc/updates.md diff --git a/doc/updates.md b/doc/updates.md new file mode 100644 index 00000000..6522fd10 --- /dev/null +++ b/doc/updates.md @@ -0,0 +1,58 @@ +# Updates + +How updates flow from the model to the view model to the UI. + +## EventEmitter, single values + +When interested in updates from a single object, chances are it inherits from `EventEmitter` and it supports a `change` event. + +`ViewModel` by default follows this pattern, but it can be overwritten, see Collections below. + +### Parameters + +Often a `parameters` or `params` argument is passed with the name of the field who's value has now changed. This parameter is currently only sometimes used, e.g. when it is too complicated or costly to check every possible field. An example of this is `TilesListView.onUpdate` to see if the `shape` property of a tile changed and hence the view needs to be recreated. Other than that, bindings in the web UI just reevaluate all bindings when receiving an update. This is a soft convention that could probably be more standardized, and it's not always clear what to pass (e.g. when multiple fields are being updated). + +Another reason to keep this convention around is that if one day we decide to add support for a different platform with a different UI, it may not be feasible to reevaluate all data-bindings in the UI for a given view model when receiving an update. + +## Collections + +As an optimization, Hydrogen uses a pattern to let updates flow over an observable collection where this makes sense. There is an `update` event for this in both `ObservableMap` and `ObservableList`. This prevents having to listen for updates on each individual item in large collections. The `update` event uses the same `params` argument as explained above. + +Some values like `BaseRoom` emit both with a `change` event on the event emitter and also over the collection. This way consumers can use what fits best for their case: the left panel can listen for updates on the room over the collection to power the room list, and the room view model can listen to the event emitter to get updates from the current room only. + +### MappedMap and mapping models to `ViewModel`s + +This can get a little complicated when using `MappedMap`, e.g. when mapping a model from `matrix/` +to a view model in `domain/`. Often, view models will want to emit updates _spontanously_, +e.g. without a prior update being sent from the lower-lying model. An example would be to change the value of a field after the view has called a method on the view model. +To support this pattern while having updates still flow over the collection requires some extra work; +`ViewModel` has a `emitChange` option which you can pass in to override +what `ViewModel.emitChange` does (by default it emits the `change` event on the view model). +`MappedMap` passes a callback to emit an update over the collection to the mapper function. +You can pass this callback as the `emitChange` option and updates will now flow over the collection. + +`MappedMap` also accepts an updater function, which you can use to make the view model respond to updates +from the lower-lying model. + +Here is an example: + +```ts +const viewModels = someCollection.mapValues( + (model, emitChange) => new SomeViewModel(this.childOptions({ + model, + // will make ViewModel.emitChange go over + // the collection rather than emit a "change" event + emitChange, + })), + // an update came in from the model, let the vm know + (vm: SomeViewModel) => vm.onUpdate(), + ); +``` + +### `ListView` & the `parentProvidesUpdates` flag. + +`ObservableList` is always rendered in the UI using `ListView`. When receiving an update over the collection, it will find the child view for the given index and call `update(params)` on it. Views will typically need to be told whether they should listen to the `change` event in their view model or rather wait for their `update()` method to be called by their parent view, `ListView`. That's why the `mount(args)` method on a view supports a `parentProvidesUpdates` flag. If `true`, the view should not subscribe to its view model, but rather updates the DOM when its `update()` method is called. Also see `BaseUpdateView` and `TemplateView` for how this is implemented in the child view. + +## `ObservableValue` + +When some method wants to return an object that can be updated, often an `ObservableValue` is used rather than an `EventEmitter`. It's not 100% clear cut when to use the former or the latter, but `ObservableValue` is often used when the returned value in it's entirety will change rather than just a property on it. `ObservableValue` also has some nice facilities like lazy evaluation when subscribed to and the `waitFor` method to work with promises. \ No newline at end of file From 8b0b81368008e9a64c43d760f61bd4589df0e617 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 17 Jan 2023 17:28:29 +0100 Subject: [PATCH 350/354] organize documention and remove obsolete documents --- doc/FAQ.md => FAQ.md | 0 doc/GOAL.md | 8 -- doc/TODO.md | 77 --------------- doc/api.md | 90 ------------------ doc/{ => architecture}/SKINNING.md | 0 doc/{ => architecture}/THEMING.md | 0 doc/{ => architecture}/UI/index.md | 0 .../UI/render-dom-elements.md | 0 doc/{ => architecture}/UI/ui.md | 0 doc/{ => architecture}/architecture.md | 0 .../images/coloring-process.png | Bin .../images/svg-icon-example.png | Bin .../images/theming-architecture.png | Bin .../persisted-network-calls.md | 0 doc/{ => architecture}/sync-updates.md | 0 doc/{ => architecture}/updates.md | 0 .../CATCHUP-BACKFILL.md | 0 doc/{ => implementation planning}/CSS.md | 0 .../DESIGN.md | 0 .../E2EE.md | 0 .../FRAGMENTS.md | 0 .../LOCAL-ECHO-STATE.md | 0 .../LOGIN.md | 0 .../MEMBERS.md | 0 .../PENDING_REPLIES.md | 0 .../PUSH.md | 0 .../QUESTIONS.md | 0 .../READ-RECEIPTS.md | 0 .../RECONNECTING.md | 0 .../RELATIONS.md | 0 doc/{ => implementation planning}/RELEASE.md | 0 .../REPLIES.md | 0 .../ROOM-VERSIONS.md | 0 .../SDK.md | 0 doc/{ => implementation planning}/SENDING.md | 0 .../SSO.md | 0 .../VIEW-UPDATES.md | 0 .../background-tasks.md | 0 .../html-messages.md | 0 doc/{ => implementation planning}/invites.md | 0 .../room-types.ts | 0 .../session-container.md | 0 .../timeline-member.md | 0 doc/{ => problem solving}/IMPORT-ISSUES.md | 0 doc/{ => problem solving}/INDEXEDDB.md | 0 .../domexception_mapping.md | 0 doc/viewhierarchy.md | 21 ---- 47 files changed, 196 deletions(-) rename doc/FAQ.md => FAQ.md (100%) delete mode 100644 doc/GOAL.md delete mode 100644 doc/TODO.md delete mode 100644 doc/api.md rename doc/{ => architecture}/SKINNING.md (100%) rename doc/{ => architecture}/THEMING.md (100%) rename doc/{ => architecture}/UI/index.md (100%) rename doc/{ => architecture}/UI/render-dom-elements.md (100%) rename doc/{ => architecture}/UI/ui.md (100%) rename doc/{ => architecture}/architecture.md (100%) rename doc/{ => architecture}/images/coloring-process.png (100%) rename doc/{ => architecture}/images/svg-icon-example.png (100%) rename doc/{ => architecture}/images/theming-architecture.png (100%) rename doc/{ => architecture}/persisted-network-calls.md (100%) rename doc/{ => architecture}/sync-updates.md (100%) rename doc/{ => architecture}/updates.md (100%) rename doc/{impl-thoughts => implementation planning}/CATCHUP-BACKFILL.md (100%) rename doc/{ => implementation planning}/CSS.md (100%) rename doc/{impl-thoughts => implementation planning}/DESIGN.md (100%) rename doc/{impl-thoughts => implementation planning}/E2EE.md (100%) rename doc/{impl-thoughts => implementation planning}/FRAGMENTS.md (100%) rename doc/{impl-thoughts => implementation planning}/LOCAL-ECHO-STATE.md (100%) rename doc/{impl-thoughts => implementation planning}/LOGIN.md (100%) rename doc/{impl-thoughts => implementation planning}/MEMBERS.md (100%) rename doc/{impl-thoughts => implementation planning}/PENDING_REPLIES.md (100%) rename doc/{impl-thoughts => implementation planning}/PUSH.md (100%) rename doc/{ => implementation planning}/QUESTIONS.md (100%) rename doc/{impl-thoughts => implementation planning}/READ-RECEIPTS.md (100%) rename doc/{impl-thoughts => implementation planning}/RECONNECTING.md (100%) rename doc/{impl-thoughts => implementation planning}/RELATIONS.md (100%) rename doc/{ => implementation planning}/RELEASE.md (100%) rename doc/{impl-thoughts => implementation planning}/REPLIES.md (100%) rename doc/{impl-thoughts => implementation planning}/ROOM-VERSIONS.md (100%) rename doc/{impl-thoughts => implementation planning}/SDK.md (100%) rename doc/{ => implementation planning}/SENDING.md (100%) rename doc/{impl-thoughts => implementation planning}/SSO.md (100%) rename doc/{impl-thoughts => implementation planning}/VIEW-UPDATES.md (100%) rename doc/{impl-thoughts => implementation planning}/background-tasks.md (100%) rename doc/{impl-thoughts => implementation planning}/html-messages.md (100%) rename doc/{ => implementation planning}/invites.md (100%) rename doc/{impl-thoughts => implementation planning}/room-types.ts (100%) rename doc/{impl-thoughts => implementation planning}/session-container.md (100%) rename doc/{impl-thoughts => implementation planning}/timeline-member.md (100%) rename doc/{ => problem solving}/IMPORT-ISSUES.md (100%) rename doc/{ => problem solving}/INDEXEDDB.md (100%) rename doc/{ => problem solving}/domexception_mapping.md (100%) delete mode 100644 doc/viewhierarchy.md diff --git a/doc/FAQ.md b/FAQ.md similarity index 100% rename from doc/FAQ.md rename to FAQ.md diff --git a/doc/GOAL.md b/doc/GOAL.md deleted file mode 100644 index 3883cf27..00000000 --- a/doc/GOAL.md +++ /dev/null @@ -1,8 +0,0 @@ -goal: - -write client that works on lumia 950 phone, so I can use matrix on my phone. - -try approach offline to indexeddb. go low-memory, and test the performance of storing every event individually in indexeddb. - -try to use little bandwidth, mainly by being an offline application and storing all requested data in indexeddb. -be as functional as possible while offline diff --git a/doc/TODO.md b/doc/TODO.md deleted file mode 100644 index 7d16400d..00000000 --- a/doc/TODO.md +++ /dev/null @@ -1,77 +0,0 @@ -# Minimal thing to get working - - - DONE: finish summary store - - DONE: move "sdk" bits over to "matrix" directory - - DONE: add eventemitter - - DONE: make sync work - - DONE: store summaries - - DONE: setup editorconfig - - DONE: setup linting (also in editor) - - DONE: store timeline - - DONE: store state - - DONE: make summary work better (name and joined/inviteCount doesn't seem to work well) - - DONE: timeline doesn't seem to recover it's key well upon loading, the query in load seems to never yield an event in the persister - - DONE: map DOMException to something better - - it's pretty opaque now when something idb related fails. DOMException has these fields: - code: 0 - message: "Key already exists in the object store." - name: "ConstraintError" - - DONE: emit events so we can start showing something on the screen maybe? - - DONE: move session._rooms over to Map, so we can iterate over it, ... - - DONE: build a very basic interface with - - DONE: a start/stop sync button - - DONE: a room list sorted alphabetically - - DONE: do some preprocessing on sync response which can then be used by persister, summary, timeline - - DONE: support timeline - - DONE: clicking on a room list, you see messages (userId -> body) - - DONE: style minimal UI - - DONE: implement gap filling and fragments (see FRAGMENTS.md) - - DONE: allow collection items (especially tiles) to self-update - - improve fragmentidcomparer::add - - DONE: better UI - - fix MappedMap update mechanism - - see if in BaseObservableMap we need to change ...params - - DONE: put sync button and status label inside SessionView - - fix some errors: - - find out if `(this._emitCollectionUpdate)(this)` is different than `this._emitCollectionUpdate(this)` - - got "database tried to mutate when not allowed" or something error as well - - find out why when RoomPersister.(\_createGapEntry/\_createEventEntry) we remove .buffer the transaction fails (good), but upon fixing and refreshing is missing a message! syncToken should not be saved, so why isn't this again in the sync response and now the txn does succeed? - - DONE: take access token out of IDB? this way it can be stored in a more secure thing for non-web clients, together wit encryption key for olm sessions ... ? like macos keychain, gnome keyring, ... maybe using https://www.npmjs.com/package/keytar - - DONE: experiment with using just a normal array with 2 numbers for sortkeys, to work in Edge as well. - - DONE: send messages - - DONE: fill gaps with call to /messages - - - DONE: build script - - DONE: take dev index.html, run some dom modifications to change script tag with `parse5`. - - DONE: create js bundle, rollup - - DONE: create css bundle, postcss, probably just need postcss-import for now, but good to have more options - - DONE: put all in /target - - have option to run it locally to test - - - deploy script - - upload /target to github pages - - - DONE: offline available - - both offline mechanisms have (filelist, version) as input for their template: - - create appcache manifest with (index.html, brawl.js, brawl.css) and print version number in it - - create service worker wit file list to cache (at top const files = "%%FILES_ARRAY%%", version = "%%VERSION%%") - - write web manifest - - DONE: delete and clear sessions from picker - - option to close current session and go back to picker - - - accept invite - - member list - - e2e encryption - - sync retry strategy - - instead of stopping sync on fetch error, show spinner and status and have auto retry strategy - - - create room - - join room - - leave room - - unread rooms, badge count, sort rooms by activity - - - DONE: create sync filter - - DONE: lazy loading members - - decide denormalized data in summary vs reading from multiple stores PER room on load - - allow Room/Summary class to be subclassed and store additional data? - - store account data, support read markers diff --git a/doc/api.md b/doc/api.md deleted file mode 100644 index 89e03639..00000000 --- a/doc/api.md +++ /dev/null @@ -1,90 +0,0 @@ -Session - properties: - rooms -> Rooms - -# storage -Storage - key...() -> KeyRange - start...Txn() -> Transaction -Transaction - store(name) -> ObjectStore - finish() - rollback() -ObjectStore : QueryTarget - index(name) -Index : QueryTarget - - -Rooms: EventEmitter, Iterator - get(id) -> RoomSummary ? -InternalRoom: EventEmitter - applySync(roomResponse, membership, txn) - - this method updates the room summary - - persists the room summary - - persists room state & timeline with RoomPersister - - updates the OpenRoom if present - - - applyAndPersistSync(roomResponse, membership, txn) { - this._summary.applySync(roomResponse, membership); - this._summary.persist(txn); - this._roomPersister.persist(roomResponse, membership, txn); - if (this._openRoom) { - this._openRoom.applySync(roomResponse); - } - } - -RoomPersister - RoomPersister (persists timeline and room state) - RoomSummary (persists room summary) -RoomSummary : EventEmitter - methods: - async open() - id - name - lastMessage - unreadCount - mentionCount - isEncrypted - isDirectMessage - membership - - should this have a custom reducer for custom fields? - - events - propChange(fieldName) - -OpenRoom : EventEmitter - properties: - timeline - events: - - -RoomState: EventEmitter - [room_id, event_type, state_key] -> [sort_key, event] -Timeline: EventEmitter - // should have a cache of recently lookup sender members? - // can we disambiguate members like this? - methods: - lastEvents(amount) - firstEvents(amount) - eventsAfter(sortKey, amount) - eventsBefore(sortKey, amount) - events: - eventsApppended - -RoomMembers : EventEmitter, Iterator - // no order, but need to be able to get all members somehow, needs to map to a ReactiveMap or something - events: - added(ids, values) - removed(ids, values) - changed(id, fieldName) -RoomMember: EventEmitter - properties: - id - name - powerLevel - membership - avatar - events: - propChange(fieldName) \ No newline at end of file diff --git a/doc/SKINNING.md b/doc/architecture/SKINNING.md similarity index 100% rename from doc/SKINNING.md rename to doc/architecture/SKINNING.md diff --git a/doc/THEMING.md b/doc/architecture/THEMING.md similarity index 100% rename from doc/THEMING.md rename to doc/architecture/THEMING.md diff --git a/doc/UI/index.md b/doc/architecture/UI/index.md similarity index 100% rename from doc/UI/index.md rename to doc/architecture/UI/index.md diff --git a/doc/UI/render-dom-elements.md b/doc/architecture/UI/render-dom-elements.md similarity index 100% rename from doc/UI/render-dom-elements.md rename to doc/architecture/UI/render-dom-elements.md diff --git a/doc/UI/ui.md b/doc/architecture/UI/ui.md similarity index 100% rename from doc/UI/ui.md rename to doc/architecture/UI/ui.md diff --git a/doc/architecture.md b/doc/architecture/architecture.md similarity index 100% rename from doc/architecture.md rename to doc/architecture/architecture.md diff --git a/doc/images/coloring-process.png b/doc/architecture/images/coloring-process.png similarity index 100% rename from doc/images/coloring-process.png rename to doc/architecture/images/coloring-process.png diff --git a/doc/images/svg-icon-example.png b/doc/architecture/images/svg-icon-example.png similarity index 100% rename from doc/images/svg-icon-example.png rename to doc/architecture/images/svg-icon-example.png diff --git a/doc/images/theming-architecture.png b/doc/architecture/images/theming-architecture.png similarity index 100% rename from doc/images/theming-architecture.png rename to doc/architecture/images/theming-architecture.png diff --git a/doc/persisted-network-calls.md b/doc/architecture/persisted-network-calls.md similarity index 100% rename from doc/persisted-network-calls.md rename to doc/architecture/persisted-network-calls.md diff --git a/doc/sync-updates.md b/doc/architecture/sync-updates.md similarity index 100% rename from doc/sync-updates.md rename to doc/architecture/sync-updates.md diff --git a/doc/updates.md b/doc/architecture/updates.md similarity index 100% rename from doc/updates.md rename to doc/architecture/updates.md diff --git a/doc/impl-thoughts/CATCHUP-BACKFILL.md b/doc/implementation planning/CATCHUP-BACKFILL.md similarity index 100% rename from doc/impl-thoughts/CATCHUP-BACKFILL.md rename to doc/implementation planning/CATCHUP-BACKFILL.md diff --git a/doc/CSS.md b/doc/implementation planning/CSS.md similarity index 100% rename from doc/CSS.md rename to doc/implementation planning/CSS.md diff --git a/doc/impl-thoughts/DESIGN.md b/doc/implementation planning/DESIGN.md similarity index 100% rename from doc/impl-thoughts/DESIGN.md rename to doc/implementation planning/DESIGN.md diff --git a/doc/impl-thoughts/E2EE.md b/doc/implementation planning/E2EE.md similarity index 100% rename from doc/impl-thoughts/E2EE.md rename to doc/implementation planning/E2EE.md diff --git a/doc/impl-thoughts/FRAGMENTS.md b/doc/implementation planning/FRAGMENTS.md similarity index 100% rename from doc/impl-thoughts/FRAGMENTS.md rename to doc/implementation planning/FRAGMENTS.md diff --git a/doc/impl-thoughts/LOCAL-ECHO-STATE.md b/doc/implementation planning/LOCAL-ECHO-STATE.md similarity index 100% rename from doc/impl-thoughts/LOCAL-ECHO-STATE.md rename to doc/implementation planning/LOCAL-ECHO-STATE.md diff --git a/doc/impl-thoughts/LOGIN.md b/doc/implementation planning/LOGIN.md similarity index 100% rename from doc/impl-thoughts/LOGIN.md rename to doc/implementation planning/LOGIN.md diff --git a/doc/impl-thoughts/MEMBERS.md b/doc/implementation planning/MEMBERS.md similarity index 100% rename from doc/impl-thoughts/MEMBERS.md rename to doc/implementation planning/MEMBERS.md diff --git a/doc/impl-thoughts/PENDING_REPLIES.md b/doc/implementation planning/PENDING_REPLIES.md similarity index 100% rename from doc/impl-thoughts/PENDING_REPLIES.md rename to doc/implementation planning/PENDING_REPLIES.md diff --git a/doc/impl-thoughts/PUSH.md b/doc/implementation planning/PUSH.md similarity index 100% rename from doc/impl-thoughts/PUSH.md rename to doc/implementation planning/PUSH.md diff --git a/doc/QUESTIONS.md b/doc/implementation planning/QUESTIONS.md similarity index 100% rename from doc/QUESTIONS.md rename to doc/implementation planning/QUESTIONS.md diff --git a/doc/impl-thoughts/READ-RECEIPTS.md b/doc/implementation planning/READ-RECEIPTS.md similarity index 100% rename from doc/impl-thoughts/READ-RECEIPTS.md rename to doc/implementation planning/READ-RECEIPTS.md diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/implementation planning/RECONNECTING.md similarity index 100% rename from doc/impl-thoughts/RECONNECTING.md rename to doc/implementation planning/RECONNECTING.md diff --git a/doc/impl-thoughts/RELATIONS.md b/doc/implementation planning/RELATIONS.md similarity index 100% rename from doc/impl-thoughts/RELATIONS.md rename to doc/implementation planning/RELATIONS.md diff --git a/doc/RELEASE.md b/doc/implementation planning/RELEASE.md similarity index 100% rename from doc/RELEASE.md rename to doc/implementation planning/RELEASE.md diff --git a/doc/impl-thoughts/REPLIES.md b/doc/implementation planning/REPLIES.md similarity index 100% rename from doc/impl-thoughts/REPLIES.md rename to doc/implementation planning/REPLIES.md diff --git a/doc/impl-thoughts/ROOM-VERSIONS.md b/doc/implementation planning/ROOM-VERSIONS.md similarity index 100% rename from doc/impl-thoughts/ROOM-VERSIONS.md rename to doc/implementation planning/ROOM-VERSIONS.md diff --git a/doc/impl-thoughts/SDK.md b/doc/implementation planning/SDK.md similarity index 100% rename from doc/impl-thoughts/SDK.md rename to doc/implementation planning/SDK.md diff --git a/doc/SENDING.md b/doc/implementation planning/SENDING.md similarity index 100% rename from doc/SENDING.md rename to doc/implementation planning/SENDING.md diff --git a/doc/impl-thoughts/SSO.md b/doc/implementation planning/SSO.md similarity index 100% rename from doc/impl-thoughts/SSO.md rename to doc/implementation planning/SSO.md diff --git a/doc/impl-thoughts/VIEW-UPDATES.md b/doc/implementation planning/VIEW-UPDATES.md similarity index 100% rename from doc/impl-thoughts/VIEW-UPDATES.md rename to doc/implementation planning/VIEW-UPDATES.md diff --git a/doc/impl-thoughts/background-tasks.md b/doc/implementation planning/background-tasks.md similarity index 100% rename from doc/impl-thoughts/background-tasks.md rename to doc/implementation planning/background-tasks.md diff --git a/doc/impl-thoughts/html-messages.md b/doc/implementation planning/html-messages.md similarity index 100% rename from doc/impl-thoughts/html-messages.md rename to doc/implementation planning/html-messages.md diff --git a/doc/invites.md b/doc/implementation planning/invites.md similarity index 100% rename from doc/invites.md rename to doc/implementation planning/invites.md diff --git a/doc/impl-thoughts/room-types.ts b/doc/implementation planning/room-types.ts similarity index 100% rename from doc/impl-thoughts/room-types.ts rename to doc/implementation planning/room-types.ts diff --git a/doc/impl-thoughts/session-container.md b/doc/implementation planning/session-container.md similarity index 100% rename from doc/impl-thoughts/session-container.md rename to doc/implementation planning/session-container.md diff --git a/doc/impl-thoughts/timeline-member.md b/doc/implementation planning/timeline-member.md similarity index 100% rename from doc/impl-thoughts/timeline-member.md rename to doc/implementation planning/timeline-member.md diff --git a/doc/IMPORT-ISSUES.md b/doc/problem solving/IMPORT-ISSUES.md similarity index 100% rename from doc/IMPORT-ISSUES.md rename to doc/problem solving/IMPORT-ISSUES.md diff --git a/doc/INDEXEDDB.md b/doc/problem solving/INDEXEDDB.md similarity index 100% rename from doc/INDEXEDDB.md rename to doc/problem solving/INDEXEDDB.md diff --git a/doc/domexception_mapping.md b/doc/problem solving/domexception_mapping.md similarity index 100% rename from doc/domexception_mapping.md rename to doc/problem solving/domexception_mapping.md diff --git a/doc/viewhierarchy.md b/doc/viewhierarchy.md deleted file mode 100644 index c4e6355a..00000000 --- a/doc/viewhierarchy.md +++ /dev/null @@ -1,21 +0,0 @@ -view hierarchy: -``` - BrawlView - SwitchView - SessionView - SyncStatusBar - ListView(left-panel) - RoomTile - SwitchView - RoomPlaceholderView - RoomView - MiddlePanel - ListView(timeline) - event tiles (see ui/session/room/timeline/) - ComposerView - RightPanel - SessionPickView - ListView - SessionPickerItemView - LoginView -``` From 13aea539fa65811952ca9085c538ae58f099e901 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 17 Jan 2023 17:30:48 +0100 Subject: [PATCH 351/354] move ts style guide to own dir --- doc/{TS-MIGRATION.md => style guide/typescript.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/{TS-MIGRATION.md => style guide/typescript.md} (100%) diff --git a/doc/TS-MIGRATION.md b/doc/style guide/typescript.md similarity index 100% rename from doc/TS-MIGRATION.md rename to doc/style guide/typescript.md From 887dea528d8721e0ac8b287c7662f245ecd551ca Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 17 Jan 2023 17:31:48 +0100 Subject: [PATCH 352/354] update dirs in docs --- FAQ.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FAQ.md b/FAQ.md index 0823ee08..43053c2f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -36,4 +36,4 @@ Published builds can be found at https://github.com/vector-im/hydrogen-web/relea ## I want to embed Hydrogen in my website, how should I do that? -Hydrogen aims to be usable as an SDK, and while it is still early days, you can find some documentation how to do that in [SDK.md](SDK.md). +Hydrogen aims to be usable as an SDK, and while it is still early days, you can find some documentation how to do that in [SDK.md](doc/SDK.md). diff --git a/README.md b/README.md index a4c1529f..7ad012a5 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,4 @@ PS: You need nodejs, running yarn on top of any other js platform is not support # FAQ -Some frequently asked questions are answered [here](doc/FAQ.md). +Some frequently asked questions are answered [here](FAQ.md). From bd648c1de33082830c9f9a5382cadc4ecb5ab341 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 19 Jan 2023 09:25:11 +0100 Subject: [PATCH 353/354] skinning support is broken, remove doc --- doc/architecture/SKINNING.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 doc/architecture/SKINNING.md diff --git a/doc/architecture/SKINNING.md b/doc/architecture/SKINNING.md deleted file mode 100644 index 5f1c735d..00000000 --- a/doc/architecture/SKINNING.md +++ /dev/null @@ -1,22 +0,0 @@ -# Replacing javascript files - -Any source file can be replaced at build time by mapping the path in a JSON file passed in to the build command, e.g. `yarn build --override-imports customizations.json`. The file should be written like so: - -```json -{ - "src/platform/web/ui/session/room/timeline/TextMessageView.js": "src/platform/web/ui/session/room/timeline/MyTextMessageView.js" -} -``` -The paths are relative to the location of the mapping file, but the mapping file should be in a parent directory of the files you want to replace. - -You should see a "replacing x with y" line (twice actually, for the normal and legacy build). - -# Injecting CSS - -You can override the location of the main css file with the `--override-css ` option to the build script. The default is `src/platform/web/ui/css/main.css`, which you probably want to import from your custom css file like so: - -```css -@import url('src/platform/web/ui/css/main.css'); - -/* additions */ -``` From 289414702736ed42c314850f5334ed2304c81125 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 19 Jan 2023 12:02:57 +0000 Subject: [PATCH 354/354] Set Quentin and Hugh as codeowners for docker related files --- .github/CODEOWNERS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..3dd1a6ab --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Docker related files are not maintained by the core Hydrogen team +/.dockerignore @hughns @sandhose +/Dockerfile @hughns @sandhose +/Dockerfile-dev @hughns @sandhose +/.github/workflows/docker-publish.yml @hughns @sandhose +/docker/ @hughns @sandhose +/doc/docker.md @hughns @sandhose