From 4d82dd22b6482a9c01dd4922bebdb5b9918a160f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Feb 2022 17:50:17 +0100 Subject: [PATCH 01/69] convert ViewModel to typescript --- src/domain/{ViewModel.js => ViewModel.ts} | 67 ++++++++++++++--------- src/utils/Disposables.ts | 23 ++++---- 2 files changed, 52 insertions(+), 38 deletions(-) rename src/domain/{ViewModel.js => ViewModel.ts} (65%) diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.ts similarity index 65% rename from src/domain/ViewModel.js rename to src/domain/ViewModel.ts index 0c665194..99b23918 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.ts @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +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. @@ -21,54 +22,70 @@ limitations under the License. import {EventEmitter} from "../utils/EventEmitter"; import {Disposables} from "../utils/Disposables"; -export class ViewModel extends EventEmitter { - constructor(options = {}) { +import type {Disposable} from "../utils/Disposables"; +import type {Platform} from "../platform/web/Platform"; +import type {Clock} from "../platform/web/dom/Clock"; +import type {ILogger} from "../logging/types"; +import type {Navigation} from "./navigation/Navigation"; +import type {URLRouter} from "./navigation/URLRouter"; + +type Options = { + platform: Platform + logger: ILogger + urlCreator: URLRouter + navigation: Navigation + emitChange?: (params: any) => void +} + +export class ViewModel extends EventEmitter<{change: never}> { + private disposables?: Disposables; + private _isDisposed = false; + private _options: O; + + constructor(options: O) { super(); - this.disposables = null; - this._isDisposed = false; this._options = options; } - childOptions(explicitOptions) { - const {navigation, urlCreator, platform} = this._options; - return Object.assign({navigation, urlCreator, platform}, explicitOptions); + childOptions(explicitOptions: T): T & Options { + return Object.assign({}, this._options, explicitOptions); } // makes it easier to pass through dependencies of a sub-view model - getOption(name) { + getOption(name: N): O[N] { return this._options[name]; } - track(disposable) { + track(disposable: D): D { if (!this.disposables) { this.disposables = new Disposables(); } return this.disposables.track(disposable); } - untrack(disposable) { + untrack(disposable: Disposable): undefined { if (this.disposables) { return this.disposables.untrack(disposable); } - return null; + return undefined; } - dispose() { + dispose(): void { if (this.disposables) { this.disposables.dispose(); } this._isDisposed = true; } - get isDisposed() { + get isDisposed(): boolean { return this._isDisposed; } - disposeTracked(disposable) { + disposeTracked(disposable: Disposable | undefined): undefined { if (this.disposables) { return this.disposables.disposeTracked(disposable); } - return null; + return undefined; } // TODO: this will need to support binding @@ -76,7 +93,7 @@ export class ViewModel extends EventEmitter { // // 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, ...expr) { + i18n(parts: string[], ...expr: any[]) { // just concat for now let result = ""; for (let i = 0; i < parts.length; ++i) { @@ -88,11 +105,11 @@ export class ViewModel extends EventEmitter { return result; } - updateOptions(options) { + updateOptions(options: O): void { this._options = Object.assign(this._options, options); } - emitChange(changedProps) { + emitChange(changedProps: any): void { if (this._options.emitChange) { this._options.emitChange(changedProps); } else { @@ -100,27 +117,23 @@ export class ViewModel extends EventEmitter { } } - get platform() { + get platform(): Platform { return this._options.platform; } - get clock() { + get clock(): Clock { return this._options.platform.clock; } - get logger() { + get logger(): ILogger { return this.platform.logger; } - /** - * The url router, only meant to be used to create urls with from view models. - * @return {URLRouter} - */ - get urlCreator() { + get urlCreator(): URLRouter { return this._options.urlCreator; } - get navigation() { + get navigation(): Navigation { return this._options.navigation; } } diff --git a/src/utils/Disposables.ts b/src/utils/Disposables.ts index 19a5983c..f7c7eb53 100644 --- a/src/utils/Disposables.ts +++ b/src/utils/Disposables.ts @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +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. @@ -18,7 +19,7 @@ export interface IDisposable { dispose(): void; } -type Disposable = IDisposable | (() => void); +export type Disposable = IDisposable | (() => void); function disposeValue(value: Disposable): void { if (typeof value === "function") { @@ -33,9 +34,9 @@ function isDisposable(value: Disposable): boolean { } export class Disposables { - private _disposables: Disposable[] | null = []; + private _disposables?: Disposable[] = []; - track(disposable: Disposable): Disposable { + track(disposable: D): D { if (!isDisposable(disposable)) { throw new Error("Not a disposable"); } @@ -48,16 +49,16 @@ export class Disposables { return disposable; } - untrack(disposable: Disposable): null { + untrack(disposable: Disposable): undefined { if (this.isDisposed) { console.warn("Disposables already disposed, cannot untrack"); - return null; + return undefined; } const idx = this._disposables!.indexOf(disposable); if (idx >= 0) { this._disposables!.splice(idx, 1); } - return null; + return undefined; } dispose(): void { @@ -65,17 +66,17 @@ export class Disposables { for (const d of this._disposables) { disposeValue(d); } - this._disposables = null; + this._disposables = undefined; } } get isDisposed(): boolean { - return this._disposables === null; + return this._disposables === undefined; } - disposeTracked(value: Disposable): null { + disposeTracked(value: Disposable | undefined): undefined { if (value === undefined || value === null || this.isDisposed) { - return null; + return undefined; } const idx = this._disposables!.indexOf(value); if (idx !== -1) { @@ -84,6 +85,6 @@ export class Disposables { } else { console.warn("disposable not found, did it leak?", value); } - return null; + return undefined; } } From 1795f58ba5bbdddcc5854a6967dccbebb6873320 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Feb 2022 17:53:59 +0100 Subject: [PATCH 02/69] rename imports --- src/domain/AccountSetupViewModel.js | 2 +- src/domain/LogoutViewModel.js | 2 +- src/domain/RootViewModel.js | 2 +- src/domain/SessionLoadViewModel.js | 2 +- src/domain/SessionPickerViewModel.js | 2 +- src/domain/login/CompleteSSOLoginViewModel.js | 2 +- src/domain/login/LoginViewModel.js | 2 +- src/domain/login/PasswordLoginViewModel.js | 2 +- src/domain/login/StartSSOLoginViewModel.js | 2 +- src/domain/session/CreateRoomViewModel.js | 2 +- src/domain/session/RoomGridViewModel.js | 2 +- src/domain/session/SessionStatusViewModel.js | 2 +- src/domain/session/SessionViewModel.js | 2 +- src/domain/session/leftpanel/BaseTileViewModel.js | 2 +- src/domain/session/leftpanel/LeftPanelViewModel.js | 2 +- src/domain/session/rightpanel/MemberDetailsViewModel.js | 2 +- src/domain/session/rightpanel/MemberListViewModel.js | 2 +- src/domain/session/rightpanel/MemberTileViewModel.js | 2 +- src/domain/session/rightpanel/RightPanelViewModel.js | 2 +- src/domain/session/rightpanel/RoomDetailsViewModel.js | 2 +- src/domain/session/room/ComposerViewModel.js | 2 +- src/domain/session/room/InviteViewModel.js | 2 +- src/domain/session/room/LightboxViewModel.js | 2 +- src/domain/session/room/RoomBeingCreatedViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 2 +- src/domain/session/room/UnknownRoomViewModel.js | 4 ++-- src/domain/session/room/timeline/TimelineViewModel.js | 2 +- src/domain/session/settings/KeyBackupViewModel.js | 2 +- src/domain/session/settings/SettingsViewModel.js | 2 +- src/lib.ts | 2 +- 30 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index 4ad0d8d5..74d680d0 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; import {KeyType} from "../matrix/ssss/index"; import {Status} from "./session/settings/KeyBackupViewModel.js"; diff --git a/src/domain/LogoutViewModel.js b/src/domain/LogoutViewModel.js index f22637de..24ca440e 100644 --- a/src/domain/LogoutViewModel.js +++ b/src/domain/LogoutViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; export class LogoutViewModel extends ViewModel { diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 70f5b554..642e43f4 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -20,7 +20,7 @@ import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel.js"; import {LogoutViewModel} from "./LogoutViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; export class RootViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 24df2546..b23b54bc 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {AccountSetupViewModel} from "./AccountSetupViewModel.js"; import {LoadStatus} from "../matrix/Client.js"; import {SyncStatus} from "../matrix/Sync.js"; -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; export class SessionLoadViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index e4bbc7ec..6714e96f 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {SortedArray} from "../observable/index.js"; -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; import {avatarInitials, getIdentifierColorNumber} from "./avatar.js"; class SessionItemViewModel extends ViewModel { diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index daa2aa9f..d41d53ec 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; export class CompleteSSOLoginViewModel extends ViewModel { diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index b91df4cc..bf77e624 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Client} from "../../matrix/Client.js"; -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 5fd8271f..7c4ff78a 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; export class PasswordLoginViewModel extends ViewModel { diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index 54218d22..dba0bcb5 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; export class StartSSOLoginViewModel extends ViewModel{ constructor(options) { diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index 51a9b7a4..12b4fbd5 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {imageToInfo} from "./common.js"; import {RoomType} from "../../matrix/room/common"; diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index d89d821a..a7d19054 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {addPanelIfNeeded} from "../navigation/index.js"; function dedupeSparse(roomIds) { diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 3f2263ac..8f1d0748 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {createEnum} from "../../utils/enum"; import {ConnectionStatus} from "../../matrix/net/Reconnector"; import {SyncStatus} from "../../matrix/Sync.js"; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 24276f42..a67df3a7 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -25,7 +25,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {CreateRoomViewModel} from "./CreateRoomViewModel.js"; -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; diff --git a/src/domain/session/leftpanel/BaseTileViewModel.js b/src/domain/session/leftpanel/BaseTileViewModel.js index b360b1d4..e1d6dfff 100644 --- a/src/domain/session/leftpanel/BaseTileViewModel.js +++ b/src/domain/session/leftpanel/BaseTileViewModel.js @@ -16,7 +16,7 @@ limitations under the License. */ import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; const KIND_ORDER = ["roomBeingCreated", "invite", "room"]; diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 843ed1ca..2fd3ca7e 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index f6cbd747..8ee50030 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomType} from "../../../matrix/room/common"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 5b8bb83e..b75a3d1c 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {createMemberComparator} from "./members/comparator.js"; import {Disambiguator} from "./members/disambiguator.js"; diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index eac6a6d4..9062ea7d 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class MemberTileViewModel extends ViewModel { diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 3cfe378b..b4b6b4eb 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js"; import {MemberListViewModel} from "./MemberListViewModel.js"; import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js"; diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index 5e509fd5..97e8588e 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class RoomDetailsViewModel extends ViewModel { diff --git a/src/domain/session/room/ComposerViewModel.js b/src/domain/session/room/ComposerViewModel.js index 730e1b20..833b17f5 100644 --- a/src/domain/session/room/ComposerViewModel.js +++ b/src/domain/session/room/ComposerViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class ComposerViewModel extends ViewModel { constructor(roomVM) { diff --git a/src/domain/session/room/InviteViewModel.js b/src/domain/session/room/InviteViewModel.js index 81a08e44..c2ff74e0 100644 --- a/src/domain/session/room/InviteViewModel.js +++ b/src/domain/session/room/InviteViewModel.js @@ -16,7 +16,7 @@ limitations under the License. */ import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class InviteViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index f6da39b0..8ce8757a 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class LightboxViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index f98c86f9..f5c5d3cd 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -16,7 +16,7 @@ limitations under the License. */ import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class RoomBeingCreatedViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 2d10c7ca..b7af00ce 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -19,7 +19,7 @@ import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {ComposerViewModel} from "./ComposerViewModel.js" import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {tilesCreator} from "./timeline/tilesCreator.js"; -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {imageToInfo} from "../common.js"; export class RoomViewModel extends ViewModel { diff --git a/src/domain/session/room/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index e7969298..8bb5fb0a 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { @@ -55,4 +55,4 @@ export class UnknownRoomViewModel extends ViewModel { get kind() { return "unknown"; } -} \ No newline at end of file +} diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 9c936218..2408146d 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -32,7 +32,7 @@ to the room timeline, which unload entries from memory. when loading, it just reads events from a sortkey backwards or forwards... */ import {TilesCollection} from "./TilesCollection.js"; -import {ViewModel} from "../../../ViewModel.js"; +import {ViewModel} from "../../../ViewModel"; export class TimelineViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index b44de7e5..243b0d7c 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 0b68f168..7464a659 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; class PushNotificationStatus { diff --git a/src/lib.ts b/src/lib.ts index 3e191d45..cc88690c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -30,6 +30,6 @@ export {Navigation} from "./domain/navigation/Navigation.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; export {TemplateView} from "./platform/web/ui/general/TemplateView"; -export {ViewModel} from "./domain/ViewModel.js"; +export {ViewModel} from "./domain/ViewModel"; export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; export {AvatarView} from "./platform/web/ui/AvatarView.js"; From 1a159f9e9a834026af3a28d8a57782937225a9c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Feb 2022 18:01:04 +0100 Subject: [PATCH 03/69] WIP --- CONTRIBUTING.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..80f1fc6a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,191 @@ +Contributing code to matrix-js-sdk +================================== + +Everyone is welcome to contribute code to hydrogen-web, provided that they are +willing to license their contributions under the same license as the project +itself. We follow a simple 'inbound=outbound' model for contributions: the act +of submitting an 'inbound' contribution means that the contributor agrees to +license the code under the same terms as the project's overall 'outbound' +license - in this case, Apache Software License v2 (see +[LICENSE](LICENSE)). + +How to contribute +----------------- + +The preferred and easiest way to contribute changes to the project is to fork +it on github, and then create a pull request to ask us to pull your changes +into our repo (https://help.github.com/articles/using-pull-requests/) + +We use GitHub's pull request workflow to review the contribution, and either +ask you to make any refinements needed or merge it and make them ourselves. + +Things that should go into your PR description: + * Please disable any automatic formatting tools you may have active. + You'll be asked to undo any unrelated whitespace changes during code review. + * References to any bugs fixed by the change (in GitHub's `Fixes` notation) + * Describe the why and what is changing in the PR description so it's easy for + onlookers and reviewers to onboard and context switch. + * Include both **before** and **after** screenshots to easily compare and discuss + what's changing. + * Include a step-by-step testing strategy so that a reviewer can check out the + code locally and easily get to the point of testing your change. + * Add comments to the diff for the reviewer that might help them to understand + why the change is necessary or how they might better understand and review it. + +To add a longer, more detailed description of the change for the changelog: + + +*Fix llama herding bug* + +``` +Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase +``` + +*Remove outdated comment from `Ungulates.ts`* +``` +Notes: none +``` + +Sometimes, you're fixing a bug in a downstream project, in which case you want +an entry in that project's changelog. You can do that too: + +*Fix another herding bug* +``` +Notes: Fix a bug where the `herd()` function would only work on Tuesdays +element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays +``` + +If your PR introduces a breaking change, add the `X-Breaking-Change` label (see below) +and remember to tell the developer how to migrate: + +*Remove legacy class* + +``` +Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. +``` + +Other metadata can be added using labels. + * `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump. + +If you don't have permission to add labels, your PR reviewer(s) can work with you +to add them: ask in the PR description or comments. + +We use continuous integration, and all pull requests get automatically tested: +if your change breaks the build, then the PR will show that there are failed +checks, so please check back after a few minutes. + +Tests +----- +If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement' +label) then we require that the PR also includes tests. These need to test that +your feature works as expected and ideally test edge cases too. For the js-sdk +itself, your tests should generally be unit tests. matrix-react-sdk also uses +these guidelines, so for that your tests can be unit tests using +react-test-utils, snapshot tests or screenshot tests. + +We don't require tests for bug fixes (T-Defect) but strongly encourage regression +tests for the bug itself wherever possible. + +In the future we may formalise this more with a minimum test coverage +percentage for the diff. + +Code style +---------- +The js-sdk aims to target TypeScript/ES6. All new files should be written in +TypeScript and existing files should use ES6 principles where possible. + +Members should not be exported as a default export in general - it causes problems +with the architecture of the SDK (index file becomes less clear) and could +introduce naming problems (as default exports get aliased upon import). In +general, avoid using `export default`. + +The remaining code-style for matrix-js-sdk is not formally documented, but +contributors are encouraged to read the +[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md) +and follow the principles set out there. + +Please ensure your changes match the cosmetic style of the existing project, +and ***never*** mix cosmetic and functional changes in the same commit, as it +makes it horribly hard to review otherwise. + +Attribution +----------- +Everyone who contributes anything to Matrix is welcome to be listed in the +AUTHORS.rst file for the project in question. Please feel free to include a +change to AUTHORS.rst in your pull request to list yourself and a short +description of the area(s) you've worked on. Also, we sometimes have swag to +give away to contributors - if you feel that Matrix-branded apparel is missing +from your life, please mail us your shipping address to matrix at matrix.org +and we'll try to fix it :) + +Sign off +-------- +In order to have a concrete record that your contribution is intentional +and you agree to license it under the same terms as the project's license, we've +adopted the same lightweight approach that the Linux Kernel +(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker +(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other +projects use: the DCO (Developer Certificate of Origin: +http://developercertificate.org/). This is a simple declaration that you wrote +the contribution or otherwise have the right to contribute it to Matrix: + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +If you agree to this for your contribution, then all that's needed is to +include the line in your commit or pull request comment: + +``` +Signed-off-by: Your Name +``` + +We accept contributions under a legally identifiable name, such as your name on +government documentation or common-law names (names claimed by legitimate usage +or repute). Unfortunately, we cannot accept anonymous contributions at this +time. + +Git allows you to add this signoff automatically when using the `-s` flag to +`git commit`, which uses the name and email set in your `user.name` and +`user.email` git configs. + +If you forgot to sign off your commits before making your pull request and are +on Git 2.17+ you can mass signoff using rebase: + +``` +git rebase --signoff origin/develop +``` From 7179758c5051b6273195f88fa027fb6607bd5e5d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 08:22:09 +0100 Subject: [PATCH 04/69] also here --- src/domain/session/room/timeline/tiles/SimpleTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 4c1c1de0..3497b689 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -15,7 +15,7 @@ limitations under the License. */ import {UpdateAction} from "../UpdateAction.js"; -import {ViewModel} from "../../../../ViewModel.js"; +import {ViewModel} from "../../../../ViewModel"; import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js"; export class SimpleTile extends ViewModel { From dea1e7eaf36896d1db89921b68cb24251eb6af5e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 11:31:50 +0100 Subject: [PATCH 05/69] bump sdk version --- 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 3ee2ca3b..3e468181 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.5", + "version": "0.0.6", "main": "./hydrogen.es.js", "type": "module" } From 7aeda70ff66614c74e69a4345202ebcbbfde8769 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:19:49 +0100 Subject: [PATCH 06/69] convert DecryptionResult --- ...ecryptionResult.js => DecryptionResult.ts} | 44 +++++++++++-------- .../megolm/decryption/SessionDecryption.ts | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) rename src/matrix/e2ee/{DecryptionResult.js => DecryptionResult.ts} (66%) diff --git a/src/matrix/e2ee/DecryptionResult.js b/src/matrix/e2ee/DecryptionResult.ts similarity index 66% rename from src/matrix/e2ee/DecryptionResult.js rename to src/matrix/e2ee/DecryptionResult.ts index e1c2bcc4..67c242bc 100644 --- a/src/matrix/e2ee/DecryptionResult.js +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -26,35 +26,41 @@ limitations under the License. * see DeviceTracker */ +import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore"; +type DecryptedEvent = { + type?: string, + content?: Record +} export class DecryptionResult { - constructor(event, senderCurve25519Key, claimedEd25519Key) { - this.event = event; - this.senderCurve25519Key = senderCurve25519Key; - this.claimedEd25519Key = claimedEd25519Key; - this._device = null; - this._roomTracked = true; + private device?: DeviceIdentity; + private roomTracked: boolean = true; + + constructor( + public readonly event: DecryptedEvent, + public readonly senderCurve25519Key: string, + public readonly claimedEd25519Key: string + ) {} + + setDevice(device: DeviceIdentity) { + this.device = device; } - setDevice(device) { - this._device = device; + setRoomNotTrackedYet(): void { + this.roomTracked = false; } - setRoomNotTrackedYet() { - this._roomTracked = false; - } - - get isVerified() { - if (this._device) { - const comesFromDevice = this._device.ed25519Key === this.claimedEd25519Key; + get isVerified(): boolean { + if (this.device) { + const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key; return comesFromDevice; } return false; } - get isUnverified() { - if (this._device) { + get isUnverified(): boolean { + if (this.device) { return !this.isVerified; } else if (this.isVerificationUnknown) { return false; @@ -63,8 +69,8 @@ export class DecryptionResult { } } - get isVerificationUnknown() { + 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 && !this.roomTracked; } } diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index f56feb47..57ef9a96 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionResult} from "../../DecryptionResult.js"; +import {DecryptionResult} from "../../DecryptionResult"; import {DecryptionError} from "../../common.js"; import {ReplayDetectionEntry} from "./ReplayDetectionEntry"; import type {RoomKey} from "./RoomKey"; From 74c640f9375d59f84ca450d4635c77b2829be94b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:20:49 +0100 Subject: [PATCH 07/69] convert Session --- src/matrix/e2ee/olm/Encryption.js | 2 +- .../e2ee/olm/{Session.js => Session.ts} | 35 +++++++++++-------- .../storage/idb/stores/OlmSessionStore.ts | 18 +++++----- 3 files changed, 31 insertions(+), 24 deletions(-) rename src/matrix/e2ee/olm/{Session.js => Session.ts} (53%) diff --git a/src/matrix/e2ee/olm/Encryption.js b/src/matrix/e2ee/olm/Encryption.js index 652c657c..3e78470d 100644 --- a/src/matrix/e2ee/olm/Encryption.js +++ b/src/matrix/e2ee/olm/Encryption.js @@ -16,7 +16,7 @@ limitations under the License. import {groupByWithCreator} from "../../../utils/groupBy"; import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js"; -import {createSessionEntry} from "./Session.js"; +import {createSessionEntry} from "./Session"; function findFirstSessionId(sessionIds) { return sessionIds.reduce((first, sessionId) => { diff --git a/src/matrix/e2ee/olm/Session.js b/src/matrix/e2ee/olm/Session.ts similarity index 53% rename from src/matrix/e2ee/olm/Session.js rename to src/matrix/e2ee/olm/Session.ts index 9b5f4db0..f97c8478 100644 --- a/src/matrix/e2ee/olm/Session.js +++ b/src/matrix/e2ee/olm/Session.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey) { +import type {OlmSessionEntry} from "../../storage/idb/stores/OlmSessionStore"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +export function createSessionEntry(olmSession: Olm.Session, senderKey: string, timestamp: number, pickleKey: string): OlmSessionEntry { return { session: olmSession.pickle(pickleKey), sessionId: olmSession.session_id(), @@ -24,35 +28,38 @@ export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey) } export class Session { - constructor(data, pickleKey, olm, isNew = false) { - this.data = data; - this._olm = olm; - this._pickleKey = pickleKey; - this.isNew = isNew; + public isModified: boolean; + + constructor( + public readonly data: OlmSessionEntry, + private readonly pickleKey: string, + private readonly olm: Olm, + public isNew: boolean = false + ) { this.isModified = isNew; } - static create(senderKey, olmSession, olm, pickleKey, timestamp) { + static create(senderKey: string, olmSession: Olm.Session, olm: Olm, pickleKey: string, timestamp: number): Session { const data = createSessionEntry(olmSession, senderKey, timestamp, pickleKey); return new Session(data, pickleKey, olm, true); } - get id() { + get id(): string { return this.data.sessionId; } - load() { - const session = new this._olm.Session(); - session.unpickle(this._pickleKey, this.data.session); + load(): Olm.Session { + const session = new this.olm.Session(); + session.unpickle(this.pickleKey, this.data.session); return session; } - unload(olmSession) { + unload(olmSession: Olm.Session): void { olmSession.free(); } - save(olmSession) { - this.data.session = olmSession.pickle(this._pickleKey); + save(olmSession: Olm.Session): void { + this.data.session = olmSession.pickle(this.pickleKey); this.isModified = true; } } diff --git a/src/matrix/storage/idb/stores/OlmSessionStore.ts b/src/matrix/storage/idb/stores/OlmSessionStore.ts index d5a79de2..1263a649 100644 --- a/src/matrix/storage/idb/stores/OlmSessionStore.ts +++ b/src/matrix/storage/idb/stores/OlmSessionStore.ts @@ -24,19 +24,19 @@ function decodeKey(key: string): { senderKey: string, sessionId: string } { return {senderKey, sessionId}; } -interface OlmSession { +export type OlmSessionEntry = { session: string; sessionId: string; senderKey: string; lastUsed: number; } -type OlmSessionEntry = OlmSession & { key: string }; +type OlmSessionStoredEntry = OlmSessionEntry & { key: string }; export class OlmSessionStore { - private _store: Store; + private _store: Store; - constructor(store: Store) { + constructor(store: Store) { this._store = store; } @@ -55,20 +55,20 @@ export class OlmSessionStore { return sessionIds; } - getAll(senderKey: string): Promise { + getAll(senderKey: string): Promise { const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); return this._store.selectWhile(range, session => { return session.senderKey === senderKey; }); } - get(senderKey: string, sessionId: string): Promise { + get(senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(senderKey, sessionId)); } - set(session: OlmSession): void { - (session as OlmSessionEntry).key = encodeKey(session.senderKey, session.sessionId); - this._store.put(session as OlmSessionEntry); + set(session: OlmSessionEntry): void { + (session as OlmSessionStoredEntry).key = encodeKey(session.senderKey, session.sessionId); + this._store.put(session as OlmSessionStoredEntry); } remove(senderKey: string, sessionId: string): void { From a4fd1615ddbc0472e02825eb9a3fe3185a225c0c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Feb 2022 18:21:29 +0100 Subject: [PATCH 08/69] convert decryption --- src/matrix/Session.js | 18 +- .../e2ee/olm/{Decryption.js => Decryption.ts} | 161 ++++++++++-------- src/matrix/e2ee/olm/types.ts | 43 +++++ src/utils/Lock.ts | 8 +- 4 files changed, 150 insertions(+), 80 deletions(-) rename src/matrix/e2ee/olm/{Decryption.js => Decryption.ts} (70%) create mode 100644 src/matrix/e2ee/olm/types.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 83a2df02..72d8a313 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -26,7 +26,7 @@ import {User} from "./User.js"; import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Account as E2EEAccount} from "./e2ee/Account.js"; import {uploadAccountAsDehydratedDevice} from "./e2ee/Dehydration.js"; -import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; +import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; @@ -123,15 +123,15 @@ export class Session { // TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account // and can create RoomEncryption objects and handle encrypted to_device messages and device list changes. const senderKeyLock = new LockMap(); - const olmDecryption = new OlmDecryption({ - account: this._e2eeAccount, - pickleKey: PICKLE_KEY, - olm: this._olm, - storage: this._storage, - now: this._platform.clock.now, - ownUserId: this._user.id, + const olmDecryption = new OlmDecryption( + this._e2eeAccount, + PICKLE_KEY, + this._olm, + this._storage, + this._platform.clock.now, + this._user.id, senderKeyLock - }); + ); this._olmEncryption = new OlmEncryption({ account: this._e2eeAccount, pickleKey: PICKLE_KEY, diff --git a/src/matrix/e2ee/olm/Decryption.js b/src/matrix/e2ee/olm/Decryption.ts similarity index 70% rename from src/matrix/e2ee/olm/Decryption.js rename to src/matrix/e2ee/olm/Decryption.ts index 16e617a5..0fd4f0f9 100644 --- a/src/matrix/e2ee/olm/Decryption.js +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -16,32 +16,52 @@ limitations under the License. import {DecryptionError} from "../common.js"; import {groupBy} from "../../../utils/groupBy"; -import {MultiLock} from "../../../utils/Lock"; +import {MultiLock, ILock} from "../../../utils/Lock"; import {Session} from "./Session.js"; -import {DecryptionResult} from "../DecryptionResult.js"; +import {DecryptionResult} from "../DecryptionResult"; + +import type {OlmMessage, OlmPayload} from "./types"; +import type {Account} from "../Account"; +import type {LockMap} from "../../../utils/LockMap"; +import type {Storage} from "../../storage/idb/Storage"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type {OlmEncryptedEvent} from "./types"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; const SESSION_LIMIT_PER_SENDER_KEY = 4; -function isPreKeyMessage(message) { +type DecryptionResults = { + results: DecryptionResult[], + errors: DecryptionError[], + senderKeyDecryption: SenderKeyDecryption +}; + +type CreateAndDecryptResult = { + session: Session, + plaintext: string +}; + +function isPreKeyMessage(message: OlmMessage): boolean { return message.type === 0; } -function sortSessions(sessions) { +function sortSessions(sessions: Session[]) { sessions.sort((a, b) => { return b.data.lastUsed - a.data.lastUsed; }); } export class Decryption { - constructor({account, pickleKey, now, ownUserId, storage, olm, senderKeyLock}) { - this._account = account; - this._pickleKey = pickleKey; - this._now = now; - this._ownUserId = ownUserId; - this._storage = storage; - this._olm = olm; - this._senderKeyLock = senderKeyLock; - } + constructor( + private readonly account: Account, + private readonly pickleKey: string, + private readonly now: () => number, + private readonly ownUserId: string, + private readonly storage: Storage, + private readonly olm: Olm, + private readonly senderKeyLock: LockMap + ) {} // we need to lock because both encryption and decryption can't be done in one txn, // so for them not to step on each other toes, we need to lock. @@ -50,8 +70,8 @@ export class Decryption { // - decryptAll below fails (to release the lock as early as we can) // - DecryptionChanges.write succeeds // - Sync finishes the writeSync phase (or an error was thrown, in case we never get to DecryptionChanges.write) - async obtainDecryptionLock(events) { - const senderKeys = new Set(); + async obtainDecryptionLock(events: OlmEncryptedEvent[]): Promise { + const senderKeys = new Set(); for (const event of events) { const senderKey = event.content?.["sender_key"]; if (senderKey) { @@ -61,7 +81,7 @@ export class Decryption { // take a lock on all senderKeys so encryption or other calls to decryptAll (should not happen) // don't modify the sessions at the same time const locks = await Promise.all(Array.from(senderKeys).map(senderKey => { - return this._senderKeyLock.takeLock(senderKey); + return this.senderKeyLock.takeLock(senderKey); })); return new MultiLock(locks); } @@ -83,18 +103,18 @@ export class Decryption { * @param {[type]} events * @return {Promise} [description] */ - async decryptAll(events, lock, txn) { + async decryptAll(events: OlmEncryptedEvent[], lock: ILock, txn: Transaction): Promise { try { - const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]); - const timestamp = this._now(); + const eventsPerSenderKey = groupBy(events, (event: OlmEncryptedEvent) => event.content?.["sender_key"]); + const timestamp = this.now(); // decrypt events for different sender keys in parallel const senderKeyOperations = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => { - return this._decryptAllForSenderKey(senderKey, events, timestamp, txn); + return this._decryptAllForSenderKey(senderKey!, events, timestamp, txn); })); - const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []); - const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []); + const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), [] as DecryptionResult[]); + const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), [] as DecryptionError[]); const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption); - return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, lock); + return new DecryptionChanges(senderKeyDecryptions, results, errors, this.account, lock); } catch (err) { // make sure the locks are release if something throws // otherwise they will be released in DecryptionChanges after having written @@ -104,11 +124,11 @@ export class Decryption { } } - async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) { + async _decryptAllForSenderKey(senderKey: string, events: OlmEncryptedEvent[], timestamp: number, readSessionsTxn: Transaction): Promise { const sessions = await this._getSessions(senderKey, readSessionsTxn); - const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp); - const results = []; - const errors = []; + const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this.olm, timestamp); + const results: DecryptionResult[] = []; + const errors: DecryptionError[] = []; // events for a single senderKey need to be decrypted one by one for (const event of events) { try { @@ -121,10 +141,10 @@ export class Decryption { return {results, errors, senderKeyDecryption}; } - _decryptForSenderKey(senderKeyDecryption, event, timestamp) { + _decryptForSenderKey(senderKeyDecryption: SenderKeyDecryption, event: OlmEncryptedEvent, timestamp: number): DecryptionResult { const senderKey = senderKeyDecryption.senderKey; const message = this._getMessageAndValidateEvent(event); - let plaintext; + let plaintext: string | undefined; try { plaintext = senderKeyDecryption.decrypt(message); } catch (err) { @@ -133,7 +153,7 @@ export class Decryption { } // could not decrypt with any existing session if (typeof plaintext !== "string" && isPreKeyMessage(message)) { - let createResult; + let createResult: CreateAndDecryptResult; try { createResult = this._createSessionAndDecrypt(senderKey, message, timestamp); } catch (error) { @@ -143,14 +163,14 @@ export class Decryption { plaintext = createResult.plaintext; } if (typeof plaintext === "string") { - let payload; + let payload: OlmPayload; try { payload = JSON.parse(plaintext); } catch (error) { throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, error}); } this._validatePayload(payload, event); - return new DecryptionResult(payload, senderKey, payload.keys.ed25519); + return new DecryptionResult(payload, senderKey, payload.keys!.ed25519!); } else { throw new DecryptionError("OLM_NO_MATCHING_SESSION", event, {knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)}); @@ -158,16 +178,16 @@ export class Decryption { } // only for pre-key messages after having attempted decryption with existing sessions - _createSessionAndDecrypt(senderKey, message, timestamp) { + _createSessionAndDecrypt(senderKey: string, message: OlmMessage, timestamp: number): CreateAndDecryptResult { let plaintext; // if we have multiple messages encrypted with the same new session, // this could create multiple sessions as the OTK isn't removed yet // (this only happens in DecryptionChanges.write) // This should be ok though as we'll first try to decrypt with the new session - const olmSession = this._account.createInboundOlmSession(senderKey, message.body); + const olmSession = this.account.createInboundOlmSession(senderKey, message.body); try { plaintext = olmSession.decrypt(message.type, message.body); - const session = Session.create(senderKey, olmSession, this._olm, this._pickleKey, timestamp); + const session = Session.create(senderKey, olmSession, this.olm, this.pickleKey, timestamp); session.unload(olmSession); return {session, plaintext}; } catch (err) { @@ -176,12 +196,12 @@ export class Decryption { } } - _getMessageAndValidateEvent(event) { + _getMessageAndValidateEvent(event: OlmEncryptedEvent): OlmMessage { const ciphertext = event.content?.ciphertext; if (!ciphertext) { throw new DecryptionError("OLM_MISSING_CIPHERTEXT", event); } - const message = ciphertext?.[this._account.identityKeys.curve25519]; + const message = ciphertext?.[this.account.identityKeys.curve25519]; if (!message) { throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", event); } @@ -189,22 +209,22 @@ export class Decryption { return message; } - async _getSessions(senderKey, txn) { + async _getSessions(senderKey: string, txn: Transaction): Promise { const sessionEntries = await txn.olmSessions.getAll(senderKey); // sort most recent used sessions first - const sessions = sessionEntries.map(s => new Session(s, this._pickleKey, this._olm)); + const sessions = sessionEntries.map(s => new Session(s, this.pickleKey, this.olm)); sortSessions(sessions); return sessions; } - _validatePayload(payload, event) { + _validatePayload(payload: OlmPayload, event: OlmEncryptedEvent): void { if (payload.sender !== event.sender) { throw new DecryptionError("OLM_FORWARDED_MESSAGE", event, {sentBy: event.sender, encryptedBy: payload.sender}); } - if (payload.recipient !== this._ownUserId) { + if (payload.recipient !== this.ownUserId) { throw new DecryptionError("OLM_BAD_RECIPIENT", event, {recipient: payload.recipient}); } - if (payload.recipient_keys?.ed25519 !== this._account.identityKeys.ed25519) { + if (payload.recipient_keys?.ed25519 !== this.account.identityKeys.ed25519) { throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", event, {key: payload.recipient_keys?.ed25519}); } // TODO: check room_id @@ -219,21 +239,21 @@ export class Decryption { // decryption helper for a single senderKey class SenderKeyDecryption { - constructor(senderKey, sessions, olm, timestamp) { - this.senderKey = senderKey; - this.sessions = sessions; - this._olm = olm; - this._timestamp = timestamp; - } + constructor( + public readonly senderKey: string, + public readonly sessions: Session[], + private readonly olm: Olm, + private readonly timestamp: number + ) {} - addNewSession(session) { + addNewSession(session: Session) { // add at top as it is most recent this.sessions.unshift(session); } - decrypt(message) { + decrypt(message: OlmMessage): string | undefined { for (const session of this.sessions) { - const plaintext = this._decryptWithSession(session, message); + const plaintext = this.decryptWithSession(session, message); if (typeof plaintext === "string") { // keep them sorted so will try the same session first for other messages // and so we can assume the excess ones are at the end @@ -244,11 +264,11 @@ class SenderKeyDecryption { } } - getModifiedSessions() { + getModifiedSessions(): Session[] { return this.sessions.filter(session => session.isModified); } - get hasNewSessions() { + get hasNewSessions(): boolean { return this.sessions.some(session => session.isNew); } @@ -257,7 +277,10 @@ class SenderKeyDecryption { // if this turns out to be a real cost for IE11, // we could look into adding a less expensive serialization mechanism // for olm sessions to libolm - _decryptWithSession(session, message) { + private decryptWithSession(session: Session, message: OlmMessage): string | undefined { + if (message.type === undefined || message.body === undefined) { + throw new Error("Invalid message without type or body"); + } const olmSession = session.load(); try { if (isPreKeyMessage(message) && !olmSession.matches_inbound(message.body)) { @@ -266,7 +289,7 @@ class SenderKeyDecryption { try { const plaintext = olmSession.decrypt(message.type, message.body); session.save(olmSession); - session.lastUsed = this._timestamp; + session.data.lastUsed = this.timestamp; return plaintext; } catch (err) { if (isPreKeyMessage(message)) { @@ -286,27 +309,27 @@ class SenderKeyDecryption { * @property {Array} errors see DecryptionError.event to retrieve the event that failed to decrypt. */ class DecryptionChanges { - constructor(senderKeyDecryptions, results, errors, account, lock) { - this._senderKeyDecryptions = senderKeyDecryptions; - this._account = account; - this.results = results; - this.errors = errors; - this._lock = lock; + constructor( + private readonly senderKeyDecryptions: SenderKeyDecryption[], + private readonly results: DecryptionResult[], + private readonly errors: DecryptionError[], + private readonly account: Account, + private readonly lock: ILock + ) {} + + get hasNewSessions(): boolean { + return this.senderKeyDecryptions.some(skd => skd.hasNewSessions); } - get hasNewSessions() { - return this._senderKeyDecryptions.some(skd => skd.hasNewSessions); - } - - write(txn) { + write(txn: Transaction): void { try { - for (const senderKeyDecryption of this._senderKeyDecryptions) { + for (const senderKeyDecryption of this.senderKeyDecryptions) { for (const session of senderKeyDecryption.getModifiedSessions()) { txn.olmSessions.set(session.data); if (session.isNew) { const olmSession = session.load(); try { - this._account.writeRemoveOneTimeKey(olmSession, txn); + this.account.writeRemoveOneTimeKey(olmSession, txn); } finally { session.unload(olmSession); } @@ -322,7 +345,7 @@ class DecryptionChanges { } } } finally { - this._lock.release(); + this.lock.release(); } } } diff --git a/src/matrix/e2ee/olm/types.ts b/src/matrix/e2ee/olm/types.ts new file mode 100644 index 00000000..b9e394d5 --- /dev/null +++ b/src/matrix/e2ee/olm/types.ts @@ -0,0 +1,43 @@ +/* +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 OlmMessage = { + type?: 0 | 1, + body?: string +} + +export type OlmEncryptedMessageContent = { + algorithm?: "m.olm.v1.curve25519-aes-sha2" + sender_key?: string, + ciphertext?: { + [deviceCurve25519Key: string]: OlmMessage + } +} + +export type OlmEncryptedEvent = { + type?: "m.room.encrypted", + content?: OlmEncryptedMessageContent + sender?: string +} + +export type OlmPayload = { + type?: string; + content?: Record; + sender?: string; + recipient?: string; + recipient_keys?: {ed25519?: string}; + keys?: {ed25519?: string}; +} diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index 238d88f9..ff623eba 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export class Lock { +export interface ILock { + release(): void; +} + +export class Lock implements ILock { private _promise?: Promise; private _resolve?: (() => void); @@ -52,7 +56,7 @@ export class Lock { } } -export class MultiLock { +export class MultiLock implements ILock { constructor(public readonly locks: Lock[]) { } From c40801efd991ba93ae09139d354a90398006a447 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 12:33:24 +0530 Subject: [PATCH 09/69] Implement the registration stage --- src/matrix/registration/stages/TokenAuth.ts | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/matrix/registration/stages/TokenAuth.ts diff --git a/src/matrix/registration/stages/TokenAuth.ts b/src/matrix/registration/stages/TokenAuth.ts new file mode 100644 index 00000000..6f1a3335 --- /dev/null +++ b/src/matrix/registration/stages/TokenAuth.ts @@ -0,0 +1,43 @@ +/* +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 {AuthenticationData} from "../types"; +import {BaseRegistrationStage} from "./BaseRegistrationStage"; + +export const TOKEN_AUTH_TYPE = "org.matrix.msc3231.login.registration_token"; + +export class TokenAuth extends BaseRegistrationStage { + private _token?: string; + + generateAuthenticationData(): AuthenticationData { + if (!this._token) { + throw new Error("No token provided for TokenAuth"); + } + return { + session: this._session, + type: this.type, + token: this._token, + }; + } + + setToken(token: string) { + this._token = token; + } + + get type(): string { + return TOKEN_AUTH_TYPE; + } +} From ed151c8567b175b2c785710b299bc7a618e7c3a5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 12:33:59 +0530 Subject: [PATCH 10/69] Return token stage from createRegistrationStage --- src/matrix/registration/Registration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index c9c9af87..0d342639 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -18,6 +18,7 @@ import type {HomeServerApi} from "../net/HomeServerApi"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; +import {TokenAuth, TOKEN_AUTH_TYPE} from "./stages/TokenAuth"; import type { AccountDetails, RegistrationFlow, @@ -108,6 +109,8 @@ export class Registration { return new DummyAuth(session, params?.[type]); case "m.login.terms": return new TermsAuth(session, params?.[type]); + case TOKEN_AUTH_TYPE: + return new TokenAuth(session, params?.[type]); default: throw new Error(`Unknown stage: ${type}`); } From 60bc4450f3c6d26bf4f2e9f17a4b6210f59d98d0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 13:21:04 +0530 Subject: [PATCH 11/69] Use type from server --- src/matrix/registration/Registration.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 0d342639..8bfce8ad 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -18,7 +18,7 @@ import type {HomeServerApi} from "../net/HomeServerApi"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; -import {TokenAuth, TOKEN_AUTH_TYPE} from "./stages/TokenAuth"; +import {TokenAuth} from "./stages/TokenAuth"; import type { AccountDetails, RegistrationFlow, @@ -94,7 +94,9 @@ export class Registration { this._sessionInfo = response; return undefined; case 401: - if (response.completed?.includes(currentStage.type)) { + // Support unstable prefix for TokenAuth + const typeFromServer = (currentStage as TokenAuth).typeFromServer; + if (response.completed?.includes(typeFromServer ?? currentStage.type)) { return currentStage.nextStage; } else { @@ -109,8 +111,9 @@ export class Registration { return new DummyAuth(session, params?.[type]); case "m.login.terms": return new TermsAuth(session, params?.[type]); - case TOKEN_AUTH_TYPE: - return new TokenAuth(session, params?.[type]); + case "org.matrix.msc3231.login.registration_token": + case "m.login.registration_token": + return new TokenAuth(session, params?.[type], type); default: throw new Error(`Unknown stage: ${type}`); } From a76bcd1739cd87770d716cdfdc0e317af970c1ac Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 13:36:24 +0530 Subject: [PATCH 12/69] Changes in TokenAuth --- src/matrix/registration/stages/TokenAuth.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/matrix/registration/stages/TokenAuth.ts b/src/matrix/registration/stages/TokenAuth.ts index 6f1a3335..607ee910 100644 --- a/src/matrix/registration/stages/TokenAuth.ts +++ b/src/matrix/registration/stages/TokenAuth.ts @@ -14,13 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {AuthenticationData} from "../types"; +import {AuthenticationData, RegistrationParams} from "../types"; import {BaseRegistrationStage} from "./BaseRegistrationStage"; -export const TOKEN_AUTH_TYPE = "org.matrix.msc3231.login.registration_token"; - export class TokenAuth extends BaseRegistrationStage { private _token?: string; + private readonly _type: string; + + constructor(session: string, params: RegistrationParams | undefined, type: string) { + super(session, params); + this._type = type; + } + generateAuthenticationData(): AuthenticationData { if (!this._token) { @@ -28,7 +33,7 @@ export class TokenAuth extends BaseRegistrationStage { } return { session: this._session, - type: this.type, + type: this._type, token: this._token, }; } @@ -38,6 +43,10 @@ export class TokenAuth extends BaseRegistrationStage { } get type(): string { - return TOKEN_AUTH_TYPE; + return "m.login.registration_token"; + } + + get typeFromServer(): string { + return this._type; } } From 7a9298328f3ac0b2348177f6919c1bc1018deca1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 16 Feb 2022 14:37:18 +0530 Subject: [PATCH 13/69] Return _type from getter --- src/matrix/registration/Registration.ts | 4 +--- src/matrix/registration/stages/TokenAuth.ts | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 8bfce8ad..ded66719 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -94,9 +94,7 @@ export class Registration { this._sessionInfo = response; return undefined; case 401: - // Support unstable prefix for TokenAuth - const typeFromServer = (currentStage as TokenAuth).typeFromServer; - if (response.completed?.includes(typeFromServer ?? currentStage.type)) { + if (response.completed?.includes(currentStage.type)) { return currentStage.nextStage; } else { diff --git a/src/matrix/registration/stages/TokenAuth.ts b/src/matrix/registration/stages/TokenAuth.ts index 607ee910..cb238bcb 100644 --- a/src/matrix/registration/stages/TokenAuth.ts +++ b/src/matrix/registration/stages/TokenAuth.ts @@ -43,10 +43,6 @@ export class TokenAuth extends BaseRegistrationStage { } get type(): string { - return "m.login.registration_token"; - } - - get typeFromServer(): string { return this._type; } } From 61b264be3b244df6e13b4c4ea9b428ddddc2cf3a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 10:20:53 +0100 Subject: [PATCH 14/69] bump sdk version to 0.0.7 --- 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 3e468181..b9e4ed5c 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.6", + "version": "0.0.7", "main": "./hydrogen.es.js", "type": "module" } From eb5ca200f2559a29545e1c3390960bc723317baf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 18:00:03 +0100 Subject: [PATCH 15/69] missed rename here --- src/matrix/e2ee/olm/Decryption.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 0fd4f0f9..7d9be4a3 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -17,7 +17,7 @@ limitations under the License. import {DecryptionError} from "../common.js"; import {groupBy} from "../../../utils/groupBy"; import {MultiLock, ILock} from "../../../utils/Lock"; -import {Session} from "./Session.js"; +import {Session} from "./Session"; import {DecryptionResult} from "../DecryptionResult"; import type {OlmMessage, OlmPayload} from "./types"; @@ -246,7 +246,7 @@ class SenderKeyDecryption { private readonly timestamp: number ) {} - addNewSession(session: Session) { + addNewSession(session: Session): void { // add at top as it is most recent this.sessions.unshift(session); } From e3e90ed1671c247f623ceba95d1ff58d7bcf6f01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 18:00:13 +0100 Subject: [PATCH 16/69] convert olm/Encryption to TS --- src/matrix/Session.js | 20 +-- .../e2ee/olm/{Encryption.js => Encryption.ts} | 141 +++++++++++------- 2 files changed, 94 insertions(+), 67 deletions(-) rename src/matrix/e2ee/olm/{Encryption.js => Encryption.ts} (64%) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 72d8a313..8652a1d7 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -27,7 +27,7 @@ import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {Account as E2EEAccount} from "./e2ee/Account.js"; import {uploadAccountAsDehydratedDevice} from "./e2ee/Dehydration.js"; import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption"; -import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; +import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; @@ -132,16 +132,16 @@ export class Session { this._user.id, senderKeyLock ); - this._olmEncryption = new OlmEncryption({ - account: this._e2eeAccount, - pickleKey: PICKLE_KEY, - olm: this._olm, - storage: this._storage, - now: this._platform.clock.now, - ownUserId: this._user.id, - olmUtil: this._olmUtil, + this._olmEncryption = new OlmEncryption( + this._e2eeAccount, + PICKLE_KEY, + this._olm, + this._storage, + this._platform.clock.now, + this._user.id, + this._olmUtil, senderKeyLock - }); + ); this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmEncryption = new MegOlmEncryption({ account: this._e2eeAccount, diff --git a/src/matrix/e2ee/olm/Encryption.js b/src/matrix/e2ee/olm/Encryption.ts similarity index 64% rename from src/matrix/e2ee/olm/Encryption.js rename to src/matrix/e2ee/olm/Encryption.ts index 3e78470d..ebc38170 100644 --- a/src/matrix/e2ee/olm/Encryption.js +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -18,6 +18,32 @@ import {groupByWithCreator} from "../../../utils/groupBy"; import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js"; import {createSessionEntry} from "./Session"; +import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types"; +import type {Account} from "../Account"; +import type {LockMap} from "../../../utils/LockMap"; +import type {Storage} from "../../storage/idb/Storage"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {ILogItem} from "../../../logging/types"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +type ClaimedOTKResponse = { + [userId: string]: { + [deviceId: string]: { + [algorithmAndOtk: string]: { + key: string, + signatures: { + [userId: string]: { + [algorithmAndDevice: string]: string + } + } + } + } + } +}; + function findFirstSessionId(sessionIds) { return sessionIds.reduce((first, sessionId) => { if (!first || sessionId < first) { @@ -36,19 +62,19 @@ const OTK_ALGORITHM = "signed_curve25519"; const MAX_BATCH_SIZE = 20; export class Encryption { - constructor({account, olm, olmUtil, ownUserId, storage, now, pickleKey, senderKeyLock}) { - this._account = account; - this._olm = olm; - this._olmUtil = olmUtil; - this._ownUserId = ownUserId; - this._storage = storage; - this._now = now; - this._pickleKey = pickleKey; - this._senderKeyLock = senderKeyLock; - } + constructor( + private readonly account: Account, + private readonly olm: Olm, + private readonly olmUtil: Olm.Utility, + private readonly ownUserId: string, + private readonly storage: Storage, + private readonly now: () => number, + private readonly pickleKey: string, + private readonly senderKeyLock: LockMap + ) {} - async encrypt(type, content, devices, hsApi, log) { - let messages = []; + async encrypt(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { + let messages: EncryptedMessage[] = []; for (let i = 0; i < devices.length ; i += MAX_BATCH_SIZE) { const batchDevices = devices.slice(i, i + MAX_BATCH_SIZE); const batchMessages = await this._encryptForMaxDevices(type, content, batchDevices, hsApi, log); @@ -57,12 +83,12 @@ export class Encryption { return messages; } - async _encryptForMaxDevices(type, content, devices, hsApi, log) { + async _encryptForMaxDevices(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { // TODO: see if we can only hold some of the locks until after the /keys/claim call (if needed) // take a lock on all senderKeys so decryption and other calls to encrypt (should not happen) // don't modify the sessions at the same time const locks = await Promise.all(devices.map(device => { - return this._senderKeyLock.takeLock(device.curve25519Key); + return this.senderKeyLock.takeLock(device.curve25519Key); })); try { const { @@ -70,9 +96,9 @@ export class Encryption { existingEncryptionTargets, } = await this._findExistingSessions(devices); - const timestamp = this._now(); + const timestamp = this.now(); - let encryptionTargets = []; + let encryptionTargets: EncryptionTarget[] = []; try { if (devicesWithoutSession.length) { const newEncryptionTargets = await log.wrap("create sessions", log => this._createNewSessions( @@ -100,8 +126,8 @@ export class Encryption { } } - async _findExistingSessions(devices) { - const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); + async _findExistingSessions(devices: DeviceIdentity[]): Promise<{devicesWithoutSession: DeviceIdentity[], existingEncryptionTargets: EncryptionTarget[]}> { + const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]); const sessionIdsForDevice = await Promise.all(devices.map(async device => { return await txn.olmSessions.getSessionIds(device.curve25519Key); })); @@ -116,18 +142,18 @@ export class Encryption { const sessionId = findFirstSessionId(sessionIds); return EncryptionTarget.fromSessionId(device, sessionId); } - }).filter(target => !!target); + }).filter(target => !!target) as EncryptionTarget[]; return {devicesWithoutSession, existingEncryptionTargets}; } - _encryptForDevice(type, content, target) { + _encryptForDevice(type: string, content: Record, target: EncryptionTarget): OlmEncryptedMessageContent { const {session, device} = target; const plaintext = JSON.stringify(this._buildPlainTextMessageForDevice(type, content, device)); - const message = session.encrypt(plaintext); + const message = session!.encrypt(plaintext); const encryptedContent = { algorithm: OLM_ALGORITHM, - sender_key: this._account.identityKeys.curve25519, + sender_key: this.account.identityKeys.curve25519, ciphertext: { [device.curve25519Key]: message } @@ -135,27 +161,27 @@ export class Encryption { return encryptedContent; } - _buildPlainTextMessageForDevice(type, content, device) { + _buildPlainTextMessageForDevice(type: string, content: Record, device: DeviceIdentity): OlmPayload { return { keys: { - "ed25519": this._account.identityKeys.ed25519 + "ed25519": this.account.identityKeys.ed25519 }, recipient_keys: { "ed25519": device.ed25519Key }, recipient: device.userId, - sender: this._ownUserId, + sender: this.ownUserId, content, type } } - async _createNewSessions(devicesWithoutSession, hsApi, timestamp, log) { + async _createNewSessions(devicesWithoutSession: DeviceIdentity[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise { const newEncryptionTargets = await log.wrap("claim", log => this._claimOneTimeKeys(hsApi, devicesWithoutSession, log)); try { for (const target of newEncryptionTargets) { const {device, oneTimeKey} = target; - target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); + target.session = await this.account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); } await this._storeSessions(newEncryptionTargets, timestamp); } catch (err) { @@ -167,12 +193,12 @@ export class Encryption { return newEncryptionTargets; } - async _claimOneTimeKeys(hsApi, deviceIdentities, log) { + async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceIdentity[], log: ILogItem): Promise { // create a Map> const devicesByUser = groupByWithCreator(deviceIdentities, - device => device.userId, - () => new Map(), - (deviceMap, device) => deviceMap.set(device.deviceId, device) + (device: DeviceIdentity) => device.userId, + (): Map => new Map(), + (deviceMap: Map, device: DeviceIdentity) => deviceMap.set(device.deviceId, device) ); const oneTimeKeys = Array.from(devicesByUser.entries()).reduce((usersObj, [userId, deviceMap]) => { usersObj[userId] = Array.from(deviceMap.values()).reduce((devicesObj, device) => { @@ -188,12 +214,12 @@ export class Encryption { if (Object.keys(claimResponse.failures).length) { log.log({l: "failures", servers: Object.keys(claimResponse.failures)}, log.level.Warn); } - const userKeyMap = claimResponse?.["one_time_keys"]; + const userKeyMap = claimResponse?.["one_time_keys"] as ClaimedOTKResponse; return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log); } - _verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log) { - const verifiedEncryptionTargets = []; + _verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map>, log: ILogItem): EncryptionTarget[] { + const verifiedEncryptionTargets: EncryptionTarget[] = []; for (const [userId, userSection] of Object.entries(userKeyMap)) { for (const [deviceId, deviceSection] of Object.entries(userSection)) { const [firstPropName, keySection] = Object.entries(deviceSection)[0]; @@ -202,7 +228,7 @@ export class Encryption { const device = devicesByUser.get(userId)?.get(deviceId); if (device) { const isValidSignature = verifyEd25519Signature( - this._olmUtil, userId, deviceId, device.ed25519Key, keySection, log); + this.olmUtil, userId, deviceId, device.ed25519Key, keySection, log); if (isValidSignature) { const target = EncryptionTarget.fromOTK(device, keySection.key); verifiedEncryptionTargets.push(target); @@ -214,8 +240,8 @@ export class Encryption { return verifiedEncryptionTargets; } - async _loadSessions(encryptionTargets) { - const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); + async _loadSessions(encryptionTargets: EncryptionTarget[]): Promise { + const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]); // given we run loading in parallel, there might still be some // storage requests that will finish later once one has failed. // those should not allocate a session anymore. @@ -223,10 +249,10 @@ export class Encryption { try { await Promise.all(encryptionTargets.map(async encryptionTarget => { const sessionEntry = await txn.olmSessions.get( - encryptionTarget.device.curve25519Key, encryptionTarget.sessionId); + encryptionTarget.device.curve25519Key, encryptionTarget.sessionId!); if (sessionEntry && !failed) { - const olmSession = new this._olm.Session(); - olmSession.unpickle(this._pickleKey, sessionEntry.session); + const olmSession = new this.olm.Session(); + olmSession.unpickle(this.pickleKey, sessionEntry.session); encryptionTarget.session = olmSession; } })); @@ -240,12 +266,12 @@ export class Encryption { } } - async _storeSessions(encryptionTargets, timestamp) { - const txn = await this._storage.readWriteTxn([this._storage.storeNames.olmSessions]); + async _storeSessions(encryptionTargets: EncryptionTarget[], timestamp: number): Promise { + const txn = await this.storage.readWriteTxn([this.storage.storeNames.olmSessions]); try { for (const target of encryptionTargets) { const sessionEntry = createSessionEntry( - target.session, target.device.curve25519Key, timestamp, this._pickleKey); + target.session!, target.device.curve25519Key, timestamp, this.pickleKey); txn.olmSessions.set(sessionEntry); } } catch (err) { @@ -261,23 +287,24 @@ export class Encryption { // (and later converted to a session) in case of a new session // or an existing session class EncryptionTarget { - constructor(device, oneTimeKey, sessionId) { - this.device = device; - this.oneTimeKey = oneTimeKey; - this.sessionId = sessionId; - // an olmSession, should probably be called olmSession - this.session = null; - } + + public session: Olm.Session | null = null; - static fromOTK(device, oneTimeKey) { + constructor( + public readonly device: DeviceIdentity, + public readonly oneTimeKey: string | null, + public readonly sessionId: string | null + ) {} + + static fromOTK(device: DeviceIdentity, oneTimeKey: string): EncryptionTarget { return new EncryptionTarget(device, oneTimeKey, null); } - static fromSessionId(device, sessionId) { + static fromSessionId(device: DeviceIdentity, sessionId: string): EncryptionTarget { return new EncryptionTarget(device, null, sessionId); } - dispose() { + dispose(): void { if (this.session) { this.session.free(); } @@ -285,8 +312,8 @@ class EncryptionTarget { } class EncryptedMessage { - constructor(content, device) { - this.content = content; - this.device = device; - } + constructor( + public readonly content: OlmEncryptedMessageContent, + public readonly device: DeviceIdentity + ) {} } From 60f5da60bb17ffb90fa2d573feff88fbb9cc033e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 16 Feb 2022 18:01:24 +0100 Subject: [PATCH 17/69] fix lint --- src/matrix/room/timeline/Timeline.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c6852492..3332a5b0 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -24,7 +24,6 @@ import {RoomMember} from "../members/RoomMember.js"; import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; import {REDACTION_TYPE} from "../common"; import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; -import {DecryptionSource} from "../../e2ee/common.js"; import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js"; export class Timeline { From 498a43327f2d0b2bdd97203879a51d800ab32aa3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Feb 2022 11:30:04 +0530 Subject: [PATCH 18/69] Check if options exist in emitChange --- src/domain/ViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 99b23918..f3e141dc 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -110,7 +110,7 @@ export class ViewModel extends EventEmitter<{change } emitChange(changedProps: any): void { - if (this._options.emitChange) { + if (this._options?.emitChange) { this._options.emitChange(changedProps); } else { this.emit("change", changedProps); From 7f1fed6f8cf3e9479380fdfc8bbc4d1fbb016ffb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 09:24:18 +0100 Subject: [PATCH 19/69] always pass options to ViewModel constructor --- src/domain/AccountSetupViewModel.js | 8 ++++---- src/domain/SessionLoadViewModel.js | 2 +- src/domain/ViewModel.ts | 4 +++- src/domain/session/room/ComposerViewModel.js | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index 74d680d0..e7c1301f 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -19,9 +19,9 @@ import {KeyType} from "../matrix/ssss/index"; import {Status} from "./session/settings/KeyBackupViewModel.js"; export class AccountSetupViewModel extends ViewModel { - constructor(accountSetup) { - super(); - this._accountSetup = accountSetup; + constructor(options) { + super(options); + this._accountSetup = options.accountSetup; this._dehydratedDevice = undefined; this._decryptDehydratedDeviceViewModel = undefined; if (this._accountSetup.encryptedDehydratedDevice) { @@ -53,7 +53,7 @@ export class AccountSetupViewModel extends ViewModel { // this vm adopts the same shape as KeyBackupViewModel so the same view can be reused. class DecryptDehydratedDeviceViewModel extends ViewModel { constructor(accountSetupViewModel, decryptedCallback) { - super(); + super(accountSetupViewModel.options); this._accountSetupViewModel = accountSetupViewModel; this._isBusy = false; this._status = Status.SetupKey; diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index b23b54bc..abc16299 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -43,7 +43,7 @@ export class SessionLoadViewModel extends ViewModel { this.emitChange("loading"); this._waitHandle = this._client.loadStatus.waitFor(s => { if (s === LoadStatus.AccountSetup) { - this._accountSetupViewModel = new AccountSetupViewModel(this._client.accountSetup); + this._accountSetupViewModel = new AccountSetupViewModel(this.childOptions({accountSetup: this._client.accountSetup})); } else { this._accountSetupViewModel = undefined; } diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index f3e141dc..458b2840 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -51,6 +51,8 @@ export class ViewModel extends EventEmitter<{change return Object.assign({}, this._options, explicitOptions); } + get options(): O { return this._options; } + // makes it easier to pass through dependencies of a sub-view model getOption(name: N): O[N] { return this._options[name]; @@ -110,7 +112,7 @@ export class ViewModel extends EventEmitter<{change } emitChange(changedProps: any): void { - if (this._options?.emitChange) { + if (this._options.emitChange) { this._options.emitChange(changedProps); } else { this.emit("change", changedProps); diff --git a/src/domain/session/room/ComposerViewModel.js b/src/domain/session/room/ComposerViewModel.js index 833b17f5..c20f6e86 100644 --- a/src/domain/session/room/ComposerViewModel.js +++ b/src/domain/session/room/ComposerViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel"; export class ComposerViewModel extends ViewModel { constructor(roomVM) { - super(); + super(roomVM.options); this._roomVM = roomVM; this._isEmpty = true; this._replyVM = null; From 2472f11ec0e6839cc78672c7781e1dbfa6495939 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 09:47:57 +0100 Subject: [PATCH 20/69] export RoomStatus --- src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.ts b/src/lib.ts index cc88690c..89cd7706 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -16,6 +16,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 main view & view models export {createNavigation, createRouter} from "./domain/navigation/index.js"; export {RootViewModel} from "./domain/RootViewModel.js"; From ac48a5a4dfc00b1671a59d4f68f13488bff426a8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 10:10:23 +0100 Subject: [PATCH 21/69] bump SDK version to 0.0.8 --- 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 b9e4ed5c..8df205cb 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.7", + "version": "0.0.8", "main": "./hydrogen.es.js", "type": "module" } From cdd6112971bd9c70b3a2601c16a1895db460ba68 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 17:39:45 +0100 Subject: [PATCH 22/69] finish adapting contribution guide --- CONTRIBUTING.md | 51 +++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80f1fc6a..7a217d7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -Contributing code to matrix-js-sdk +Contributing code to hydrogen-web ================================== Everyone is welcome to contribute code to hydrogen-web, provided that they are @@ -20,13 +20,11 @@ We use GitHub's pull request workflow to review the contribution, and either ask you to make any refinements needed or merge it and make them ourselves. Things that should go into your PR description: - * Please disable any automatic formatting tools you may have active. - You'll be asked to undo any unrelated whitespace changes during code review. * References to any bugs fixed by the change (in GitHub's `Fixes` notation) * Describe the why and what is changing in the PR description so it's easy for onlookers and reviewers to onboard and context switch. - * Include both **before** and **after** screenshots to easily compare and discuss - what's changing. + * If your PR makes visual changes, include both **before** and **after** screenshots + to easily compare and discuss what's changing. * Include a step-by-step testing strategy so that a reviewer can check out the code locally and easily get to the point of testing your change. * Add comments to the diff for the reviewer that might help them to understand @@ -76,30 +74,34 @@ checks, so please check back after a few minutes. Tests ----- -If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement' -label) then we require that the PR also includes tests. These need to test that -your feature works as expected and ideally test edge cases too. For the js-sdk -itself, your tests should generally be unit tests. matrix-react-sdk also uses -these guidelines, so for that your tests can be unit tests using -react-test-utils, snapshot tests or screenshot tests. +If your PR is a feature then we require that the PR also includes tests. +These need to test that your feature works as expected and ideally test edge cases too. -We don't require tests for bug fixes (T-Defect) but strongly encourage regression -tests for the bug itself wherever possible. +Tests are written as unit tests by exporting a `tests` function from the file to be tested. +The function returns an object where the key is the test label, and the value is a +function that accepts an [assert](https://nodejs.org/api/assert.html) object, and return a Promise or nothing. -In the future we may formalise this more with a minimum test coverage -percentage for the diff. +Note that there is currently a limitation that files that are not indirectly included from `src/platform/web/main.js` won't be found by the runner. + +You can run the tests by running `yarn test`. +This uses the [impunity](https://github.com/bwindels/impunity) runner. + +We don't require tests for bug fixes. + +In the future we may formalise this more. Code style ---------- The js-sdk aims to target TypeScript/ES6. All new files should be written in TypeScript and existing files should use ES6 principles where possible. -Members should not be exported as a default export in general - it causes problems -with the architecture of the SDK (index file becomes less clear) and could -introduce naming problems (as default exports get aliased upon import). In -general, avoid using `export default`. +Please disable any automatic formatting tools you may have active. +If present, you'll be asked to undo any unrelated whitespace changes during code review. -The remaining code-style for matrix-js-sdk is not formally documented, but +Members should not be exported as a default export in general. +In general, avoid using `export default`. + +The remaining code-style for hydrogen is not formally documented, but contributors are encouraged to read the [code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md) and follow the principles set out there. @@ -110,13 +112,8 @@ makes it horribly hard to review otherwise. Attribution ----------- -Everyone who contributes anything to Matrix is welcome to be listed in the -AUTHORS.rst file for the project in question. Please feel free to include a -change to AUTHORS.rst in your pull request to list yourself and a short -description of the area(s) you've worked on. Also, we sometimes have swag to -give away to contributors - if you feel that Matrix-branded apparel is missing -from your life, please mail us your shipping address to matrix at matrix.org -and we'll try to fix it :) +If you change or create a file, feel free to add yourself to the copyright holders +in the license header of that file. Sign off -------- From 91fd0e433a40c75832b182eba7469bb4622db261 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 17 Feb 2022 17:44:44 +0100 Subject: [PATCH 23/69] remove changelog notes remainder --- CONTRIBUTING.md | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a217d7f..25d8e3c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,44 +30,6 @@ Things that should go into your PR description: * Add comments to the diff for the reviewer that might help them to understand why the change is necessary or how they might better understand and review it. -To add a longer, more detailed description of the change for the changelog: - - -*Fix llama herding bug* - -``` -Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase -``` - -*Remove outdated comment from `Ungulates.ts`* -``` -Notes: none -``` - -Sometimes, you're fixing a bug in a downstream project, in which case you want -an entry in that project's changelog. You can do that too: - -*Fix another herding bug* -``` -Notes: Fix a bug where the `herd()` function would only work on Tuesdays -element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays -``` - -If your PR introduces a breaking change, add the `X-Breaking-Change` label (see below) -and remember to tell the developer how to migrate: - -*Remove legacy class* - -``` -Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. -``` - -Other metadata can be added using labels. - * `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump. - -If you don't have permission to add labels, your PR reviewer(s) can work with you -to add them: ask in the PR description or comments. - We use continuous integration, and all pull requests get automatically tested: if your change breaks the build, then the PR will show that there are failed checks, so please check back after a few minutes. @@ -101,7 +63,7 @@ If present, you'll be asked to undo any unrelated whitespace changes during code Members should not be exported as a default export in general. In general, avoid using `export default`. -The remaining code-style for hydrogen is not formally documented, but +The remaining code-style for hydrogen is [in the process of being documented](codestyle.md), but contributors are encouraged to read the [code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md) and follow the principles set out there. From 347edb5988c7c133f7d1b08cb6e952251a659a0d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 16:47:47 +0100 Subject: [PATCH 24/69] remove unused storage property --- src/matrix/Session.js | 1 - src/matrix/e2ee/olm/Decryption.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 8652a1d7..ac060b0b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -127,7 +127,6 @@ export class Session { this._e2eeAccount, PICKLE_KEY, this._olm, - this._storage, this._platform.clock.now, this._user.id, senderKeyLock diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 7d9be4a3..77990586 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -58,7 +58,6 @@ export class Decryption { private readonly pickleKey: string, private readonly now: () => number, private readonly ownUserId: string, - private readonly storage: Storage, private readonly olm: Olm, private readonly senderKeyLock: LockMap ) {} From 78e0bb1ff0feac3f545ad0921ac8cd57fadd3f0e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:00:56 +0100 Subject: [PATCH 25/69] replace isPreKeyMessage with const enum --- src/matrix/e2ee/olm/Decryption.ts | 13 +++++-------- src/matrix/e2ee/olm/types.ts | 7 ++++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 77990586..9698add9 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -19,6 +19,7 @@ import {groupBy} from "../../../utils/groupBy"; import {MultiLock, ILock} from "../../../utils/Lock"; import {Session} from "./Session"; import {DecryptionResult} from "../DecryptionResult"; +import {OlmPayloadType} from "./types"; import type {OlmMessage, OlmPayload} from "./types"; import type {Account} from "../Account"; @@ -42,10 +43,6 @@ type CreateAndDecryptResult = { plaintext: string }; -function isPreKeyMessage(message: OlmMessage): boolean { - return message.type === 0; -} - function sortSessions(sessions: Session[]) { sessions.sort((a, b) => { return b.data.lastUsed - a.data.lastUsed; @@ -151,7 +148,7 @@ export class Decryption { throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", event, {senderKey, error: err.message}); } // could not decrypt with any existing session - if (typeof plaintext !== "string" && isPreKeyMessage(message)) { + if (typeof plaintext !== "string" && message.type === OlmPayloadType.PreKey) { let createResult: CreateAndDecryptResult; try { createResult = this._createSessionAndDecrypt(senderKey, message, timestamp); @@ -282,16 +279,16 @@ class SenderKeyDecryption { } const olmSession = session.load(); try { - if (isPreKeyMessage(message) && !olmSession.matches_inbound(message.body)) { + if (message.type === OlmPayloadType.PreKey && !olmSession.matches_inbound(message.body)) { return; } try { - const plaintext = olmSession.decrypt(message.type, message.body); + const plaintext = olmSession.decrypt(message.type as number, message.body!); session.save(olmSession); session.data.lastUsed = this.timestamp; return plaintext; } catch (err) { - if (isPreKeyMessage(message)) { + if (message.type === OlmPayloadType.PreKey) { throw new Error(`Error decrypting prekey message with existing session id ${session.id}: ${err.message}`); } // decryption failed, bail out diff --git a/src/matrix/e2ee/olm/types.ts b/src/matrix/e2ee/olm/types.ts index b9e394d5..5302dad8 100644 --- a/src/matrix/e2ee/olm/types.ts +++ b/src/matrix/e2ee/olm/types.ts @@ -14,8 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +export const enum OlmPayloadType { + PreKey = 0, + Normal = 1 +} + export type OlmMessage = { - type?: 0 | 1, + type?: OlmPayloadType, body?: string } From 620409b3f0bace8694ebd3698cf3dd9d4dfc351e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:10:26 +0100 Subject: [PATCH 26/69] fixup: ctor argument order as it was an object before, order didn't matter --- src/matrix/Session.js | 2 +- src/matrix/e2ee/olm/Encryption.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ac060b0b..ae1dea61 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -126,9 +126,9 @@ export class Session { const olmDecryption = new OlmDecryption( this._e2eeAccount, PICKLE_KEY, - this._olm, this._platform.clock.now, this._user.id, + this._olm, senderKeyLock ); this._olmEncryption = new OlmEncryption( diff --git a/src/matrix/e2ee/olm/Encryption.ts b/src/matrix/e2ee/olm/Encryption.ts index ebc38170..9b754272 100644 --- a/src/matrix/e2ee/olm/Encryption.ts +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -64,12 +64,12 @@ const MAX_BATCH_SIZE = 20; export class Encryption { constructor( private readonly account: Account, + private readonly pickleKey: string, private readonly olm: Olm, - private readonly olmUtil: Olm.Utility, - private readonly ownUserId: string, private readonly storage: Storage, private readonly now: () => number, - private readonly pickleKey: string, + private readonly ownUserId: string, + private readonly olmUtil: Olm.Utility, private readonly senderKeyLock: LockMap ) {} From 3330530f68e44a2d088c9a1fe09ebed5a757b9d0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:18:25 +0100 Subject: [PATCH 27/69] Update src/matrix/e2ee/DecryptionResult.ts Co-authored-by: R Midhun Suresh --- src/matrix/e2ee/DecryptionResult.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/DecryptionResult.ts b/src/matrix/e2ee/DecryptionResult.ts index 67c242bc..7735856a 100644 --- a/src/matrix/e2ee/DecryptionResult.ts +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -43,7 +43,7 @@ export class DecryptionResult { public readonly claimedEd25519Key: string ) {} - setDevice(device: DeviceIdentity) { + setDevice(device: DeviceIdentity): void { this.device = device; } From 82299e5aeab357750ca7f27f271da5d73f6f2829 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:18:33 +0100 Subject: [PATCH 28/69] Update src/matrix/e2ee/olm/Decryption.ts Co-authored-by: R Midhun Suresh --- src/matrix/e2ee/olm/Decryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 9698add9..e437716a 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -43,7 +43,7 @@ type CreateAndDecryptResult = { plaintext: string }; -function sortSessions(sessions: Session[]) { +function sortSessions(sessions: Session[]): void { sessions.sort((a, b) => { return b.data.lastUsed - a.data.lastUsed; }); From 3f9f0e98c7166af620dcc26297ebac0765ff6313 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:21:14 +0100 Subject: [PATCH 29/69] remove unused olm property in SenderKeyDecryption --- src/matrix/e2ee/olm/Decryption.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index e437716a..06ad18dc 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -122,7 +122,7 @@ export class Decryption { async _decryptAllForSenderKey(senderKey: string, events: OlmEncryptedEvent[], timestamp: number, readSessionsTxn: Transaction): Promise { const sessions = await this._getSessions(senderKey, readSessionsTxn); - const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this.olm, timestamp); + const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, timestamp); const results: DecryptionResult[] = []; const errors: DecryptionError[] = []; // events for a single senderKey need to be decrypted one by one @@ -238,7 +238,6 @@ class SenderKeyDecryption { constructor( public readonly senderKey: string, public readonly sessions: Session[], - private readonly olm: Olm, private readonly timestamp: number ) {} From 8adc5a9faea2b30390944e01273c6ca0038cf133 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Feb 2022 17:24:55 +0100 Subject: [PATCH 30/69] these were public actually --- src/matrix/e2ee/olm/Decryption.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 06ad18dc..9db198be 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -306,8 +306,8 @@ class SenderKeyDecryption { class DecryptionChanges { constructor( private readonly senderKeyDecryptions: SenderKeyDecryption[], - private readonly results: DecryptionResult[], - private readonly errors: DecryptionError[], + public readonly results: DecryptionResult[], + public readonly errors: DecryptionError[], private readonly account: Account, private readonly lock: ILock ) {} From b993331e06e94971f575046e90d8d344dc4737c9 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 25 Feb 2022 01:40:52 -0600 Subject: [PATCH 31/69] Add more HTML form and SVG elements Split off from https://github.com/vector-im/hydrogen-web/pull/653 Personally using `select`, `option`, and `path` currently in https://github.com/matrix-org/matrix-public-archive but added a few extra SVG elements that seemed common to me. --- 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 9ebcfaaf..44f7476a 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -104,8 +104,9 @@ export const TAG_NAMES = { "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", - "pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"], - [SVG_NS]: ["svg", "circle"] + "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", + "progress", "output", "video"], + [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; export const tag: { [tagName in typeof TAG_NAMES[string][number]]: (attributes?: BasicAttributes | Child | Child[], children?: Child | Child[]) => Element } = {} as any; From 0935f2d23aebccf3bb1bdd7974189dde4c8fc2bc Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 25 Feb 2022 01:59:48 -0600 Subject: [PATCH 32/69] Only try to use window.crypto.subtle in secure contexts to avoid it throwing and stopping all JavaScript Relevant error if you crypto is used in a non-secure context like a local LAN IP `http://192.168.1.151:3050/` ``` Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'deriveBits') at new Crypto at new Platform at mountHydrogen ``` For my use-case with https://github.com/matrix-org/matrix-public-archive, I don't need crypto/encryption at all. Docs: - https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts - https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle - "Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers." --- Related to https://github.com/vector-im/hydrogen-web/issues/579 --- src/platform/web/Platform.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 9de3d4ce..b6fdfae5 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -143,7 +143,10 @@ export class Platform { this._serviceWorkerHandler.registerAndStart(assetPaths.serviceWorker); } this.notificationService = new NotificationService(this._serviceWorkerHandler, config.push); - this.crypto = new Crypto(cryptoExtras); + // `window.crypto.subtle` is only available in a secure context + if(window.isSecureContext) { + this.crypto = new Crypto(cryptoExtras); + } this.storageFactory = new StorageFactory(this._serviceWorkerHandler); this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1"); this.estimateStorageUsage = estimateStorageUsage; From 7055f02f168f0689c340e0b080129261015999a1 Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 25 Feb 2022 15:52:54 +0530 Subject: [PATCH 33/69] typescriptify domain/avatar.js --- src/domain/SessionPickerViewModel.js | 2 +- src/domain/{avatar.js => avatar.ts} | 15 +++++++++------ src/domain/session/leftpanel/BaseTileViewModel.js | 2 +- .../session/rightpanel/MemberDetailsViewModel.js | 2 +- .../session/rightpanel/MemberTileViewModel.js | 2 +- .../session/rightpanel/RoomDetailsViewModel.js | 2 +- src/domain/session/room/InviteViewModel.js | 2 +- .../session/room/RoomBeingCreatedViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 2 +- src/domain/session/room/timeline/MessageBody.js | 2 +- .../room/timeline/tiles/BaseMessageTile.js | 2 +- src/platform/web/Platform.js | 2 +- src/platform/web/ui/AvatarView.js | 2 +- src/platform/web/ui/session/room/InviteView.js | 2 +- .../ui/session/room/timeline/BaseMessageView.js | 2 +- 15 files changed, 23 insertions(+), 20 deletions(-) rename src/domain/{avatar.js => avatar.ts} (74%) diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 6714e96f..e486c64f 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {SortedArray} from "../observable/index.js"; import {ViewModel} from "./ViewModel"; -import {avatarInitials, getIdentifierColorNumber} from "./avatar.js"; +import {avatarInitials, getIdentifierColorNumber} from "./avatar"; class SessionItemViewModel extends ViewModel { constructor(options, pickerVM) { diff --git a/src/domain/avatar.js b/src/domain/avatar.ts similarity index 74% rename from src/domain/avatar.js rename to src/domain/avatar.ts index 5b32020b..6f1ef8b0 100644 --- a/src/domain/avatar.js +++ b/src/domain/avatar.ts @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function avatarInitials(name) { +import { Platform } from "../platform/web/Platform"; +import { MediaRepository } from "../matrix/net/MediaRepository"; + +export function avatarInitials(name: string): string { let firstChar = name.charAt(0); if (firstChar === "!" || firstChar === "@" || firstChar === "#") { firstChar = name.charAt(1); @@ -29,10 +32,10 @@ export function avatarInitials(name) { * * @return {number} */ -function hashCode(str) { +function hashCode(str: string): number { let hash = 0; - let i; - let chr; + let i: number; + let chr: number; if (str.length === 0) { return hash; } @@ -44,11 +47,11 @@ function hashCode(str) { return Math.abs(hash); } -export function getIdentifierColorNumber(id) { +export function getIdentifierColorNumber(id: string): number { return (hashCode(id) % 8) + 1; } -export function getAvatarHttpUrl(avatarUrl, cssSize, platform, mediaRepository) { +export function getAvatarHttpUrl(avatarUrl: string, cssSize: number, platform: Platform, mediaRepository: MediaRepository): string | null { if (avatarUrl) { const imageSize = cssSize * platform.devicePixelRatio; return mediaRepository.mxcUrlThumbnail(avatarUrl, imageSize, imageSize, "crop"); diff --git a/src/domain/session/leftpanel/BaseTileViewModel.js b/src/domain/session/leftpanel/BaseTileViewModel.js index e1d6dfff..8f5106bf 100644 --- a/src/domain/session/leftpanel/BaseTileViewModel.js +++ b/src/domain/session/leftpanel/BaseTileViewModel.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; import {ViewModel} from "../../ViewModel"; const KIND_ORDER = ["roomBeingCreated", "invite", "room"]; diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 8ee50030..b3c8278c 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {RoomType} from "../../../matrix/room/common"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class MemberDetailsViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index 9062ea7d..153c70c8 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class MemberTileViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index 97e8588e..4e2735b1 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class RoomDetailsViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/InviteViewModel.js b/src/domain/session/room/InviteViewModel.js index c2ff74e0..00697642 100644 --- a/src/domain/session/room/InviteViewModel.js +++ b/src/domain/session/room/InviteViewModel.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; import {ViewModel} from "../../ViewModel"; export class InviteViewModel extends ViewModel { diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index f5c5d3cd..b503af73 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; import {ViewModel} from "../../ViewModel"; export class RoomBeingCreatedViewModel extends ViewModel { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index b7af00ce..71060728 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {ComposerViewModel} from "./ComposerViewModel.js" -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel"; import {imageToInfo} from "../common.js"; diff --git a/src/domain/session/room/timeline/MessageBody.js b/src/domain/session/room/timeline/MessageBody.js index a8bf2497..65b487a9 100644 --- a/src/domain/session/room/timeline/MessageBody.js +++ b/src/domain/session/room/timeline/MessageBody.js @@ -1,5 +1,5 @@ import { linkify } from "./linkify/linkify.js"; -import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js"; +import { getIdentifierColorNumber, avatarInitials } from "../../../avatar"; /** * Parse text into parts such as newline, links and text. diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 6b0b4356..3385a587 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -16,7 +16,7 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; -import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar.js"; +import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; export class BaseMessageTile extends SimpleTile { constructor(options) { diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 9de3d4ce..56cf4fc5 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -37,7 +37,7 @@ import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandl import {downloadInIframe} from "./dom/download.js"; import {Disposables} from "../../utils/Disposables"; import {parseHTML} from "./parsehtml.js"; -import {handleAvatarError} from "./ui/avatar.js"; +import {handleAvatarError} from "./ui/avatar"; function addScript(src) { return new Promise(function (resolve, reject) { diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js index f2d94e3b..551f7307 100644 --- a/src/platform/web/ui/AvatarView.js +++ b/src/platform/web/ui/AvatarView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseUpdateView} from "./general/BaseUpdateView"; -import {renderStaticAvatar, renderImg} from "./avatar.js"; +import {renderStaticAvatar, renderImg} from "./avatar"; /* optimization to not use a sub view when changing between img and text diff --git a/src/platform/web/ui/session/room/InviteView.js b/src/platform/web/ui/session/room/InviteView.js index 9d808abf..99345360 100644 --- a/src/platform/web/ui/session/room/InviteView.js +++ b/src/platform/web/ui/session/room/InviteView.js @@ -16,7 +16,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {renderStaticAvatar} from "../../avatar.js"; +import {renderStaticAvatar} from "../../avatar"; export class InviteView extends TemplateView { render(t, vm) { diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index a6fbb9be..9b583103 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {renderStaticAvatar} from "../../../avatar.js"; +import {renderStaticAvatar} from "../../../avatar"; import {tag} from "../../../general/html"; import {mountView} from "../../../general/utils"; import {TemplateView} from "../../../general/TemplateView"; From 17acda77414fe0d4ca5ae74c949dfd65b1e75a20 Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 25 Feb 2022 16:45:07 +0530 Subject: [PATCH 34/69] typescriptify domain/LogoutViewModel.js --- ...{LogoutViewModel.js => LogoutViewModel.ts} | 23 ++++++++++++------- src/domain/RootViewModel.js | 2 +- src/domain/ViewModel.ts | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) rename src/domain/{LogoutViewModel.js => LogoutViewModel.ts} (76%) diff --git a/src/domain/LogoutViewModel.js b/src/domain/LogoutViewModel.ts similarity index 76% rename from src/domain/LogoutViewModel.js rename to src/domain/LogoutViewModel.ts index 24ca440e..3edfcad5 100644 --- a/src/domain/LogoutViewModel.js +++ b/src/domain/LogoutViewModel.ts @@ -14,11 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "./ViewModel"; +import {Options, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; -export class LogoutViewModel extends ViewModel { - constructor(options) { +type LogoutOptions = { sessionId: string; } & Options; + +export class LogoutViewModel extends ViewModel { + private _sessionId: string; + private _busy: boolean; + private _showConfirm: boolean; + private _error?: Error; + + constructor(options: LogoutOptions) { super(options); this._sessionId = options.sessionId; this._busy = false; @@ -26,19 +33,19 @@ export class LogoutViewModel extends ViewModel { this._error = undefined; } - get showConfirm() { + get showConfirm(): boolean { return this._showConfirm; } - get busy() { + get busy(): boolean { return this._busy; } - get cancelUrl() { + get cancelUrl(): string { return this.urlCreator.urlForSegment("session", true); } - async logout() { + async logout(): Promise { this._busy = true; this._showConfirm = false; this.emitChange("busy"); @@ -53,7 +60,7 @@ export class LogoutViewModel extends ViewModel { } } - get status() { + get status(): string { if (this._error) { return this.i18n`Could not log out of device: ${this._error.message}`; } else { diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 642e43f4..2711cd2f 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -18,7 +18,7 @@ import {Client} from "../matrix/Client.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel.js"; -import {LogoutViewModel} from "./LogoutViewModel.js"; +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 458b2840..cfe22326 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -29,7 +29,7 @@ import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {URLRouter} from "./navigation/URLRouter"; -type Options = { +export type Options = { platform: Platform logger: ILogger urlCreator: URLRouter @@ -95,7 +95,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: string[], ...expr: any[]) { + i18n(parts: TemplateStringsArray, ...expr: any[]) { // just concat for now let result = ""; for (let i = 0; i < parts.length; ++i) { From 4a4856a29e973dc8076c2b02eb738141b0383af9 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:19:01 +0530 Subject: [PATCH 35/69] export module --- src/lib.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index 89cd7706..ca75f6c9 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -34,3 +34,7 @@ export {TemplateView} from "./platform/web/ui/general/TemplateView"; export {ViewModel} from "./domain/ViewModel"; export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; export {AvatarView} from "./platform/web/ui/AvatarView.js"; +export {RoomType} from "./matrix/room/common"; +export {EventEmitter} from "./utils/EventEmitter"; +export {Disposables} from "./utils/Disposables"; +export * from "./observable/"; From 4c50dbf7ec5d204183a58c4a2d95af9a46dff27c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Mar 2022 15:41:44 +0100 Subject: [PATCH 36/69] make SDK exports explicit --- src/lib.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib.ts b/src/lib.ts index ca75f6c9..e846a378 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -37,4 +37,12 @@ export {AvatarView} from "./platform/web/ui/AvatarView.js"; export {RoomType} from "./matrix/room/common"; export {EventEmitter} from "./utils/EventEmitter"; export {Disposables} from "./utils/Disposables"; -export * from "./observable/"; +// these should eventually be moved to another library +export { + ObservableArray, + SortedArray, + MappedList, + AsyncMappedList, + ConcatList, + ObservableMap +} from "./observable/index"; From ee8e45926f89d7cf8516a840fa392680d2d74dda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Mar 2022 15:42:04 +0100 Subject: [PATCH 37/69] also export observable value classes --- src/lib.ts | 6 ++++++ src/observable/ObservableValue.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib.ts b/src/lib.ts index e846a378..c0b05032 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -46,3 +46,9 @@ export { ConcatList, ObservableMap } from "./observable/index"; +export { + BaseObservableValue, + IWaitHandle, + ObservableValue, + RetainedObservableValue +} from "./observable/ObservableValue"; diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index ad0a226d..65406700 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -41,7 +41,7 @@ export abstract class BaseObservableValue extends BaseObservable<(value: T) = } } -interface IWaitHandle { +export interface IWaitHandle { promise: Promise; dispose(): void; } From 42141c7063c91c909dc40e4c72ddb6fd7e6e3bfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Mar 2022 15:45:24 +0100 Subject: [PATCH 38/69] bump SDK version --- 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 8df205cb..ba0e1f4f 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.8", + "version": "0.0.9", "main": "./hydrogen.es.js", "type": "module" } From 643ab1a5f302594f315e1476573740d53425a90f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Mar 2022 15:48:42 +0100 Subject: [PATCH 39/69] cant export this for some reason --- src/lib.ts | 1 - src/observable/ObservableValue.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index c0b05032..a0ada84f 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -48,7 +48,6 @@ export { } from "./observable/index"; export { BaseObservableValue, - IWaitHandle, ObservableValue, RetainedObservableValue } from "./observable/ObservableValue"; diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index 65406700..ad0a226d 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -41,7 +41,7 @@ export abstract class BaseObservableValue extends BaseObservable<(value: T) = } } -export interface IWaitHandle { +interface IWaitHandle { promise: Promise; dispose(): void; } From b6d9993ed03e52fa052ec8c306aae7027e56ba56 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Mar 2022 17:08:49 +0100 Subject: [PATCH 40/69] remove unused import --- src/matrix/e2ee/olm/Decryption.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 9db198be..0f96f2fc 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -24,7 +24,6 @@ import {OlmPayloadType} from "./types"; import type {OlmMessage, OlmPayload} from "./types"; import type {Account} from "../Account"; import type {LockMap} from "../../../utils/LockMap"; -import type {Storage} from "../../storage/idb/Storage"; import type {Transaction} from "../../storage/idb/Transaction"; import type {OlmEncryptedEvent} from "./types"; import type * as OlmNamespace from "@matrix-org/olm"; From c09964dc30c0782e84427413d2493de7e673018e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 1 Mar 2022 18:36:14 -0600 Subject: [PATCH 41/69] Add `data-event-id="$xxx"` attributes to timeline items for easy selecting in end-to-end tests (#690) Split out from https://github.com/vector-im/hydrogen-web/pull/653 Example test assertions: https://github.com/matrix-org/matrix-public-archive/blob/db6d3797d74104ad7c93e572ed106f1a685a90d0/test/e2e-tests.js#L248-L252 ```js // Make sure the $abc event on the page has "foobarbaz" text in it assert.match( dom.document.querySelector(`[data-event-id="$abc"]`).outerHTML, new RegExp(`.*foobarbaz.*`) ); ``` --- .../session/room/timeline/tiles/SimpleTile.js | 4 ++++ .../session/room/timeline/BaseMessageView.js | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 3497b689..af2b0e12 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -44,6 +44,10 @@ export class SimpleTile extends ViewModel { return this._entry.asEventKey(); } + get eventId() { + return this._entry.id; + } + get isPending() { return this._entry.isPending; } diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 9b583103..7356cd2b 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -40,14 +40,17 @@ export class BaseMessageView extends TemplateView { if (this._interactive) { children.push(t.button({className: "Timeline_messageOptions"}, "⋯")); } - const li = t.el(this._tagName, {className: { - "Timeline_message": true, - own: vm.isOwn, - unsent: vm.isUnsent, - unverified: vm.isUnverified, - disabled: !this._interactive, - continuation: vm => vm.isContinuation, - }}, children); + const li = t.el(this._tagName, { + className: { + "Timeline_message": true, + own: vm.isOwn, + unsent: vm.isUnsent, + unverified: vm.isUnverified, + disabled: !this._interactive, + continuation: vm => vm.isContinuation, + }, + 'data-event-id': vm.eventId + }, children); // given that there can be many tiles, we don't add // unneeded DOM nodes in case of a continuation, and we add it // with a side-effect binding to not have to create sub views, From 2f4c639cef3246386cf9e739ffbd1039184f5678 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 2 Mar 2022 03:17:59 -0600 Subject: [PATCH 42/69] Only initialize Crypto when olm is provided See https://github.com/vector-im/hydrogen-web/pull/691#discussion_r816988082 --- src/platform/web/Platform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index b6fdfae5..8c8b4fac 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -143,8 +143,8 @@ export class Platform { this._serviceWorkerHandler.registerAndStart(assetPaths.serviceWorker); } this.notificationService = new NotificationService(this._serviceWorkerHandler, config.push); - // `window.crypto.subtle` is only available in a secure context - if(window.isSecureContext) { + // Only try to use crypto when olm is provided + if(this._assetPaths.olm) { this.crypto = new Crypto(cryptoExtras); } this.storageFactory = new StorageFactory(this._serviceWorkerHandler); From 60d60e95723de1e3d64ebd0bdc71d3314d67875b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Mar 2022 19:58:46 +0530 Subject: [PATCH 43/69] WIP --- package.json | 5 +- postcss/css-compile-variables.js | 87 ++++++++++++++++++++++++++++++++ yarn.lock | 30 ++++++++++- 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 postcss/css-compile-variables.js diff --git a/package.json b/package.json index 8fa27f47..3c007244 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "acorn": "^8.6.0", "acorn-walk": "^8.2.0", "aes-js": "^3.1.2", + "bs58": "^4.0.1", "core-js": "^3.6.5", "es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush", "escodegen": "^2.0.0", @@ -45,13 +46,13 @@ "text-encoding": "^0.7.0", "typescript": "^4.3.5", "vite": "^2.6.14", - "xxhashjs": "^0.2.2", - "bs58": "^4.0.1" + "xxhashjs": "^0.2.2" }, "dependencies": { "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", + "color": "^4.2.1", "dompurify": "^2.3.0" } } diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js new file mode 100644 index 00000000..2395f87d --- /dev/null +++ b/postcss/css-compile-variables.js @@ -0,0 +1,87 @@ +import { Color } from "color"; + +let aliasMap; +let resolvedMap; +const RE_VARIABLE_VALUE = /var\((--(.+)--(.+)-(.+))\)/; + +function getValueFromAlias(alias) { + const derivedVariable = aliasMap.get(`--${alias}`); + return resolvedMap.get(derivedVariable); // what if we haven't resolved this variable yet? +} + +function resolveDerivedVariable(decl, variables) { + const matches = decl.value.match(RE_VARIABLE_VALUE); + if (matches) { + const [,wholeVariable, baseVariable, operation, argument] = matches; + if (!variables[baseVariable]) { + // hmm.. baseVariable should be in config..., maybe this is an alias? + if (!aliasMap.get(`--${baseVariable}`)) { + throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); + } + } + switch (operation) { + case "darker": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = new Color(colorString).darken(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } + case "lighter": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = new Color(colorString).lighten(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } + } + } +} + +function extractAlias(decl) { + const RE_VARIABLE_PROP = /--(.+)/; + const wholeVariable = decl.value.match(RE_VARIABLE_VALUE)?.[1]; + if (RE_VARIABLE_PROP.test(decl.prop) && wholeVariable) { + aliasMap.set(decl.prop, wholeVariable); + } +} + +/* * + * @type {import('postcss').PluginCreator} + */ +module.exports = (opts = {}) => { + aliasMap = new Map(); + resolvedMap = new Map(); + const { variables } = opts; + return { + postcssPlugin: "postcss-compile-variables", + + Once(root) { + /* + Go through the CSS file once to extract all aliases. + We use the extracted alias when resolving derived variables + later. + */ + root.walkDecls(decl => extractAlias(decl)); + }, + + Declaration(declaration) { + resolveDerivedVariable(declaration, variables); + }, + + OnceExit(root, { Rule, Declaration }) { + const newRule = new Rule({ selector: ":root", source: root.source }) + // Add base css variables to :root + for (const [key, value] of Object.entries(variables)) { + const declaration = new Declaration({ prop: `--${key}`, value }); + newRule.append(declaration); + } + // Add derived css variables to :root + resolvedMap.forEach((value, key) => { + const declaration = new Declaration({ prop: key, value }); + newRule.append(declaration); + }); + root.append(newRule); + }, + }; +}; + +module.exports.postcss = true; diff --git a/yarn.lock b/yarn.lock index 87b8ef96..31dde2dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -332,11 +332,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" + integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colors@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -961,6 +977,11 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-core-module@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" @@ -1342,6 +1363,13 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" From 92084e80056d5869be4f2461ed39d0013fa96452 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 7 Mar 2022 11:32:30 +0530 Subject: [PATCH 44/69] Move all code under the Once event Apparently the other events are common to all plugins. --- postcss/css-compile-variables.js | 40 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 2395f87d..faf46fe2 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,4 +1,4 @@ -import { Color } from "color"; +const Color = require("color"); let aliasMap; let resolvedMap; @@ -44,6 +44,21 @@ function extractAlias(decl) { } } +function addResolvedVariablesToRootSelector( root, variables, { Rule, Declaration }) { + const newRule = new Rule({ selector: ":root", source: root.source }); + // Add base css variables to :root + for (const [key, value] of Object.entries(variables)) { + const declaration = new Declaration({ prop: `--${key}`, value }); + newRule.append(declaration); + } + // Add derived css variables to :root + resolvedMap.forEach((value, key) => { + const declaration = new Declaration({ prop: key, value }); + newRule.append(declaration); + }); + root.append(newRule); +} + /* * * @type {import('postcss').PluginCreator} */ @@ -54,32 +69,15 @@ module.exports = (opts = {}) => { return { postcssPlugin: "postcss-compile-variables", - Once(root) { + Once(root, {Rule, Declaration}) { /* Go through the CSS file once to extract all aliases. We use the extracted alias when resolving derived variables later. */ root.walkDecls(decl => extractAlias(decl)); - }, - - Declaration(declaration) { - resolveDerivedVariable(declaration, variables); - }, - - OnceExit(root, { Rule, Declaration }) { - const newRule = new Rule({ selector: ":root", source: root.source }) - // Add base css variables to :root - for (const [key, value] of Object.entries(variables)) { - const declaration = new Declaration({ prop: `--${key}`, value }); - newRule.append(declaration); - } - // Add derived css variables to :root - resolvedMap.forEach((value, key) => { - const declaration = new Declaration({ prop: key, value }); - newRule.append(declaration); - }); - root.append(newRule); + root.walkDecls(decl => resolveDerivedVariable(decl, variables)); + addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }); }, }; }; From b6f5e68e9e1580d2b32bcb0eb74cacbd68ff4277 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 7 Mar 2022 11:33:44 +0530 Subject: [PATCH 45/69] Format file --- postcss/css-compile-variables.js | 84 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index faf46fe2..125e79ca 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -5,35 +5,35 @@ let resolvedMap; const RE_VARIABLE_VALUE = /var\((--(.+)--(.+)-(.+))\)/; function getValueFromAlias(alias) { - const derivedVariable = aliasMap.get(`--${alias}`); - return resolvedMap.get(derivedVariable); // what if we haven't resolved this variable yet? + const derivedVariable = aliasMap.get(`--${alias}`); + return resolvedMap.get(derivedVariable); // what if we haven't resolved this variable yet? } function resolveDerivedVariable(decl, variables) { - const matches = decl.value.match(RE_VARIABLE_VALUE); - if (matches) { - const [,wholeVariable, baseVariable, operation, argument] = matches; - if (!variables[baseVariable]) { - // hmm.. baseVariable should be in config..., maybe this is an alias? - if (!aliasMap.get(`--${baseVariable}`)) { - throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); - } + const matches = decl.value.match(RE_VARIABLE_VALUE); + if (matches) { + const [, wholeVariable, baseVariable, operation, argument] = matches; + if (!variables[baseVariable]) { + // hmm.. baseVariable should be in config..., maybe this is an alias? + if (!aliasMap.get(`--${baseVariable}`)) { + throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); + } + } + switch (operation) { + case "darker": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = new Color(colorString).darken(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } + case "lighter": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = new Color(colorString).lighten(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } + } } - switch (operation) { - case "darker": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new Color(colorString).darken(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; - } - case "lighter": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new Color(colorString).lighten(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; - } - } - } } function extractAlias(decl) { @@ -44,7 +44,7 @@ function extractAlias(decl) { } } -function addResolvedVariablesToRootSelector( root, variables, { Rule, Declaration }) { +function addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add base css variables to :root for (const [key, value] of Object.entries(variables)) { @@ -63,23 +63,23 @@ function addResolvedVariablesToRootSelector( root, variables, { Rule, Declaratio * @type {import('postcss').PluginCreator} */ module.exports = (opts = {}) => { - aliasMap = new Map(); - resolvedMap = new Map(); - const { variables } = opts; - return { - postcssPlugin: "postcss-compile-variables", + aliasMap = new Map(); + resolvedMap = new Map(); + const { variables } = opts; + return { + postcssPlugin: "postcss-compile-variables", - Once(root, {Rule, Declaration}) { - /* - Go through the CSS file once to extract all aliases. - We use the extracted alias when resolving derived variables - later. - */ - root.walkDecls(decl => extractAlias(decl)); - root.walkDecls(decl => resolveDerivedVariable(decl, variables)); - addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }); - }, - }; + Once(root, { Rule, Declaration }) { + /* + Go through the CSS file once to extract all aliases. + We use the extracted alias when resolving derived variables + later. + */ + root.walkDecls(decl => extractAlias(decl)); + root.walkDecls(decl => resolveDerivedVariable(decl, variables)); + addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }); + }, + }; }; module.exports.postcss = true; From f170ef0206ec7b9a5797af103a468fc167ba52d5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 7 Mar 2022 11:38:39 +0530 Subject: [PATCH 46/69] Switch over to off-color --- package.json | 4 ++-- postcss/css-compile-variables.js | 6 +++--- yarn.lock | 37 +++++++------------------------- 3 files changed, 13 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 3c007244..041100a5 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", - "color": "^4.2.1", - "dompurify": "^2.3.0" + "dompurify": "^2.3.0", + "off-color": "^2.0.0" } } diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 125e79ca..e212ee69 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,4 +1,4 @@ -const Color = require("color"); +import { offColor } from 'off-color'; let aliasMap; let resolvedMap; @@ -22,13 +22,13 @@ function resolveDerivedVariable(decl, variables) { switch (operation) { case "darker": { const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new Color(colorString).darken(argument / 100).hex(); + const newColorString = new offColor(colorString).darken(argument / 100).hex(); resolvedMap.set(wholeVariable, newColorString); break; } case "lighter": { const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new Color(colorString).lighten(argument / 100).hex(); + const newColorString = new offColor(colorString).lighten(argument / 100).hex(); resolvedMap.set(wholeVariable, newColorString); break; } diff --git a/yarn.lock b/yarn.lock index 31dde2dd..5836b2af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -332,27 +332,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" - integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" - integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - colors@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -977,11 +961,6 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - is-core-module@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" @@ -1154,6 +1133,13 @@ nth-check@^2.0.0: dependencies: boolbase "^1.0.0" +off-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/off-color/-/off-color-2.0.0.tgz#ecf3bda52e9a78dde535db86361e048741a56631" + integrity sha512-JJ9ObbY2CzgT7F8PpdpHGNjQa7QbU8f4DkY3cCxYUq9NezYUMmL/oSofCc5MMaiUnNNBEFCc4w1unMA+R8syvw== + dependencies: + core-js "^3.6.5" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1363,13 +1349,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" From a5d46bb40cd7d0ba2451476e1d9277e779da681c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 7 Mar 2022 13:10:44 +0530 Subject: [PATCH 47/69] Move over tests to Hydrogen using impunity --- package.json | 1 + postcss/css-compile-variables.js | 6 +-- postcss/test.js | 66 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 postcss/test.js diff --git a/package.json b/package.json index 041100a5..5ca5b05f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/", + "test:postcss": "impunity --entry-point postcss/test.js ", "start": "vite --port 3000", "build": "vite build", "build:sdk": "./scripts/sdk/build.sh" diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index e212ee69..8737067d 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,4 +1,4 @@ -import { offColor } from 'off-color'; +const offColor = require("off-color").offColor; let aliasMap; let resolvedMap; @@ -22,13 +22,13 @@ function resolveDerivedVariable(decl, variables) { switch (operation) { case "darker": { const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new offColor(colorString).darken(argument / 100).hex(); + const newColorString = offColor(colorString).darken(argument / 100).hex(); resolvedMap.set(wholeVariable, newColorString); break; } case "lighter": { const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = new offColor(colorString).lighten(argument / 100).hex(); + const newColorString = offColor(colorString).lighten(argument / 100).hex(); resolvedMap.set(wholeVariable, newColorString); break; } diff --git a/postcss/test.js b/postcss/test.js new file mode 100644 index 00000000..104696aa --- /dev/null +++ b/postcss/test.js @@ -0,0 +1,66 @@ +const offColor = require("off-color").offColor; +const postcss = require("postcss"); +const plugin = require("./css-compile-variables"); + +async function run(input, output, opts = {}, assert) { + let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); + assert.strictEqual( + result.css.replaceAll(/\s/g, ""), + output.replaceAll(/\s/g, "") + ); + assert.strictEqual(result.warnings().length, 0); +} + +module.exports.tests = function tests() { + return { + "derived variables are resolved": async (assert) => { + const inputCSS = `div { + background-color: var(--foo-color--lighter-50); + }`; + const transformedColor = offColor("#ff0").lighten(0.5); + const outputCSS = + inputCSS + + ` + :root { + --foo-color: #ff0; + --foo-color--lighter-50: ${transformedColor.hex()}; + } + `; + await run( + inputCSS, + outputCSS, + { variables: { "foo-color": "#ff0" } }, + assert + ); + }, + + "derived variables work with alias": async (assert) => { + const inputCSS = `div { + background: var(--icon-color--darker-20); + --my-alias: var(--icon-color--darker-20); + color: var(--my-alias--lighter-15); + }`; + const colorDarker = offColor("#fff").darken(0.2).hex(); + const aliasLighter = offColor(colorDarker).lighten(0.15).hex(); + const outputCSS = `div { + background: var(--icon-color--darker-20); + --my-alias: var(--icon-color--darker-20); + color: var(--my-alias--lighter-15); + } + :root { + --icon-color: #fff; + --icon-color--darker-20: ${colorDarker}; + --my-alias--lighter-15: ${aliasLighter}; + } + `; + await run(inputCSS, outputCSS, { variables: { "icon-color": "#fff" }, }, assert); + }, + + "derived variable throws if base not present in config": async (assert) => { + const css = `:root { + color: var(--icon-color--darker-20); + }`; + assert.rejects(async () => await postcss([plugin({ variables: {} })]).process(css, { from: undefined, })); + } + }; +}; From 41f6b6ab6bd52a55f024cb31d8f78c389e7f02df Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 7 Mar 2022 13:25:53 +0530 Subject: [PATCH 48/69] Use startsWith instead of regex testing --- postcss/css-compile-variables.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 8737067d..8d4b0fd9 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -37,9 +37,8 @@ function resolveDerivedVariable(decl, variables) { } function extractAlias(decl) { - const RE_VARIABLE_PROP = /--(.+)/; const wholeVariable = decl.value.match(RE_VARIABLE_VALUE)?.[1]; - if (RE_VARIABLE_PROP.test(decl.prop) && wholeVariable) { + if (decl.prop.startsWith("--") && wholeVariable) { aliasMap.set(decl.prop, wholeVariable); } } From a83850ebf38f3de28bb182470fd009b1fabade9c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 9 Mar 2022 11:48:53 +0530 Subject: [PATCH 49/69] Use postcss value parser to find variables --- package.json | 3 +- postcss/css-compile-variables.js | 58 +++++++++++++++++++++----------- postcss/test.js | 23 +++++++++++++ yarn.lock | 5 +++ 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 5ca5b05f..fa9c4344 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", - "off-color": "^2.0.0" + "off-color": "^2.0.0", + "postcss-value-parser": "^4.2.0" } } diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 8d4b0fd9..bd952ac8 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,4 +1,5 @@ const offColor = require("off-color").offColor; +const valueParser = require("postcss-value-parser"); let aliasMap; let resolvedMap; @@ -9,28 +10,45 @@ function getValueFromAlias(alias) { return resolvedMap.get(derivedVariable); // what if we haven't resolved this variable yet? } -function resolveDerivedVariable(decl, variables) { - const matches = decl.value.match(RE_VARIABLE_VALUE); - if (matches) { - const [, wholeVariable, baseVariable, operation, argument] = matches; - if (!variables[baseVariable]) { - // hmm.. baseVariable should be in config..., maybe this is an alias? - if (!aliasMap.get(`--${baseVariable}`)) { - throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); - } +function parseDeclarationValue(value) { + const parsed = valueParser(value); + const variables = []; + parsed.walk(node => { + if (node.type !== "function" && node.value !== "var") { + return; } - switch (operation) { - case "darker": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = offColor(colorString).darken(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; + const variable = node.nodes[0]; + variables.push(variable.value); + }); + return variables; +} + +function resolveDerivedVariable(decl, variables) { + const RE_VARIABLE_VALUE = /--(.+)--(.+)-(.+)/; + const variableCollection = parseDeclarationValue(decl.value); + for (const variable of variableCollection) { + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [wholeVariable, baseVariable, operation, argument] = matches; + if (!variables[baseVariable]) { + // hmm.. baseVariable should be in config..., maybe this is an alias? + if (!aliasMap.get(`--${baseVariable}`)) { + throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); + } } - case "lighter": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = offColor(colorString).lighten(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; + switch (operation) { + case "darker": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = offColor(colorString).darken(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } + case "lighter": { + const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); + const newColorString = offColor(colorString).lighten(argument / 100).hex(); + resolvedMap.set(wholeVariable, newColorString); + break; + } } } } diff --git a/postcss/test.js b/postcss/test.js index 104696aa..e67016b6 100644 --- a/postcss/test.js +++ b/postcss/test.js @@ -61,6 +61,29 @@ module.exports.tests = function tests() { color: var(--icon-color--darker-20); }`; assert.rejects(async () => await postcss([plugin({ variables: {} })]).process(css, { from: undefined, })); + }, + + "multiple derived variable in single declaration is parsed correctly": async (assert) => { + const inputCSS = `div { + background-color: linear-gradient(var(--foo-color--lighter-50), var(--foo-color--darker-20)); + }`; + const transformedColor1 = offColor("#ff0").lighten(0.5); + const transformedColor2 = offColor("#ff0").darken(0.2); + const outputCSS = + inputCSS + + ` + :root { + --foo-color: #ff0; + --foo-color--lighter-50: ${transformedColor1.hex()}; + --foo-color--darker-20: ${transformedColor2.hex()}; + } + `; + await run( + inputCSS, + outputCSS, + { variables: { "foo-color": "#ff0" } }, + assert + ); } }; }; diff --git a/yarn.lock b/yarn.lock index 5836b2af..7bcefdd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1222,6 +1222,11 @@ postcss-flexbugs-fixes@^5.0.2: resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + postcss@^8.3.8: version "8.3.9" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31" From 6d7c983e8e8f7d33f1a704fe82b5a30972431cf8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Mar 2022 11:28:19 +0100 Subject: [PATCH 50/69] convert (Base)ObservableMap to typescript --- .../room/timeline/ReactionsViewModel.js | 2 +- src/matrix/room/members/MemberList.js | 2 +- src/observable/index.js | 4 +-- src/observable/list/SortedMapList.js | 2 +- src/observable/map/ApplyMap.js | 2 +- ...eObservableMap.js => BaseObservableMap.ts} | 30 +++++++++---------- src/observable/map/FilteredMap.js | 4 +-- src/observable/map/JoinedMap.js | 4 +-- src/observable/map/LogMap.js | 2 +- src/observable/map/MappedMap.js | 2 +- .../{ObservableMap.js => ObservableMap.ts} | 30 ++++++++++--------- 11 files changed, 42 insertions(+), 42 deletions(-) rename src/observable/map/{BaseObservableMap.js => BaseObservableMap.ts} (69%) rename src/observable/map/{ObservableMap.js => ObservableMap.ts} (90%) diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index fa48bec0..4f366af0 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.js"; +import {ObservableMap} from "../../../../observable/map/ObservableMap"; export class ReactionsViewModel { constructor(parentTile) { diff --git a/src/matrix/room/members/MemberList.js b/src/matrix/room/members/MemberList.js index 9923fb87..f32a63d3 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.js"; +import {ObservableMap} from "../../../observable/map/ObservableMap"; import {RetainedValue} from "../../../utils/RetainedValue"; export class MemberList extends RetainedValue { diff --git a/src/observable/index.js b/src/observable/index.js index 4d7f18a3..6057174b 100644 --- a/src/observable/index.js +++ b/src/observable/index.js @@ -18,14 +18,14 @@ 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.js"; +import {BaseObservableMap} from "./map/BaseObservableMap"; // 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.js"; +export { ObservableMap } from "./map/ObservableMap"; // avoid circular dependency between these classes // and BaseObservableMap (as they extend it) diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index 38900380..d74dbade 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -133,7 +133,7 @@ export class SortedMapList extends BaseObservableList { } } -import {ObservableMap} from "../map/ObservableMap.js"; +import {ObservableMap} from "../map/ObservableMap"; export function tests() { return { diff --git a/src/observable/map/ApplyMap.js b/src/observable/map/ApplyMap.js index ad345595..6be7278a 100644 --- a/src/observable/map/ApplyMap.js +++ b/src/observable/map/ApplyMap.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; export class ApplyMap extends BaseObservableMap { constructor(source, apply) { diff --git a/src/observable/map/BaseObservableMap.js b/src/observable/map/BaseObservableMap.ts similarity index 69% rename from src/observable/map/BaseObservableMap.js rename to src/observable/map/BaseObservableMap.ts index d3193931..694c017e 100644 --- a/src/observable/map/BaseObservableMap.js +++ b/src/observable/map/BaseObservableMap.ts @@ -16,7 +16,14 @@ limitations under the License. import {BaseObservable} from "../BaseObservable"; -export class BaseObservableMap extends BaseObservable { +export interface IMapObserver { + onReset(): void; + onAdd(key: K, value:V): void; + onUpdate(key: K, value: V, params: any): void; + onRemove(key: K, value: V): void +} + +export abstract class BaseObservableMap extends BaseObservable> { emitReset() { for(let h of this._handlers) { h.onReset(); @@ -24,15 +31,15 @@ export class BaseObservableMap extends BaseObservable { } // we need batch events, mostly on index based collection though? // maybe we should get started without? - emitAdd(key, value) { + emitAdd(key: K, value: V) { for(let h of this._handlers) { h.onAdd(key, value); } } - emitUpdate(key, value, ...params) { + emitUpdate(key, value, params) { for(let h of this._handlers) { - h.onUpdate(key, value, ...params); + h.onUpdate(key, value, params); } } @@ -42,16 +49,7 @@ export class BaseObservableMap extends BaseObservable { } } - [Symbol.iterator]() { - throw new Error("unimplemented"); - } - - get size() { - throw new Error("unimplemented"); - } - - // eslint-disable-next-line no-unused-vars - get(key) { - throw new Error("unimplemented"); - } + 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 f7090502..d7e11fbe 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; export class FilteredMap extends BaseObservableMap { constructor(source, filter) { @@ -166,7 +166,7 @@ class FilterIterator { } } -import {ObservableMap} from "./ObservableMap.js"; +import {ObservableMap} from "./ObservableMap"; export function tests() { return { "filter preloaded list": assert => { diff --git a/src/observable/map/JoinedMap.js b/src/observable/map/JoinedMap.js index 7db04be1..d97c5677 100644 --- a/src/observable/map/JoinedMap.js +++ b/src/observable/map/JoinedMap.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; export class JoinedMap extends BaseObservableMap { constructor(sources) { @@ -191,7 +191,7 @@ class SourceSubscriptionHandler { } -import { ObservableMap } from "./ObservableMap.js"; +import { ObservableMap } from "./ObservableMap"; export function tests() { diff --git a/src/observable/map/LogMap.js b/src/observable/map/LogMap.js index 4b8bb686..1beb4846 100644 --- a/src/observable/map/LogMap.js +++ b/src/observable/map/LogMap.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; export class LogMap extends BaseObservableMap { constructor(source, log) { diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js index 2a810058..a6b65c41 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; /* 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? diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.ts similarity index 90% rename from src/observable/map/ObservableMap.js rename to src/observable/map/ObservableMap.ts index 8f5a0922..0f681879 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.ts @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableMap} from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap"; -export class ObservableMap extends BaseObservableMap { - constructor(initialValues) { +export class ObservableMap extends BaseObservableMap { + private readonly _values: Map; + + constructor(initialValues: Iterable<[K, V]>) { super(); this._values = new Map(initialValues); } - update(key, params) { + update(key: K, params: any): boolean { const value = this._values.get(key); if (value !== undefined) { // could be the same value, so it's already updated @@ -34,7 +36,7 @@ export class ObservableMap extends BaseObservableMap { return false; // or return existing value? } - add(key, value) { + add(key: K, value: V): boolean { if (!this._values.has(key)) { this._values.set(key, value); this.emitAdd(key, value); @@ -43,7 +45,7 @@ export class ObservableMap extends BaseObservableMap { return false; // or return existing value? } - remove(key) { + remove(key: K): boolean { const value = this._values.get(key); if (value !== undefined) { this._values.delete(key); @@ -54,39 +56,39 @@ export class ObservableMap extends BaseObservableMap { } } - set(key, value) { + set(key: K, value: V): boolean { if (this._values.has(key)) { // We set the value here because update only supports inline updates this._values.set(key, value); - return this.update(key); + return this.update(key, undefined); } else { return this.add(key, value); } } - reset() { + reset(): void { this._values.clear(); this.emitReset(); } - get(key) { + get(key: K): V | undefined { return this._values.get(key); } - get size() { + get size(): number { return this._values.size; } - [Symbol.iterator]() { + [Symbol.iterator](): Iterator<[K, V]> { return this._values.entries(); } - values() { + values(): Iterator { return this._values.values(); } - keys() { + keys(): Iterator { return this._values.keys(); } } From 21080d2110849531087500328b558b02b5f941b1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:41:26 +0100 Subject: [PATCH 51/69] fix tests --- src/observable/map/ObservableMap.ts | 44 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 0f681879..719ce4b7 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -19,12 +19,12 @@ import {BaseObservableMap} from "./BaseObservableMap"; export class ObservableMap extends BaseObservableMap { private readonly _values: Map; - constructor(initialValues: Iterable<[K, V]>) { + constructor(initialValues?: Iterable<[K, V]>) { super(); this._values = new Map(initialValues); } - update(key: K, params: any): boolean { + update(key: K, params?: any): boolean { const value = this._values.get(key); if (value !== undefined) { // could be the same value, so it's already updated @@ -107,13 +107,16 @@ export function tests() { test_add(assert) { let fired = 0; - const map = new ObservableMap(); + 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); @@ -122,7 +125,7 @@ export function tests() { test_update(assert) { let fired = 0; - const map = new ObservableMap(); + const map = new ObservableMap(); const value = {number: 5}; map.add(1, value); map.subscribe({ @@ -131,7 +134,10 @@ export function tests() { assert.equal(key, 1); assert.deepEqual(value, {number: 6}); assert.equal(params, "test"); - } + }, + onAdd() {}, + onRemove() {}, + onReset() {} }); value.number = 6; map.update(1, "test"); @@ -140,9 +146,12 @@ export function tests() { test_update_unknown(assert) { let fired = 0; - const map = new ObservableMap(); + const map = new ObservableMap(); map.subscribe({ - onUpdate() { fired += 1; } + onUpdate() { fired += 1; }, + onAdd() {}, + onRemove() {}, + onReset() {} }); const result = map.update(1); assert.equal(fired, 0); @@ -151,7 +160,7 @@ export function tests() { test_set(assert) { let add_fired = 0, update_fired = 0; - const map = new ObservableMap(); + const map = new ObservableMap(); map.subscribe({ onAdd(key, value) { add_fired += 1; @@ -162,7 +171,9 @@ export function tests() { update_fired += 1; assert.equal(key, 1); assert.deepEqual(value, {value: 7}); - } + }, + onRemove() {}, + onReset() {} }); // Add map.set(1, {value: 5}); @@ -176,7 +187,7 @@ export function tests() { test_remove(assert) { let fired = 0; - const map = new ObservableMap(); + const map = new ObservableMap(); const value = {value: 5}; map.add(1, value); map.subscribe({ @@ -184,7 +195,10 @@ export function tests() { fired += 1; assert.equal(key, 1); assert.deepEqual(value, {value: 5}); - } + }, + onAdd() {}, + onUpdate() {}, + onReset() {} }); map.remove(1); assert.equal(map.size, 0); @@ -192,8 +206,8 @@ export function tests() { }, test_iterate(assert) { - const results = []; - const map = new ObservableMap(); + const results: any[] = []; + const map = new ObservableMap(); map.add(1, {number: 5}); map.add(2, {number: 6}); map.add(3, {number: 7}); @@ -206,7 +220,7 @@ export function tests() { assert.equal(results.find(([key]) => key === 3)[1].number, 7); }, test_size(assert) { - const map = new ObservableMap(); + const map = new ObservableMap(); map.add(1, {number: 5}); map.add(2, {number: 6}); assert.equal(map.size, 2); From 762925d4a591b58235fa3846d3022daa75071898 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:44:49 +0100 Subject: [PATCH 52/69] fix type error --- src/observable/map/ObservableMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 719ce4b7..8ada1843 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -19,7 +19,7 @@ import {BaseObservableMap} from "./BaseObservableMap"; export class ObservableMap extends BaseObservableMap { private readonly _values: Map; - constructor(initialValues?: Iterable<[K, V]>) { + constructor(initialValues?: Iterable) { super(); this._values = new Map(initialValues); } From 6150e91c3f82ab7c772dd2eb9841fddddae32bd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:51:11 +0100 Subject: [PATCH 53/69] fix type error again --- src/observable/map/ObservableMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index 8ada1843..d604ab0a 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -19,7 +19,7 @@ import {BaseObservableMap} from "./BaseObservableMap"; export class ObservableMap extends BaseObservableMap { private readonly _values: Map; - constructor(initialValues?: Iterable) { + constructor(initialValues?: (readonly [K, V])[]) { super(); this._values = new Map(initialValues); } From 79f363fb9d9e4406daf65244419849cf422df1c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 9 Mar 2022 17:20:05 +0530 Subject: [PATCH 54/69] Move code to callback and fix alias code --- postcss/color.js | 14 ++++++++++++++ postcss/css-compile-variables.js | 31 +++++++++---------------------- postcss/test.js | 28 +++++++++++++++++++++------- 3 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 postcss/color.js diff --git a/postcss/color.js b/postcss/color.js new file mode 100644 index 00000000..cad91019 --- /dev/null +++ b/postcss/color.js @@ -0,0 +1,14 @@ +const offColor = require("off-color").offColor; + +module.exports.derive = function (value, operation, argument) { + switch (operation) { + case "darker": { + const newColorString = offColor(value).darken(argument / 100).hex(); + return newColorString; + } + case "lighter": { + const newColorString = offColor(value).lighten(argument / 100).hex(); + return newColorString; + } + } +} diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index bd952ac8..bc91dc06 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,13 +1,11 @@ -const offColor = require("off-color").offColor; const valueParser = require("postcss-value-parser"); let aliasMap; let resolvedMap; -const RE_VARIABLE_VALUE = /var\((--(.+)--(.+)-(.+))\)/; -function getValueFromAlias(alias) { +function getValueFromAlias(alias, variables) { const derivedVariable = aliasMap.get(`--${alias}`); - return resolvedMap.get(derivedVariable); // what if we haven't resolved this variable yet? + return variables[derivedVariable] ?? resolvedMap.get(`--${derivedVariable}`); // what if we haven't resolved this variable yet? } function parseDeclarationValue(value) { @@ -23,7 +21,7 @@ function parseDeclarationValue(value) { return variables; } -function resolveDerivedVariable(decl, variables) { +function resolveDerivedVariable(decl, {variables, derive}) { const RE_VARIABLE_VALUE = /--(.+)--(.+)-(.+)/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { @@ -36,26 +34,15 @@ function resolveDerivedVariable(decl, variables) { throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); } } - switch (operation) { - case "darker": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = offColor(colorString).darken(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; - } - case "lighter": { - const colorString = variables[baseVariable] ?? getValueFromAlias(baseVariable); - const newColorString = offColor(colorString).lighten(argument / 100).hex(); - resolvedMap.set(wholeVariable, newColorString); - break; - } - } + const value = variables[baseVariable] ?? getValueFromAlias(baseVariable, variables); + const derivedValue = derive(value, operation, argument); + resolvedMap.set(wholeVariable, derivedValue); } } } function extractAlias(decl) { - const wholeVariable = decl.value.match(RE_VARIABLE_VALUE)?.[1]; + const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; if (decl.prop.startsWith("--") && wholeVariable) { aliasMap.set(decl.prop, wholeVariable); } @@ -82,7 +69,7 @@ function addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration module.exports = (opts = {}) => { aliasMap = new Map(); resolvedMap = new Map(); - const { variables } = opts; + const {variables} = opts; return { postcssPlugin: "postcss-compile-variables", @@ -93,7 +80,7 @@ module.exports = (opts = {}) => { later. */ root.walkDecls(decl => extractAlias(decl)); - root.walkDecls(decl => resolveDerivedVariable(decl, variables)); + root.walkDecls(decl => resolveDerivedVariable(decl, opts)); addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }); }, }; diff --git a/postcss/test.js b/postcss/test.js index e67016b6..d07a6689 100644 --- a/postcss/test.js +++ b/postcss/test.js @@ -1,9 +1,10 @@ const offColor = require("off-color").offColor; const postcss = require("postcss"); const plugin = require("./css-compile-variables"); +const derive = require("./color").derive; async function run(input, output, opts = {}, assert) { - let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); + let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, }); assert.strictEqual( result.css.replaceAll(/\s/g, ""), output.replaceAll(/\s/g, "") @@ -78,12 +79,25 @@ module.exports.tests = function tests() { --foo-color--darker-20: ${transformedColor2.hex()}; } `; - await run( - inputCSS, - outputCSS, - { variables: { "foo-color": "#ff0" } }, - assert - ); + await run( inputCSS, outputCSS, { variables: { "foo-color": "#ff0" } }, assert); + }, + "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { + const inputCSS = `div { + --my-alias: var(--foo-color); + background-color: linear-gradient(var(--my-alias--lighter-50), var(--my-alias--darker-20)); + }`; + const transformedColor1 = offColor("#ff0").lighten(0.5); + const transformedColor2 = offColor("#ff0").darken(0.2); + const outputCSS = + inputCSS + + ` + :root { + --foo-color: #ff0; + --my-alias--lighter-50: ${transformedColor1.hex()}; + --my-alias--darker-20: ${transformedColor2.hex()}; + } + `; + await run( inputCSS, outputCSS, { variables: { "foo-color": "#ff0" } }, assert); } }; }; From 96fa83b5084ac0fc76d3dad2e67ee32cc3b224a8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 9 Mar 2022 17:22:11 +0530 Subject: [PATCH 55/69] Add license header --- postcss/color.js | 16 ++++++++++++++++ postcss/css-compile-variables.js | 16 ++++++++++++++++ postcss/test.js | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/postcss/color.js b/postcss/color.js index cad91019..2251a56e 100644 --- a/postcss/color.js +++ b/postcss/color.js @@ -1,3 +1,19 @@ +/* +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. +*/ + const offColor = require("off-color").offColor; module.exports.derive = function (value, operation, argument) { diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index bc91dc06..195526ef 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -1,3 +1,19 @@ +/* +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. +*/ + const valueParser = require("postcss-value-parser"); let aliasMap; diff --git a/postcss/test.js b/postcss/test.js index d07a6689..4ed74c67 100644 --- a/postcss/test.js +++ b/postcss/test.js @@ -1,3 +1,19 @@ +/* +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. +*/ + const offColor = require("off-color").offColor; const postcss = require("postcss"); const plugin = require("./css-compile-variables"); From 63c1f2a7a39ee9e1f3d3633d47aee49743f801ba Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 9 Mar 2022 17:22:45 +0530 Subject: [PATCH 56/69] Add node as env to eslint --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index cb28f4c8..1985ccc1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { "env": { "browser": true, + "node": true, "es6": true }, "extends": "eslint:recommended", From 1663782954ef1de0e0bce0ba9d759f733bd95f2e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 16:05:13 +0530 Subject: [PATCH 57/69] Throw after fetching value --- postcss/css-compile-variables.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 195526ef..732ed8b3 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -44,13 +44,10 @@ function resolveDerivedVariable(decl, {variables, derive}) { const matches = variable.match(RE_VARIABLE_VALUE); if (matches) { const [wholeVariable, baseVariable, operation, argument] = matches; - if (!variables[baseVariable]) { - // hmm.. baseVariable should be in config..., maybe this is an alias? - if (!aliasMap.get(`--${baseVariable}`)) { - throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); - } - } const value = variables[baseVariable] ?? getValueFromAlias(baseVariable, variables); + if (!value) { + throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); + } const derivedValue = derive(value, operation, argument); resolvedMap.set(wholeVariable, derivedValue); } From 52101239773d598c3fdc94b6b6a778ad6d3db147 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:19:04 +0530 Subject: [PATCH 58/69] Document options --- postcss/css-compile-variables.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 732ed8b3..38f5c471 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -76,13 +76,21 @@ function addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration root.append(newRule); } -/* * - * @type {import('postcss').PluginCreator} +/** + * @callback derive + * @param {string} value - The base value on which an operation is applied + * @param {string} operation - The operation to be applied (eg: darker, lighter...) + * @param {string} argument - The argument for this operation + */ +/** + * + * @param {Object} opts - Options for the plugin + * @param {Object} opts.variables - An object with records of the form: {base_variable_name: value} + * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables */ module.exports = (opts = {}) => { aliasMap = new Map(); resolvedMap = new Map(); - const {variables} = opts; return { postcssPlugin: "postcss-compile-variables", @@ -94,7 +102,7 @@ module.exports = (opts = {}) => { */ root.walkDecls(decl => extractAlias(decl)); root.walkDecls(decl => resolveDerivedVariable(decl, opts)); - addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }); + addResolvedVariablesToRootSelector(root, opts.variables, { Rule, Declaration }); }, }; }; From f732164b5fb253ad1dfe88e05d50ffdcfba2df8a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:21:38 +0530 Subject: [PATCH 59/69] Formatting change --- postcss/css-compile-variables.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 38f5c471..cf2b451d 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -61,16 +61,16 @@ function extractAlias(decl) { } } -function addResolvedVariablesToRootSelector(root, variables, { Rule, Declaration }) { +function addResolvedVariablesToRootSelector(root, variables, {Rule, Declaration}) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add base css variables to :root for (const [key, value] of Object.entries(variables)) { - const declaration = new Declaration({ prop: `--${key}`, value }); + const declaration = new Declaration({prop: `--${key}`, value}); newRule.append(declaration); } // Add derived css variables to :root resolvedMap.forEach((value, key) => { - const declaration = new Declaration({ prop: key, value }); + const declaration = new Declaration({prop: key, value}); newRule.append(declaration); }); root.append(newRule); @@ -94,7 +94,7 @@ module.exports = (opts = {}) => { return { postcssPlugin: "postcss-compile-variables", - Once(root, { Rule, Declaration }) { + Once(root, {Rule, Declaration}) { /* Go through the CSS file once to extract all aliases. We use the extracted alias when resolving derived variables @@ -102,7 +102,7 @@ module.exports = (opts = {}) => { */ root.walkDecls(decl => extractAlias(decl)); root.walkDecls(decl => resolveDerivedVariable(decl, opts)); - addResolvedVariablesToRootSelector(root, opts.variables, { Rule, Declaration }); + addResolvedVariablesToRootSelector(root, opts.variables, {Rule, Declaration}); }, }; }; From ff10297bf88802e35f9a11309e52ee8558348f28 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:22:02 +0530 Subject: [PATCH 60/69] Explicitly convert to number --- postcss/color.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/postcss/color.js b/postcss/color.js index 2251a56e..f61dac1e 100644 --- a/postcss/color.js +++ b/postcss/color.js @@ -17,13 +17,14 @@ limitations under the License. const offColor = require("off-color").offColor; module.exports.derive = function (value, operation, argument) { + const argumentAsNumber = parseInt(argument); switch (operation) { case "darker": { - const newColorString = offColor(value).darken(argument / 100).hex(); + const newColorString = offColor(value).darken(argumentAsNumber / 100).hex(); return newColorString; } case "lighter": { - const newColorString = offColor(value).lighten(argument / 100).hex(); + const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); return newColorString; } } From 9f77df0bff2d1fd9d7f8216f7fca35a28ad08713 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:24:32 +0530 Subject: [PATCH 61/69] Match regex only if declaration is a variable --- postcss/css-compile-variables.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index cf2b451d..d5e78352 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -55,9 +55,11 @@ function resolveDerivedVariable(decl, {variables, derive}) { } function extractAlias(decl) { - const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; - if (decl.prop.startsWith("--") && wholeVariable) { - aliasMap.set(decl.prop, wholeVariable); + if (decl.variable) { + const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; + if (wholeVariable) { + aliasMap.set(decl.prop, wholeVariable); + } } } From 6f4a7e074a4f6b32a6a716957ea1adb850bb94ae Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:27:12 +0530 Subject: [PATCH 62/69] Change confusing doc --- postcss/css-compile-variables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index d5e78352..4adfc7b1 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -87,7 +87,7 @@ function addResolvedVariablesToRootSelector(root, variables, {Rule, Declaration} /** * * @param {Object} opts - Options for the plugin - * @param {Object} opts.variables - An object with records of the form: {base_variable_name: value} + * @param {Object} opts.variables - An object with of the form: {base_variable_name_1: value, base_variable_name_2: value, ...} * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables */ module.exports = (opts = {}) => { From 2c068cc3ced36dd791bf0106ca827b13a13dcd31 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:42:12 +0530 Subject: [PATCH 63/69] typo --- postcss/css-compile-variables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 4adfc7b1..023c8eb7 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -87,7 +87,7 @@ function addResolvedVariablesToRootSelector(root, variables, {Rule, Declaration} /** * * @param {Object} opts - Options for the plugin - * @param {Object} opts.variables - An object with of the form: {base_variable_name_1: value, base_variable_name_2: value, ...} + * @param {Object} opts.variables - An object of the form: {base_variable_name_1: value, base_variable_name_2: value, ...} * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables */ module.exports = (opts = {}) => { From 4020ade70c7cfd15b7665aa26dc07f3657cc98dd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 10 Mar 2022 17:51:25 +0530 Subject: [PATCH 64/69] Remove redundant comment --- postcss/css-compile-variables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postcss/css-compile-variables.js b/postcss/css-compile-variables.js index 023c8eb7..d7eca7f2 100644 --- a/postcss/css-compile-variables.js +++ b/postcss/css-compile-variables.js @@ -21,7 +21,7 @@ let resolvedMap; function getValueFromAlias(alias, variables) { const derivedVariable = aliasMap.get(`--${alias}`); - return variables[derivedVariable] ?? resolvedMap.get(`--${derivedVariable}`); // what if we haven't resolved this variable yet? + return variables[derivedVariable] ?? resolvedMap.get(`--${derivedVariable}`); } function parseDeclarationValue(value) { From bca1648df6a03370fc071c1723f504cc146e8b16 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 14 Mar 2022 11:34:58 +0530 Subject: [PATCH 65/69] Move plugin to /scripts and create eslintrc --- .eslintrc.js | 1 - package.json | 2 +- scripts/.eslintrc.js | 18 ++++++++++++++++++ {postcss => scripts/postcss}/color.js | 0 .../postcss}/css-compile-variables.js | 0 {postcss => scripts/postcss}/test.js | 0 6 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 scripts/.eslintrc.js rename {postcss => scripts/postcss}/color.js (100%) rename {postcss => scripts/postcss}/css-compile-variables.js (100%) rename {postcss => scripts/postcss}/test.js (100%) diff --git a/.eslintrc.js b/.eslintrc.js index 1985ccc1..cb28f4c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,6 @@ module.exports = { "env": { "browser": true, - "node": true, "es6": true }, "extends": "eslint:recommended", diff --git a/package.json b/package.json index fa9c4344..12c73994 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/", - "test:postcss": "impunity --entry-point postcss/test.js ", + "test:postcss": "impunity --entry-point scripts/postcss/test.js ", "start": "vite --port 3000", "build": "vite build", "build:sdk": "./scripts/sdk/build.sh" diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 00000000..1cdfca84 --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + "env": { + "node": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "no-console": "off", + "no-empty": "off", + "no-prototype-builtins": "off", + "no-unused-vars": "warn" + }, +}; + diff --git a/postcss/color.js b/scripts/postcss/color.js similarity index 100% rename from postcss/color.js rename to scripts/postcss/color.js diff --git a/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js similarity index 100% rename from postcss/css-compile-variables.js rename to scripts/postcss/css-compile-variables.js diff --git a/postcss/test.js b/scripts/postcss/test.js similarity index 100% rename from postcss/test.js rename to scripts/postcss/test.js From 19a6d669a94172f38ab7dd50c8d8a95fada4bc5d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 14 Mar 2022 23:26:37 +0530 Subject: [PATCH 66/69] Extract base variables from css --- scripts/postcss/css-compile-variables.js | 37 +++++++++--------- scripts/postcss/test.js | 48 ++++++++++++------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index d7eca7f2..ea939780 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -18,10 +18,11 @@ const valueParser = require("postcss-value-parser"); let aliasMap; let resolvedMap; +let baseVariables; -function getValueFromAlias(alias, variables) { +function getValueFromAlias(alias) { const derivedVariable = aliasMap.get(`--${alias}`); - return variables[derivedVariable] ?? resolvedMap.get(`--${derivedVariable}`); + return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable); } function parseDeclarationValue(value) { @@ -37,14 +38,14 @@ function parseDeclarationValue(value) { return variables; } -function resolveDerivedVariable(decl, {variables, derive}) { +function resolveDerivedVariable(decl, derive) { const RE_VARIABLE_VALUE = /--(.+)--(.+)-(.+)/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); if (matches) { const [wholeVariable, baseVariable, operation, argument] = matches; - const value = variables[baseVariable] ?? getValueFromAlias(baseVariable, variables); + const value = baseVariables.get(`--${baseVariable}`) ?? getValueFromAlias(baseVariable); if (!value) { throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); } @@ -54,22 +55,21 @@ function resolveDerivedVariable(decl, {variables, derive}) { } } -function extractAlias(decl) { +function extract(decl) { if (decl.variable) { - const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; + // see if right side is of form "var(--foo)" + const wholeVariable = decl.value.match(/var\((--.+)\)/)?.[1]; if (wholeVariable) { aliasMap.set(decl.prop, wholeVariable); + // Since this is an alias, we shouldn't store it in baseVariables + return; } + baseVariables.set(decl.prop, decl.value); } } -function addResolvedVariablesToRootSelector(root, variables, {Rule, Declaration}) { +function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { const newRule = new Rule({ selector: ":root", source: root.source }); - // Add base css variables to :root - for (const [key, value] of Object.entries(variables)) { - const declaration = new Declaration({prop: `--${key}`, value}); - newRule.append(declaration); - } // Add derived css variables to :root resolvedMap.forEach((value, key) => { const declaration = new Declaration({prop: key, value}); @@ -87,24 +87,23 @@ function addResolvedVariablesToRootSelector(root, variables, {Rule, Declaration} /** * * @param {Object} opts - Options for the plugin - * @param {Object} opts.variables - An object of the form: {base_variable_name_1: value, base_variable_name_2: value, ...} * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables */ module.exports = (opts = {}) => { aliasMap = new Map(); resolvedMap = new Map(); + baseVariables = new Map(); return { postcssPlugin: "postcss-compile-variables", Once(root, {Rule, Declaration}) { /* - Go through the CSS file once to extract all aliases. - We use the extracted alias when resolving derived variables - later. + Go through the CSS file once to extract all aliases and base variables. + We use these when resolving derived variables later. */ - root.walkDecls(decl => extractAlias(decl)); - root.walkDecls(decl => resolveDerivedVariable(decl, opts)); - addResolvedVariablesToRootSelector(root, opts.variables, {Rule, Declaration}); + root.walkDecls(decl => extract(decl)); + root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive)); + addResolvedVariablesToRootSelector(root, {Rule, Declaration}); }, }; }; diff --git a/scripts/postcss/test.js b/scripts/postcss/test.js index 4ed74c67..36ff9282 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/test.js @@ -31,7 +31,11 @@ async function run(input, output, opts = {}, assert) { module.exports.tests = function tests() { return { "derived variables are resolved": async (assert) => { - const inputCSS = `div { + const inputCSS = ` + :root { + --foo-color: #ff0; + } + div { background-color: var(--foo-color--lighter-50); }`; const transformedColor = offColor("#ff0").lighten(0.5); @@ -39,38 +43,30 @@ module.exports.tests = function tests() { inputCSS + ` :root { - --foo-color: #ff0; --foo-color--lighter-50: ${transformedColor.hex()}; } `; - await run( - inputCSS, - outputCSS, - { variables: { "foo-color": "#ff0" } }, - assert - ); + await run( inputCSS, outputCSS, {}, assert); }, "derived variables work with alias": async (assert) => { - const inputCSS = `div { + const inputCSS = ` + :root { + --icon-color: #fff; + } + div { background: var(--icon-color--darker-20); --my-alias: var(--icon-color--darker-20); color: var(--my-alias--lighter-15); }`; const colorDarker = offColor("#fff").darken(0.2).hex(); const aliasLighter = offColor(colorDarker).lighten(0.15).hex(); - const outputCSS = `div { - background: var(--icon-color--darker-20); - --my-alias: var(--icon-color--darker-20); - color: var(--my-alias--lighter-15); - } - :root { - --icon-color: #fff; + const outputCSS = inputCSS + `:root { --icon-color--darker-20: ${colorDarker}; --my-alias--lighter-15: ${aliasLighter}; } `; - await run(inputCSS, outputCSS, { variables: { "icon-color": "#fff" }, }, assert); + await run(inputCSS, outputCSS, { }, assert); }, "derived variable throws if base not present in config": async (assert) => { @@ -81,7 +77,11 @@ module.exports.tests = function tests() { }, "multiple derived variable in single declaration is parsed correctly": async (assert) => { - const inputCSS = `div { + const inputCSS = ` + :root { + --foo-color: #ff0; + } + div { background-color: linear-gradient(var(--foo-color--lighter-50), var(--foo-color--darker-20)); }`; const transformedColor1 = offColor("#ff0").lighten(0.5); @@ -90,15 +90,18 @@ module.exports.tests = function tests() { inputCSS + ` :root { - --foo-color: #ff0; --foo-color--lighter-50: ${transformedColor1.hex()}; --foo-color--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { variables: { "foo-color": "#ff0" } }, assert); + await run( inputCSS, outputCSS, { }, assert); }, "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { - const inputCSS = `div { + const inputCSS = ` + :root { + --foo-color: #ff0; + } + div { --my-alias: var(--foo-color); background-color: linear-gradient(var(--my-alias--lighter-50), var(--my-alias--darker-20)); }`; @@ -108,12 +111,11 @@ module.exports.tests = function tests() { inputCSS + ` :root { - --foo-color: #ff0; --my-alias--lighter-50: ${transformedColor1.hex()}; --my-alias--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { variables: { "foo-color": "#ff0" } }, assert); + await run( inputCSS, outputCSS, { }, assert); } }; }; From 5d4323cd1dcabaacdfb98bb0b44ce09cc25a739d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 23 Mar 2022 17:12:14 +0530 Subject: [PATCH 67/69] Remove stray "--" from code --- scripts/postcss/css-compile-variables.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index ea939780..23795145 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -21,7 +21,7 @@ let resolvedMap; let baseVariables; function getValueFromAlias(alias) { - const derivedVariable = aliasMap.get(`--${alias}`); + const derivedVariable = aliasMap.get(alias); return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable); } @@ -39,13 +39,13 @@ function parseDeclarationValue(value) { } function resolveDerivedVariable(decl, derive) { - const RE_VARIABLE_VALUE = /--(.+)--(.+)-(.+)/; + const RE_VARIABLE_VALUE = /(--.+)--(.+)-(.+)/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); if (matches) { const [wholeVariable, baseVariable, operation, argument] = matches; - const value = baseVariables.get(`--${baseVariable}`) ?? getValueFromAlias(baseVariable); + const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable); if (!value) { throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); } From 59ca8e63095d7c81e41e2e82647663a5a6e4afe8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 23 Mar 2022 17:25:12 +0530 Subject: [PATCH 68/69] Add explanation of plugin --- scripts/postcss/css-compile-variables.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 23795145..a3c3aa30 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -16,6 +16,20 @@ limitations under the License. const valueParser = require("postcss-value-parser"); +/** + * This plugin derives new css variables from a given set of base variables. + * A derived css variable has the form --base--operation-argument; meaning that the derived + * variable has a value that is generated from the base variable "base" by applying "operation" + * with given "argument". + * + * eg: given the base variable --foo-color: #40E0D0, --foo-color--darker-20 is a css variable + * derived from foo-color by making it 20% more darker. + * + * All derived variables are added to the :root section. + * + * The actual derivation is done outside the plugin in a callback. + */ + let aliasMap; let resolvedMap; let baseVariables; From 72785e7c3ebc5d0ca1c5208011c14831ab859116 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 23 Mar 2022 20:39:24 +0530 Subject: [PATCH 69/69] Remove -- from everywhere --- scripts/postcss/css-compile-variables.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index a3c3aa30..3ed34513 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -53,12 +53,12 @@ function parseDeclarationValue(value) { } function resolveDerivedVariable(decl, derive) { - const RE_VARIABLE_VALUE = /(--.+)--(.+)-(.+)/; + const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); if (matches) { - const [wholeVariable, baseVariable, operation, argument] = matches; + const [, wholeVariable, baseVariable, operation, argument] = matches; const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable); if (!value) { throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); @@ -72,13 +72,15 @@ function resolveDerivedVariable(decl, derive) { function extract(decl) { if (decl.variable) { // see if right side is of form "var(--foo)" - const wholeVariable = decl.value.match(/var\((--.+)\)/)?.[1]; + const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; + // remove -- from the prop + const prop = decl.prop.substring(2); if (wholeVariable) { - aliasMap.set(decl.prop, wholeVariable); + aliasMap.set(prop, wholeVariable); // Since this is an alias, we shouldn't store it in baseVariables return; } - baseVariables.set(decl.prop, decl.value); + baseVariables.set(prop, decl.value); } } @@ -86,7 +88,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root resolvedMap.forEach((value, key) => { - const declaration = new Declaration({prop: key, value}); + const declaration = new Declaration({prop: `--${key}`, value}); newRule.append(declaration); }); root.append(newRule);