Skip to content

Commit 17a7089

Browse files
Merge pull request #109 from element-hq/midhun/multiroom/client-api
Allow modules to access a part of `MatrixClient` functionality
2 parents 5478cdc + a284c9c commit 17a7089

File tree

8 files changed

+232
-4
lines changed

8 files changed

+232
-4
lines changed

packages/element-web-module-api/element-web-module-api.api.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export interface AccountAuthInfo {
2424
userId: string;
2525
}
2626

27+
// @public
28+
export interface AccountDataApi {
29+
delete(eventType: string): Promise<void>;
30+
get(eventType: string): Watchable<unknown>;
31+
set(eventType: string, content: unknown): Promise<void>;
32+
}
33+
2734
// @alpha @deprecated (undocumented)
2835
export interface AliasCustomisations {
2936
// (undocumented)
@@ -37,6 +44,7 @@ export interface AliasCustomisations {
3744
export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension, DialogApiExtension, AccountAuthApiExtension, ProfileApiExtension {
3845
// @alpha
3946
readonly builtins: BuiltinsApi;
47+
readonly client: ClientApi;
4048
readonly config: ConfigApi;
4149
createRoot(element: Element): Root;
4250
// @alpha
@@ -46,6 +54,7 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx
4654
readonly i18n: I18nApi;
4755
readonly navigation: NavigationApi;
4856
readonly rootNode: HTMLElement;
57+
readonly stores: StoresApi;
4958
}
5059

5160
// @alpha
@@ -65,6 +74,12 @@ export interface ChatExportCustomisations<ExportFormat, ExportType> {
6574
};
6675
}
6776

77+
// @public
78+
export interface ClientApi {
79+
accountData: AccountDataApi;
80+
getRoom: (id: string) => Room | null;
81+
}
82+
6883
// @alpha @deprecated (undocumented)
6984
export interface ComponentVisibilityCustomisations {
7085
shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean;
@@ -312,11 +327,24 @@ export interface ProfileApiExtension {
312327
readonly profile: Watchable<Profile>;
313328
}
314329

330+
// @public
331+
export interface Room {
332+
getLastActiveTimestamp: () => number;
333+
id: string;
334+
name: Watchable<string>;
335+
}
336+
315337
// @alpha @deprecated (undocumented)
316338
export interface RoomListCustomisations<Room> {
317339
isRoomVisible?(room: Room): boolean;
318340
}
319341

342+
// @public
343+
export interface RoomListStoreApi {
344+
getRooms(): Watchable<Room[]>;
345+
waitForReady(): Promise<void>;
346+
}
347+
320348
// @alpha
321349
export interface RoomViewProps {
322350
roomId?: string;
@@ -335,6 +363,11 @@ export interface SpacePanelItemProps {
335363
tooltip?: string;
336364
}
337365

366+
// @public
367+
export interface StoresApi {
368+
roomListStore: RoomListStoreApi;
369+
}
370+
338371
// @public
339372
export type Translations = Record<string, {
340373
[ietfLanguageTag: string]: string;
@@ -360,9 +393,14 @@ export type Variables = {
360393
// @public
361394
export class Watchable<T> {
362395
constructor(currentValue: T);
396+
// Warning: (ae-forgotten-export) The symbol "WatchFn" needs to be exported by the entry point index.d.ts
397+
//
363398
// (undocumented)
364-
unwatch(listener: (value: T) => void): void;
399+
protected readonly listeners: Set<WatchFn<T>>;
400+
protected onFirstWatch(): void;
401+
protected onLastWatch(): void;
365402
// (undocumented)
403+
unwatch(listener: (value: T) => void): void;
366404
get value(): T;
367405
set value(value: T);
368406
// (undocumented)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import type { Room } from "../models/Room";
9+
import { Watchable } from "./watchable";
10+
11+
/**
12+
* Modify account data stored on the homeserver.
13+
* @public
14+
*/
15+
export interface AccountDataApi {
16+
/**
17+
* Returns a watchable with account data for this event type.
18+
*/
19+
get(eventType: string): Watchable<unknown>;
20+
/**
21+
* Set account data on the homeserver.
22+
*/
23+
set(eventType: string, content: unknown): Promise<void>;
24+
/**
25+
* Changes the content of this event to be empty.
26+
*/
27+
delete(eventType: string): Promise<void>;
28+
}
29+
30+
/**
31+
* Access some limited functionality from the SDK.
32+
* @public
33+
*/
34+
export interface ClientApi {
35+
/**
36+
* Use this to modify account data on the homeserver.
37+
*/
38+
accountData: AccountDataApi;
39+
40+
/**
41+
* Fetch room by id from SDK.
42+
* @param id - Id of the room to get
43+
* @returns Room object from SDK
44+
*/
45+
getRoom: (id: string) => Room | null;
46+
}

packages/element-web-module-api/src/api/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { AccountAuthApiExtension } from "./auth.ts";
1717
import { ProfileApiExtension } from "./profile.ts";
1818
import { ExtrasApi } from "./extras.ts";
1919
import { BuiltinsApi } from "./builtins.ts";
20+
import { StoresApi } from "./stores.ts";
21+
import { ClientApi } from "./client.ts";
2022

2123
/**
2224
* Module interface for modules to implement.
@@ -123,6 +125,16 @@ export interface Api
123125
*/
124126
readonly extras: ExtrasApi;
125127

128+
/**
129+
* Allows modules to access a limited functionality of certain stores from Element Web.
130+
*/
131+
readonly stores: StoresApi;
132+
133+
/**
134+
* Access some very specific functionality from the client.
135+
*/
136+
readonly client: ClientApi;
137+
126138
/**
127139
* Create a ReactDOM root for rendering React components.
128140
* Exposed to allow modules to avoid needing to bundle their own ReactDOM.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import type { Room } from "../models/Room";
9+
import { Watchable } from "./watchable";
10+
11+
/**
12+
* Provides some basic functionality of the Room List Store from element-web.
13+
* @public
14+
*/
15+
export interface RoomListStoreApi {
16+
/**
17+
* Returns a watchable holding a flat list of sorted room.
18+
*/
19+
getRooms(): Watchable<Room[]>;
20+
21+
/**
22+
* Returns a promise that resolves when RLS is ready.
23+
*/
24+
waitForReady(): Promise<void>;
25+
}
26+
27+
/**
28+
* Provides access to certain stores from element-web.
29+
* @public
30+
*/
31+
export interface StoresApi {
32+
/**
33+
* Use this to access limited functionality of the RLS from element-web.
34+
*/
35+
roomListStore: RoomListStoreApi;
36+
}

packages/element-web-module-api/src/api/watchable.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import { expect, test, vitest } from "vitest";
8+
import { expect, test, vi, vitest } from "vitest";
99

1010
import { Watchable } from "./watchable";
1111

@@ -56,3 +56,44 @@ test("when value is an object, shallow comparison works", () => {
5656

5757
watchable.unwatch(listener); // Clean up after the test
5858
});
59+
60+
test("onFirstWatch and onLastWatch are called when appropriate", () => {
61+
const onFirstWatch = vi.fn();
62+
const onLastWatch = vi.fn();
63+
class CustomWatchable extends Watchable<number> {
64+
protected onFirstWatch(): void {
65+
onFirstWatch();
66+
}
67+
protected onLastWatch(): void {
68+
onLastWatch();
69+
}
70+
}
71+
72+
const watchable = new CustomWatchable(10);
73+
// No listeners yet, so expect no calls
74+
expect(onFirstWatch).not.toHaveBeenCalled();
75+
expect(onLastWatch).not.toHaveBeenCalled();
76+
77+
// Let's say that we have three listeners
78+
const listeners = [vi.fn(), vi.fn(), vi.fn()];
79+
80+
// Let's add all of them via watch
81+
for (const listener of listeners) {
82+
watchable.watch(listener);
83+
}
84+
85+
// Only expect onFirstWatch() to have been called once
86+
expect(onFirstWatch).toHaveBeenCalledOnce();
87+
88+
// Let's remove all the listeners
89+
for (const listener of listeners) {
90+
watchable.unwatch(listener);
91+
}
92+
93+
// Only expect onLastWatch to have been called once
94+
expect(onLastWatch).toHaveBeenCalledOnce();
95+
96+
// Should call onFirstWatch again once we have more listeners
97+
watchable.watch(vi.fn());
98+
expect(onFirstWatch).toHaveBeenCalledTimes(2);
99+
});

packages/element-web-module-api/src/api/watchable.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ function isObject(value: unknown): value is object {
2626
* @public
2727
*/
2828
export class Watchable<T> {
29-
private readonly listeners = new Set<WatchFn<T>>();
29+
protected readonly listeners = new Set<WatchFn<T>>();
3030

3131
public constructor(private currentValue: T) {}
3232

33+
/**
34+
* The value stored in this watchable.
35+
* Warning: Could potentially return stale data if you haven't called {@link Watchable#watch}.
36+
*/
3337
public get value(): T {
3438
return this.currentValue;
3539
}
@@ -50,12 +54,32 @@ export class Watchable<T> {
5054
}
5155

5256
public watch(listener: (value: T) => void): void {
57+
// Call onFirstWatch if there was no listener before.
58+
if (this.listeners.size === 0) {
59+
this.onFirstWatch();
60+
}
5361
this.listeners.add(listener);
5462
}
5563

5664
public unwatch(listener: (value: T) => void): void {
57-
this.listeners.delete(listener);
65+
const hasDeleted = this.listeners.delete(listener);
66+
// Call onLastWatch if every listener has been removed.
67+
if (hasDeleted && this.listeners.size === 0) {
68+
this.onLastWatch();
69+
}
5870
}
71+
72+
/**
73+
* This is called when the number of listeners go from zero to one.
74+
* Could be used to add external event listeners.
75+
*/
76+
protected onFirstWatch(): void {}
77+
78+
/**
79+
* This is called when the number of listeners go from one to zero.
80+
* Could be used to remove external event listeners.
81+
*/
82+
protected onLastWatch(): void {}
5983
}
6084

6185
/**

packages/element-web-module-api/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type { Api, Module, ModuleFactory } from "./api";
1010
export type { Config, ConfigApi } from "./api/config";
1111
export type { I18nApi, Variables, Translations } from "./api/i18n";
1212
export type * from "./models/event";
13+
export type * from "./models/Room";
1314
export type * from "./api/custom-components";
1415
export type * from "./api/extras";
1516
export type * from "./api/legacy-modules";
@@ -19,4 +20,6 @@ export type * from "./api/dialog";
1920
export type * from "./api/profile";
2021
export type * from "./api/navigation";
2122
export type * from "./api/builtins";
23+
export type * from "./api/stores";
24+
export type * from "./api/client";
2225
export * from "./api/watchable";
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { Watchable } from "../api/watchable";
9+
10+
/**
11+
* Represents a room from element-web.
12+
* @public
13+
*/
14+
export interface Room {
15+
/**
16+
* Id of this room.
17+
*/
18+
id: string;
19+
/**
20+
* {@link Watchable} holding the name for this room.
21+
*/
22+
name: Watchable<string>;
23+
/**
24+
* Get the timestamp of the last message in this room.
25+
* @returns last active timestamp
26+
*/
27+
getLastActiveTimestamp: () => number;
28+
}

0 commit comments

Comments
 (0)