agora-appbuilder-core 4.1.8 → 4.1.9-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,540 +1,472 @@
1
- /*
2
- ********************************************
3
- Copyright © 2021 Agora Lab, Inc., all rights reserved.
4
- AppBuilder and all associated components, source code, APIs, services, and documentation
5
- (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be
6
- accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc.
7
- Use without a license or in violation of any license terms and conditions (including use for
8
- any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more
9
- information visit https://appbuilder.agora.io.
10
- *********************************************
11
- */
12
- // @ts-nocheck
13
1
  import {
14
- ChannelAttributeOptions,
15
- RtmAttribute,
16
- RtmChannelAttribute,
17
- Subscription,
18
- } from 'agora-react-native-rtm/lib/typescript/src';
19
- import {RtmClientEvents} from 'agora-react-native-rtm/lib/typescript/src/RtmEngine';
20
- import AgoraRTM, {VERSION} from 'agora-rtm-sdk';
21
- import RtmClient from 'agora-react-native-rtm';
22
- import {LogSource, logger} from '../../../src/logger/AppBuilderLogger';
23
- // export {RtmAttribute}
24
- //
25
- interface RtmAttributePlaceholder {}
26
- export {RtmAttributePlaceholder as RtmAttribute};
27
-
28
- type callbackType = (args?: any) => void;
29
-
30
- export default class RtmEngine {
31
- public appId: string;
32
- public client: RtmClient;
33
- public channelMap = new Map<string, any>([]);
34
- public remoteInvititations = new Map<string, any>([]);
35
- public localInvititations = new Map<string, any>([]);
36
- public channelEventsMap = new Map<string, any>([
37
- ['channelMessageReceived', () => null],
38
- ['channelMemberJoined', () => null],
39
- ['channelMemberLeft', () => null],
40
- ]);
41
- public clientEventsMap = new Map<string, any>([
42
- ['connectionStateChanged', () => null],
43
- ['messageReceived', () => null],
44
- ['remoteInvitationReceived', () => null],
45
- ['tokenExpired', () => null],
46
- ]);
47
- public localInvitationEventsMap = new Map<string, any>([
48
- ['localInvitationAccepted', () => null],
49
- ['localInvitationCanceled', () => null],
50
- ['localInvitationFailure', () => null],
51
- ['localInvitationReceivedByPeer', () => null],
52
- ['localInvitationRefused', () => null],
53
- ]);
54
- public remoteInvitationEventsMap = new Map<string, any>([
55
- ['remoteInvitationAccepted', () => null],
56
- ['remoteInvitationCanceled', () => null],
57
- ['remoteInvitationFailure', () => null],
58
- ['remoteInvitationRefused', () => null],
2
+ type Metadata as NativeMetadata,
3
+ type MetadataItem as NativeMetadataItem,
4
+ type GetUserMetadataOptions as NativeGetUserMetadataOptions,
5
+ type RtmChannelType as NativeRtmChannelType,
6
+ type SetUserMetadataResponse,
7
+ type LoginOptions as NativeLoginOptions,
8
+ type RTMClientEventMap as NativeRTMClientEventMap,
9
+ type GetUserMetadataResponse as NativeGetUserMetadataResponse,
10
+ type GetChannelMetadataResponse as NativeGetChannelMetadataResponse,
11
+ type SetOrUpdateUserMetadataOptions as NativeSetOrUpdateUserMetadataOptions,
12
+ type IMetadataOptions as NativeIMetadataOptions,
13
+ type StorageEvent as NativeStorageEvent,
14
+ type PresenceEvent as NativePresenceEvent,
15
+ type MessageEvent as NativeMessageEvent,
16
+ type SubscribeOptions as NativeSubscribeOptions,
17
+ type PublishOptions as NativePublishOptions,
18
+ } from 'agora-react-native-rtm';
19
+ import AgoraRTM, {
20
+ RTMClient,
21
+ GetUserMetadataResponse,
22
+ GetChannelMetadataResponse,
23
+ PublishOptions,
24
+ ChannelType,
25
+ MetaDataDetail,
26
+ } from 'agora-rtm-sdk';
27
+ import {
28
+ linkStatusReasonCodeMapping,
29
+ nativeChannelTypeMapping,
30
+ nativeLinkStateMapping,
31
+ nativeMessageEventTypeMapping,
32
+ nativePresenceEventTypeMapping,
33
+ nativeStorageEventTypeMapping,
34
+ nativeStorageTypeMapping,
35
+ webChannelTypeMapping,
36
+ } from './Types';
37
+
38
+ type CallbackType = (args?: any) => void;
39
+
40
+ // Conversion function
41
+ const convertWebToNativeMetadata = (webMetadata: any): NativeMetadata => {
42
+ // Convert object entries to MetadataItem array
43
+ const items: NativeMetadataItem[] =
44
+ Object.entries(webMetadata.metadata).map(
45
+ ([key, metadataItem]: [string, MetaDataDetail]) => {
46
+ return {
47
+ key: key,
48
+ value: metadataItem.value,
49
+ revision: metadataItem.revision,
50
+ authorUserId: metadataItem.authorUid,
51
+ updateTs: metadataItem.updated,
52
+ };
53
+ },
54
+ ) || [];
55
+
56
+ // Create native Metadata object
57
+ const nativeMetadata: NativeMetadata = {
58
+ majorRevision: webMetadata?.revision || -1, // Use first item's revision as major revision
59
+ items: items,
60
+ itemCount: webMetadata?.totalCount || 0,
61
+ };
62
+
63
+ return nativeMetadata;
64
+ };
65
+
66
+ export class RTMWebClient {
67
+ private client: RTMClient;
68
+ private appId: string;
69
+ private userId: string;
70
+ private eventsMap = new Map<keyof NativeRTMClientEventMap, CallbackType>([
71
+ ['linkState', () => null],
72
+ ['storage', () => null],
73
+ ['presence', () => null],
74
+ ['message', () => null],
59
75
  ]);
60
- constructor() {
61
- this.appId = '';
62
- logger.debug(LogSource.AgoraSDK, 'Log', 'Using RTM Bridge');
63
- }
64
-
65
- on(event: any, listener: any) {
66
- if (
67
- event === 'channelMessageReceived' ||
68
- event === 'channelMemberJoined' ||
69
- event === 'channelMemberLeft'
70
- ) {
71
- this.channelEventsMap.set(event, listener);
72
- } else if (
73
- event === 'connectionStateChanged' ||
74
- event === 'messageReceived' ||
75
- event === 'remoteInvitationReceived' ||
76
- event === 'tokenExpired'
77
- ) {
78
- this.clientEventsMap.set(event, listener);
79
- } else if (
80
- event === 'localInvitationAccepted' ||
81
- event === 'localInvitationCanceled' ||
82
- event === 'localInvitationFailure' ||
83
- event === 'localInvitationReceivedByPeer' ||
84
- event === 'localInvitationRefused'
85
- ) {
86
- this.localInvitationEventsMap.set(event, listener);
87
- } else if (
88
- event === 'remoteInvitationAccepted' ||
89
- event === 'remoteInvitationCanceled' ||
90
- event === 'remoteInvitationFailure' ||
91
- event === 'remoteInvitationRefused'
92
- ) {
93
- this.remoteInvitationEventsMap.set(event, listener);
94
- }
95
- }
96
76
 
97
- createClient(APP_ID: string) {
98
- this.appId = APP_ID;
99
- this.client = AgoraRTM.createInstance(this.appId);
100
-
101
- if ($config.GEO_FENCING) {
102
- try {
103
- //include area is comma seperated value
104
- let includeArea = $config.GEO_FENCING_INCLUDE_AREA
105
- ? $config.GEO_FENCING_INCLUDE_AREA
106
- : AREAS.GLOBAL;
107
-
108
- //exclude area is single value
109
- let excludeArea = $config.GEO_FENCING_EXCLUDE_AREA
110
- ? $config.GEO_FENCING_EXCLUDE_AREA
111
- : '';
112
-
113
- includeArea = includeArea?.split(',');
114
-
115
- //pass excludedArea if only its provided
116
- if (excludeArea) {
117
- AgoraRTM.setArea({
118
- areaCodes: includeArea,
119
- excludedArea: excludeArea,
120
- });
121
- }
122
- //otherwise we can pass area directly
123
- else {
124
- AgoraRTM.setArea({areaCodes: includeArea});
125
- }
126
- } catch (setAeraError) {
127
- console.log('error on RTM setArea', setAeraError);
128
- }
129
- }
130
-
131
- window.rtmClient = this.client;
132
-
133
- this.client.on('ConnectionStateChanged', (state, reason) => {
134
- this.clientEventsMap.get('connectionStateChanged')({state, reason});
135
- });
136
-
137
- this.client.on('MessageFromPeer', (msg, uid, msgProps) => {
138
- this.clientEventsMap.get('messageReceived')({
139
- text: msg.text,
140
- ts: msgProps.serverReceivedTs,
141
- offline: msgProps.isOfflineMessage,
142
- peerId: uid,
77
+ constructor(appId: string, userId: string) {
78
+ this.appId = appId;
79
+ this.userId = `${userId}`;
80
+ try {
81
+ // Create the actual web RTM client
82
+ this.client = new AgoraRTM.RTM(this.appId, this.userId);
83
+
84
+ this.client.addEventListener('linkState', data => {
85
+ const nativeState = {
86
+ ...data,
87
+ currentState:
88
+ nativeLinkStateMapping[data.currentState] ||
89
+ nativeLinkStateMapping.IDLE,
90
+ previousState:
91
+ nativeLinkStateMapping[data.previousState] ||
92
+ nativeLinkStateMapping.IDLE,
93
+ reasonCode: linkStatusReasonCodeMapping[data.reasonCode] || 0,
94
+ };
95
+ (this.eventsMap.get('linkState') ?? (() => {}))(nativeState);
143
96
  });
144
- });
145
97
 
146
- this.client.on('RemoteInvitationReceived', (remoteInvitation: any) => {
147
- this.remoteInvititations.set(remoteInvitation.callerId, remoteInvitation);
148
- this.clientEventsMap.get('remoteInvitationReceived')({
149
- callerId: remoteInvitation.callerId,
150
- content: remoteInvitation.content,
151
- state: remoteInvitation.state,
152
- channelId: remoteInvitation.channelId,
153
- response: remoteInvitation.response,
98
+ this.client.addEventListener('storage', data => {
99
+ const nativeStorageEvent: NativeStorageEvent = {
100
+ channelType: nativeChannelTypeMapping[data.channelType],
101
+ storageType: nativeStorageTypeMapping[data.storageType],
102
+ eventType: nativeStorageEventTypeMapping[data.eventType],
103
+ data: convertWebToNativeMetadata(data.data),
104
+ timestamp: data.timestamp,
105
+ };
106
+ (this.eventsMap.get('storage') ?? (() => {}))(nativeStorageEvent);
154
107
  });
155
108
 
156
- remoteInvitation.on('RemoteInvitationAccepted', () => {
157
- this.remoteInvitationEventsMap.get('RemoteInvitationAccepted')({
158
- callerId: remoteInvitation.callerId,
159
- content: remoteInvitation.content,
160
- state: remoteInvitation.state,
161
- channelId: remoteInvitation.channelId,
162
- response: remoteInvitation.response,
163
- });
109
+ this.client.addEventListener('presence', data => {
110
+ const nativePresenceEvent: NativePresenceEvent = {
111
+ channelName: data.channelName,
112
+ channelType: nativeChannelTypeMapping[data.channelType],
113
+ type: nativePresenceEventTypeMapping[data.eventType],
114
+ publisher: data.publisher,
115
+ timestamp: data.timestamp,
116
+ };
117
+ (this.eventsMap.get('presence') ?? (() => {}))(nativePresenceEvent);
164
118
  });
165
119
 
166
- remoteInvitation.on('RemoteInvitationCanceled', (content: string) => {
167
- this.remoteInvitationEventsMap.get('remoteInvitationCanceled')({
168
- callerId: remoteInvitation.callerId,
169
- content: content,
170
- state: remoteInvitation.state,
171
- channelId: remoteInvitation.channelId,
172
- response: remoteInvitation.response,
173
- });
120
+ this.client.addEventListener('message', data => {
121
+ const nativeMessageEvent: NativeMessageEvent = {
122
+ ...data,
123
+ channelType: nativeChannelTypeMapping[data.channelType],
124
+ messageType: nativeMessageEventTypeMapping[data.messageType],
125
+ message: `${data.message}`,
126
+ };
127
+ (this.eventsMap.get('message') ?? (() => {}))(nativeMessageEvent);
174
128
  });
129
+ } catch (error) {
130
+ const contextError = new Error(
131
+ `Failed to create RTMWebClient for appId: ${this.appId}, userId: ${
132
+ this.userId
133
+ }. Error: ${error.message || error}`,
134
+ );
135
+ console.error('RTMWebClient constructor error:', contextError);
136
+ throw contextError;
137
+ }
138
+ }
175
139
 
176
- remoteInvitation.on('RemoteInvitationFailure', (reason: string) => {
177
- this.remoteInvitationEventsMap.get('remoteInvitationFailure')({
178
- callerId: remoteInvitation.callerId,
179
- content: remoteInvitation.content,
180
- state: remoteInvitation.state,
181
- channelId: remoteInvitation.channelId,
182
- response: remoteInvitation.response,
183
- code: -1, //Web sends string, RN expect number but can't find enum
140
+ // Storage methods
141
+ get storage() {
142
+ return {
143
+ setUserMetadata: (
144
+ data: NativeMetadata,
145
+ options?: NativeSetOrUpdateUserMetadataOptions,
146
+ ): Promise<SetUserMetadataResponse> => {
147
+ // 1. Validate input parameters
148
+ if (!data) {
149
+ throw new Error('setUserMetadata: data parameter is required');
150
+ }
151
+ if (!data.items || !Array.isArray(data.items)) {
152
+ throw new Error(
153
+ 'setUserMetadata: data.items must be a non-empty array',
154
+ );
155
+ }
156
+ if (data.items.length === 0) {
157
+ throw new Error('setUserMetadata: data.items cannot be empty');
158
+ }
159
+ // 2. Make sure key is present as this is mandatory
160
+ // https://docs.agora.io/en/signaling/reference/api?platform=web#storagesetuserpropsag_platform
161
+ const validatedItems = data.items.map((item, index) => {
162
+ if (!item.key || typeof item.key !== 'string') {
163
+ throw new Error(
164
+ `setUserMetadata: item at index ${index} missing required 'key' property`,
165
+ );
166
+ }
167
+ return {
168
+ key: item.key,
169
+ value: item.value || '', // Default to empty string if not provided
170
+ revision: item.revision || -1, // Default to -1 if not provided
171
+ };
184
172
  });
185
- });
186
-
187
- remoteInvitation.on('RemoteInvitationRefused', () => {
188
- this.remoteInvitationEventsMap.get('remoteInvitationRefused')({
189
- callerId: remoteInvitation.callerId,
190
- content: remoteInvitation.content,
191
- state: remoteInvitation.state,
192
- channelId: remoteInvitation.channelId,
193
- response: remoteInvitation.response,
173
+ // Map native signature to web signature
174
+ return this.client.storage.setUserMetadata(validatedItems, {
175
+ addTimeStamp: options?.addTimeStamp || true,
176
+ addUserId: options?.addUserId || true,
194
177
  });
195
- });
196
- });
197
-
198
- this.client.on('TokenExpired', () => {
199
- this.clientEventsMap.get('tokenExpired')({}); //RN expect evt: any
200
- });
201
- }
202
-
203
- async login(loginParam: {uid: string; token?: string}): Promise<any> {
204
- return this.client.login(loginParam);
205
- }
206
-
207
- async logout(): Promise<any> {
208
- return await this.client.logout();
209
- }
178
+ },
210
179
 
211
- async joinChannel(channelId: string): Promise<any> {
212
- this.channelMap.set(channelId, this.client.createChannel(channelId));
213
- this.channelMap
214
- .get(channelId)
215
- .on('ChannelMessage', (msg: {text: string}, uid: string, messagePros) => {
216
- let text = msg.text;
217
- let ts = messagePros.serverReceivedTs;
218
- this.channelEventsMap.get('channelMessageReceived')({
219
- uid,
220
- channelId,
221
- text,
222
- ts,
223
- });
224
- });
225
- this.channelMap.get(channelId).on('MemberJoined', (uid: string) => {
226
- this.channelEventsMap.get('channelMemberJoined')({uid, channelId});
227
- });
228
- this.channelMap.get(channelId).on('MemberLeft', (uid: string) => {
229
- console.log('Member Left', this.channelEventsMap);
230
- this.channelEventsMap.get('channelMemberLeft')({uid});
231
- });
232
- this.channelMap
233
- .get(channelId)
234
- .on('AttributesUpdated', (attributes: RtmChannelAttribute) => {
235
- /**
236
- * a) Kindly note the below event listener 'channelAttributesUpdated' expects type
237
- * RtmChannelAttribute[] (array of objects [{key: 'valueOfKey', value: 'valueOfValue}])
238
- * whereas the above listener 'AttributesUpdated' receives attributes in object form
239
- * {[valueOfKey]: valueOfValue} of type RtmChannelAttribute
240
- * b) Hence in this bridge the data should be modified to keep in sync with both the
241
- * listeners for web and listener for native
242
- */
180
+ getUserMetadata: async (options: NativeGetUserMetadataOptions) => {
181
+ // Validate input parameters
182
+ if (!options) {
183
+ throw new Error('getUserMetadata: options parameter is required');
184
+ }
185
+ if (
186
+ !options.userId ||
187
+ typeof options.userId !== 'string' ||
188
+ options.userId.trim() === ''
189
+ ) {
190
+ throw new Error(
191
+ 'getUserMetadata: options.userId must be a non-empty string',
192
+ );
193
+ }
194
+ const webResponse: GetUserMetadataResponse =
195
+ await this.client.storage.getUserMetadata({
196
+ userId: options.userId,
197
+ });
243
198
  /**
244
- * 1. Loop through object
245
- * 2. Create a object {key: "", value: ""} and push into array
246
- * 3. Return the Array
199
+ * majorRevision : 13483783553
200
+ * metadata :
201
+ * {
202
+ * isHost: {authorUid: "", revision: 13483783553, updated: 0, value : "true"},
203
+ * screenUid: {…}}
204
+ * }
205
+ * timestamp: 0
206
+ * totalCount: 2
207
+ * userId: "xxx"
247
208
  */
248
- const channelAttributes = Object.keys(attributes).reduce((acc, key) => {
249
- const {value, lastUpdateTs, lastUpdateUserId} = attributes[key];
250
- acc.push({key, value, lastUpdateTs, lastUpdateUserId});
251
- return acc;
252
- }, []);
253
-
254
- this.channelEventsMap.get('ChannelAttributesUpdated')(
255
- channelAttributes,
209
+ const items = Object.entries(webResponse.metadata).map(
210
+ ([key, metadataItem]) => ({
211
+ key: key,
212
+ value: metadataItem.value,
213
+ }),
256
214
  );
257
- });
258
-
259
- return this.channelMap.get(channelId).join();
260
- }
261
-
262
- async leaveChannel(channelId: string): Promise<any> {
263
- if (this.channelMap.get(channelId)) {
264
- return this.channelMap.get(channelId).leave();
265
- } else {
266
- Promise.reject('Wrong channel');
267
- }
268
- }
269
-
270
- async sendMessageByChannelId(channel: string, message: string): Promise<any> {
271
- if (this.channelMap.get(channel)) {
272
- return this.channelMap.get(channel).sendMessage({text: message});
273
- } else {
274
- console.log(this.channelMap, channel);
275
- Promise.reject('Wrong channel');
276
- }
277
- }
278
-
279
- destroyClient() {
280
- console.log('Destroy called');
281
- this.channelEventsMap.forEach((callback, event) => {
282
- this.client.off(event, callback);
283
- });
284
- this.channelEventsMap.clear();
285
- this.channelMap.clear();
286
- this.clientEventsMap.clear();
287
- this.remoteInvitationEventsMap.clear();
288
- this.localInvitationEventsMap.clear();
289
- }
215
+ const nativeResponse: NativeGetUserMetadataResponse = {
216
+ items: [...items],
217
+ itemCount: webResponse.totalCount,
218
+ userId: webResponse.userId,
219
+ timestamp: webResponse.timestamp,
220
+ };
221
+ return nativeResponse;
222
+ },
290
223
 
291
- async getChannelMembersBychannelId(channel: string) {
292
- if (this.channelMap.get(channel)) {
293
- let memberArray: Array<any> = [];
294
- let currentChannel = this.channelMap.get(channel);
295
- await currentChannel.getMembers().then((arr: Array<number>) => {
296
- arr.map((elem: number) => {
297
- memberArray.push({
298
- channelId: channel,
299
- uid: elem,
300
- });
224
+ setChannelMetadata: async (
225
+ channelName: string,
226
+ channelType: NativeRtmChannelType,
227
+ data: NativeMetadata,
228
+ options?: NativeIMetadataOptions,
229
+ ) => {
230
+ // Validate input parameters
231
+ if (
232
+ !channelName ||
233
+ typeof channelName !== 'string' ||
234
+ channelName.trim() === ''
235
+ ) {
236
+ throw new Error(
237
+ 'setChannelMetadata: channelName must be a non-empty string',
238
+ );
239
+ }
240
+ if (typeof channelType !== 'number') {
241
+ throw new Error('setChannelMetadata: channelType must be a number');
242
+ }
243
+ if (!data) {
244
+ throw new Error('setChannelMetadata: data parameter is required');
245
+ }
246
+ if (!data.items || !Array.isArray(data.items)) {
247
+ throw new Error('setChannelMetadata: data.items must be an array');
248
+ }
249
+ if (data.items.length === 0) {
250
+ throw new Error('setChannelMetadata: data.items cannot be empty');
251
+ }
252
+ // 2. Make sure key is present as this is mandatory
253
+ // https://docs.agora.io/en/signaling/reference/api?platform=web#storagesetuserpropsag_platform
254
+ const validatedItems = data.items.map((item, index) => {
255
+ if (!item.key || typeof item.key !== 'string') {
256
+ throw new Error(
257
+ `setChannelMetadata: item at index ${index} missing required 'key' property`,
258
+ );
259
+ }
260
+ return {
261
+ key: item.key,
262
+ value: item.value || '', // Default to empty string if not provided
263
+ revision: item.revision || -1, // Default to -1 if not provided
264
+ };
301
265
  });
302
- });
303
- return {members: memberArray};
304
- } else {
305
- Promise.reject('Wrong channel');
306
- }
307
- }
266
+ return this.client.storage.setChannelMetadata(
267
+ channelName,
268
+ (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE',
269
+ validatedItems,
270
+ {
271
+ addUserId: options?.addUserId || true,
272
+ addTimeStamp: options?.addTimeStamp || true,
273
+ },
274
+ );
275
+ },
308
276
 
309
- async queryPeersOnlineStatus(uid: Array<String>) {
310
- let peerArray: Array<any> = [];
311
- await this.client.queryPeersOnlineStatus(uid).then(list => {
312
- Object.entries(list).forEach(value => {
313
- peerArray.push({
314
- online: value[1],
315
- uid: value[0],
316
- });
317
- });
318
- });
319
- return {items: peerArray};
277
+ getChannelMetadata: async (
278
+ channelName: string,
279
+ channelType: NativeRtmChannelType,
280
+ ) => {
281
+ try {
282
+ const webResponse: GetChannelMetadataResponse =
283
+ await this.client.storage.getChannelMetadata(
284
+ channelName,
285
+ (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE',
286
+ );
287
+
288
+ const items = Object.entries(webResponse.metadata).map(
289
+ ([key, metadataItem]) => ({
290
+ key: key,
291
+ value: metadataItem.value,
292
+ }),
293
+ );
294
+ const nativeResponse: NativeGetChannelMetadataResponse = {
295
+ items: [...items],
296
+ itemCount: webResponse.totalCount,
297
+ timestamp: webResponse.timestamp,
298
+ channelName: webResponse.channelName,
299
+ channelType: nativeChannelTypeMapping.MESSAGE,
300
+ };
301
+ return nativeResponse;
302
+ } catch (error) {
303
+ const contextError = new Error(
304
+ `Failed to get channel metadata for channel '${channelName}' with type ${channelType}: ${
305
+ error.message || error
306
+ }`,
307
+ );
308
+ console.error('BRIDGE getChannelMetadata error:', contextError);
309
+ throw contextError;
310
+ }
311
+ },
312
+ };
320
313
  }
321
314
 
322
- async renewToken(token: string) {
323
- return this.client.renewToken(token);
324
- }
315
+ get presence() {
316
+ return {
317
+ getOnlineUsers: async (
318
+ channelName: string,
319
+ channelType: NativeRtmChannelType,
320
+ ) => {
321
+ // Validate input parameters
322
+ if (
323
+ !channelName ||
324
+ typeof channelName !== 'string' ||
325
+ channelName.trim() === ''
326
+ ) {
327
+ throw new Error(
328
+ 'getOnlineUsers: channelName must be a non-empty string',
329
+ );
330
+ }
331
+ if (typeof channelType !== 'number') {
332
+ throw new Error('getOnlineUsers: channelType must be a number');
333
+ }
325
334
 
326
- async getUserAttributesByUid(uid: string) {
327
- let response = {};
328
- await this.client
329
- .getUserAttributes(uid)
330
- .then((attributes: string) => {
331
- response = {attributes, uid};
332
- })
333
- .catch((e: any) => {
334
- Promise.reject(e);
335
- });
336
- return response;
337
- }
335
+ try {
336
+ // Call web SDK's presence method
337
+ const result = await this.client.presence.getOnlineUsers(
338
+ channelName,
339
+ (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE',
340
+ );
341
+ return result;
342
+ } catch (error) {
343
+ const contextError = new Error(
344
+ `Failed to get online users for channel '${channelName}' with type ${channelType}: ${
345
+ error.message || error
346
+ }`,
347
+ );
348
+ console.error('BRIDGE presence error:', contextError);
349
+ throw contextError;
350
+ }
351
+ },
338
352
 
339
- async getChannelAttributes(channelId: string) {
340
- let response = {};
341
- await this.client
342
- .getChannelAttributes(channelId)
343
- .then((attributes: RtmChannelAttribute) => {
344
- /**
345
- * Here the attributes received are in the format {[valueOfKey]: valueOfValue} of type RtmChannelAttribute
346
- * We need to convert it into (array of objects [{key: 'valueOfKey', value: 'valueOfValue}])
347
- /**
348
- * 1. Loop through object
349
- * 2. Create a object {key: "", value: ""} and push into array
350
- * 3. Return the Array
351
- */
352
- const channelAttributes = Object.keys(attributes).reduce((acc, key) => {
353
- const {value, lastUpdateTs, lastUpdateUserId} = attributes[key];
354
- acc.push({key, value, lastUpdateTs, lastUpdateUserId});
355
- return acc;
356
- }, []);
357
- response = channelAttributes;
358
- })
359
- .catch((e: any) => {
360
- Promise.reject(e);
361
- });
362
- return response;
363
- }
353
+ whoNow: async (
354
+ channelName: string,
355
+ channelType?: NativeRtmChannelType,
356
+ ) => {
357
+ const webChannelType = channelType
358
+ ? (webChannelTypeMapping[channelType] as ChannelType)
359
+ : 'MESSAGE';
360
+ return this.client.presence.whoNow(channelName, webChannelType);
361
+ },
364
362
 
365
- async removeAllLocalUserAttributes() {
366
- return this.client.clearLocalUserAttributes();
363
+ whereNow: async (userId: string) => {
364
+ return this.client.presence.whereNow(userId);
365
+ },
366
+ };
367
367
  }
368
368
 
369
- async removeLocalUserAttributesByKeys(keys: string[]) {
370
- return this.client.deleteLocalUserAttributesByKeys(keys);
369
+ addEventListener(
370
+ event: keyof NativeRTMClientEventMap,
371
+ listener: (event: any) => void,
372
+ ) {
373
+ if (this.client) {
374
+ // Simply replace the handler in our map - web client listeners are fixed in constructor
375
+ this.eventsMap.set(event, listener as CallbackType);
376
+ }
371
377
  }
372
378
 
373
- async replaceLocalUserAttributes(attributes: string[]) {
374
- let formattedAttributes: any = {};
375
- attributes.map(attribute => {
376
- let key = Object.values(attribute)[0];
377
- let value = Object.values(attribute)[1];
378
- formattedAttributes[key] = value;
379
- });
380
- return this.client.setLocalUserAttributes({...formattedAttributes});
379
+ removeEventListener(
380
+ event: keyof NativeRTMClientEventMap,
381
+ _listener: (event: any) => void,
382
+ ) {
383
+ if (this.client && this.eventsMap.has(event)) {
384
+ const prevListener = this.eventsMap.get(event);
385
+ if (prevListener) {
386
+ this.client.removeEventListener(event, prevListener);
387
+ }
388
+ this.eventsMap.set(event, () => null); // reset to no-op
389
+ }
381
390
  }
382
391
 
383
- async setLocalUserAttributes(attributes: string[]) {
384
- let formattedAttributes: any = {};
385
- attributes.map(attribute => {
386
- let key = Object.values(attribute)[0];
387
- let value = Object.values(attribute)[1];
388
- formattedAttributes[key] = value;
389
- // console.log('!!!!formattedAttributes', formattedAttributes, key, value);
390
- });
391
- return this.client.setLocalUserAttributes({...formattedAttributes});
392
+ // Core RTM methods - direct delegation to web SDK
393
+ async login(options?: NativeLoginOptions) {
394
+ if (!options?.token) {
395
+ throw new Error('login: token is required in options');
396
+ }
397
+ return this.client.login({token: options.token});
392
398
  }
393
399
 
394
- async addOrUpdateLocalUserAttributes(attributes: RtmAttribute[]) {
395
- let formattedAttributes: any = {};
396
- attributes.map(attribute => {
397
- let key = Object.values(attribute)[0];
398
- let value = Object.values(attribute)[1];
399
- formattedAttributes[key] = value;
400
- });
401
- return this.client.addOrUpdateLocalUserAttributes({...formattedAttributes});
400
+ async logout() {
401
+ return this.client.logout();
402
402
  }
403
403
 
404
- async addOrUpdateChannelAttributes(
405
- channelId: string,
406
- attributes: RtmChannelAttribute[],
407
- option: ChannelAttributeOptions,
408
- ): Promise<void> {
409
- let formattedAttributes: any = {};
410
- attributes.map(attribute => {
411
- let key = Object.values(attribute)[0];
412
- let value = Object.values(attribute)[1];
413
- formattedAttributes[key] = value;
414
- });
415
- return this.client.addOrUpdateChannelAttributes(
416
- channelId,
417
- {...formattedAttributes},
418
- option,
419
- );
404
+ async subscribe(channelName: string, options?: NativeSubscribeOptions) {
405
+ if (
406
+ !channelName ||
407
+ typeof channelName !== 'string' ||
408
+ channelName.trim() === ''
409
+ ) {
410
+ throw new Error('subscribe: channelName must be a non-empty string');
411
+ }
412
+ return this.client.subscribe(channelName, options);
420
413
  }
421
414
 
422
- async sendLocalInvitation(invitationProps: any) {
423
- let invite = this.client.createLocalInvitation(invitationProps.uid);
424
- this.localInvititations.set(invitationProps.uid, invite);
425
- invite.content = invitationProps.content;
426
-
427
- invite.on('LocalInvitationAccepted', (response: string) => {
428
- this.localInvitationEventsMap.get('localInvitationAccepted')({
429
- calleeId: invite.calleeId,
430
- content: invite.content,
431
- state: invite.state,
432
- channelId: invite.channelId,
433
- response,
434
- });
435
- });
436
-
437
- invite.on('LocalInvitationCanceled', () => {
438
- this.localInvitationEventsMap.get('localInvitationCanceled')({
439
- calleeId: invite.calleeId,
440
- content: invite.content,
441
- state: invite.state,
442
- channelId: invite.channelId,
443
- response: invite.response,
444
- });
445
- });
446
-
447
- invite.on('LocalInvitationFailure', (reason: string) => {
448
- this.localInvitationEventsMap.get('localInvitationFailure')({
449
- calleeId: invite.calleeId,
450
- content: invite.content,
451
- state: invite.state,
452
- channelId: invite.channelId,
453
- response: invite.response,
454
- code: -1, //Web sends string, RN expect number but can't find enum
455
- });
456
- });
457
-
458
- invite.on('LocalInvitationReceivedByPeer', () => {
459
- this.localInvitationEventsMap.get('localInvitationReceivedByPeer')({
460
- calleeId: invite.calleeId,
461
- content: invite.content,
462
- state: invite.state,
463
- channelId: invite.channelId,
464
- response: invite.response,
465
- });
466
- });
467
-
468
- invite.on('LocalInvitationRefused', (response: string) => {
469
- this.localInvitationEventsMap.get('localInvitationRefused')({
470
- calleeId: invite.calleeId,
471
- content: invite.content,
472
- state: invite.state,
473
- channelId: invite.channelId,
474
- response: response,
475
- });
476
- });
477
- return invite.send();
415
+ async unsubscribe(channelName: string) {
416
+ return this.client.unsubscribe(channelName);
478
417
  }
479
418
 
480
- async sendMessageToPeer(AgoraPeerMessage: {
481
- peerId: string;
482
- offline: boolean;
483
- text: string;
484
- }) {
485
- return this.client.sendMessageToPeer(
486
- {text: AgoraPeerMessage.text},
487
- AgoraPeerMessage.peerId,
488
- );
489
- //check promise result
490
- }
419
+ async publish(
420
+ channelName: string,
421
+ message: string,
422
+ options?: NativePublishOptions,
423
+ ) {
424
+ // Validate input parameters
425
+ if (
426
+ !channelName ||
427
+ typeof channelName !== 'string' ||
428
+ channelName.trim() === ''
429
+ ) {
430
+ throw new Error('publish: channelName must be a non-empty string');
431
+ }
432
+ if (typeof message !== 'string') {
433
+ throw new Error('publish: message must be a string');
434
+ }
491
435
 
492
- async acceptRemoteInvitation(remoteInvitationProps: {
493
- uid: string;
494
- response?: string;
495
- channelId: string;
496
- }) {
497
- let invite = this.remoteInvititations.get(remoteInvitationProps.uid);
498
- // console.log(invite);
499
- // console.log(this.remoteInvititations);
500
- // console.log(remoteInvitationProps.uid);
501
- return invite.accept();
436
+ const webOptions: PublishOptions = {
437
+ ...options,
438
+ channelType:
439
+ (webChannelTypeMapping[options?.channelType] as ChannelType) ||
440
+ 'MESSAGE',
441
+ };
442
+ return this.client.publish(channelName, message, webOptions);
502
443
  }
503
444
 
504
- async refuseRemoteInvitation(remoteInvitationProps: {
505
- uid: string;
506
- response?: string;
507
- channelId: string;
508
- }) {
509
- return this.remoteInvititations.get(remoteInvitationProps.uid).refuse();
445
+ async renewToken(token: string) {
446
+ return this.client.renewToken(token);
510
447
  }
511
448
 
512
- async cancelLocalInvitation(LocalInvitationProps: {
513
- uid: string;
514
- content?: string;
515
- channelId?: string;
516
- }) {
517
- console.log(this.localInvititations.get(LocalInvitationProps.uid));
518
- return this.localInvititations.get(LocalInvitationProps.uid).cancel();
449
+ removeAllListeners() {
450
+ this.eventsMap = new Map([
451
+ ['linkState', () => null],
452
+ ['storage', () => null],
453
+ ['presence', () => null],
454
+ ['message', () => null],
455
+ ]);
456
+ return this.client.removeAllListeners();
519
457
  }
458
+ }
520
459
 
521
- getSdkVersion(callback: (version: string) => void) {
522
- callback(VERSION);
523
- }
460
+ export class RtmConfig {
461
+ public appId: string;
462
+ public userId: string;
524
463
 
525
- addListener<EventType extends keyof RtmClientEvents>(
526
- event: EventType,
527
- listener: RtmClientEvents[EventType],
528
- ): Subscription {
529
- if (event === 'ChannelAttributesUpdated') {
530
- this.channelEventsMap.set(event, listener as callbackType);
531
- }
532
- return {
533
- remove: () => {
534
- console.log(
535
- 'Use destroy method to remove all the event listeners from the RtcEngine instead.',
536
- );
537
- },
538
- };
464
+ constructor(config: {appId: string; userId: string}) {
465
+ this.appId = config.appId;
466
+ this.userId = config.userId;
539
467
  }
540
468
  }
469
+ // Factory function to create RTM client
470
+ export function createAgoraRtmClient(config: RtmConfig): RTMWebClient {
471
+ return new RTMWebClient(config.appId, config.userId);
472
+ }