Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/element-web-module-api/element-web-module-api.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export interface AccountAuthInfo {
userId: string;
}

// @public
export interface AccountDataApi {
delete(eventType: string): Promise<void>;
get(eventType: string): Watchable<unknown>;
set(eventType: string, content: unknown): Promise<void>;
}

// @alpha @deprecated (undocumented)
export interface AliasCustomisations {
// (undocumented)
Expand All @@ -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
Expand All @@ -46,6 +54,7 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx
readonly i18n: I18nApi;
readonly navigation: NavigationApi;
readonly rootNode: HTMLElement;
readonly stores: StoresApi;
}

// @alpha
Expand All @@ -64,6 +73,12 @@ export interface ChatExportCustomisations<ExportFormat, ExportType> {
};
}

// @public
export interface ClientApi {
accountData: 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;
Expand Down Expand Up @@ -311,11 +326,24 @@ export interface ProfileApiExtension {
readonly profile: Watchable<Profile>;
}

// @public
export interface Room {
getLastActiveTimestamp: () => number;
id: string;
name: Watchable<string>;
}

// @alpha @deprecated (undocumented)
export interface RoomListCustomisations<Room> {
isRoomVisible?(room: Room): boolean;
}

// @public
export interface RoomListStoreApi {
getRooms(): Watchable<Room[]>;
waitForReady(): Promise<void>;
}

// @alpha
export interface RoomViewProps {
roomId?: string;
Expand All @@ -334,6 +362,11 @@ export interface SpacePanelItemProps {
tooltip?: string;
}

// @public
export interface StoresApi {
roomListStore: RoomListStoreApi;
}

// @public
export type Translations = Record<string, {
[ietfLanguageTag: string]: string;
Expand All @@ -359,9 +392,14 @@ export type Variables = {
// @public
export class Watchable<T> {
constructor(currentValue: T);
// Warning: (ae-forgotten-export) The symbol "WatchFn" needs to be exported by the entry point index.d.ts
//
// (undocumented)
unwatch(listener: (value: T) => void): void;
protected readonly listeners: Set<WatchFn<T>>;
protected onFirstWatch(): void;
protected onLastWatch(): void;
// (undocumented)
unwatch(listener: (value: T) => void): void;
get value(): T;
set value(value: T);
// (undocumented)
Expand Down
46 changes: 46 additions & 0 deletions packages/element-web-module-api/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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";
import { Watchable } from "./watchable";

/**
* Modify account data stored on the homeserver.
* @public
*/
export interface AccountDataApi {
/**
* Returns a watchable with account data for this event type.
*/
get(eventType: string): Watchable<unknown>;
/**
* Set account data on the homeserver.
*/
set(eventType: string, content: unknown): Promise<void>;
/**
* Changes the content of this event to be empty.
*/
delete(eventType: string): Promise<void>;
Comment thread
MidhunSureshR marked this conversation as resolved.
}

/**
* Access some limited functionality from the SDK.
* @public
*/
export interface ClientApi {
/**
* Use this to modify account data on the homeserver.
*/
accountData: 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;
}
12 changes: 12 additions & 0 deletions packages/element-web-module-api/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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";

/**
* Module interface for modules to implement.
Expand Down Expand Up @@ -123,6 +125,16 @@ 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.
*/
readonly client: ClientApi;

/**
* Create a ReactDOM root for rendering React components.
* Exposed to allow modules to avoid needing to bundle their own ReactDOM.
Expand Down
36 changes: 36 additions & 0 deletions packages/element-web-module-api/src/api/stores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
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";
import { Watchable } from "./watchable";

/**
* Provides some basic functionality of the Room List Store from element-web.
* @public
*/
export interface RoomListStoreApi {
/**
* Returns a watchable holding a flat list of sorted room.
*/
getRooms(): Watchable<Room[]>;

/**
* Returns a promise that resolves when RLS is ready.
*/
waitForReady(): Promise<void>;
}

/**
* Provides access to certain stores from element-web.
* @public
*/
export interface StoresApi {
/**
* Use this to access limited functionality of the RLS from element-web.
*/
roomListStore: RoomListStoreApi;
}
43 changes: 42 additions & 1 deletion packages/element-web-module-api/src/api/watchable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<number> {
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);
});
28 changes: 26 additions & 2 deletions packages/element-web-module-api/src/api/watchable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ function isObject(value: unknown): value is object {
* @public
*/
export class Watchable<T> {
private readonly listeners = new Set<WatchFn<T>>();
protected readonly listeners = new Set<WatchFn<T>>();

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;
}
Expand All @@ -50,12 +54,32 @@ export class Watchable<T> {
}

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 {}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/element-web-module-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,4 +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";
28 changes: 28 additions & 0 deletions packages/element-web-module-api/src/models/Room.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
/**
* Get the timestamp of the last message in this room.
* @returns last active timestamp
*/
getLastActiveTimestamp: () => number;
Comment thread
MidhunSureshR marked this conversation as resolved.
}