agora-appbuilder-core 4.1.20 → 4.1.21

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 (89) hide show
  1. package/package.json +1 -1
  2. package/template/bridge/rtm/web/__tests__/storage.test.ts +130 -0
  3. package/template/bridge/rtm/web/index.ts +44 -19
  4. package/template/customization-api/customize.tsx +6 -0
  5. package/template/customization-api/index.ts +11 -0
  6. package/template/customization-api/typeDefinition.ts +6 -0
  7. package/template/defaultConfig.js +2 -2
  8. package/template/src/assets/live-reactions/1f440.gif +0 -0
  9. package/template/src/assets/live-reactions/1f44d_1f3fb.gif +0 -0
  10. package/template/src/assets/live-reactions/1f44d_1f3fc.gif +0 -0
  11. package/template/src/assets/live-reactions/1f44d_1f3fd.gif +0 -0
  12. package/template/src/assets/live-reactions/1f44d_1f3fe.gif +0 -0
  13. package/template/src/assets/live-reactions/1f44d_1f3ff.gif +0 -0
  14. package/template/src/assets/live-reactions/1f44f_1f3fb.gif +0 -0
  15. package/template/src/assets/live-reactions/1f44f_1f3fc.gif +0 -0
  16. package/template/src/assets/live-reactions/1f44f_1f3fd.gif +0 -0
  17. package/template/src/assets/live-reactions/1f44f_1f3fe.gif +0 -0
  18. package/template/src/assets/live-reactions/1f44f_1f3ff.gif +0 -0
  19. package/template/src/assets/live-reactions/1f4a1.gif +0 -0
  20. package/template/src/assets/live-reactions/1f4af.gif +0 -0
  21. package/template/src/assets/live-reactions/1f525.gif +0 -0
  22. package/template/src/assets/live-reactions/1f60e.gif +0 -0
  23. package/template/src/assets/live-reactions/1f643.gif +0 -0
  24. package/template/src/assets/live-reactions/1f64c.gif +0 -0
  25. package/template/src/assets/live-reactions/1f64c_1f3fb.gif +0 -0
  26. package/template/src/assets/live-reactions/1f64c_1f3fc.gif +0 -0
  27. package/template/src/assets/live-reactions/1f64c_1f3fd.gif +0 -0
  28. package/template/src/assets/live-reactions/1f64c_1f3fe.gif +0 -0
  29. package/template/src/assets/live-reactions/1f64c_1f3ff.gif +0 -0
  30. package/template/src/assets/live-reactions/1f64f.gif +0 -0
  31. package/template/src/assets/live-reactions/1f64f_1f3fb.gif +0 -0
  32. package/template/src/assets/live-reactions/1f64f_1f3fc.gif +0 -0
  33. package/template/src/assets/live-reactions/1f64f_1f3fd.gif +0 -0
  34. package/template/src/assets/live-reactions/1f64f_1f3fe.gif +0 -0
  35. package/template/src/assets/live-reactions/1f64f_1f3ff.gif +0 -0
  36. package/template/src/assets/live-reactions/1f680.gif +0 -0
  37. package/template/src/assets/live-reactions/2705.gif +0 -0
  38. package/template/src/assets/live-reactions/animated/1f440.json +1 -0
  39. package/template/src/assets/live-reactions/animated/1f44d_1f3fb.json +1 -0
  40. package/template/src/assets/live-reactions/animated/1f44d_1f3fc.json +1 -0
  41. package/template/src/assets/live-reactions/animated/1f44d_1f3fd.json +1 -0
  42. package/template/src/assets/live-reactions/animated/1f44d_1f3fe.json +1 -0
  43. package/template/src/assets/live-reactions/animated/1f44d_1f3ff.json +1 -0
  44. package/template/src/assets/live-reactions/animated/1f44f_1f3fb.json +1 -0
  45. package/template/src/assets/live-reactions/animated/1f44f_1f3fc.json +1 -0
  46. package/template/src/assets/live-reactions/animated/1f44f_1f3fd.json +1 -0
  47. package/template/src/assets/live-reactions/animated/1f44f_1f3fe.json +1 -0
  48. package/template/src/assets/live-reactions/animated/1f44f_1f3ff.json +1 -0
  49. package/template/src/assets/live-reactions/animated/1f4a1.json +1 -0
  50. package/template/src/assets/live-reactions/animated/1f4af.json +1 -0
  51. package/template/src/assets/live-reactions/animated/1f525.json +1 -0
  52. package/template/src/assets/live-reactions/animated/1f60e.json +1 -0
  53. package/template/src/assets/live-reactions/animated/1f643.json +1 -0
  54. package/template/src/assets/live-reactions/animated/1f64c.json +1 -0
  55. package/template/src/assets/live-reactions/animated/1f64c_1f3fb.json +1 -0
  56. package/template/src/assets/live-reactions/animated/1f64c_1f3fc.json +1 -0
  57. package/template/src/assets/live-reactions/animated/1f64c_1f3fd.json +1 -0
  58. package/template/src/assets/live-reactions/animated/1f64c_1f3fe.json +1 -0
  59. package/template/src/assets/live-reactions/animated/1f64c_1f3ff.json +1 -0
  60. package/template/src/assets/live-reactions/animated/1f64f.json +1 -0
  61. package/template/src/assets/live-reactions/animated/1f64f_1f3fb.json +1 -0
  62. package/template/src/assets/live-reactions/animated/1f64f_1f3fc.json +1 -0
  63. package/template/src/assets/live-reactions/animated/1f64f_1f3fd.json +1 -0
  64. package/template/src/assets/live-reactions/animated/1f64f_1f3fe.json +1 -0
  65. package/template/src/assets/live-reactions/animated/1f64f_1f3ff.json +1 -0
  66. package/template/src/assets/live-reactions/animated/1f680.json +1 -0
  67. package/template/src/assets/live-reactions/animated/2705.json +1 -0
  68. package/template/src/assets/live-reactions/custom/ty.png +0 -0
  69. package/template/src/components/RTMConfigure.tsx +15 -10
  70. package/template/src/components/StorageContext.tsx +3 -0
  71. package/template/src/components/reactions/LiveReactionBadge.tsx +20 -2
  72. package/template/src/components/reactions/LiveReactionButton.tsx +200 -44
  73. package/template/src/components/reactions/LiveReactionStageOverlay.native.tsx +34 -10
  74. package/template/src/components/reactions/LiveReactionStageOverlay.tsx +30 -7
  75. package/template/src/components/reactions/catalog.ts +241 -0
  76. package/template/src/components/useVideoCall.tsx +20 -2
  77. package/template/src/rtm/__tests__/isRemoteDeparture.test.ts +19 -0
  78. package/template/src/rtm/isRemoteDeparture.ts +4 -0
  79. package/template/src/subComponents/caption/__tests__/sttRequestBody.test.ts +61 -0
  80. package/template/src/subComponents/caption/__tests__/sttSessionId.native.test.ts +76 -0
  81. package/template/src/subComponents/caption/__tests__/sttSessionId.web.test.ts +190 -0
  82. package/template/src/subComponents/caption/__tests__/useSTTAPI.test.tsx +129 -0
  83. package/template/src/subComponents/caption/sttRequestBody.ts +54 -0
  84. package/template/src/subComponents/caption/sttSessionCoordinator.ts +315 -0
  85. package/template/src/subComponents/caption/sttSessionId.ts +21 -0
  86. package/template/src/subComponents/caption/sttSessionId.web.ts +21 -0
  87. package/template/src/subComponents/caption/useSTTAPI.tsx +13 -33
  88. package/template/src/utils/__tests__/useEndCall.test.tsx +282 -0
  89. package/template/src/utils/useEndCall.ts +63 -22
@@ -4,7 +4,8 @@ import {useVideoCall} from '../useVideoCall';
4
4
  import {
5
5
  LIVE_REACTION_FLOAT_DURATION,
6
6
  LIVE_REACTION_LANE_COUNT,
7
- LIVE_REACTION_MAP,
7
+ getLiveReactionMap,
8
+ resolveReactionVisual,
8
9
  } from './catalog';
9
10
 
10
11
  // Use the bundled Lottie player directly instead of relying on a global.
@@ -123,6 +124,11 @@ const css = `
123
124
  object-fit: contain;
124
125
  display: block;
125
126
  }
127
+ .live-reaction-stage-item img.live-reaction-custom {
128
+ width: 36px;
129
+ height: 36px;
130
+ border-radius: 6px;
131
+ }
126
132
  .live-reaction-clap {
127
133
  width: 48px;
128
134
  height: 48px;
@@ -181,13 +187,15 @@ const css = `
181
187
  const ReactionArt = ({
182
188
  fallbackSrc,
183
189
  lottieData,
190
+ isCustom,
184
191
  }: {
185
192
  fallbackSrc: string;
186
193
  lottieData?: any;
194
+ isCustom?: boolean;
187
195
  }) => {
188
196
  const containerRef = React.useRef<HTMLDivElement | null>(null);
189
197
  const animationRef = React.useRef<any>(null);
190
- const [shouldFallback, setShouldFallback] = React.useState(false);
198
+ const [shouldFallback, setShouldFallback] = React.useState(!lottieData);
191
199
 
192
200
  React.useEffect(() => {
193
201
  const container = containerRef.current;
@@ -222,7 +230,14 @@ const ReactionArt = ({
222
230
  }, [lottieData, shouldFallback]);
223
231
 
224
232
  if (shouldFallback) {
225
- return <img src={fallbackSrc} alt="" aria-hidden="true" />;
233
+ return (
234
+ <img
235
+ src={fallbackSrc}
236
+ alt=""
237
+ aria-hidden="true"
238
+ className={isCustom ? 'live-reaction-custom' : undefined}
239
+ />
240
+ );
226
241
  }
227
242
 
228
243
  return (
@@ -278,8 +293,15 @@ const LiveReactionStageOverlay = () => {
278
293
  </div>
279
294
  ) : null}
280
295
  {floatingReactions.map((reaction, index) => {
281
- const reactionDefinition = LIVE_REACTION_MAP[reaction.assetKey];
282
- if (!reactionDefinition) {
296
+ const catalogEntry = getLiveReactionMap()[reaction.assetKey];
297
+ if (!catalogEntry) {
298
+ return null;
299
+ }
300
+ const visual = resolveReactionVisual(
301
+ reaction.assetKey,
302
+ reaction.skinTone,
303
+ );
304
+ if (!visual) {
283
305
  return null;
284
306
  }
285
307
  const lane =
@@ -304,8 +326,9 @@ const LiveReactionStageOverlay = () => {
304
326
  }>
305
327
  <div className="live-reaction-stage-item-content">
306
328
  <ReactionArt
307
- fallbackSrc={reactionDefinition.asset}
308
- lottieData={reactionDefinition.lottieData}
329
+ fallbackSrc={visual.asset}
330
+ lottieData={visual.lottieData}
331
+ isCustom={!!catalogEntry.custom}
309
332
  />
310
333
  <div
311
334
  className="live-reaction-sender-name"
@@ -1,8 +1,26 @@
1
+ // @ts-ignore
2
+ import tyCustom from '../../assets/live-reactions/custom/ty.png';
3
+
4
+ export type SkinToneCode = '1f3fb' | '1f3fc' | '1f3fd' | '1f3fe' | '1f3ff';
5
+ export type SkinTonePreference = 'default' | SkinToneCode;
6
+
7
+ export interface LiveReactionSkinToneAsset {
8
+ asset: any;
9
+ lottieData?: any;
10
+ }
11
+
12
+ export interface LiveReactionCustomMeta {
13
+ label: string;
14
+ fallbackEmoji: string;
15
+ }
16
+
1
17
  export interface LiveReactionDefinition {
2
18
  key: string;
3
19
  emoji: string;
4
20
  asset: any;
5
21
  lottieData?: any;
22
+ skinToneVariants?: Record<SkinToneCode, LiveReactionSkinToneAsset>;
23
+ custom?: LiveReactionCustomMeta;
6
24
  }
7
25
 
8
26
  export interface LiveReactionEvent {
@@ -13,6 +31,7 @@ export interface LiveReactionEvent {
13
31
  senderDisplayName?: string;
14
32
  timestamp: number;
15
33
  lane?: number;
34
+ skinTone?: SkinToneCode;
16
35
  }
17
36
 
18
37
  export const LIVE_REACTION_LANE_COUNT = 5;
@@ -20,6 +39,22 @@ export const LIVE_REACTION_BADGE_DURATION = 10_000;
20
39
  export const LIVE_REACTION_FLOAT_DURATION = 4_200;
21
40
  export const LIVE_REACTION_MAX_FLOATING_ITEMS = 20;
22
41
 
42
+ export const SKIN_TONE_CODES: SkinToneCode[] = [
43
+ '1f3fb',
44
+ '1f3fc',
45
+ '1f3fd',
46
+ '1f3fe',
47
+ '1f3ff',
48
+ ];
49
+
50
+ export const SKIN_TONE_MODIFIER: Record<SkinToneCode, string> = {
51
+ '1f3fb': '\u{1F3FB}',
52
+ '1f3fc': '\u{1F3FC}',
53
+ '1f3fd': '\u{1F3FD}',
54
+ '1f3fe': '\u{1F3FE}',
55
+ '1f3ff': '\u{1F3FF}',
56
+ };
57
+
23
58
  export const LIVE_REACTIONS: LiveReactionDefinition[] = [
24
59
  {
25
60
  key: 'sparkling-heart',
@@ -32,6 +67,28 @@ export const LIVE_REACTIONS: LiveReactionDefinition[] = [
32
67
  emoji: '👍',
33
68
  asset: require('../../assets/live-reactions/1f44d.gif'),
34
69
  lottieData: require('../../assets/live-reactions/animated/1f44d.json'),
70
+ skinToneVariants: {
71
+ '1f3fb': {
72
+ asset: require('../../assets/live-reactions/1f44d_1f3fb.gif'),
73
+ lottieData: require('../../assets/live-reactions/animated/1f44d_1f3fb.json'),
74
+ },
75
+ '1f3fc': {
76
+ asset: require('../../assets/live-reactions/1f44d_1f3fc.gif'),
77
+ lottieData: require('../../assets/live-reactions/animated/1f44d_1f3fc.json'),
78
+ },
79
+ '1f3fd': {
80
+ asset: require('../../assets/live-reactions/1f44d_1f3fd.gif'),
81
+ lottieData: require('../../assets/live-reactions/animated/1f44d_1f3fd.json'),
82
+ },
83
+ '1f3fe': {
84
+ asset: require('../../assets/live-reactions/1f44d_1f3fe.gif'),
85
+ lottieData: require('../../assets/live-reactions/animated/1f44d_1f3fe.json'),
86
+ },
87
+ '1f3ff': {
88
+ asset: require('../../assets/live-reactions/1f44d_1f3ff.gif'),
89
+ lottieData: require('../../assets/live-reactions/animated/1f44d_1f3ff.json'),
90
+ },
91
+ },
35
92
  },
36
93
  {
37
94
  key: 'party-popper',
@@ -44,6 +101,28 @@ export const LIVE_REACTIONS: LiveReactionDefinition[] = [
44
101
  emoji: '👏',
45
102
  asset: require('../../assets/live-reactions/1f44f.gif'),
46
103
  lottieData: require('../../assets/live-reactions/animated/1f44f.json'),
104
+ skinToneVariants: {
105
+ '1f3fb': {
106
+ asset: require('../../assets/live-reactions/1f44f_1f3fb.gif'),
107
+ lottieData: require('../../assets/live-reactions/animated/1f44f_1f3fb.json'),
108
+ },
109
+ '1f3fc': {
110
+ asset: require('../../assets/live-reactions/1f44f_1f3fc.gif'),
111
+ lottieData: require('../../assets/live-reactions/animated/1f44f_1f3fc.json'),
112
+ },
113
+ '1f3fd': {
114
+ asset: require('../../assets/live-reactions/1f44f_1f3fd.gif'),
115
+ lottieData: require('../../assets/live-reactions/animated/1f44f_1f3fd.json'),
116
+ },
117
+ '1f3fe': {
118
+ asset: require('../../assets/live-reactions/1f44f_1f3fe.gif'),
119
+ lottieData: require('../../assets/live-reactions/animated/1f44f_1f3fe.json'),
120
+ },
121
+ '1f3ff': {
122
+ asset: require('../../assets/live-reactions/1f44f_1f3ff.gif'),
123
+ lottieData: require('../../assets/live-reactions/animated/1f44f_1f3ff.json'),
124
+ },
125
+ },
47
126
  },
48
127
  {
49
128
  key: 'joy',
@@ -69,11 +148,173 @@ export const LIVE_REACTIONS: LiveReactionDefinition[] = [
69
148
  asset: require('../../assets/live-reactions/1f914.gif'),
70
149
  lottieData: require('../../assets/live-reactions/animated/1f914.json'),
71
150
  },
151
+ {
152
+ key: 'eyes',
153
+ emoji: '👀',
154
+ asset: require('../../assets/live-reactions/1f440.gif'),
155
+ lottieData: require('../../assets/live-reactions/animated/1f440.json'),
156
+ },
157
+ {
158
+ key: 'rocket',
159
+ emoji: '🚀',
160
+ asset: require('../../assets/live-reactions/1f680.gif'),
161
+ lottieData: require('../../assets/live-reactions/animated/1f680.json'),
162
+ },
163
+ {
164
+ key: 'upside-down-face',
165
+ emoji: '🙃',
166
+ asset: require('../../assets/live-reactions/1f643.gif'),
167
+ lottieData: require('../../assets/live-reactions/animated/1f643.json'),
168
+ },
169
+ {
170
+ key: 'fire',
171
+ emoji: '🔥',
172
+ asset: require('../../assets/live-reactions/1f525.gif'),
173
+ lottieData: require('../../assets/live-reactions/animated/1f525.json'),
174
+ },
175
+ {
176
+ key: 'hundred',
177
+ emoji: '💯',
178
+ asset: require('../../assets/live-reactions/1f4af.gif'),
179
+ lottieData: require('../../assets/live-reactions/animated/1f4af.json'),
180
+ },
181
+ {
182
+ key: 'raising-hands',
183
+ emoji: '🙌',
184
+ asset: require('../../assets/live-reactions/1f64c.gif'),
185
+ lottieData: require('../../assets/live-reactions/animated/1f64c.json'),
186
+ skinToneVariants: {
187
+ '1f3fb': {
188
+ asset: require('../../assets/live-reactions/1f64c_1f3fb.gif'),
189
+ lottieData: require('../../assets/live-reactions/animated/1f64c_1f3fb.json'),
190
+ },
191
+ '1f3fc': {
192
+ asset: require('../../assets/live-reactions/1f64c_1f3fc.gif'),
193
+ lottieData: require('../../assets/live-reactions/animated/1f64c_1f3fc.json'),
194
+ },
195
+ '1f3fd': {
196
+ asset: require('../../assets/live-reactions/1f64c_1f3fd.gif'),
197
+ lottieData: require('../../assets/live-reactions/animated/1f64c_1f3fd.json'),
198
+ },
199
+ '1f3fe': {
200
+ asset: require('../../assets/live-reactions/1f64c_1f3fe.gif'),
201
+ lottieData: require('../../assets/live-reactions/animated/1f64c_1f3fe.json'),
202
+ },
203
+ '1f3ff': {
204
+ asset: require('../../assets/live-reactions/1f64c_1f3ff.gif'),
205
+ lottieData: require('../../assets/live-reactions/animated/1f64c_1f3ff.json'),
206
+ },
207
+ },
208
+ },
209
+ {
210
+ key: 'light-bulb',
211
+ emoji: '💡',
212
+ asset: require('../../assets/live-reactions/1f4a1.gif'),
213
+ lottieData: require('../../assets/live-reactions/animated/1f4a1.json'),
214
+ },
215
+ {
216
+ key: 'folded-hands',
217
+ emoji: '🙏',
218
+ asset: require('../../assets/live-reactions/1f64f.gif'),
219
+ lottieData: require('../../assets/live-reactions/animated/1f64f.json'),
220
+ skinToneVariants: {
221
+ '1f3fb': {
222
+ asset: require('../../assets/live-reactions/1f64f_1f3fb.gif'),
223
+ lottieData: require('../../assets/live-reactions/animated/1f64f_1f3fb.json'),
224
+ },
225
+ '1f3fc': {
226
+ asset: require('../../assets/live-reactions/1f64f_1f3fc.gif'),
227
+ lottieData: require('../../assets/live-reactions/animated/1f64f_1f3fc.json'),
228
+ },
229
+ '1f3fd': {
230
+ asset: require('../../assets/live-reactions/1f64f_1f3fd.gif'),
231
+ lottieData: require('../../assets/live-reactions/animated/1f64f_1f3fd.json'),
232
+ },
233
+ '1f3fe': {
234
+ asset: require('../../assets/live-reactions/1f64f_1f3fe.gif'),
235
+ lottieData: require('../../assets/live-reactions/animated/1f64f_1f3fe.json'),
236
+ },
237
+ '1f3ff': {
238
+ asset: require('../../assets/live-reactions/1f64f_1f3ff.gif'),
239
+ lottieData: require('../../assets/live-reactions/animated/1f64f_1f3ff.json'),
240
+ },
241
+ },
242
+ },
243
+ {
244
+ key: 'check-mark',
245
+ emoji: '✅',
246
+ asset: require('../../assets/live-reactions/2705.gif'),
247
+ lottieData: require('../../assets/live-reactions/animated/2705.json'),
248
+ },
249
+ {
250
+ key: 'sunglasses',
251
+ emoji: '😎',
252
+ asset: require('../../assets/live-reactions/1f60e.gif'),
253
+ lottieData: require('../../assets/live-reactions/animated/1f60e.json'),
254
+ },
255
+ {
256
+ key: 'thank-you',
257
+ emoji: '🙏',
258
+ asset: tyCustom,
259
+ custom: {label: 'Thank you', fallbackEmoji: '🙏'},
260
+ },
72
261
  ];
73
262
 
263
+ let _customLiveReactions: LiveReactionDefinition[] = [];
264
+
265
+ export function registerCustomLiveReactions(
266
+ reactions: LiveReactionDefinition[],
267
+ ): void {
268
+ _customLiveReactions = Array.isArray(reactions) ? [...reactions] : [];
269
+ }
270
+
271
+ export function getLiveReactions(): LiveReactionDefinition[] {
272
+ return [...LIVE_REACTIONS, ..._customLiveReactions];
273
+ }
274
+
275
+ export function getLiveReactionMap(): Record<string, LiveReactionDefinition> {
276
+ return getLiveReactions().reduce<Record<string, LiveReactionDefinition>>(
277
+ (acc, reaction) => {
278
+ acc[reaction.key] = reaction;
279
+ return acc;
280
+ },
281
+ {},
282
+ );
283
+ }
284
+
74
285
  export const LIVE_REACTION_MAP = LIVE_REACTIONS.reduce<
75
286
  Record<string, LiveReactionDefinition>
76
287
  >((acc, reaction) => {
77
288
  acc[reaction.key] = reaction;
78
289
  return acc;
79
290
  }, {});
291
+
292
+ export function supportsSkinTone(
293
+ reaction: LiveReactionDefinition | undefined,
294
+ ): boolean {
295
+ return !!reaction?.skinToneVariants;
296
+ }
297
+
298
+ export function resolveReactionVisual(
299
+ assetKey: string,
300
+ skinTone?: SkinToneCode,
301
+ ): LiveReactionSkinToneAsset | undefined {
302
+ const reaction = getLiveReactionMap()[assetKey];
303
+ if (!reaction) {
304
+ return undefined;
305
+ }
306
+ if (skinTone && reaction.skinToneVariants?.[skinTone]) {
307
+ return reaction.skinToneVariants[skinTone];
308
+ }
309
+ return {asset: reaction.asset, lottieData: reaction.lottieData};
310
+ }
311
+
312
+ export function applySkinToneToEmoji(
313
+ reaction: LiveReactionDefinition,
314
+ tone: SkinTonePreference,
315
+ ): string {
316
+ if (tone === 'default' || !reaction.skinToneVariants?.[tone]) {
317
+ return reaction.emoji;
318
+ }
319
+ return `${reaction.emoji}${SKIN_TONE_MODIFIER[tone]}`;
320
+ }
@@ -45,7 +45,10 @@ import {
45
45
  LIVE_REACTION_MAX_FLOATING_ITEMS,
46
46
  LiveReactionDefinition,
47
47
  LiveReactionEvent,
48
+ SkinToneCode,
49
+ applySkinToneToEmoji,
48
50
  } from './reactions/catalog';
51
+ import StorageContext from './StorageContext';
49
52
  import {useString} from '../utils/useString';
50
53
  import {videoRoomUserFallbackText} from '../language/default-labels/videoCallScreenLabels';
51
54
 
@@ -117,6 +120,7 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
117
120
  const {uids} = useUserPreference();
118
121
  const remoteUserFallbackName = useString(videoRoomUserFallbackText)();
119
122
  const {deviceList} = useContext(DeviceContext);
123
+ const {store} = useContext(StorageContext);
120
124
  const setUsername = useSetName();
121
125
  //const videoTileInViewPortStateRef = useRef({});
122
126
  const [videoTileInViewPortState, setVideoTileInViewPortStateL] = useState({});
@@ -254,13 +258,20 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
254
258
  reaction.key
255
259
  }-${Date.now()}-${nanoid(4)}`;
256
260
  const senderDisplayName = getReactionSenderName(String(localUser.uid));
261
+ const tonePreference = store.liveReactionSkinTone ?? 'default';
262
+ const skinTone: SkinToneCode | undefined =
263
+ tonePreference !== 'default' && reaction.skinToneVariants?.[tonePreference]
264
+ ? tonePreference
265
+ : undefined;
266
+ const emoji = applySkinToneToEmoji(reaction, tonePreference);
257
267
  const nextReaction: LiveReactionEvent = {
258
268
  reactionId,
259
269
  assetKey: reaction.key,
260
- emoji: reaction.emoji,
270
+ emoji,
261
271
  senderUid: String(localUser.uid),
262
272
  senderDisplayName,
263
273
  timestamp: Date.now(),
274
+ skinTone,
264
275
  };
265
276
 
266
277
  ingestReaction(nextReaction);
@@ -271,11 +282,17 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
271
282
  assetKey: nextReaction.assetKey,
272
283
  emoji: nextReaction.emoji,
273
284
  timestamp: nextReaction.timestamp,
285
+ skinTone: nextReaction.skinTone,
274
286
  }),
275
287
  PersistanceLevel.None,
276
288
  );
277
289
  },
278
- [getReactionSenderName, ingestReaction, localUser.uid],
290
+ [
291
+ getReactionSenderName,
292
+ ingestReaction,
293
+ localUser.uid,
294
+ store.liveReactionSkinTone,
295
+ ],
279
296
  );
280
297
 
281
298
  useEffect(() => {
@@ -312,6 +329,7 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
312
329
  emoji: payload.emoji,
313
330
  senderUid: String(data.sender),
314
331
  timestamp: payload.timestamp || data.ts || Date.now(),
332
+ skinTone: payload.skinTone,
315
333
  });
316
334
  } catch (error) {
317
335
  console.warn('Failed to parse live reaction payload', error);
@@ -0,0 +1,19 @@
1
+ import {describe, expect, it} from '@jest/globals';
2
+ import {isRemoteDeparture} from '../isRemoteDeparture';
3
+
4
+ describe('isRemoteDeparture', () => {
5
+ it('always treats an explicit remote leave as a departure', () => {
6
+ expect(isRemoteDeparture(4, false)).toBe(true);
7
+ expect(isRemoteDeparture(4, true)).toBe(true);
8
+ });
9
+
10
+ it('treats a remote timeout as a departure only when enabled', () => {
11
+ expect(isRemoteDeparture(5, true)).toBe(true);
12
+ expect(isRemoteDeparture(5, false)).toBe(false);
13
+ });
14
+
15
+ it('ignores unrelated presence event types', () => {
16
+ expect(isRemoteDeparture(3, true)).toBe(false);
17
+ expect(isRemoteDeparture(6, true)).toBe(false);
18
+ });
19
+ });
@@ -0,0 +1,4 @@
1
+ export const isRemoteDeparture = (
2
+ type: number,
3
+ includeTimeout: boolean,
4
+ ): boolean => type === 4 || (includeTimeout && type === 5);
@@ -0,0 +1,61 @@
1
+ import {describe, expect, it, jest} from '@jest/globals';
2
+ import {buildSTTRequestBody, type STTMethod} from '../sttRequestBody';
3
+
4
+ const createOptions = (method: STTMethod) => ({
5
+ method,
6
+ botUid: 900000123,
7
+ passphrase: 'phrase',
8
+ encryptionMode: 8,
9
+ localUid: 123,
10
+ channelName: 'room',
11
+ translationConfig:
12
+ method === 'stopv7'
13
+ ? undefined
14
+ : {source: ['en-US'] as any, targets: ['de-DE'] as any},
15
+ resolveSessionId: jest.fn(async () => 'shared-session'),
16
+ });
17
+
18
+ describe('buildSTTRequestBody', () => {
19
+ it.each(['startv7', 'update', 'stopv7'] as STTMethod[])(
20
+ 'adds the coordinated session ID to %s on every client platform',
21
+ async method => {
22
+ const body = await buildSTTRequestBody(createOptions(method));
23
+
24
+ expect(body.session_id).toBe('shared-session');
25
+ expect(body.passphrase).toBe('phrase');
26
+ expect(body.dataStream_uid).toBe(900000123);
27
+ expect(body.encryption_mode).toBe(8);
28
+ },
29
+ );
30
+
31
+ it('keeps the existing update translation payload behavior', async () => {
32
+ const body = await buildSTTRequestBody({
33
+ ...createOptions('update'),
34
+ translationConfig: {
35
+ source: ['en-US'],
36
+ targets: ['en-US', 'de-DE'],
37
+ },
38
+ });
39
+
40
+ expect(body).toMatchObject({
41
+ lang: ['en-US'],
42
+ translate: true,
43
+ translate_config: [
44
+ {
45
+ source_lang: 'en-US',
46
+ target_lang: ['de-DE'],
47
+ },
48
+ ],
49
+ subscribeAudioUids: ['123'],
50
+ });
51
+ });
52
+
53
+ it('rejects construction when the shared ID is unavailable', async () => {
54
+ await expect(
55
+ buildSTTRequestBody({
56
+ ...createOptions('stopv7'),
57
+ resolveSessionId: jest.fn(async () => undefined),
58
+ }),
59
+ ).rejects.toThrow('Unable to resolve the shared STT session ID');
60
+ });
61
+ });
@@ -0,0 +1,76 @@
1
+ import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
2
+
3
+ const mockStorage = {
4
+ getChannelMetadata: jest.fn<any>(),
5
+ setChannelMetadata: jest.fn<any>(),
6
+ removeChannelMetadata: jest.fn<any>(),
7
+ };
8
+ const mockPresence = {
9
+ getOnlineUsers: jest.fn<any>(),
10
+ };
11
+
12
+ jest.mock('../../../rtm/RTMEngine', () => ({
13
+ __esModule: true,
14
+ default: {
15
+ getInstance: () => ({
16
+ engine: {storage: mockStorage, presence: mockPresence},
17
+ }),
18
+ },
19
+ }));
20
+
21
+ jest.mock('../../../utils/getUniqueID', () => ({
22
+ __esModule: true,
23
+ default: jest.fn(() => 'native-session'),
24
+ }));
25
+
26
+ import {
27
+ ensureSTTSessionId,
28
+ resetSTTSessionIdCache,
29
+ STT_SESSION_ID_KEY,
30
+ } from '../sttSessionId';
31
+
32
+ const emptyChannelMetadata = {
33
+ timestamp: 100,
34
+ channelName: 'room',
35
+ channelType: 1,
36
+ majorRevision: 0,
37
+ itemCount: 0,
38
+ items: [],
39
+ };
40
+
41
+ describe('native STT session coordination', () => {
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ jest.spyOn(console, 'log').mockImplementation(() => {});
45
+ resetSTTSessionIdCache();
46
+ mockStorage.getChannelMetadata.mockResolvedValue(emptyChannelMetadata);
47
+ mockStorage.setChannelMetadata.mockResolvedValue({
48
+ timestamp: 100,
49
+ channelName: 'room',
50
+ channelType: 1,
51
+ });
52
+ });
53
+
54
+ afterEach(() => {
55
+ jest.restoreAllMocks();
56
+ });
57
+
58
+ it('creates the shared session ID through the native RTM client', async () => {
59
+ await expect(ensureSTTSessionId('room')).resolves.toBe('native-session');
60
+
61
+ expect(mockStorage.setChannelMetadata).toHaveBeenCalledWith(
62
+ 'room',
63
+ 1,
64
+ {
65
+ items: [
66
+ {
67
+ key: STT_SESSION_ID_KEY,
68
+ value: 'native-session',
69
+ revision: 0,
70
+ },
71
+ ],
72
+ },
73
+ {addUserId: true, addTimeStamp: true},
74
+ );
75
+ });
76
+ });