From c24cfb63110a3382a5df9d4754aadb8c936ab0e9 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:18:33 +0530 Subject: [PATCH 01/14] Introduce an abstraction for Room All APIs that need to return a room will use this type. --- packages/element-web-module-api/src/index.ts | 1 + .../element-web-module-api/src/models/Room.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/element-web-module-api/src/models/Room.ts diff --git a/packages/element-web-module-api/src/index.ts b/packages/element-web-module-api/src/index.ts index e89adc482d..4935d10681 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -10,6 +10,7 @@ export type { Api, Module, ModuleFactory } from "./api"; export type { Config, ConfigApi } from "./api/config"; export type { I18nApi, Variables, Translations } from "./api/i18n"; export type * from "./models/event"; +export type * from "./models/Room"; export type * from "./api/custom-components"; export type * from "./api/extras"; export type * from "./api/legacy-modules"; diff --git a/packages/element-web-module-api/src/models/Room.ts b/packages/element-web-module-api/src/models/Room.ts new file mode 100644 index 0000000000..df2f1c8643 --- /dev/null +++ b/packages/element-web-module-api/src/models/Room.ts @@ -0,0 +1,28 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { Watchable } from "../api/watchable"; + +/** + * Represents a room from element-web. + * @public + */ +export interface Room { + /** + * Id of this room. + */ + id: string; + /** + * {@link Watchable} holding the name for this room. + */ + name: Watchable; + /** + * Get the timestamp of the last message in this room. + * @returns last active timestamp + */ + getLastActiveTimestamp: () => number; +} From 949e64c7b526c9c5a81fbbf2a1ba990a92981949 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:19:22 +0530 Subject: [PATCH 02/14] Make field protected so that Watchable can be extended So that you can have custom watchables that have very specific behaviour, think `class NameWatchable extends Watchable`. Useful to have access to `listeners` when you do this. --- packages/element-web-module-api/src/api/watchable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/element-web-module-api/src/api/watchable.ts b/packages/element-web-module-api/src/api/watchable.ts index 2f82a9b732..3fda595d2c 100644 --- a/packages/element-web-module-api/src/api/watchable.ts +++ b/packages/element-web-module-api/src/api/watchable.ts @@ -26,7 +26,7 @@ function isObject(value: unknown): value is object { * @public */ export class Watchable { - private readonly listeners = new Set>(); + protected readonly listeners = new Set>(); public constructor(private currentValue: T) {} From 00f9283262b01935da40d06c4dc23fa6130e6f3b Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:21:22 +0530 Subject: [PATCH 03/14] Add client API --- .../element-web-module-api/src/api/client.ts | 45 +++++++++++++++++++ .../element-web-module-api/src/api/index.ts | 6 +++ packages/element-web-module-api/src/index.ts | 1 + 3 files changed, 52 insertions(+) create mode 100644 packages/element-web-module-api/src/api/client.ts diff --git a/packages/element-web-module-api/src/api/client.ts b/packages/element-web-module-api/src/api/client.ts new file mode 100644 index 0000000000..1b9bed97dd --- /dev/null +++ b/packages/element-web-module-api/src/api/client.ts @@ -0,0 +1,45 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { Room } from "../models/Room"; + +/** + * Modify account data stored on the homeserver. + * @public + */ +export interface AccountDataApi { + /** + * Fetch account data stored from homeserver. + */ + get(eventType: string): unknown; + /** + * Sett account data on the homeserver. + */ + set(eventType: string, content: unknown): Promise; + /** + * Delete account data from homeserver. + */ + delete(eventType: string): Promise; +} + +/** + * Access some limited functionality from the SDK. + * @public + */ +export interface ClientApi { + /** + * Use this to modify account data on the homeserver. + */ + getAccountDataApi: () => AccountDataApi; + + /** + * Fetch room by id from SDK. + * @param id - Id of the room to get + * @returns Room object from SDK + */ + getRoom: (id: string) => Room | null; +} diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index dacc7501be..f95479012c 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -17,6 +17,7 @@ import { AccountAuthApiExtension } from "./auth.ts"; import { ProfileApiExtension } from "./profile.ts"; import { ExtrasApi } from "./extras.ts"; import { BuiltinsApi } from "./builtins.ts"; +import { ClientApi } from "./client.ts"; /** * Module interface for modules to implement. @@ -123,6 +124,11 @@ export interface Api */ readonly extras: ExtrasApi; + /** + * Access some very specific functionality from the client. + */ + readonly client: ClientApi; + /** * Create a ReactDOM root for rendering React components. * Exposed to allow modules to avoid needing to bundle their own ReactDOM. diff --git a/packages/element-web-module-api/src/index.ts b/packages/element-web-module-api/src/index.ts index 4935d10681..64f2002a27 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -20,4 +20,5 @@ export type * from "./api/dialog"; export type * from "./api/profile"; export type * from "./api/navigation"; export type * from "./api/builtins"; +export type * from "./api/client"; export * from "./api/watchable"; From d0923c7b76f72d3bf01a75b55c48fe472098b6d7 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:22:39 +0530 Subject: [PATCH 04/14] Update api doc --- .../element-web-module-api.api.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/element-web-module-api/element-web-module-api.api.md b/packages/element-web-module-api/element-web-module-api.api.md index 2d341fde03..042f83601b 100644 --- a/packages/element-web-module-api/element-web-module-api.api.md +++ b/packages/element-web-module-api/element-web-module-api.api.md @@ -24,6 +24,13 @@ export interface AccountAuthInfo { userId: string; } +// @public +export interface AccountDataApi { + delete(eventType: string): Promise; + get(eventType: string): unknown; + set(eventType: string, content: unknown): Promise; +} + // @alpha @deprecated (undocumented) export interface AliasCustomisations { // (undocumented) @@ -37,6 +44,7 @@ export interface AliasCustomisations { export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension, DialogApiExtension, AccountAuthApiExtension, ProfileApiExtension { // @alpha readonly builtins: BuiltinsApi; + readonly client: ClientApi; readonly config: ConfigApi; createRoot(element: Element): Root; // @alpha @@ -64,6 +72,12 @@ export interface ChatExportCustomisations { }; } +// @public +export interface ClientApi { + getAccountDataApi: () => AccountDataApi; + getRoom: (id: string) => Room | null; +} + // @alpha @deprecated (undocumented) export interface ComponentVisibilityCustomisations { shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean; @@ -311,6 +325,13 @@ export interface ProfileApiExtension { readonly profile: Watchable; } +// @public +export interface Room { + getLastActiveTimestamp: () => number; + id: string; + name: Watchable; +} + // @alpha @deprecated (undocumented) export interface RoomListCustomisations { isRoomVisible?(room: Room): boolean; @@ -359,6 +380,10 @@ export type Variables = { // @public export class Watchable { constructor(currentValue: T); + // Warning: (ae-forgotten-export) The symbol "WatchFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + protected readonly listeners: Set>; // (undocumented) unwatch(listener: (value: T) => void): void; // (undocumented) From 7c40be90546a73b26ae618b3f552c7acd68db73e Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 16:07:07 +0530 Subject: [PATCH 05/14] Change method to property --- packages/element-web-module-api/src/api/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/element-web-module-api/src/api/client.ts b/packages/element-web-module-api/src/api/client.ts index 1b9bed97dd..b7e70abaf9 100644 --- a/packages/element-web-module-api/src/api/client.ts +++ b/packages/element-web-module-api/src/api/client.ts @@ -34,7 +34,7 @@ export interface ClientApi { /** * Use this to modify account data on the homeserver. */ - getAccountDataApi: () => AccountDataApi; + accountData: AccountDataApi; /** * Fetch room by id from SDK. From 17f1a54a1facaf6e751d80374374cddda395789b Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 16:11:50 +0530 Subject: [PATCH 06/14] Fix comment --- packages/element-web-module-api/src/api/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/element-web-module-api/src/api/client.ts b/packages/element-web-module-api/src/api/client.ts index b7e70abaf9..e670f38432 100644 --- a/packages/element-web-module-api/src/api/client.ts +++ b/packages/element-web-module-api/src/api/client.ts @@ -17,11 +17,11 @@ export interface AccountDataApi { */ get(eventType: string): unknown; /** - * Sett account data on the homeserver. + * Set account data on the homeserver. */ set(eventType: string, content: unknown): Promise; /** - * Delete account data from homeserver. + * Changes the content of this event to be empty. */ delete(eventType: string): Promise; } From cd9a21ac9391e2c45cdf2b2a7de2392c09aac803 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 23:38:31 +0530 Subject: [PATCH 07/14] Add onFirstWatch and onLastWatch to watchable So that we can create custom watchable objects that can add/remove event listeners as necessary. --- .../src/api/watchable.test.ts | 43 ++++++++++++++++++- .../src/api/watchable.ts | 26 ++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/element-web-module-api/src/api/watchable.test.ts b/packages/element-web-module-api/src/api/watchable.test.ts index c045286e2c..e55695c29c 100644 --- a/packages/element-web-module-api/src/api/watchable.test.ts +++ b/packages/element-web-module-api/src/api/watchable.test.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import { expect, test, vitest } from "vitest"; +import { expect, test, vi, vitest } from "vitest"; import { Watchable } from "./watchable"; @@ -56,3 +56,44 @@ test("when value is an object, shallow comparison works", () => { watchable.unwatch(listener); // Clean up after the test }); + +test("onFirstWatch and onLastWatch are called when appropriate", () => { + const onFirstWatch = vi.fn(); + const onLastWatch = vi.fn(); + class CustomWatchable extends Watchable { + protected onFirstWatch(): void { + onFirstWatch(); + } + protected onLastWatch(): void { + onLastWatch(); + } + } + + const watchable = new CustomWatchable(10); + // No listeners yet, so expect no calls + expect(onFirstWatch).not.toHaveBeenCalled(); + expect(onLastWatch).not.toHaveBeenCalled(); + + // Let's say that we have three listeners + const listeners = [vi.fn(), vi.fn(), vi.fn()]; + + // Let's add all of them via watch + for (const listener of listeners) { + watchable.watch(listener); + } + + // Only expect onFirstWatch() to have been called once + expect(onFirstWatch).toHaveBeenCalledOnce(); + + // Let's remove all the listeners + for (const listener of listeners) { + watchable.unwatch(listener); + } + + // Only expect onLastWatch to have been called once + expect(onLastWatch).toHaveBeenCalledOnce(); + + // Should call onFirstWatch again once we have more listeners + watchable.watch(vi.fn()); + expect(onFirstWatch).toHaveBeenCalledTimes(2); +}); diff --git a/packages/element-web-module-api/src/api/watchable.ts b/packages/element-web-module-api/src/api/watchable.ts index 3fda595d2c..69296571c8 100644 --- a/packages/element-web-module-api/src/api/watchable.ts +++ b/packages/element-web-module-api/src/api/watchable.ts @@ -30,6 +30,10 @@ export class Watchable { public constructor(private currentValue: T) {} + /** + * The value stored in this watchable. + * Warning: Could potentially return stale data if you haven't called {@link Watchable#watch}. + */ public get value(): T { return this.currentValue; } @@ -50,12 +54,32 @@ export class Watchable { } public watch(listener: (value: T) => void): void { + // Call onFirstWatch if there was no listener before. + if (this.listeners.size === 0) { + this.onFirstWatch(); + } this.listeners.add(listener); } public unwatch(listener: (value: T) => void): void { - this.listeners.delete(listener); + const hasDeleted = this.listeners.delete(listener); + // Call onLastWatch if every listener has been removed. + if (hasDeleted && this.listeners.size === 0) { + this.onLastWatch(); + } } + + /** + * This is called when the number of listeners go from zero to one. + * Could be used to add external event listeners. + */ + protected onFirstWatch(): void {} + + /** + * This is called when the number of listeners go from one to zero. + * Could be used to remove external event listeners. + */ + protected onLastWatch(): void {} } /** From 04379cc0897534a3c49b1953ec2597dd2c33c560 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 23:41:08 +0530 Subject: [PATCH 08/14] Return watchable in account data api --- packages/element-web-module-api/src/api/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/element-web-module-api/src/api/client.ts b/packages/element-web-module-api/src/api/client.ts index e670f38432..45f8e77e30 100644 --- a/packages/element-web-module-api/src/api/client.ts +++ b/packages/element-web-module-api/src/api/client.ts @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import type { Room } from "../models/Room"; +import { Watchable } from "./watchable"; /** * Modify account data stored on the homeserver. @@ -13,9 +14,9 @@ import type { Room } from "../models/Room"; */ export interface AccountDataApi { /** - * Fetch account data stored from homeserver. + * Returns a watchable with account data for this event type. */ - get(eventType: string): unknown; + get(eventType: string): Watchable; /** * Set account data on the homeserver. */ From 6b4f33bee1760d06463049830ba52813c8ecf573 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 23:45:36 +0530 Subject: [PATCH 09/14] Update API doc --- .../element-web-module-api/element-web-module-api.api.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/element-web-module-api/element-web-module-api.api.md b/packages/element-web-module-api/element-web-module-api.api.md index 042f83601b..4fcbd748a9 100644 --- a/packages/element-web-module-api/element-web-module-api.api.md +++ b/packages/element-web-module-api/element-web-module-api.api.md @@ -27,7 +27,7 @@ export interface AccountAuthInfo { // @public export interface AccountDataApi { delete(eventType: string): Promise; - get(eventType: string): unknown; + get(eventType: string): Watchable; set(eventType: string, content: unknown): Promise; } @@ -74,7 +74,7 @@ export interface ChatExportCustomisations { // @public export interface ClientApi { - getAccountDataApi: () => AccountDataApi; + accountData: AccountDataApi; getRoom: (id: string) => Room | null; } @@ -384,9 +384,10 @@ export class Watchable { // // (undocumented) protected readonly listeners: Set>; + protected onFirstWatch(): void; + protected onLastWatch(): void; // (undocumented) unwatch(listener: (value: T) => void): void; - // (undocumented) get value(): T; set value(value: T); // (undocumented) From 200d52a1621ecdc5790cd80d4a5d94762a2ed32c Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:32:01 +0530 Subject: [PATCH 10/14] Add Stores API --- .../element-web-module-api/src/api/index.ts | 6 ++++ .../element-web-module-api/src/api/stores.ts | 35 +++++++++++++++++++ packages/element-web-module-api/src/index.ts | 1 + 3 files changed, 42 insertions(+) create mode 100644 packages/element-web-module-api/src/api/stores.ts diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index f95479012c..f99a8bbbe8 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -17,6 +17,7 @@ import { AccountAuthApiExtension } from "./auth.ts"; import { ProfileApiExtension } from "./profile.ts"; import { ExtrasApi } from "./extras.ts"; import { BuiltinsApi } from "./builtins.ts"; +import { StoresApi } from "./stores.ts"; import { ClientApi } from "./client.ts"; /** @@ -124,6 +125,11 @@ export interface Api */ readonly extras: ExtrasApi; + /** + * Allows modules to access a limited functionality of certain stores from Element Web. + */ + readonly stores: StoresApi; + /** * Access some very specific functionality from the client. */ diff --git a/packages/element-web-module-api/src/api/stores.ts b/packages/element-web-module-api/src/api/stores.ts new file mode 100644 index 0000000000..469a52bdd2 --- /dev/null +++ b/packages/element-web-module-api/src/api/stores.ts @@ -0,0 +1,35 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { Room } from "../models/Room"; + +/** + * Provides some basic functionality of the Room List Store from element-web. + * @public + */ +export interface RoomListStoreApi { + /** + * Get a flat list of sorted room from the RLS. + */ + getRooms(): Room[]; + + /** + * Returns a promise that resolves when RLS is ready. + */ + waitForReady(): Promise; +} + +/** + * Provides access to certain stores from element-web. + * @public + */ +export interface StoresApi { + /** + * Use this to access limited functionality of the RLS from element-web. + */ + getRoomListStore(): RoomListStoreApi; +} diff --git a/packages/element-web-module-api/src/index.ts b/packages/element-web-module-api/src/index.ts index 64f2002a27..52a0593bc5 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -20,5 +20,6 @@ export type * from "./api/dialog"; export type * from "./api/profile"; export type * from "./api/navigation"; export type * from "./api/builtins"; +export type * from "./api/stores"; export type * from "./api/client"; export * from "./api/watchable"; From 97e146247492b969139d75d894428c182bc49948 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 23 Oct 2025 12:32:48 +0530 Subject: [PATCH 11/14] Updat api doc --- .../element-web-module-api.api.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/element-web-module-api/element-web-module-api.api.md b/packages/element-web-module-api/element-web-module-api.api.md index 4fcbd748a9..48b781fa2d 100644 --- a/packages/element-web-module-api/element-web-module-api.api.md +++ b/packages/element-web-module-api/element-web-module-api.api.md @@ -54,6 +54,7 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx readonly i18n: I18nApi; readonly navigation: NavigationApi; readonly rootNode: HTMLElement; + readonly stores: StoresApi; } // @alpha @@ -337,6 +338,12 @@ export interface RoomListCustomisations { isRoomVisible?(room: Room): boolean; } +// @public +export interface RoomListStoreApi { + getRooms(): Room[]; + waitForReady(): Promise; +} + // @alpha export interface RoomViewProps { roomId?: string; @@ -355,6 +362,11 @@ export interface SpacePanelItemProps { tooltip?: string; } +// @public +export interface StoresApi { + getRoomListStore(): RoomListStoreApi; +} + // @public export type Translations = Record Date: Fri, 24 Oct 2025 00:28:54 +0530 Subject: [PATCH 12/14] Convert to property --- packages/element-web-module-api/src/api/stores.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/element-web-module-api/src/api/stores.ts b/packages/element-web-module-api/src/api/stores.ts index 469a52bdd2..e27a72266d 100644 --- a/packages/element-web-module-api/src/api/stores.ts +++ b/packages/element-web-module-api/src/api/stores.ts @@ -31,5 +31,5 @@ export interface StoresApi { /** * Use this to access limited functionality of the RLS from element-web. */ - getRoomListStore(): RoomListStoreApi; + roomListStore: RoomListStoreApi; } From 4ee9b05608aa632ddeaf165d31d1bf0c0fb45f1a Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 24 Oct 2025 00:29:18 +0530 Subject: [PATCH 13/14] Return a watchable --- packages/element-web-module-api/src/api/stores.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/element-web-module-api/src/api/stores.ts b/packages/element-web-module-api/src/api/stores.ts index e27a72266d..057caec87c 100644 --- a/packages/element-web-module-api/src/api/stores.ts +++ b/packages/element-web-module-api/src/api/stores.ts @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import type { Room } from "../models/Room"; +import { Watchable } from "./watchable"; /** * Provides some basic functionality of the Room List Store from element-web. @@ -13,9 +14,9 @@ import type { Room } from "../models/Room"; */ export interface RoomListStoreApi { /** - * Get a flat list of sorted room from the RLS. + * Returns a watchable holding a flat list of sorted room. */ - getRooms(): Room[]; + getRooms(): Watchable; /** * Returns a promise that resolves when RLS is ready. From 4381032d63a5fdc5c81aa308a2f4d121acd2d8c5 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 24 Oct 2025 00:30:01 +0530 Subject: [PATCH 14/14] Update API doc --- packages/element-web-module-api/element-web-module-api.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/element-web-module-api/element-web-module-api.api.md b/packages/element-web-module-api/element-web-module-api.api.md index 48b781fa2d..0b836bd551 100644 --- a/packages/element-web-module-api/element-web-module-api.api.md +++ b/packages/element-web-module-api/element-web-module-api.api.md @@ -340,7 +340,7 @@ export interface RoomListCustomisations { // @public export interface RoomListStoreApi { - getRooms(): Room[]; + getRooms(): Watchable; waitForReady(): Promise; } @@ -364,7 +364,7 @@ export interface SpacePanelItemProps { // @public export interface StoresApi { - getRoomListStore(): RoomListStoreApi; + roomListStore: RoomListStoreApi; } // @public