agora-appbuilder-core 4.1.13-beta.1 → 4.1.13-beta.3

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/template/customization-api/temp.ts +1 -9
  3. package/template/defaultConfig.js +2 -2
  4. package/template/package.json +1 -1
  5. package/template/src/assets/font-styles.css +4 -0
  6. package/template/src/assets/fonts/icomoon.ttf +0 -0
  7. package/template/src/assets/selection.json +1 -1
  8. package/template/src/atoms/ActionMenu.tsx +1 -1
  9. package/template/src/atoms/CustomIcon.tsx +1 -0
  10. package/template/src/atoms/Popup.tsx +1 -1
  11. package/template/src/components/Controls.tsx +22 -41
  12. package/template/src/components/whiteboard/StrokeWidthTool.tsx +1 -5
  13. package/template/src/components/whiteboard/WhiteboardButton.tsx +1 -9
  14. package/template/src/components/whiteboard/WhiteboardCanvas.tsx +1 -9
  15. package/template/src/components/whiteboard/WhiteboardConfigure.tsx +215 -95
  16. package/template/src/components/whiteboard/WhiteboardCursor.tsx +9 -17
  17. package/template/src/components/whiteboard/WhiteboardToolBox.tsx +1 -15
  18. package/template/src/components/whiteboard/WhiteboardView.tsx +1 -9
  19. package/template/src/components/whiteboard/WhiteboardWidget.tsx +1 -4
  20. package/template/src/components/whiteboard/WhiteboardWrapper.tsx +2 -3
  21. package/template/src/language/default-labels/videoCallScreenLabels.ts +9 -9
  22. package/template/src/pages/VideoCall.tsx +2 -1
  23. package/template/src/pages/video-call/ActionSheetContent.tsx +0 -7
  24. package/template/src/pages/video-call/SidePanelHeader.tsx +80 -63
  25. package/template/src/rtm-events/constants.ts +9 -0
  26. package/template/src/subComponents/caption/Caption.tsx +4 -20
  27. package/template/src/subComponents/caption/CaptionContainer.tsx +262 -250
  28. package/template/src/subComponents/caption/CaptionIcon.tsx +6 -4
  29. package/template/src/subComponents/caption/CaptionText.tsx +26 -20
  30. package/template/src/subComponents/caption/LanguageSelectorPopup.tsx +30 -142
  31. package/template/src/subComponents/caption/Transcript.tsx +77 -32
  32. package/template/src/subComponents/caption/TranscriptIcon.tsx +7 -6
  33. package/template/src/subComponents/caption/TranslateActionMenu.tsx +128 -0
  34. package/template/src/subComponents/caption/useCaption.tsx +645 -482
  35. package/template/src/subComponents/caption/useSTTAPI.tsx +25 -4
  36. package/template/src/subComponents/caption/useStreamMessageUtils.native.ts +1 -1
  37. package/template/src/subComponents/caption/useStreamMessageUtils.ts +1 -1
  38. package/template/src/subComponents/caption/utils.ts +48 -40
  39. package/template/src/components/whiteboard/FastBoardView.tsx +0 -227
@@ -36,6 +36,26 @@ const useSTTAPI = (): IuseSTTAPI => {
36
36
  const STT_API_URL = `${$config.BACKEND_ENDPOINT}/v1/stt`;
37
37
  const localUid = useLocalUid();
38
38
 
39
+ const roomIdRef = React.useRef(roomId);
40
+ React.useEffect(() => {
41
+ roomIdRef.current = roomId;
42
+ }, [roomId]);
43
+
44
+ const localUidRef = React.useRef(localUid);
45
+ React.useEffect(() => {
46
+ localUidRef.current = localUid;
47
+ }, [localUid]);
48
+
49
+ const tokenRef = React.useRef(store.token);
50
+ React.useEffect(() => {
51
+ tokenRef.current = store.token;
52
+ }, [store.token]);
53
+
54
+ const rtcPropsRef = React.useRef(rtcProps);
55
+ React.useEffect(() => {
56
+ rtcPropsRef.current = rtcProps;
57
+ }, [rtcProps]);
58
+
39
59
  const apiCall = async (
40
60
  method: 'startv7' | 'update' | 'stopv7',
41
61
  botUid: number,
@@ -49,10 +69,11 @@ const useSTTAPI = (): IuseSTTAPI => {
49
69
  const ownerUid = botUid - 900000000;
50
70
 
51
71
  let requestBody: any = {
52
- passphrase: roomId?.host || roomId?.attendee || '',
72
+ passphrase:
73
+ roomIdRef?.current?.host || roomIdRef?.current?.attendee || '',
53
74
  dataStream_uid: botUid,
54
75
  encryption_mode: $config.ENCRYPTION_ENABLED
55
- ? rtcProps.encryption.mode
76
+ ? rtcPropsRef?.current.encryption.mode
56
77
  : null,
57
78
  };
58
79
 
@@ -89,14 +110,14 @@ const useSTTAPI = (): IuseSTTAPI => {
89
110
  // If method is update and no targets are passed
90
111
  requestBody.translate = false;
91
112
  }
92
- requestBody.subscribeAudioUids = [`${localUid}`];
113
+ requestBody.subscribeAudioUids = [`${localUidRef.current}`];
93
114
  }
94
115
 
95
116
  const response = await fetch(`${STT_API_URL}/${method}`, {
96
117
  method: 'POST',
97
118
  headers: {
98
119
  'Content-Type': 'application/json',
99
- authorization: store.token ? `Bearer ${store.token}` : '',
120
+ authorization: tokenRef?.current ? `Bearer ${tokenRef?.current}` : '',
100
121
  'X-Request-Id': requestId,
101
122
  'X-Session-Id': logger.getSessionId(),
102
123
  },
@@ -50,7 +50,7 @@ const useStreamMessageUtils = (): {
50
50
  .lookupType('agora.audio2text.Text')
51
51
  .decode(payload as Uint8Array) as any;
52
52
 
53
- console.log('[STT_PER_USER_BOT] stt v7 textstream', botUid, textstream);
53
+ console.log('[STT_GLOBAL] stt v7 textstream', botUid, textstream);
54
54
  // console.log('STT - Parsed Textstream : ', textstream);
55
55
 
56
56
  // Identifing Current & Prev Speakers for the Captions
@@ -52,7 +52,7 @@ const useStreamMessageUtils = (): {
52
52
  .lookupType('agora.audio2text.Text')
53
53
  .decode(payload as Uint8Array) as any;
54
54
 
55
- console.log('[STT_PER_USER_BOT] stt v7 textstream', botUid, textstream);
55
+ console.log('[STT_GLOBAL] stt v7 textstream', botUid, textstream);
56
56
  //console.log('STT - Parsed Textstream : ', textstream);
57
57
  // console.log(
58
58
  // `STT-callback(${++counter}): %c${textstream.uid} %c${textstream.words
@@ -110,7 +110,7 @@ export function getLanguageLabel(
110
110
  languageCode: LanguageType[],
111
111
  ): string | undefined {
112
112
  const langLabels = languageCode.map(langCode => {
113
- return langData.find(data => data.value === langCode).label;
113
+ return langData.find(data => data.value === langCode)?.label;
114
114
  });
115
115
  return langLabels ? langLabels.join(', ') : undefined;
116
116
  }
@@ -163,31 +163,32 @@ export const formatTranscriptContent = (
163
163
  const formattedContent = meetingTranscript
164
164
  .map(item => {
165
165
  if (
166
- item.uid.toString().indexOf('langUpdate') !== -1 ||
167
- item.uid.toString().indexOf('translationUpdate') !== -1
166
+ item.uid.toString().includes('langUpdate') ||
167
+ item.uid.toString().includes('translationUpdate')
168
168
  ) {
169
- return `${defaultContent[item?.uid?.split('-')[1]]?.name} ${item.text}`;
169
+ // return `${defaultContent[item?.uid?.split('-')[1]]?.name} ${item.text}`;
170
+ return item.text;
170
171
  }
171
172
 
172
- // Build transcript entry with original text and all translations
173
- let transcriptEntry = `${defaultContent[item.uid]?.name} ${formatTime(
174
- Number(item?.time),
175
- )}:\n${item.text}`;
176
-
177
- // Add all translations with language labels
178
- if (item.translations && item.translations.length > 0) {
179
- const translationLines = item.translations
180
- .map(trans => {
181
- const langLabel =
182
- langData.find(l => l.value === trans.lang)?.label || trans.lang;
183
- return `${langLabel}: ${trans.text}`;
184
- })
185
- .join('\n');
186
-
187
- transcriptEntry += `\n${translationLines}`;
173
+ const speakerName = defaultContent[item.uid]?.name || 'Speaker';
174
+
175
+ // Original
176
+ let entry = `${speakerName}:\n${item.text}`;
177
+
178
+ // Selected Translation
179
+ const storedLang = item.selectedTranslationLanguage;
180
+ const selectedTranslation = storedLang
181
+ ? item.translations?.find(t => t.lang === storedLang) || null
182
+ : null;
183
+ if (selectedTranslation) {
184
+ const langLabel =
185
+ langData.find(l => l.value === selectedTranslation.lang)?.label ||
186
+ selectedTranslation.lang;
187
+
188
+ entry += `\n→ (${langLabel}) ${selectedTranslation.text}`;
188
189
  }
189
190
 
190
- return transcriptEntry;
191
+ return entry;
191
192
  })
192
193
  .join('\n\n');
193
194
 
@@ -196,13 +197,20 @@ export const formatTranscriptContent = (
196
197
  );
197
198
 
198
199
  const attendees = Object.entries(defaultContent)
199
- .filter(
200
- arr =>
201
- arr[1].type === 'rtc' &&
202
- arr[0] !== '100000' && // exclude recording bot
203
- (arr[1]?.isInWaitingRoom === true ? false : true),
204
- )
205
- .map(arr => arr[1].name)
200
+ .filter(([uid, user]) => {
201
+ const uidNum = Number(uid);
202
+
203
+ const isBot =
204
+ uidNum === 111111 || // STT bot (web)
205
+ uidNum > 900000000 || // STT bots (native)
206
+ uidNum === 100000 || // Recording bot (web user)
207
+ uidNum === 100001; // Recording bot (web screen)
208
+
209
+ const isWaitingRoom = user?.isInWaitingRoom === true;
210
+
211
+ return user.type === 'rtc' && !isBot && !isWaitingRoom;
212
+ })
213
+ .map(([_, user]) => user.name)
206
214
  .join(',');
207
215
 
208
216
  const info =
@@ -232,19 +240,18 @@ export const formatTranscriptContent = (
232
240
  * @param captionText - The original caption text
233
241
  * @param translations - Array of available translations
234
242
  * @param viewerSourceLanguage - The user's source (spoken) language
235
- * @param speakerUid - The UID of the person speaking
236
- * @param currentUserUid - The UID of the current user
237
243
  * @returns The appropriate caption text to display
238
244
  */
239
245
  export const getUserTranslatedText = (
240
246
  captionText: string,
241
247
  translations: Array<{lang: string; text: string; isFinal: boolean}> = [],
242
- viewerSourceLanguage: LanguageType,
243
- speakerUid: string | number,
244
- currentUserUid: string | number,
248
+ sourceLanguage: LanguageType,
249
+ selectedTranslationLanguage: LanguageType,
250
+ // speakerUid: string | number,
251
+ // currentUserUid: string | number,
245
252
  ): {
246
253
  value: string;
247
- langCode: string;
254
+ langLabel: string;
248
255
  } => {
249
256
  // console.log(
250
257
  // 'getUserTranslatedText input params',
@@ -256,16 +263,17 @@ export const getUserTranslatedText = (
256
263
  // );
257
264
 
258
265
  // 1. If the speaker is the local user, always show their own source text
259
- if (speakerUid === currentUserUid) {
266
+ if (!selectedTranslationLanguage) {
260
267
  return {
261
268
  value: captionText,
262
- langCode: getLanguageLabel([viewerSourceLanguage]) || '',
269
+ langLabel: getLanguageLabel([sourceLanguage]) || '',
263
270
  };
264
271
  }
272
+
265
273
  // For other users' captions, try to find translation matching viewer's source language
266
- if (viewerSourceLanguage && translations && translations.length > 0) {
274
+ if (selectedTranslationLanguage && translations && translations.length > 0) {
267
275
  const matchingTranslation = translations.find(
268
- t => t.lang === viewerSourceLanguage,
276
+ t => t.lang === selectedTranslationLanguage,
269
277
  );
270
278
  if (matchingTranslation) {
271
279
  // Translation exists (even if empty)
@@ -274,7 +282,7 @@ export const getUserTranslatedText = (
274
282
  const translatedText = matchingTranslation.text?.trim() || '';
275
283
  return {
276
284
  value: translatedText,
277
- langCode: getLanguageLabel([matchingTranslation.lang]) || '',
285
+ langLabel: getLanguageLabel([selectedTranslationLanguage]) || '',
278
286
  };
279
287
  }
280
288
  }
@@ -282,7 +290,7 @@ export const getUserTranslatedText = (
282
290
  // Fallback to original text if no translation found
283
291
  return {
284
292
  value: captionText,
285
- langCode: 'Original',
293
+ langLabel: 'Original',
286
294
  };
287
295
  };
288
296
 
@@ -1,227 +0,0 @@
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
-
13
- import React, {useRef, useEffect} from 'react';
14
- import {StyleSheet, View, Text} from 'react-native';
15
- import {useRoomInfo} from 'customization-api';
16
- import {useString} from '../../utils/useString';
17
- import {whiteboardInitializingText} from '../../language/default-labels/videoCallScreenLabels';
18
- import {useFastboard, Fastboard, createFastboard, FastboardApp} from '@netless/fastboard-react/full';
19
-
20
- import {useLocalUid} from '../../../agora-rn-uikit';
21
- import useUserName from '../../utils/useUserName';
22
-
23
- interface FastBoardViewProps {
24
- showToolbox?: boolean;
25
- }
26
-
27
- /**
28
- * Technical Note: Why we are not using the 'useFastboard' hook?
29
- *
30
- * We replaced the standard 'useFastboard' hook with a manual 'FastboardManager' implementation
31
- * to solve a critical race condition in React 18+ Strict Mode.
32
- *
33
- * Issue: "[WindowManager]: Already created cannot be created again"
34
- *
35
- * In Strict Mode, React intentionally mounts, unmounts, and remounts components rapidly
36
- * to detect side-effects. The standard hook attempts to initialize the Fastboard singleton
37
- * twice in parallel. The first initialization is still "pending" when the second one starts,
38
- * causing the underlying WindowManager to crash because it doesn't support concurrent init calls.
39
- *
40
- * Solution: The 'FastboardManager' below uses Reference Counting and Singleton pattern to:
41
- * 1. Deduplicate initialization requests (reusing the same Promise).
42
- * 2. Delay cleanup slightly to handle the rapid Unmount -> Mount cycle without destroying the instance.
43
- */
44
- const FastBoardView: React.FC<FastBoardViewProps> = ({showToolbox = true}) => {
45
- const {
46
- data: {whiteboard: {room_token, room_uuid} = {}, isHost},
47
- } = useRoomInfo();
48
-
49
- const localUid = useLocalUid();
50
- const [name] = useUserName();
51
-
52
- const whiteboardInitializing = useString(whiteboardInitializingText)();
53
-
54
- // Generate stable UID - only once
55
- // Use local user's name or fallback to UID if name is not available
56
- const uidRef = useRef<string>(name || String(localUid));
57
- const [fastboard, setFastboard] = React.useState<FastboardApp | null>(null);
58
-
59
- // Configuration object - Memoize stringified version to detect changes
60
- const config = React.useMemo(() => ({
61
- sdkConfig: {
62
- appIdentifier: 'EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig',
63
- region: 'us-sv',
64
- },
65
- joinRoom: {
66
- uid: uidRef.current,
67
- uuid: room_uuid,
68
- roomToken: room_token,
69
- },
70
- }), [room_uuid, room_token]);
71
-
72
- const configStr = JSON.stringify(config);
73
-
74
- useEffect(() => {
75
- let isMounted = true;
76
-
77
- const initFastboard = async () => {
78
- try {
79
- const app = await FastboardManager.acquire(configStr, config);
80
- if (isMounted) {
81
- setFastboard(app);
82
- }
83
- } catch (error) {
84
- console.error("Fastboard initialization failed:", error);
85
- }
86
- };
87
-
88
- initFastboard();
89
-
90
- return () => {
91
- isMounted = false;
92
- FastboardManager.release(configStr);
93
- };
94
- }, [configStr, config]);
95
-
96
- // Show loading if fastboard not ready yet
97
- if (!fastboard) {
98
- return (
99
- <View style={styles.container}>
100
- <View style={styles.placeholder}>
101
- <Text style={styles.placeholderText}>{whiteboardInitializing}</Text>
102
- </View>
103
- </View>
104
- );
105
- }
106
-
107
- return (
108
- <View style={styles.container}>
109
- <div style={divStyles.fastboardContainer}>
110
- <Fastboard app={fastboard} />
111
- </div>
112
- </View>
113
- );
114
- };
115
-
116
- // Global Manager to handle Strict Mode and Singleton constraints
117
- const FastboardManager = {
118
- activeConfig: null as string | null,
119
- instance: null as FastboardApp | null,
120
- promise: null as Promise<FastboardApp> | null,
121
- subscribers: 0,
122
-
123
- acquire: async (configStr: string, config: any): Promise<FastboardApp> => {
124
- FastboardManager.subscribers++;
125
-
126
- // If config matches and we have an instance/promise, reuse it
127
- if (FastboardManager.activeConfig === configStr) {
128
- if (FastboardManager.instance) return FastboardManager.instance;
129
- if (FastboardManager.promise) return FastboardManager.promise;
130
- }
131
-
132
- // Config mismatch! We must cleanup the previous board before creating a new one.
133
- // This happens if component remounts with new UID (e.g. Pin to Top).
134
-
135
- // 1. If there's a pending loading operation, wait for it and destroy the result.
136
- if (FastboardManager.promise) {
137
- try {
138
- const oldApp = await FastboardManager.promise;
139
- // Double check if it wasn't already destroyed or reassigned
140
- if (oldApp) oldApp.destroy();
141
- } catch (e) {
142
- console.warn("Error cleaning up previous pending Fastboard:", e);
143
- }
144
- }
145
-
146
- // 2. If there's an active instance, destroy it.
147
- if (FastboardManager.instance) {
148
- try {
149
- FastboardManager.instance.destroy();
150
- } catch (e) {
151
- console.warn("Error cleaning up previous active Fastboard:", e);
152
- }
153
- }
154
-
155
- // Reset state
156
- FastboardManager.instance = null;
157
- FastboardManager.promise = null;
158
- FastboardManager.activeConfig = configStr;
159
-
160
- // 3. Create new instance
161
- FastboardManager.promise = createFastboard(config).then(app => {
162
- // Check if we are still the active config
163
- if (FastboardManager.activeConfig === configStr) {
164
- FastboardManager.instance = app;
165
- return app;
166
- } else {
167
- // Config changed while loading
168
- app.destroy();
169
- throw new Error("Fastboard config changed during initialization");
170
- }
171
- });
172
-
173
- return FastboardManager.promise;
174
- },
175
-
176
- release: (configStr: string) => {
177
- FastboardManager.subscribers--;
178
-
179
- // Delayed cleanup to handle Strict Mode "Unmount -> Mount" flicker
180
- setTimeout(() => {
181
- if (FastboardManager.subscribers === 0 && FastboardManager.activeConfig === configStr) {
182
- if (FastboardManager.instance) {
183
- FastboardManager.instance.destroy();
184
- FastboardManager.instance = null;
185
- }
186
- FastboardManager.promise = null;
187
- FastboardManager.activeConfig = null;
188
- }
189
- }, 200);
190
- }
191
- };
192
-
193
- const styles = StyleSheet.create({
194
- container: {
195
- flex: 1,
196
- position: 'relative',
197
- backgroundColor: '#ffffff',
198
- },
199
- placeholder: {
200
- position: 'absolute',
201
- width: '100%',
202
- height: '100%',
203
- backgroundColor: '#f5f5f5',
204
- display: 'flex',
205
- justifyContent: 'center',
206
- alignItems: 'center',
207
- },
208
- placeholderText: {
209
- color: '#666666',
210
- fontSize: 16,
211
- },
212
- errorText: {
213
- color: '#ff4444',
214
- fontSize: 16,
215
- },
216
- });
217
-
218
- const divStyles = {
219
- fastboardContainer: {
220
- width: '100%',
221
- height: '100%',
222
- borderRadius: 4,
223
- overflow: 'hidden' as const,
224
- },
225
- };
226
-
227
- export default FastBoardView;