Merge pull request #109 from element-hq/midhun/multiroom/client-api
Allow modules to access a part of `MatrixClient` functionality
This commit is contained in:
@@ -24,6 +24,13 @@ export interface AccountAuthInfo {
|
|||||||
userId: string;
|
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)
|
// @alpha @deprecated (undocumented)
|
||||||
export interface AliasCustomisations {
|
export interface AliasCustomisations {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
@@ -37,6 +44,7 @@ export interface AliasCustomisations {
|
|||||||
export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension, DialogApiExtension, AccountAuthApiExtension, ProfileApiExtension {
|
export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension, DialogApiExtension, AccountAuthApiExtension, ProfileApiExtension {
|
||||||
// @alpha
|
// @alpha
|
||||||
readonly builtins: BuiltinsApi;
|
readonly builtins: BuiltinsApi;
|
||||||
|
readonly client: ClientApi;
|
||||||
readonly config: ConfigApi;
|
readonly config: ConfigApi;
|
||||||
createRoot(element: Element): Root;
|
createRoot(element: Element): Root;
|
||||||
// @alpha
|
// @alpha
|
||||||
@@ -46,6 +54,7 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx
|
|||||||
readonly i18n: I18nApi;
|
readonly i18n: I18nApi;
|
||||||
readonly navigation: NavigationApi;
|
readonly navigation: NavigationApi;
|
||||||
readonly rootNode: HTMLElement;
|
readonly rootNode: HTMLElement;
|
||||||
|
readonly stores: StoresApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @alpha
|
// @alpha
|
||||||
@@ -65,6 +74,12 @@ export interface ChatExportCustomisations<ExportFormat, ExportType> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface ClientApi {
|
||||||
|
accountData: AccountDataApi;
|
||||||
|
getRoom: (id: string) => Room | null;
|
||||||
|
}
|
||||||
|
|
||||||
// @alpha @deprecated (undocumented)
|
// @alpha @deprecated (undocumented)
|
||||||
export interface ComponentVisibilityCustomisations {
|
export interface ComponentVisibilityCustomisations {
|
||||||
shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean;
|
shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean;
|
||||||
@@ -312,11 +327,24 @@ export interface ProfileApiExtension {
|
|||||||
readonly profile: Watchable<Profile>;
|
readonly profile: Watchable<Profile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface Room {
|
||||||
|
getLastActiveTimestamp: () => number;
|
||||||
|
id: string;
|
||||||
|
name: Watchable<string>;
|
||||||
|
}
|
||||||
|
|
||||||
// @alpha @deprecated (undocumented)
|
// @alpha @deprecated (undocumented)
|
||||||
export interface RoomListCustomisations<Room> {
|
export interface RoomListCustomisations<Room> {
|
||||||
isRoomVisible?(room: Room): boolean;
|
isRoomVisible?(room: Room): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface RoomListStoreApi {
|
||||||
|
getRooms(): Watchable<Room[]>;
|
||||||
|
waitForReady(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
// @alpha
|
// @alpha
|
||||||
export interface RoomViewProps {
|
export interface RoomViewProps {
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
@@ -335,6 +363,11 @@ export interface SpacePanelItemProps {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface StoresApi {
|
||||||
|
roomListStore: RoomListStoreApi;
|
||||||
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type Translations = Record<string, {
|
export type Translations = Record<string, {
|
||||||
[ietfLanguageTag: string]: string;
|
[ietfLanguageTag: string]: string;
|
||||||
@@ -360,9 +393,14 @@ export type Variables = {
|
|||||||
// @public
|
// @public
|
||||||
export class Watchable<T> {
|
export class Watchable<T> {
|
||||||
constructor(currentValue: T);
|
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<WatchFn<T>>;
|
||||||
|
protected onFirstWatch(): void;
|
||||||
|
protected onLastWatch(): void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
unwatch(listener: (value: T) => void): void;
|
unwatch(listener: (value: T) => void): void;
|
||||||
// (undocumented)
|
|
||||||
get value(): T;
|
get value(): T;
|
||||||
set value(value: T);
|
set value(value: T);
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import { AccountAuthApiExtension } from "./auth.ts";
|
|||||||
import { ProfileApiExtension } from "./profile.ts";
|
import { ProfileApiExtension } from "./profile.ts";
|
||||||
import { ExtrasApi } from "./extras.ts";
|
import { ExtrasApi } from "./extras.ts";
|
||||||
import { BuiltinsApi } from "./builtins.ts";
|
import { BuiltinsApi } from "./builtins.ts";
|
||||||
|
import { StoresApi } from "./stores.ts";
|
||||||
|
import { ClientApi } from "./client.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module interface for modules to implement.
|
* Module interface for modules to implement.
|
||||||
@@ -123,6 +125,16 @@ export interface Api
|
|||||||
*/
|
*/
|
||||||
readonly extras: ExtrasApi;
|
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.
|
* Create a ReactDOM root for rendering React components.
|
||||||
* Exposed to allow modules to avoid needing to bundle their own ReactDOM.
|
* Exposed to allow modules to avoid needing to bundle their own ReactDOM.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.
|
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";
|
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
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ function isObject(value: unknown): value is object {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class Watchable<T> {
|
export class Watchable<T> {
|
||||||
private readonly listeners = new Set<WatchFn<T>>();
|
protected readonly listeners = new Set<WatchFn<T>>();
|
||||||
|
|
||||||
public constructor(private currentValue: 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 {
|
public get value(): T {
|
||||||
return this.currentValue;
|
return this.currentValue;
|
||||||
}
|
}
|
||||||
@@ -50,12 +54,32 @@ export class Watchable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public watch(listener: (value: T) => void): void {
|
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);
|
this.listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unwatch(listener: (value: T) => void): void {
|
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 {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type { Api, Module, ModuleFactory } from "./api";
|
|||||||
export type { Config, ConfigApi } from "./api/config";
|
export type { Config, ConfigApi } from "./api/config";
|
||||||
export type { I18nApi, Variables, Translations } from "./api/i18n";
|
export type { I18nApi, Variables, Translations } from "./api/i18n";
|
||||||
export type * from "./models/event";
|
export type * from "./models/event";
|
||||||
|
export type * from "./models/Room";
|
||||||
export type * from "./api/custom-components";
|
export type * from "./api/custom-components";
|
||||||
export type * from "./api/extras";
|
export type * from "./api/extras";
|
||||||
export type * from "./api/legacy-modules";
|
export type * from "./api/legacy-modules";
|
||||||
@@ -19,4 +20,6 @@ export type * from "./api/dialog";
|
|||||||
export type * from "./api/profile";
|
export type * from "./api/profile";
|
||||||
export type * from "./api/navigation";
|
export type * from "./api/navigation";
|
||||||
export type * from "./api/builtins";
|
export type * from "./api/builtins";
|
||||||
|
export type * from "./api/stores";
|
||||||
|
export type * from "./api/client";
|
||||||
export * from "./api/watchable";
|
export * from "./api/watchable";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user