Skip to content

Commit a652860

Browse files
d-gubertclaude
andcommitted
feat(apps): copy apps-engine client UI host code into @rocket.chat/apps
Copies src/client/ (AppClientManager, AppsEngineUIHost, AppsEngineUIClient) from @rocket.chat/apps-engine, rewriting relative definition/ imports to package imports. This code is a known rough edge: browser-side UI host logic does not semantically belong in a server orchestration package. It is consolidated here for pragmatic simplicity during the apps-engine split. A future @rocket.chat/apps-client package is tracked in the TODO comment added to src/client/index.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c486a48 commit a652860

12 files changed

Lines changed: 289 additions & 0 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { AppServerCommunicator } from './AppServerCommunicator';
2+
import { AppsEngineUIHost } from './AppsEngineUIHost';
3+
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
4+
5+
export class AppClientManager {
6+
private apps: Array<IAppInfo>;
7+
8+
constructor(
9+
private readonly appsEngineUIHost: AppsEngineUIHost,
10+
private readonly communicator?: AppServerCommunicator,
11+
) {
12+
if (!(appsEngineUIHost instanceof AppsEngineUIHost)) {
13+
throw new Error('The appClientUIHost must extend appClientUIHost');
14+
}
15+
16+
if (communicator && !(communicator instanceof AppServerCommunicator)) {
17+
throw new Error('The communicator must extend AppServerCommunicator');
18+
}
19+
20+
this.apps = [];
21+
}
22+
23+
public async load(): Promise<void> {
24+
this.apps = await this.communicator.getEnabledApps();
25+
console.log('Enabled apps:', this.apps);
26+
}
27+
28+
public async initialize(): Promise<void> {
29+
this.appsEngineUIHost.initialize();
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
2+
3+
export abstract class AppServerCommunicator {
4+
public abstract getEnabledApps(): Promise<Array<IAppInfo>>;
5+
6+
public abstract getDisabledApps(): Promise<Array<IAppInfo>>;
7+
8+
// Map<appId, Map<language, translations>>
9+
public abstract getLanguageAdditions(): Promise<Map<string, Map<string, object>>>;
10+
11+
// Map<appId, Array<commands>>
12+
public abstract getSlashCommands(): Promise<Map<string, Array<string>>>;
13+
14+
// Map<appId, Array<to-be-determined>>
15+
public abstract getContextualBarButtons(): Promise<Map<string, Array<object>>>;
16+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ACTION_ID_LENGTH, MESSAGE_ID } from './constants';
2+
import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition';
3+
import { AppsEngineUIMethods } from './definition/AppsEngineUIMethods';
4+
import { randomString } from './utils';
5+
6+
/**
7+
* Represents the SDK provided to the external component.
8+
*/
9+
export class AppsEngineUIClient {
10+
private listener: (this: Window, ev: MessageEvent) => any;
11+
12+
private callbacks: Map<string, (response: any) => any>;
13+
14+
constructor() {
15+
this.listener = () => console.log('init');
16+
this.callbacks = new Map();
17+
}
18+
19+
/**
20+
* Get the current user's information.
21+
*
22+
* @return the information of the current user.
23+
*/
24+
public getUserInfo(): Promise<IExternalComponentUserInfo> {
25+
return this.call(AppsEngineUIMethods.GET_USER_INFO);
26+
}
27+
28+
/**
29+
* Get the current room's information.
30+
*
31+
* @return the information of the current room.
32+
*/
33+
public getRoomInfo(): Promise<IExternalComponentRoomInfo> {
34+
return this.call(AppsEngineUIMethods.GET_ROOM_INFO);
35+
}
36+
37+
/**
38+
* Initialize the app SDK for communicating with Rocket.Chat
39+
*/
40+
public init(): void {
41+
this.listener = ({ data }) => {
42+
if (!data?.hasOwnProperty(MESSAGE_ID)) {
43+
return;
44+
}
45+
46+
const {
47+
[MESSAGE_ID]: { id, payload },
48+
} = data;
49+
50+
if (this.callbacks.has(id)) {
51+
const resolve = this.callbacks.get(id);
52+
53+
if (typeof resolve === 'function') {
54+
resolve(payload);
55+
}
56+
this.callbacks.delete(id);
57+
}
58+
};
59+
window.addEventListener('message', this.listener);
60+
}
61+
62+
private call(action: string, payload?: any): Promise<any> {
63+
return new Promise((resolve) => {
64+
const id = randomString(ACTION_ID_LENGTH);
65+
66+
window.parent.postMessage({ [MESSAGE_ID]: { action, payload, id } }, '*');
67+
this.callbacks.set(id, resolve);
68+
});
69+
}
70+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { MESSAGE_ID } from './constants';
2+
import type { IAppsEngineUIResponse, IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition';
3+
import { AppsEngineUIMethods } from './definition';
4+
5+
type HandleActionData = IExternalComponentUserInfo | IExternalComponentRoomInfo;
6+
7+
/**
8+
* Represents the host which handles API calls from external components.
9+
*/
10+
export abstract class AppsEngineUIHost {
11+
/**
12+
* The message emitter who calling the API.
13+
*/
14+
private responseDestination!: Window;
15+
16+
constructor() {
17+
this.initialize();
18+
}
19+
20+
/**
21+
* initialize the AppClientUIHost by registering window `message` listener
22+
*/
23+
public initialize() {
24+
window.addEventListener('message', async ({ data, source }) => {
25+
if (!data?.hasOwnProperty(MESSAGE_ID)) {
26+
return;
27+
}
28+
29+
this.responseDestination = source as Window;
30+
31+
const {
32+
[MESSAGE_ID]: { action, id },
33+
} = data;
34+
35+
switch (action) {
36+
case AppsEngineUIMethods.GET_USER_INFO:
37+
this.handleAction(action, id, await this.getClientUserInfo());
38+
break;
39+
case AppsEngineUIMethods.GET_ROOM_INFO:
40+
this.handleAction(action, id, await this.getClientRoomInfo());
41+
break;
42+
}
43+
});
44+
}
45+
46+
/**
47+
* Get the current user's information.
48+
*/
49+
public abstract getClientUserInfo(): Promise<IExternalComponentUserInfo>;
50+
51+
/**
52+
* Get the opened room's information.
53+
*/
54+
public abstract getClientRoomInfo(): Promise<IExternalComponentRoomInfo>;
55+
56+
/**
57+
* Handle the action sent from the external component.
58+
* @param action the name of the action
59+
* @param id the unique id of the API call
60+
* @param data The data that will return to the caller
61+
*/
62+
private async handleAction(action: AppsEngineUIMethods, id: string, data: HandleActionData): Promise<void> {
63+
if (this.responseDestination instanceof MessagePort || this.responseDestination instanceof ServiceWorker) {
64+
return;
65+
}
66+
67+
this.responseDestination.postMessage(
68+
{
69+
[MESSAGE_ID]: {
70+
id,
71+
action,
72+
payload: data,
73+
} as IAppsEngineUIResponse,
74+
},
75+
'*',
76+
);
77+
}
78+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* The id length of each action.
3+
*/
4+
export const ACTION_ID_LENGTH = 80;
5+
6+
export const MESSAGE_ID = 'rc-apps-engine-ui';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The actions provided by the AppClientSDK.
3+
*/
4+
export enum AppsEngineUIMethods {
5+
GET_USER_INFO = 'getUserInfo',
6+
GET_ROOM_INFO = 'getRoomInfo',
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './index';
2+
3+
/**
4+
* The response to the AppClientSDK's API call.
5+
*/
6+
export interface IAppsEngineUIResponse {
7+
/**
8+
* The name of the action
9+
*/
10+
action: string;
11+
/**
12+
* The unique id of the API call
13+
*/
14+
id: string;
15+
/**
16+
* The data that will return to the caller
17+
*/
18+
payload: IExternalComponentUserInfo | IExternalComponentRoomInfo;
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo';
2+
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
3+
4+
type ClientRoomInfo = Pick<IRoom, 'id' | 'slugifiedName'>;
5+
6+
/**
7+
* Represents the room's information returned to the
8+
* external component.
9+
*/
10+
export interface IExternalComponentRoomInfo extends ClientRoomInfo {
11+
/**
12+
* the list that contains all the users belonging
13+
* to this room.
14+
*/
15+
members: Array<IExternalComponentUserInfo>;
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
2+
3+
type ClientUserInfo = Pick<IUser, 'id' | 'username'>;
4+
5+
/**
6+
* Represents the user's information returned to
7+
* the external component.
8+
*/
9+
export interface IExternalComponentUserInfo extends ClientUserInfo {
10+
/**
11+
* the avatar URL of the Rocket.Chat user
12+
*/
13+
avatarUrl: string;
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './AppsEngineUIMethods';
2+
export * from './IExternalComponentUserInfo';
3+
export * from './IExternalComponentRoomInfo';
4+
export * from './IAppsEngineUIResponse';

0 commit comments

Comments
 (0)