agora-appbuilder-core 4.1.16 → 4.1.17

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 (34) hide show
  1. package/package.json +1 -1
  2. package/template/defaultConfig.js +3 -2
  3. package/template/global.d.ts +1 -0
  4. package/template/package.json +1 -0
  5. package/template/src/assets/live-reactions/1f389.gif +0 -0
  6. package/template/src/assets/live-reactions/1f44d.gif +0 -0
  7. package/template/src/assets/live-reactions/1f44f.gif +0 -0
  8. package/template/src/assets/live-reactions/1f496.gif +0 -0
  9. package/template/src/assets/live-reactions/1f602.gif +0 -0
  10. package/template/src/assets/live-reactions/1f622.gif +0 -0
  11. package/template/src/assets/live-reactions/1f62e.gif +0 -0
  12. package/template/src/assets/live-reactions/1f914.gif +0 -0
  13. package/template/src/assets/live-reactions/animated/1f389.json +1 -0
  14. package/template/src/assets/live-reactions/animated/1f44d.json +1 -0
  15. package/template/src/assets/live-reactions/animated/1f44f.json +1 -0
  16. package/template/src/assets/live-reactions/animated/1f496.json +1 -0
  17. package/template/src/assets/live-reactions/animated/1f602.json +1 -0
  18. package/template/src/assets/live-reactions/animated/1f622.json +1 -0
  19. package/template/src/assets/live-reactions/animated/1f62e.json +1 -0
  20. package/template/src/assets/live-reactions/animated/1f914.json +1 -0
  21. package/template/src/components/Controls.tsx +21 -5
  22. package/template/src/components/reactions/LiveReactionBadge.tsx +57 -0
  23. package/template/src/components/reactions/LiveReactionButton.tsx +257 -0
  24. package/template/src/components/reactions/LiveReactionStageOverlay.native.tsx +256 -0
  25. package/template/src/components/reactions/LiveReactionStageOverlay.tsx +326 -0
  26. package/template/src/components/reactions/catalog.ts +79 -0
  27. package/template/src/components/useVideoCall.tsx +219 -1
  28. package/template/src/language/default-labels/videoCallScreenLabels.ts +3 -0
  29. package/template/src/pages/video-call/ActionSheetContent.tsx +14 -1
  30. package/template/src/pages/video-call/VideoComponent.tsx +9 -1
  31. package/template/src/pages/video-call/VideoRenderer.tsx +8 -0
  32. package/template/src/rtm-events/constants.ts +2 -0
  33. package/template/webpack.commons.js +1 -1
  34. package/template/webpack.ts.config.js +1 -1
@@ -13,6 +13,7 @@
13
13
  import React, {
14
14
  SetStateAction,
15
15
  useState,
16
+ useCallback,
16
17
  useContext,
17
18
  useEffect,
18
19
  useRef,
@@ -23,11 +24,30 @@ import StopRecordingPopup from './popups/StopRecordingPopup';
23
24
  import StartScreenSharePopup from './popups/StartScreenSharePopup';
24
25
  import StopScreenSharePopup from './popups/StopScreenSharePopup';
25
26
  import {SdkApiContext} from './SdkApiContext';
26
- import {UidType, useRoomInfo} from 'customization-api';
27
+ import {
28
+ UidType,
29
+ useContent,
30
+ useLocalUserInfo,
31
+ useRoomInfo,
32
+ } from 'customization-api';
27
33
  import SDKEvents from '../utils/SdkEvents';
28
34
  import DeviceContext from './DeviceContext';
29
35
  import useSetName from '../utils/useSetName';
30
36
  import WhiteboardClearAllPopup from './popups/WhiteboardClearAllPopup';
37
+ import events from '../rtm-events-api';
38
+ import {PersistanceLevel} from '../rtm-events-api/types';
39
+ import {EventNames} from '../rtm-events';
40
+ import {nanoid} from 'nanoid/non-secure';
41
+ import {useUserPreference} from './useUserPreference';
42
+ import {
43
+ LIVE_REACTION_BADGE_DURATION,
44
+ LIVE_REACTION_FLOAT_DURATION,
45
+ LIVE_REACTION_MAX_FLOATING_ITEMS,
46
+ LiveReactionDefinition,
47
+ LiveReactionEvent,
48
+ } from './reactions/catalog';
49
+ import {useString} from '../utils/useString';
50
+ import {videoRoomUserFallbackText} from '../language/default-labels/videoCallScreenLabels';
31
51
 
32
52
  interface InViewPortState {
33
53
  [key: number]: boolean;
@@ -49,6 +69,9 @@ export interface VideoCallContextInterface {
49
69
  setVideoTileInViewPortState: (uid: UidType, visible: boolean) => void;
50
70
  showWhiteboardClearAllPopup: boolean;
51
71
  setShowWhiteboardClearAllPopup: React.Dispatch<SetStateAction<boolean>>;
72
+ latestReactionByUid: Record<string, LiveReactionEvent>;
73
+ floatingReactions: LiveReactionEvent[];
74
+ emitLiveReaction: (reaction: LiveReactionDefinition) => void;
52
75
  }
53
76
 
54
77
  const VideoCallContext = React.createContext<VideoCallContextInterface>({
@@ -68,6 +91,9 @@ const VideoCallContext = React.createContext<VideoCallContextInterface>({
68
91
  setVideoTileInViewPortState: () => {},
69
92
  showWhiteboardClearAllPopup: false,
70
93
  setShowWhiteboardClearAllPopup: () => {},
94
+ latestReactionByUid: {},
95
+ floatingReactions: [],
96
+ emitLiveReaction: () => {},
71
97
  });
72
98
 
73
99
  interface VideoCallProviderProps {
@@ -86,10 +112,50 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
86
112
  useState(false);
87
113
  const {join, enterRoom} = useContext(SdkApiContext);
88
114
  const roomInfo = useRoomInfo();
115
+ const localUser = useLocalUserInfo();
116
+ const {defaultContent} = useContent();
117
+ const {uids} = useUserPreference();
118
+ const remoteUserFallbackName = useString(videoRoomUserFallbackText)();
89
119
  const {deviceList} = useContext(DeviceContext);
90
120
  const setUsername = useSetName();
91
121
  //const videoTileInViewPortStateRef = useRef({});
92
122
  const [videoTileInViewPortState, setVideoTileInViewPortStateL] = useState({});
123
+ const [latestReactionByUid, setLatestReactionByUid] = useState<
124
+ Record<string, LiveReactionEvent>
125
+ >({});
126
+ const [floatingReactions, setFloatingReactions] = useState<
127
+ LiveReactionEvent[]
128
+ >([]);
129
+ const reactionBadgeTimeoutsRef = useRef<
130
+ Record<string, ReturnType<typeof setTimeout>>
131
+ >({});
132
+ const floatingReactionTimeoutsRef = useRef<
133
+ Record<string, ReturnType<typeof setTimeout>>
134
+ >({});
135
+ const processedReactionIdsRef = useRef<Set<string>>(new Set());
136
+
137
+ const assignReactionLane = useCallback((reaction: LiveReactionEvent) => {
138
+ if (typeof reaction.lane === 'number') {
139
+ return reaction;
140
+ }
141
+ const lane = Math.floor(Math.random() * 5);
142
+ return {...reaction, lane};
143
+ }, []);
144
+
145
+ const getReactionSenderName = useCallback(
146
+ (senderUid: string) => {
147
+ if (String(localUser.uid) === String(senderUid)) {
148
+ return 'You';
149
+ }
150
+ return (
151
+ uids[String(senderUid)]?.name ||
152
+ defaultContent[Number(senderUid)]?.name ||
153
+ defaultContent[String(senderUid)]?.name ||
154
+ remoteUserFallbackName
155
+ );
156
+ },
157
+ [defaultContent, localUser.uid, remoteUserFallbackName, uids],
158
+ );
93
159
 
94
160
  const setVideoTileInViewPortState = (uid: UidType, visible: boolean) => {
95
161
  //videoTileInViewPortStateRef.current[uid] = visible;
@@ -101,6 +167,117 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
101
167
  });
102
168
  };
103
169
 
170
+ const cleanupReactionBadgeTimeout = useCallback((uid: string) => {
171
+ if (reactionBadgeTimeoutsRef.current[uid]) {
172
+ clearTimeout(reactionBadgeTimeoutsRef.current[uid]);
173
+ delete reactionBadgeTimeoutsRef.current[uid];
174
+ }
175
+ }, []);
176
+
177
+ const cleanupFloatingReactionTimeout = useCallback((reactionId: string) => {
178
+ if (floatingReactionTimeoutsRef.current[reactionId]) {
179
+ clearTimeout(floatingReactionTimeoutsRef.current[reactionId]);
180
+ delete floatingReactionTimeoutsRef.current[reactionId];
181
+ }
182
+ }, []);
183
+
184
+ const ingestReaction = useCallback(
185
+ (reaction: LiveReactionEvent) => {
186
+ if (!$config.ENABLE_LIVE_REACTIONS) {
187
+ return;
188
+ }
189
+ if (processedReactionIdsRef.current.has(reaction.reactionId)) {
190
+ return;
191
+ }
192
+ const nextReaction = assignReactionLane({
193
+ ...reaction,
194
+ senderDisplayName: getReactionSenderName(reaction.senderUid),
195
+ });
196
+ processedReactionIdsRef.current.add(nextReaction.reactionId);
197
+ if (processedReactionIdsRef.current.size > 200) {
198
+ processedReactionIdsRef.current = new Set(
199
+ Array.from(processedReactionIdsRef.current).slice(-100),
200
+ );
201
+ }
202
+
203
+ setLatestReactionByUid(prev => ({
204
+ ...prev,
205
+ [nextReaction.senderUid]: nextReaction,
206
+ }));
207
+ cleanupReactionBadgeTimeout(nextReaction.senderUid);
208
+ reactionBadgeTimeoutsRef.current[nextReaction.senderUid] = setTimeout(
209
+ () => {
210
+ setLatestReactionByUid(prev => {
211
+ if (
212
+ prev[nextReaction.senderUid]?.reactionId !==
213
+ nextReaction.reactionId
214
+ ) {
215
+ return prev;
216
+ }
217
+ const next = {...prev};
218
+ delete next[nextReaction.senderUid];
219
+ return next;
220
+ });
221
+ cleanupReactionBadgeTimeout(nextReaction.senderUid);
222
+ },
223
+ LIVE_REACTION_BADGE_DURATION,
224
+ );
225
+
226
+ setFloatingReactions(prev => {
227
+ const next = [...prev, nextReaction];
228
+ return next.length > LIVE_REACTION_MAX_FLOATING_ITEMS
229
+ ? next.slice(next.length - LIVE_REACTION_MAX_FLOATING_ITEMS)
230
+ : next;
231
+ });
232
+ cleanupFloatingReactionTimeout(nextReaction.reactionId);
233
+ floatingReactionTimeoutsRef.current[nextReaction.reactionId] = setTimeout(
234
+ () => {
235
+ setFloatingReactions(prev =>
236
+ prev.filter(item => item.reactionId !== nextReaction.reactionId),
237
+ );
238
+ cleanupFloatingReactionTimeout(nextReaction.reactionId);
239
+ },
240
+ LIVE_REACTION_FLOAT_DURATION,
241
+ );
242
+ },
243
+ [
244
+ assignReactionLane,
245
+ cleanupFloatingReactionTimeout,
246
+ cleanupReactionBadgeTimeout,
247
+ getReactionSenderName,
248
+ ],
249
+ );
250
+
251
+ const emitLiveReaction = useCallback(
252
+ (reaction: LiveReactionDefinition) => {
253
+ const reactionId = `${localUser.uid}-${
254
+ reaction.key
255
+ }-${Date.now()}-${nanoid(4)}`;
256
+ const senderDisplayName = getReactionSenderName(String(localUser.uid));
257
+ const nextReaction: LiveReactionEvent = {
258
+ reactionId,
259
+ assetKey: reaction.key,
260
+ emoji: reaction.emoji,
261
+ senderUid: String(localUser.uid),
262
+ senderDisplayName,
263
+ timestamp: Date.now(),
264
+ };
265
+
266
+ ingestReaction(nextReaction);
267
+ events.send(
268
+ EventNames.LIVE_REACTION,
269
+ JSON.stringify({
270
+ reactionId: nextReaction.reactionId,
271
+ assetKey: nextReaction.assetKey,
272
+ emoji: nextReaction.emoji,
273
+ timestamp: nextReaction.timestamp,
274
+ }),
275
+ PersistanceLevel.None,
276
+ );
277
+ },
278
+ [getReactionSenderName, ingestReaction, localUser.uid],
279
+ );
280
+
104
281
  useEffect(() => {
105
282
  if (join.initialized && join.phrase) {
106
283
  if (join.userName && join.skipPrecall) {
@@ -118,6 +295,44 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
118
295
  roomInfo.data.isHost,
119
296
  );
120
297
  }, []);
298
+
299
+ useEffect(() => {
300
+ if (!$config.ENABLE_LIVE_REACTIONS) {
301
+ return;
302
+ }
303
+ const unsubscribe = events.on(EventNames.LIVE_REACTION, data => {
304
+ try {
305
+ const payload =
306
+ typeof data.payload === 'string'
307
+ ? JSON.parse(data.payload)
308
+ : data.payload;
309
+ ingestReaction({
310
+ reactionId: payload.reactionId,
311
+ assetKey: payload.assetKey,
312
+ emoji: payload.emoji,
313
+ senderUid: String(data.sender),
314
+ timestamp: payload.timestamp || data.ts || Date.now(),
315
+ });
316
+ } catch (error) {
317
+ console.warn('Failed to parse live reaction payload', error);
318
+ }
319
+ });
320
+
321
+ return () => {
322
+ unsubscribe();
323
+ };
324
+ }, [ingestReaction]);
325
+
326
+ useEffect(() => {
327
+ return () => {
328
+ Object.keys(reactionBadgeTimeoutsRef.current).forEach(uid => {
329
+ cleanupReactionBadgeTimeout(uid);
330
+ });
331
+ Object.keys(floatingReactionTimeoutsRef.current).forEach(reactionId => {
332
+ cleanupFloatingReactionTimeout(reactionId);
333
+ });
334
+ };
335
+ }, [cleanupFloatingReactionTimeout, cleanupReactionBadgeTimeout]);
121
336
  return (
122
337
  <VideoCallContext.Provider
123
338
  value={{
@@ -138,6 +353,9 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
138
353
  videoTileInViewPortState,
139
354
  showWhiteboardClearAllPopup,
140
355
  setShowWhiteboardClearAllPopup,
356
+ latestReactionByUid,
357
+ floatingReactions,
358
+ emitLiveReaction,
141
359
  }}>
142
360
  <StartScreenSharePopup />
143
361
  <StopScreenSharePopup />
@@ -114,6 +114,7 @@ export const toolbarItemManageTextTracksText =
114
114
  export const toolbarItemVirtualBackgroundText =
115
115
  'toolbarItemVirtualBackgroundText';
116
116
  export const toolbarItemViewRecordingText = 'toolbarItemViewRecordingText';
117
+ export const toolbarItemReactionText = 'toolbarItemReactionText';
117
118
 
118
119
  export const toolbarItemRaiseHandText = 'toolbarItemRaiseHandText';
119
120
 
@@ -585,6 +586,7 @@ export interface I18nVideoCallScreenLabelsInterface {
585
586
  [toolbarItemManageTextTracksText]?: I18nConditionalType;
586
587
  [toolbarItemVirtualBackgroundText]?: I18nBaseType;
587
588
  [toolbarItemViewRecordingText]?: I18nConditionalType;
589
+ [toolbarItemReactionText]?: I18nBaseType;
588
590
 
589
591
  [toolbarItemRaiseHandText]?: I18nConditionalType;
590
592
 
@@ -957,6 +959,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = {
957
959
  [toolbarItemTranscriptText]: active =>
958
960
  active ? 'Hide Meeting Transcript' : 'Show Meeting Transcript',
959
961
  [toolbarItemViewRecordingText]: 'View Recordings',
962
+ [toolbarItemReactionText]: 'React',
960
963
  [toolbarItemManageTextTracksText]: 'View Text-tracks',
961
964
 
962
965
  [toolbarItemRaiseHandText]: active => (active ? 'Lower Hand' : 'Raise Hand'),
@@ -65,6 +65,7 @@ import {
65
65
  ScreenshareToolbarItem,
66
66
  } from '../../components/controls/toolbar-items';
67
67
  import {useControlPermissionMatrix} from '../../components/controls/useControlPermissionMatrix';
68
+ import LiveReactionButton from '../../components/reactions/LiveReactionButton';
68
69
  //Icon for expanding Action Sheet
69
70
  interface ShowMoreIconProps {
70
71
  isExpanded: boolean;
@@ -157,6 +158,14 @@ const LayoutIcon = props => {
157
158
  );
158
159
  };
159
160
 
161
+ const LiveReactionIcon = props => {
162
+ return (
163
+ <ToolbarItem toolbarProps={props}>
164
+ <LiveReactionButton />
165
+ </ToolbarItem>
166
+ );
167
+ };
168
+
160
169
  interface CaptionIconBtnProps {
161
170
  showLabel?: boolean;
162
171
  onPressCallback?: () => void;
@@ -341,8 +350,12 @@ const ActionSheetContent = props => {
341
350
  component:
342
351
  !isAudioRoom && (isAudioVideoControlsDisabled ? null : CamIcon),
343
352
  },
344
- 'end-call': {
353
+ reactions: {
345
354
  order: 2,
355
+ component: $config.ENABLE_LIVE_REACTIONS ? LiveReactionIcon : null,
356
+ },
357
+ 'end-call': {
358
+ order: 3,
346
359
  component: EndCallIcon,
347
360
  },
348
361
  chat: {
@@ -13,6 +13,7 @@ import {useLiveStreamDataContext} from '../../components/contexts/LiveStreamData
13
13
  import {useCustomization} from 'customization-implementation';
14
14
  import useMount from '../../components/useMount';
15
15
  import {whiteboardContext} from '../../components/whiteboard/WhiteboardConfigure';
16
+ import LiveReactionStageOverlay from '../../components/reactions/LiveReactionStageOverlay';
16
17
 
17
18
  const VideoComponent = () => {
18
19
  const {dispatch} = useContext(DispatchContext);
@@ -108,10 +109,12 @@ const VideoComponent = () => {
108
109
  <View
109
110
  style={{
110
111
  flex: 1,
112
+ position: 'relative',
111
113
  flexDirection: isDesktop() ? 'row' : 'column',
112
114
  justifyContent: 'space-between',
113
115
  }}>
114
116
  <CurrentLayout renderData={activeUids} />
117
+ <LiveReactionStageOverlay />
115
118
  {((!$config.EVENT_MODE && activeUids.length === 1) ||
116
119
  ($config.EVENT_MODE &&
117
120
  hostUids.concat(audienceUids)?.length === 1)) &&
@@ -127,7 +130,12 @@ const VideoComponent = () => {
127
130
  </View>
128
131
  );
129
132
  }
130
- return <CurrentLayout renderData={activeUids} />;
133
+ return (
134
+ <View style={{flex: 1, position: 'relative'}}>
135
+ <CurrentLayout renderData={activeUids} />
136
+ <LiveReactionStageOverlay />
137
+ </View>
138
+ );
131
139
  } else {
132
140
  return <></>;
133
141
  }
@@ -52,6 +52,7 @@ import {LogSource, logger} from '../../logger/AppBuilderLogger';
52
52
  import {useFullScreen} from '../../utils/useFullScreen';
53
53
  import SpotlightHighligher from './SpotlightHighlighter';
54
54
  import {AgentContext} from '../../ai-agent/components/AgentControls/AgentContext';
55
+ import LiveReactionBadge from '../../components/reactions/LiveReactionBadge';
55
56
 
56
57
  export interface VideoRendererProps {
57
58
  user: ContentInterface;
@@ -238,6 +239,13 @@ const VideoRenderer: React.FC<VideoRendererProps> = ({
238
239
  {!showReplacePin && !showPinForMe && (
239
240
  <ScreenShareNotice uid={user.uid} isMax={isMax} />
240
241
  )}
242
+ <LiveReactionBadge
243
+ uid={user.uid}
244
+ hasLeadingIcon={
245
+ currentLayout === DefaultLayouts[1].name &&
246
+ user.uid === secondaryPinnedUid
247
+ }
248
+ />
241
249
  {currentLayout === DefaultLayouts[1].name &&
242
250
  user.uid === secondaryPinnedUid ? (
243
251
  <View
@@ -47,6 +47,7 @@ const BOARD_COLOR_CHANGED = 'BOARD_COLOR_CHANGED';
47
47
  const WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION = 'WHITEBOARD_L_I_U_P';
48
48
  const RECORDING_DELETED = 'RECORDING_DELETED';
49
49
  const SPOTLIGHT_USER_CHANGED = 'SPOTLIGHT_USER_CHANGED';
50
+ const LIVE_REACTION = 'LIVE_REACTION';
50
51
  const EventNames = {
51
52
  RECORDING_STATE_ATTRIBUTE,
52
53
  RECORDING_STARTED_BY_ATTRIBUTE,
@@ -76,6 +77,7 @@ const EventNames = {
76
77
  WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION,
77
78
  RECORDING_DELETED,
78
79
  SPOTLIGHT_USER_CHANGED,
80
+ LIVE_REACTION,
79
81
  };
80
82
  /** ***** EVENT NAMES ENDS ***** */
81
83
 
@@ -132,7 +132,7 @@ module.exports = {
132
132
  },
133
133
  },
134
134
  {
135
- test: /\.(mp4|png|jpe?g|gif)$/i,
135
+ test: /\.(mp4|png|jpe?g|gif|webp)$/i,
136
136
  use: [
137
137
  {
138
138
  loader: 'file-loader',
@@ -71,7 +71,7 @@ module.exports = merge(commons, {
71
71
  },
72
72
  },
73
73
  {
74
- test: /\.(png|jpe?g|gif)$/i,
74
+ test: /\.(png|jpe?g|gif|webp)$/i,
75
75
  use: [
76
76
  {
77
77
  loader: 'file-loader',