@stream-io/video-react-sdk 0.4.26 → 0.5.0

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 (91) hide show
  1. package/CHANGELOG.md +297 -238
  2. package/README.md +5 -5
  3. package/dist/css/styles.css +952 -481
  4. package/dist/css/styles.css.map +1 -1
  5. package/dist/index.cjs.js +946 -639
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.es.js +939 -639
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/components/Button/CompositeButton.d.ts +9 -11
  10. package/dist/src/components/Button/index.d.ts +0 -1
  11. package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
  12. package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
  13. package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
  14. package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
  15. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
  16. package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
  17. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
  18. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
  19. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
  20. package/dist/src/components/CallStats/CallStats.d.ts +25 -2
  21. package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
  22. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
  23. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
  24. package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
  25. package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
  26. package/dist/src/components/DropdownSelect/index.d.ts +1 -0
  27. package/dist/src/components/Icon/Icon.d.ts +2 -1
  28. package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
  29. package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
  30. package/dist/src/components/Notification/Notification.d.ts +1 -0
  31. package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
  32. package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
  33. package/dist/src/components/Notification/index.d.ts +1 -0
  34. package/dist/src/components/index.d.ts +2 -0
  35. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
  36. package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
  37. package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
  38. package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
  39. package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
  40. package/dist/src/translations/index.d.ts +9 -0
  41. package/package.json +7 -9
  42. package/src/components/Button/CompositeButton.tsx +78 -26
  43. package/src/components/Button/IconButton.tsx +22 -21
  44. package/src/components/Button/index.ts +0 -1
  45. package/src/components/CallControls/AcceptCallButton.tsx +1 -0
  46. package/src/components/CallControls/CallControls.tsx +2 -2
  47. package/src/components/CallControls/CallStatsButton.tsx +24 -7
  48. package/src/components/CallControls/CancelCallButton.tsx +102 -3
  49. package/src/components/CallControls/ReactionsButton.tsx +37 -17
  50. package/src/components/CallControls/RecordCallButton.tsx +131 -21
  51. package/src/components/CallControls/ScreenShareButton.tsx +29 -15
  52. package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
  53. package/src/components/CallControls/ToggleAudioOutputButton.tsx +14 -10
  54. package/src/components/CallControls/ToggleVideoButton.tsx +83 -33
  55. package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
  56. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
  57. package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
  58. package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
  59. package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
  60. package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
  61. package/src/components/CallStats/CallStats.tsx +167 -10
  62. package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
  63. package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
  64. package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
  65. package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
  66. package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
  67. package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
  68. package/src/components/DropdownSelect/index.ts +1 -0
  69. package/src/components/Icon/Icon.tsx +7 -2
  70. package/src/components/Menu/GenericMenu.tsx +25 -3
  71. package/src/components/Menu/MenuToggle.tsx +79 -14
  72. package/src/components/Notification/Notification.tsx +8 -0
  73. package/src/components/Notification/PermissionNotification.tsx +2 -1
  74. package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
  75. package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
  76. package/src/components/Notification/index.ts +1 -0
  77. package/src/components/Permissions/PermissionRequests.tsx +9 -21
  78. package/src/components/Search/hooks/useSearch.ts +5 -1
  79. package/src/components/index.ts +2 -0
  80. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
  81. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
  82. package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
  83. package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
  84. package/src/core/components/ParticipantView/index.ts +1 -0
  85. package/src/core/components/Video/BaseVideo.tsx +1 -1
  86. package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
  87. package/src/hooks/useFloatingUIPreset.ts +3 -2
  88. package/src/hooks/useRequestPermission.ts +2 -1
  89. package/src/translations/en.json +9 -0
  90. package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
  91. package/src/components/Button/CopyToClipboardButton.tsx +0 -129
package/dist/index.es.js CHANGED
@@ -1,12 +1,13 @@
1
- import { SfuModels, Browsers, VisibilityState, OwnCapability, name, CallingState, paginatedLayoutSortPreset, combineComparators, screenSharing, speakerLayoutSortPreset, CallTypes, defaultSortPreset, setSdkInfo } from '@stream-io/video-client';
1
+ import { SfuModels, OwnCapability, name, CallingState, VisibilityState, Browsers, paginatedLayoutSortPreset, combineComparators, screenSharing, speakerLayoutSortPreset, CallTypes, defaultSortPreset, setSdkInfo } from '@stream-io/video-client';
2
2
  export * from '@stream-io/video-client';
3
- import { useCall, useI18n, useCallStateHooks, useHasPermissions, Restricted, useConnectedUser, StreamCallProvider, StreamVideoProvider } from '@stream-io/video-react-bindings';
3
+ import { useCall, useCallStateHooks, useI18n, Restricted, useConnectedUser, StreamCallProvider, StreamVideoProvider } from '@stream-io/video-react-bindings';
4
4
  export * from '@stream-io/video-react-bindings';
5
5
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
- import { useState, useEffect, Fragment as Fragment$1, isValidElement, forwardRef, useLayoutEffect, useCallback, useRef, createContext, useContext, useMemo } from 'react';
6
+ import { useState, useEffect, Fragment as Fragment$1, createContext, useContext, useCallback, useMemo, useRef, forwardRef, isValidElement, useLayoutEffect } from 'react';
7
7
  import clsx from 'clsx';
8
- import { useFloating, offset, shift, flip, size, autoUpdate } from '@floating-ui/react';
9
- import { ResponsiveLine } from '@nivo/line';
8
+ import { useFloating, offset, shift, flip, size, autoUpdate, FloatingOverlay, FloatingPortal, useListItem, useListNavigation, useTypeahead, useClick, useDismiss, useRole, useInteractions, FloatingFocusManager, FloatingList, useHover } from '@floating-ui/react';
9
+ import { Chart, CategoryScale, LinearScale, LineElement, PointElement } from 'chart.js';
10
+ import { Line } from 'react-chartjs-2';
10
11
 
11
12
  const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
12
13
  const call = useCall();
@@ -38,146 +39,8 @@ const ParticipantsAudio = (props) => {
38
39
  }) }));
39
40
  };
40
41
 
41
- const isComponentType = (elementOrComponent) => {
42
- return elementOrComponent === null
43
- ? false
44
- : !isValidElement(elementOrComponent);
45
- };
46
-
47
- const chunk = (array, size) => {
48
- const chunkCount = Math.ceil(array.length / size);
49
- return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
50
- };
51
-
52
- const applyElementToRef = (ref, element) => {
53
- if (!ref)
54
- return;
55
- if (typeof ref === 'function')
56
- return ref(element);
57
- ref.current = element;
58
- };
59
-
60
- /**
61
- * @description Extends video element with `stream` property
62
- * (`srcObject`) to reactively handle stream changes
63
- */
64
- const BaseVideo = forwardRef(({ stream, ...rest }, ref) => {
65
- const [videoElement, setVideoElement] = useState(null);
66
- useEffect(() => {
67
- if (!videoElement || !stream)
68
- return;
69
- if (stream === videoElement.srcObject)
70
- return;
71
- videoElement.srcObject = stream;
72
- if (Browsers.isSafari() || Browsers.isFirefox()) {
73
- // Firefox and Safari have some timing issue
74
- setTimeout(() => {
75
- videoElement.srcObject = stream;
76
- videoElement.play().catch((e) => {
77
- console.error(`Failed to play stream`, e);
78
- });
79
- }, 0);
80
- }
81
- return () => {
82
- videoElement.pause();
83
- videoElement.srcObject = null;
84
- };
85
- }, [stream, videoElement]);
86
- return (jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
87
- applyElementToRef(ref, element);
88
- setVideoElement(element);
89
- } }));
90
- });
91
-
92
- const DefaultVideoPlaceholder = forwardRef(({ participant, style }, ref) => {
93
- const { t } = useI18n();
94
- const [error, setError] = useState(false);
95
- const name = participant.name || participant.userId;
96
- return (jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
97
- (name ? (jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: jsx("div", { children: name[0] }) })) : (jsx("div", { children: t('Video is disabled') }))), participant.image && !error && (jsx("img", { onError: () => setError(true), alt: "video-placeholder", className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
98
- });
99
-
100
- const Video$1 = ({ trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
101
- const { sessionId, videoStream, screenShareStream, publishedTracks, viewportVisibilityState, isLocalParticipant, userId, } = participant;
102
- const call = useCall();
103
- const [videoElement, setVideoElement] = useState(null);
104
- // start with true, will flip once the video starts playing
105
- const [isVideoPaused, setIsVideoPaused] = useState(true);
106
- const [isWideMode, setIsWideMode] = useState(true);
107
- const stream = trackType === 'videoTrack'
108
- ? videoStream
109
- : trackType === 'screenShareTrack'
110
- ? screenShareStream
111
- : undefined;
112
- useLayoutEffect(() => {
113
- if (!call || !videoElement || trackType === 'none')
114
- return;
115
- const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
116
- return () => {
117
- cleanup?.();
118
- };
119
- }, [call, trackType, sessionId, videoElement]);
120
- useEffect(() => {
121
- if (!stream || !videoElement)
122
- return;
123
- const [track] = stream.getVideoTracks();
124
- if (!track)
125
- return;
126
- const handlePlayPause = () => {
127
- setIsVideoPaused(videoElement.paused);
128
- const { width = 0, height = 0 } = track.getSettings();
129
- setIsWideMode(width >= height);
130
- };
131
- // playback may have started before we had a chance to
132
- // attach the 'play/pause' event listener, so we set the state
133
- // here to make sure it's in sync
134
- setIsVideoPaused(videoElement.paused);
135
- videoElement.addEventListener('play', handlePlayPause);
136
- videoElement.addEventListener('pause', handlePlayPause);
137
- track.addEventListener('unmute', handlePlayPause);
138
- return () => {
139
- videoElement.removeEventListener('play', handlePlayPause);
140
- videoElement.removeEventListener('pause', handlePlayPause);
141
- track.removeEventListener('unmute', handlePlayPause);
142
- // reset the 'pause' state once we unmount the video element
143
- setIsVideoPaused(true);
144
- };
145
- }, [stream, videoElement]);
146
- if (!call)
147
- return null;
148
- const isPublishingTrack = trackType === 'videoTrack'
149
- ? publishedTracks.includes(SfuModels.TrackType.VIDEO)
150
- : trackType === 'screenShareTrack'
151
- ? publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE)
152
- : false;
153
- const isInvisible = trackType === 'none' ||
154
- viewportVisibilityState?.[trackType] === VisibilityState.INVISIBLE;
155
- const hasNoVideoOrInvisible = !isPublishingTrack || isInvisible;
156
- const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
157
- const isScreenShareTrack = trackType === 'screenShareTrack';
158
- return (jsxs(Fragment, { children: [!hasNoVideoOrInvisible && (jsx("video", { ...rest, className: clsx('str-video__video', className, {
159
- 'str-video__video--not-playing': isVideoPaused,
160
- 'str-video__video--tall': !isWideMode,
161
- 'str-video__video--mirror': mirrorVideo,
162
- 'str-video__video--screen-share': isScreenShareTrack,
163
- }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
164
- setVideoElement(element);
165
- refs?.setVideoElement?.(element);
166
- } })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
167
- };
168
-
169
- const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
170
- const call = useCall();
171
- const manager = propsDynascaleManager ?? call?.dynascaleManager;
172
- useEffect(() => {
173
- if (!trackedElement || !manager || !call || trackType === 'none')
174
- return;
175
- const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
176
- return () => {
177
- unobserve();
178
- };
179
- }, [trackedElement, manager, call, sessionId, trackType]);
180
- };
42
+ const ParticipantViewContext = createContext(undefined);
43
+ const useParticipantViewContext = () => useContext(ParticipantViewContext);
181
44
 
182
45
  const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
183
46
  const [error, setError] = useState(false);
@@ -187,12 +50,12 @@ const AvatarFallback = ({ className, names, style, }) => {
187
50
  return (jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
188
51
  };
189
52
 
190
- const useFloatingUIPreset = ({ placement, strategy, }) => {
53
+ const useFloatingUIPreset = ({ placement, strategy, offset: offsetInPx = 10, }) => {
191
54
  const { refs, x, y, update, elements: { domReference, floating }, } = useFloating({
192
55
  placement,
193
56
  strategy,
194
57
  middleware: [
195
- offset(10),
58
+ offset(offsetInPx),
196
59
  shift(),
197
60
  flip(),
198
61
  size({
@@ -428,6 +291,7 @@ const useToggleCallRecording = () => {
428
291
 
429
292
  const useRequestPermission = (permission) => {
430
293
  const call = useCall();
294
+ const { useHasPermissions } = useCallStateHooks();
431
295
  const hasPermission = useHasPermissions(permission);
432
296
  const [isAwaitingPermission, setIsAwaitingPermission] = useState(false); // TODO: load with possibly pending state
433
297
  useEffect(() => {
@@ -461,11 +325,31 @@ const useRequestPermission = (permission) => {
461
325
  };
462
326
  };
463
327
 
464
- const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', children, }) => {
328
+ var MenuVisualType;
329
+ (function (MenuVisualType) {
330
+ MenuVisualType["PORTAL"] = "portal";
331
+ MenuVisualType["MENU"] = "menu";
332
+ })(MenuVisualType || (MenuVisualType = {}));
333
+ /**
334
+ * Used to provide utility APIs to the components rendered inside the portal.
335
+ */
336
+ const MenuContext = createContext({});
337
+ /**
338
+ * Access to the closes MenuContext.
339
+ */
340
+ const useMenuContext = () => {
341
+ return useContext(MenuContext);
342
+ };
343
+ const MenuPortal = ({ children, refs, }) => {
344
+ const portalId = useMemo(() => `str-video-portal-${Math.random().toString(36).substring(2, 9)}`, []);
345
+ return (jsxs(Fragment, { children: [jsx("div", { id: portalId, className: "str-video__portal" }), jsx(FloatingOverlay, { children: jsx(FloatingPortal, { id: portalId, children: jsx("div", { className: "str-video__portal-content", ref: refs.setFloating, children: children }) }) })] }));
346
+ };
347
+ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', offset, visualType = MenuVisualType.MENU, children, }) => {
465
348
  const [menuShown, setMenuShown] = useState(false);
466
349
  const { floating, domReference, refs, x, y } = useFloatingUIPreset({
467
350
  placement,
468
351
  strategy,
352
+ offset,
469
353
  });
470
354
  useEffect(() => {
471
355
  const handleClick = (event) => {
@@ -490,24 +374,31 @@ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolut
490
374
  document?.removeEventListener('keydown', handleKeyDown);
491
375
  };
492
376
  }, [floating, domReference]);
493
- return (jsxs(Fragment, { children: [menuShown && (jsx("div", { className: "str-video__menu-container", ref: refs.setFloating, style: {
494
- position: strategy,
495
- top: y ?? 0,
496
- left: x ?? 0,
497
- overflowY: 'auto',
498
- }, children: children })), jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
499
- };
500
-
501
- const GenericMenu = ({ children }) => {
502
- return jsx("ul", { className: "str-video__generic-menu", children: children });
377
+ return (jsxs(Fragment, { children: [menuShown && (jsx(MenuContext.Provider, { value: { close: () => setMenuShown(false) }, children: visualType === MenuVisualType.PORTAL ? (jsx(MenuPortal, { refs: refs, children: children })) : visualType === MenuVisualType.MENU ? (jsx("div", { className: "str-video__menu-container", ref: refs.setFloating, style: {
378
+ position: strategy,
379
+ top: y ?? 0,
380
+ left: x ?? 0,
381
+ overflowY: 'auto',
382
+ }, children: children })) : null })), jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
383
+ };
384
+
385
+ const GenericMenu = ({ children, onItemClick, }) => {
386
+ const ref = useRef(null);
387
+ return (jsx("ul", { className: "str-video__generic-menu", ref: ref, onClick: (e) => {
388
+ if (onItemClick &&
389
+ e.target !== ref.current &&
390
+ ref.current?.contains(e.target)) {
391
+ onItemClick(e);
392
+ }
393
+ }, children: children }));
503
394
  };
504
395
  const GenericMenuButtonItem = ({ children, ...rest }) => {
505
396
  return (jsx("li", { className: "str-video__generic-menu--item", children: jsx("button", { ...rest, children: children }) }));
506
397
  };
507
398
 
508
- const Icon = ({ icon }) => (jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`) }));
399
+ const Icon = ({ className, icon }) => (jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`, className) }));
509
400
 
510
- const IconButton = forwardRef((props, ref) => {
401
+ const IconButton = forwardRef(function IconButton(props, ref) {
511
402
  const { icon, enabled, variant, onClick, className, ...rest } = props;
512
403
  return (jsx("button", { className: clsx('str-video__call-controls__button', className, {
513
404
  [`str-video__call-controls__button--variant-${variant}`]: variant,
@@ -518,86 +409,43 @@ const IconButton = forwardRef((props, ref) => {
518
409
  }, ref: ref, ...rest, children: jsx(Icon, { icon: icon }) }));
519
410
  });
520
411
 
521
- const CompositeButton = forwardRef(({ caption, children, active, Menu, menuPlacement }, ref) => {
522
- return (jsxs("div", { className: "str-video__composite-button", ref: ref, children: [jsxs("div", { className: clsx('str-video__composite-button__button-group', {
523
- 'str-video__composite-button__button-group--active': active,
524
- }), children: [children, Menu && (jsx(MenuToggle, { placement: menuPlacement, ToggleButton: ToggleMenuButton$2, children: isComponentType(Menu) ? jsx(Menu, {}) : Menu }))] }), caption && (jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
525
- });
526
- const ToggleMenuButton$2 = forwardRef(({ menuShown }, ref) => {
527
- const { t } = useI18n();
528
- return (jsx(IconButton, { className: 'str-video__menu-toggle-button', icon: menuShown ? 'caret-down' : 'caret-up', title: t('Toggle device menu'), ref: ref }));
529
- });
530
-
531
- const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
532
- const { refs, x, y, strategy } = useFloatingUIPreset({
533
- placement: tooltipPlacement,
534
- strategy: 'absolute',
535
- });
536
- useEffect(() => {
537
- refs.setReference(referenceElement);
538
- }, [referenceElement, refs]);
539
- if (!visible)
540
- return null;
541
- return (jsx("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
542
- position: strategy,
543
- top: y ?? 0,
544
- left: x ?? 0,
545
- overflowY: 'auto',
546
- }, children: children }));
412
+ const isComponentType = (elementOrComponent) => {
413
+ return elementOrComponent === null
414
+ ? false
415
+ : !isValidElement(elementOrComponent);
547
416
  };
548
417
 
549
- const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
550
- const [tooltipVisible, setTooltipVisible] = useState(false);
551
- const handleMouseEnter = useCallback((e) => {
552
- setTooltipVisible(true);
553
- onMouseEnter?.(e);
554
- }, [onMouseEnter]);
555
- const handleMouseLeave = useCallback((e) => {
556
- setTooltipVisible(false);
557
- onMouseLeave?.(e);
558
- }, [onMouseLeave]);
559
- return { handleMouseEnter, handleMouseLeave, tooltipVisible };
418
+ const chunk = (array, size) => {
419
+ const chunkCount = Math.ceil(array.length / size);
420
+ return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
560
421
  };
561
422
 
562
- // todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
563
- const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, ...props }) => {
564
- const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
565
- const [tooltipAnchor, setTooltipAnchor] = useState(null);
566
- return (jsxs(Fragment, { children: [jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
423
+ const applyElementToRef = (ref, element) => {
424
+ if (!ref)
425
+ return;
426
+ if (typeof ref === 'function')
427
+ return ref(element);
428
+ ref.current = element;
567
429
  };
568
430
 
569
- const CopyToClipboardButton = forwardRef(({ Button, className, copyValue, onClick, onError, onSuccess, ...restProps }, ref) => {
570
- const handleClick = useCallback(async (event) => {
571
- if (onClick)
572
- onClick(event);
573
- const value = typeof copyValue === 'function' ? copyValue() : copyValue;
574
- try {
575
- await navigator?.clipboard.writeText(value);
576
- onSuccess?.(event.target);
577
- }
578
- catch (error) {
579
- onError?.(event.target, error);
580
- }
581
- }, [copyValue, onClick, onError, onSuccess]);
582
- const props = {
583
- ...restProps,
584
- ref: ref,
585
- className: clsx('str-video__copy-to-clipboard-button', className),
586
- onClick: handleClick,
587
- };
588
- return Button ? jsx(Button, { ...props }) : jsx("button", { ...props });
431
+ const CompositeButton = forwardRef(function CompositeButton({ caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, ...restButtonProps }, ref) {
432
+ return (jsxs("div", { className: clsx('str-video__composite-button', className, {
433
+ 'str-video__composite-button--caption': caption,
434
+ 'str-video__composite-button--menu': Menu,
435
+ }), title: title, ref: ref, children: [jsxs("div", { className: clsx('str-video__composite-button__button-group', {
436
+ 'str-video__composite-button__button-group--active': active,
437
+ 'str-video__composite-button__button-group--active-primary': active && variant === 'primary',
438
+ 'str-video__composite-button__button-group--active-secondary': active && variant === 'secondary',
439
+ }), children: [jsx("button", { type: "button", className: "str-video__composite-button__button", onClick: (e) => {
440
+ e.preventDefault();
441
+ onClick?.(e);
442
+ }, ...restButtonProps, children: children }), Menu && (jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, children: isComponentType(Menu) ? jsx(Menu, {}) : Menu }))] }), caption && (jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
443
+ });
444
+ const DefaultToggleMenuButton = forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
445
+ return (jsx(IconButton, { className: clsx('str-video__menu-toggle-button', {
446
+ 'str-video__menu-toggle-button--active': menuShown,
447
+ }), icon: menuShown ? 'caret-down' : 'caret-up', ref: ref }));
589
448
  });
590
- const CopyToClipboardButtonWithPopup = ({ dismissAfterMs = 1500, onErrorMessage = 'Failed to copy', onSuccessMessage = 'Copied to clipboard', popupClassName, popupPlacement, ...restProps }) => {
591
- const [tooltipText, setTooltipText] = useState('');
592
- const [tooltipAnchor, setTooltipAnchor] = useState(null);
593
- const setTemporaryPopup = useCallback((popupText) => {
594
- setTooltipText(popupText);
595
- setTimeout(() => setTooltipText(''), dismissAfterMs);
596
- }, [dismissAfterMs]);
597
- const onSuccess = useCallback(() => setTemporaryPopup(onSuccessMessage), [onSuccessMessage, setTemporaryPopup]);
598
- const onError = useCallback(() => setTemporaryPopup(onErrorMessage), [onErrorMessage, setTemporaryPopup]);
599
- return (jsxs(Fragment, { children: [jsx(Tooltip, { tooltipClassName: clsx('str-video__copy-to-clipboard-button__popup', popupClassName), tooltipPlacement: popupPlacement, referenceElement: tooltipAnchor, visible: !!tooltipText, children: tooltipText }), jsx(CopyToClipboardButton, { ...restProps, onError: onError, onSuccess: onSuccess, ref: setTooltipAnchor })] }));
600
- };
601
449
 
602
450
  const TextButton = ({ children, ...rest }) => {
603
451
  return (jsx("button", { ...rest, className: "str-video__text-button", children: children }));
@@ -614,11 +462,11 @@ const AcceptCallButton = ({ disabled, onAccept, onClick, }) => {
614
462
  onAccept?.();
615
463
  }
616
464
  }, [onClick, onAccept, call]);
617
- return (jsx(IconButton, { disabled: disabled, icon: "call-accept", variant: "success", onClick: handleClick }));
465
+ return (jsx(IconButton, { disabled: disabled, icon: "call-accept", variant: "success", "data-testid": "accept-call-button", onClick: handleClick }));
618
466
  };
619
467
 
620
468
  const Notification = (props) => {
621
- const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', iconClassName = 'str-video__notification__icon', } = props;
469
+ const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', iconClassName = 'str-video__notification__icon', close, } = props;
622
470
  const { refs, x, y, strategy } = useFloatingUIPreset({
623
471
  placement,
624
472
  strategy: 'absolute',
@@ -636,11 +484,12 @@ const Notification = (props) => {
636
484
  top: y ?? 0,
637
485
  left: x ?? 0,
638
486
  overflowY: 'auto',
639
- }, children: [iconClassName && jsx("i", { className: iconClassName }), jsx("span", { className: "str-video__notification__message", children: message })] })), children] }));
487
+ }, children: [iconClassName && jsx("i", { className: iconClassName }), jsx("span", { className: "str-video__notification__message", children: message }), close ? (jsx("i", { className: "str-video__icon str-video__icon--close str-video__notification__close", onClick: close })) : null] })), children] }));
640
488
  };
641
489
 
642
490
  const PermissionNotification = (props) => {
643
491
  const { permission, isAwaitingApproval, messageApproved, messageAwaitingApproval, messageRevoked, visibilityTimeout = 3500, children, } = props;
492
+ const { useHasPermissions } = useCallStateHooks();
644
493
  const hasPermission = useHasPermissions(permission);
645
494
  const prevHasPermission = useRef(hasPermission);
646
495
  const [showNotification, setShowNotification] = useState();
@@ -661,158 +510,237 @@ const PermissionNotification = (props) => {
661
510
  return (jsx(Notification, { isVisible: !!showNotification, visibilityTimeout: visibilityTimeout, resetIsVisible: resetIsVisible, message: showNotification === 'granted' ? messageApproved : messageRevoked, children: children }));
662
511
  };
663
512
 
664
- const SpeakingWhileMutedNotification = ({ children, text, }) => {
513
+ const SpeakingWhileMutedNotification = ({ children, text, placement, }) => {
665
514
  const { useMicrophoneState } = useCallStateHooks();
666
515
  const { isSpeakingWhileMuted } = useMicrophoneState();
667
516
  const { t } = useI18n();
668
517
  const message = text ?? t('You are muted. Unmute to speak.');
669
- return (jsx(Notification, { message: message, isVisible: isSpeakingWhileMuted, children: children }));
518
+ return (jsx(Notification, { message: message, isVisible: isSpeakingWhileMuted, placement: placement || 'top-start', children: children }));
519
+ };
520
+
521
+ const RecordingInProgressNotification = ({ children, text, }) => {
522
+ const { t } = useI18n();
523
+ const { isCallRecordingInProgress } = useToggleCallRecording();
524
+ const [isVisible, setVisible] = useState(false);
525
+ const message = text ?? t('Recording in progress...');
526
+ useEffect(() => {
527
+ if (isCallRecordingInProgress) {
528
+ setVisible(true);
529
+ }
530
+ else {
531
+ setVisible(false);
532
+ }
533
+ }, [isCallRecordingInProgress]);
534
+ return (jsx(Notification, { message: message, iconClassName: "str-video__icon str-video__icon--recording-on", isVisible: isVisible, placement: "top-start", close: () => setVisible(false), children: children }));
670
535
  };
671
536
 
672
537
  const LoadingIndicator = ({ className, type = 'spinner', text, tooltip, }) => {
673
538
  return (jsxs("div", { className: clsx('str-video__loading-indicator', className), title: tooltip, children: [jsx("div", { className: clsx('str-video__loading-indicator__icon', type) }), text && jsx("p", { className: "str-video__loading-indicator-text", children: text })] }));
674
539
  };
675
540
 
676
- const RecordCallButton = ({ caption = 'Record', }) => {
677
- const call = useCall();
541
+ const RecordEndConfirmation = () => {
542
+ const { t } = useI18n();
543
+ const { toggleCallRecording, isAwaitingResponse } = useToggleCallRecording();
544
+ const { close } = useMenuContext();
545
+ return (jsxs("div", { className: "str-video__end-recording__confirmation", children: [jsxs("div", { className: "str-video__end-recording__header", children: [jsx(Icon, { icon: "recording-on" }), jsx("h2", { className: "str-video__end-recording__heading", children: t('End recording') })] }), jsx("p", { className: "str-video__end-recording__description", children: t('Are you sure you want end the recording?') }), jsxs("div", { className: "str-video__end-recording__actions", children: [jsx(CompositeButton, { variant: "secondary", onClick: close, children: t('Cancel') }), jsx(CompositeButton, { variant: "primary", onClick: toggleCallRecording, children: isAwaitingResponse ? jsx(LoadingIndicator, {}) : t('End recording') })] })] }));
546
+ };
547
+ const ToggleEndRecordingMenuButton = forwardRef(function ToggleEndRecordingMenuButton(props, ref) {
548
+ return (jsx(CompositeButton, { ref: ref, active: true, variant: "secondary", "data-testid": "recording-stop-button", children: jsx(Icon, { icon: "recording-off" }) }));
549
+ });
550
+ const RecordCallConfirmationButton = ({ caption, }) => {
678
551
  const { t } = useI18n();
679
552
  const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = useToggleCallRecording();
553
+ if (isCallRecordingInProgress) {
554
+ return (jsx(Restricted, { requiredGrants: [
555
+ OwnCapability.START_RECORD_CALL,
556
+ OwnCapability.STOP_RECORD_CALL,
557
+ ], children: jsx(MenuToggle, { ToggleButton: ToggleEndRecordingMenuButton, visualType: MenuVisualType.PORTAL, children: jsx(RecordEndConfirmation, {}) }) }));
558
+ }
680
559
  return (jsx(Restricted, { requiredGrants: [
681
560
  OwnCapability.START_RECORD_CALL,
682
561
  OwnCapability.STOP_RECORD_CALL,
683
- ], children: jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, children: isAwaitingResponse ? (jsx(LoadingIndicator, { tooltip: isCallRecordingInProgress
684
- ? t('Waiting for recording to stop...')
685
- : t('Waiting for recording to start...') })) : (jsx(IconButton
686
- // FIXME OL: sort out this ambiguity
687
- , {
688
- // FIXME OL: sort out this ambiguity
689
- enabled: !!call, disabled: !call, icon: isCallRecordingInProgress ? 'recording-on' : 'recording-off', title: t('Record call'), onClick: toggleCallRecording })) }) }));
562
+ ], children: jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, title: caption || t('Record call'), variant: "secondary", "data-testid": "recording-start-button", onClick: isAwaitingResponse ? undefined : toggleCallRecording, children: isAwaitingResponse ? (jsx(LoadingIndicator, { tooltip: t('Waiting for recording to start...') })) : (jsx(Icon, { icon: "recording-off" })) }) }));
690
563
  };
691
-
692
- const CallStatsLatencyChart = (props) => {
693
- const { values } = props;
694
- let max = 0;
695
- const data = values.map((point) => {
696
- const { y } = point;
697
- max = Math.max(max, y);
698
- return point;
699
- });
700
- return (jsx("div", { className: "str-video__call-stats-line-chart-container", children: jsx(ResponsiveLine, { colors: { scheme: 'blues' }, data: [
701
- {
702
- id: 'Latency',
703
- data: data,
704
- },
705
- ], animate: false, margin: { top: 10, right: 5, bottom: 5, left: 30 }, enablePoints: true, enableGridX: false, enableGridY: true, enableSlices: "x", isInteractive: true, useMesh: false, xScale: { type: 'point' }, yScale: {
706
- type: 'linear',
707
- min: 0,
708
- max: max < 220 ? 220 : max + 30,
709
- nice: true,
710
- }, theme: {
711
- axis: {
712
- ticks: {
713
- text: {
714
- fill: '#FCFCFD',
715
- },
716
- line: {
717
- stroke: '#FCFCFD',
718
- },
719
- },
720
- },
721
- grid: {
722
- line: {
723
- strokeWidth: 0.1,
724
- },
725
- },
726
- } }) }));
564
+ const RecordCallButton = ({ caption }) => {
565
+ const { t } = useI18n();
566
+ const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = useToggleCallRecording();
567
+ let title = caption || t('Record call');
568
+ if (isAwaitingResponse) {
569
+ title = isCallRecordingInProgress
570
+ ? t('Waiting for recording to stop...')
571
+ : t('Waiting for recording to start...');
572
+ }
573
+ return (jsx(Restricted, { requiredGrants: [
574
+ OwnCapability.START_RECORD_CALL,
575
+ OwnCapability.STOP_RECORD_CALL,
576
+ ], children: jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, variant: "secondary", "data-testid": isCallRecordingInProgress
577
+ ? 'recording-stop-button'
578
+ : 'recording-start-button', title: title, onClick: isAwaitingResponse ? undefined : toggleCallRecording, children: isAwaitingResponse ? (jsx(LoadingIndicator, {})) : (jsx(Icon, { icon: isCallRecordingInProgress ? 'recording-on' : 'recording-off' })) }) }));
727
579
  };
728
580
 
729
- const CallStats = () => {
730
- const [latencyBuffer, setLatencyBuffer] = useState(() => {
731
- const now = Date.now();
732
- return Array.from({ length: 20 }, (_, i) => ({ x: now + i, y: 0 }));
733
- });
734
- const [publishBitrate, setPublishBitrate] = useState('-');
735
- const [subscribeBitrate, setSubscribeBitrate] = useState('-');
736
- const previousStats = useRef();
737
- const { useCallStatsReport } = useCallStateHooks();
738
- const callStatsReport = useCallStatsReport();
739
- useEffect(() => {
740
- if (!callStatsReport)
741
- return;
742
- if (!previousStats.current) {
743
- previousStats.current = callStatsReport;
744
- return;
745
- }
746
- const previousCallStatsReport = previousStats.current;
747
- setPublishBitrate(() => {
748
- return calculatePublishBitrate(previousCallStatsReport, callStatsReport);
749
- });
750
- setSubscribeBitrate(() => {
751
- return calculateSubscribeBitrate(previousCallStatsReport, callStatsReport);
752
- });
753
- setLatencyBuffer((latencyBuf) => {
754
- const newLatencyBuffer = latencyBuf.slice(-19);
755
- newLatencyBuffer.push({
756
- x: callStatsReport.timestamp,
757
- y: callStatsReport.publisherStats.averageRoundTripTimeInMs,
758
- });
759
- return newLatencyBuffer;
760
- });
761
- previousStats.current = callStatsReport;
762
- }, [callStatsReport]);
763
- return (jsx("div", { className: "str-video__call-stats", children: callStatsReport && (jsxs(Fragment, { children: [jsx("h3", { children: "Call Latency" }), jsx(CallStatsLatencyChart, { values: latencyBuffer }), jsx("h3", { children: "Call performance" }), jsxs("div", { className: "str-video__call-stats__card-container", children: [jsx(StatCard, { label: "Region", value: callStatsReport.datacenter }), jsx(StatCard, { label: "Latency", value: `${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.` }), jsx(StatCard, { label: "Receive jitter", value: `${callStatsReport.subscriberStats.averageJitterInMs} ms.` }), jsx(StatCard, { label: "Publish jitter", value: `${callStatsReport.publisherStats.averageJitterInMs} ms.` }), jsx(StatCard, { label: "Publish resolution", value: toFrameSize(callStatsReport.publisherStats) }), jsx(StatCard, { label: "Publish quality drop reason", value: callStatsReport.publisherStats.qualityLimitationReasons }), jsx(StatCard, { label: "Receiving resolution", value: toFrameSize(callStatsReport.subscriberStats) }), jsx(StatCard, { label: "Receive quality drop reason", value: callStatsReport.subscriberStats.qualityLimitationReasons }), jsx(StatCard, { label: "Publish bitrate", value: publishBitrate }), jsx(StatCard, { label: "Receiving bitrate", value: subscribeBitrate })] })] })) }));
764
- };
765
- const StatCard = (props) => {
766
- const { label, value } = props;
767
- return (jsxs("div", { className: "str-video__call-stats__card", children: [jsx("div", { className: "str-video__call-stats__card_label", children: label }), jsx("div", { className: "str-video__call-stats__card_value", children: value })] }));
581
+ const defaultEmojiReactionMap = {
582
+ ':like:': '👍',
583
+ ':raise-hand:': '✋',
584
+ ':fireworks:': '🎉',
585
+ ':dislike:': '👎',
586
+ ':heart:': '❤️',
587
+ ':smile:': '😀',
768
588
  };
769
- const toFrameSize = (stats) => {
770
- const { highestFrameWidth: w, highestFrameHeight: h, highestFramesPerSecond: fps, } = stats;
771
- let size = `-`;
772
- if (w && h) {
773
- size = `${w}x${h}`;
774
- if (fps) {
775
- size += `@${fps}fps.`;
776
- }
777
- }
778
- return size;
589
+ const Reaction = ({ participant: { reaction, sessionId }, hideAfterTimeoutInMs = 5500, emojiReactionMap = defaultEmojiReactionMap, }) => {
590
+ const call = useCall();
591
+ useEffect(() => {
592
+ if (!call || !reaction)
593
+ return;
594
+ const timeoutId = setTimeout(() => {
595
+ call.resetReaction(sessionId);
596
+ }, hideAfterTimeoutInMs);
597
+ return () => {
598
+ clearTimeout(timeoutId);
599
+ };
600
+ }, [call, hideAfterTimeoutInMs, reaction, sessionId]);
601
+ if (!reaction)
602
+ return null;
603
+ const { emoji_code: emojiCode } = reaction;
604
+ return (jsx("div", { className: "str-video__reaction", children: jsx("span", { className: "str-video__reaction__emoji", children: emojiCode && emojiReactionMap[emojiCode] }) }));
779
605
  };
780
- const calculatePublishBitrate = (previousCallStatsReport, callStatsReport) => {
781
- const { publisherStats: { totalBytesSent: previousTotalBytesSent, timestamp: previousTimestamp, }, } = previousCallStatsReport;
782
- const { publisherStats: { totalBytesSent, timestamp }, } = callStatsReport;
783
- const bytesSent = totalBytesSent - previousTotalBytesSent;
784
- const timeElapsed = timestamp - previousTimestamp;
785
- return `${((bytesSent * 8) / timeElapsed).toFixed(2)} kbps`;
606
+
607
+ const defaultReactions = [
608
+ {
609
+ type: 'reaction',
610
+ emoji_code: ':like:',
611
+ },
612
+ {
613
+ // TODO OL: use `prompt` type?
614
+ type: 'raised-hand',
615
+ emoji_code: ':raise-hand:',
616
+ },
617
+ {
618
+ type: 'reaction',
619
+ emoji_code: ':fireworks:',
620
+ },
621
+ {
622
+ type: 'reaction',
623
+ emoji_code: ':dislike:',
624
+ },
625
+ {
626
+ type: 'reaction',
627
+ emoji_code: ':heart:',
628
+ },
629
+ {
630
+ type: 'reaction',
631
+ emoji_code: ':smile:',
632
+ },
633
+ ];
634
+ const ReactionsButton = ({ reactions = defaultReactions, }) => {
635
+ return (jsx(Restricted, { requiredGrants: [OwnCapability.CREATE_REACTION], children: jsx(MenuToggle, { placement: "top", ToggleButton: ToggleReactionsMenuButton, visualType: MenuVisualType.MENU, children: jsx(DefaultReactionsMenu, { reactions: reactions }) }) }));
786
636
  };
787
- const calculateSubscribeBitrate = (previousCallStatsReport, callStatsReport) => {
788
- const { subscriberStats: { totalBytesReceived: previousTotalBytesReceived, timestamp: previousTimestamp, }, } = previousCallStatsReport;
789
- const { subscriberStats: { totalBytesReceived, timestamp }, } = callStatsReport;
790
- const bytesReceived = totalBytesReceived - previousTotalBytesReceived;
791
- const timeElapsed = timestamp - previousTimestamp;
792
- return `${((bytesReceived * 8) / timeElapsed).toFixed(2)} kbps`;
637
+ const ToggleReactionsMenuButton = forwardRef(function ToggleReactionsMenuButton({ menuShown }, ref) {
638
+ const { t } = useI18n();
639
+ return (jsx(CompositeButton, { ref: ref, active: menuShown, variant: "primary", title: t('Reactions'), children: jsx(Icon, { icon: "reactions" }) }));
640
+ });
641
+ const DefaultReactionsMenu = ({ reactions, layout = 'horizontal', }) => {
642
+ const call = useCall();
643
+ return (jsx("div", { className: clsx('str-video__reactions-menu', {
644
+ 'str-video__reactions-menu--horizontal': layout === 'horizontal',
645
+ 'str-video__reactions-menu--vertical': layout === 'vertical',
646
+ }), children: reactions.map((reaction) => (jsx("button", { type: "button", className: "str-video__reactions-menu__button", onClick: () => {
647
+ call?.sendReaction(reaction);
648
+ }, children: reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code] }, reaction.emoji_code))) }));
793
649
  };
794
650
 
795
- const CallStatsButton = () => (jsx(MenuToggle, { placement: "top-end", ToggleButton: ToggleMenuButton$1, children: jsx(CallStats, {}) }));
796
- const ToggleMenuButton$1 = forwardRef(({ menuShown }, ref) => (jsx(CompositeButton, { ref: ref, active: menuShown, caption: 'Stats', children: jsx(IconButton, { icon: "stats", title: "Statistics" }) })));
797
-
798
651
  const ScreenShareButton = (props) => {
799
652
  const { t } = useI18n();
800
- const { caption = t('Screen Share') } = props;
801
- const { useHasOngoingScreenShare, useScreenShareState } = useCallStateHooks();
653
+ const { caption } = props;
654
+ const { useHasOngoingScreenShare, useScreenShareState, useCallSettings } = useCallStateHooks();
802
655
  const isSomeoneScreenSharing = useHasOngoingScreenShare();
803
656
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(OwnCapability.SCREENSHARE);
657
+ const callSettings = useCallSettings();
658
+ const isScreenSharingAllowed = callSettings?.screensharing.enabled;
804
659
  const { screenShare, isMute: amIScreenSharing } = useScreenShareState();
805
660
  const disableScreenShareButton = amIScreenSharing
806
- ? isSomeoneScreenSharing
661
+ ? isSomeoneScreenSharing || isScreenSharingAllowed === false
807
662
  : false;
808
- return (jsx(Restricted, { requiredGrants: [OwnCapability.SCREENSHARE], children: jsx(PermissionNotification, { permission: OwnCapability.SCREENSHARE, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your screen.'), messageAwaitingApproval: t('Awaiting for an approval to share screen.'), messageRevoked: t('You can no longer share your screen.'), children: jsx(CompositeButton, { active: isSomeoneScreenSharing, caption: caption, children: jsx(IconButton, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off', title: t('Share screen'), disabled: disableScreenShareButton, onClick: async () => {
809
- if (!hasPermission) {
810
- await requestPermission();
811
- }
812
- else {
813
- await screenShare.toggle();
814
- }
815
- } }) }) }) }));
663
+ return (jsx(Restricted, { requiredGrants: [OwnCapability.SCREENSHARE], children: jsx(PermissionNotification, { permission: OwnCapability.SCREENSHARE, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your screen.'), messageAwaitingApproval: t('Awaiting for an approval to share screen.'), messageRevoked: t('You can no longer share your screen.'), children: jsx(CompositeButton, { active: isSomeoneScreenSharing, caption: caption, title: caption || t('Share screen'), variant: "primary", "data-testid": isSomeoneScreenSharing
664
+ ? 'screen-share-stop-button'
665
+ : 'screen-share-start-button', disabled: disableScreenShareButton, onClick: async () => {
666
+ if (!hasPermission) {
667
+ await requestPermission();
668
+ }
669
+ else {
670
+ await screenShare.toggle();
671
+ }
672
+ }, children: jsx(Icon, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off' }) }) }) }));
673
+ };
674
+
675
+ const SelectContext = createContext({});
676
+ const Select = (props) => {
677
+ const { children, icon, defaultSelectedLabel, defaultSelectedIndex, handleSelect: handleSelectProp, } = props;
678
+ const [isOpen, setIsOpen] = useState(false);
679
+ const [activeIndex, setActiveIndex] = useState(null);
680
+ const [selectedIndex, setSelectedIndex] = useState(defaultSelectedIndex);
681
+ const [selectedLabel, setSelectedLabel] = useState(defaultSelectedLabel);
682
+ const { refs, context } = useFloating({
683
+ placement: 'bottom-start',
684
+ open: isOpen,
685
+ onOpenChange: setIsOpen,
686
+ whileElementsMounted: autoUpdate,
687
+ middleware: [flip()],
688
+ });
689
+ const elementsRef = useRef([]);
690
+ const labelsRef = useRef([]);
691
+ const handleSelect = useCallback((index) => {
692
+ setSelectedIndex(index);
693
+ handleSelectProp(index || 0);
694
+ setIsOpen(false);
695
+ if (index !== null) {
696
+ setSelectedLabel(labelsRef.current[index]);
697
+ }
698
+ }, [handleSelectProp]);
699
+ const handleTypeaheadMatch = (index) => {
700
+ if (isOpen) {
701
+ setActiveIndex(index);
702
+ }
703
+ else {
704
+ handleSelect(index);
705
+ }
706
+ };
707
+ const listNav = useListNavigation(context, {
708
+ listRef: elementsRef,
709
+ activeIndex,
710
+ selectedIndex,
711
+ onNavigate: setActiveIndex,
712
+ });
713
+ const typeahead = useTypeahead(context, {
714
+ listRef: labelsRef,
715
+ activeIndex,
716
+ selectedIndex,
717
+ onMatch: handleTypeaheadMatch,
718
+ });
719
+ const click = useClick(context);
720
+ const dismiss = useDismiss(context);
721
+ const role = useRole(context, { role: 'listbox' });
722
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([listNav, typeahead, click, dismiss, role]);
723
+ const selectContext = useMemo(() => ({
724
+ activeIndex,
725
+ selectedIndex,
726
+ getItemProps,
727
+ handleSelect,
728
+ }), [activeIndex, selectedIndex, getItemProps, handleSelect]);
729
+ return (jsxs("div", { className: "str-video__dropdown", children: [jsxs("div", { className: "str-video__dropdown-selected", ref: refs.setReference, tabIndex: 0, ...getReferenceProps(), children: [jsxs("label", { className: "str-video__dropdown-selected__label", children: [icon && (jsx(Icon, { className: "str-video__dropdown-selected__icon", icon: icon })), selectedLabel] }), jsx(Icon, { className: "str-video__dropdown-selected__chevron", icon: isOpen ? 'chevron-up' : 'chevron-down' })] }), jsx(SelectContext.Provider, { value: selectContext, children: isOpen && (jsx(FloatingFocusManager, { context: context, modal: false, children: jsx("div", { className: "str-video__dropdown-list", ref: refs.setFloating, ...getFloatingProps(), children: jsx(FloatingList, { elementsRef: elementsRef, labelsRef: labelsRef, children: children }) }) })) })] }));
730
+ };
731
+ const DropDownSelectOption = (props) => {
732
+ const { selected, label, icon } = props;
733
+ const { getItemProps, handleSelect } = useContext(SelectContext);
734
+ const { ref, index } = useListItem();
735
+ return (jsxs("div", { className: clsx('str-video__dropdown-option', {
736
+ 'str-video__dropdown-option--selected': selected,
737
+ }), ref: ref, ...getItemProps({
738
+ onClick: () => handleSelect(index),
739
+ }), children: [jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsx("span", { className: "str-video__dropdown-label", children: label })] }));
740
+ };
741
+ const DropDownSelect = (props) => {
742
+ const { children, icon, handleSelect, defaultSelectedLabel, defaultSelectedIndex, } = props;
743
+ return (jsx(Select, { icon: icon, handleSelect: handleSelect, defaultSelectedIndex: defaultSelectedIndex, defaultSelectedLabel: defaultSelectedLabel, children: children }));
816
744
  };
817
745
 
818
746
  const DeviceSelectorOption = ({ disabled, id, label, onChange, name, selected, defaultChecked, value, }) => {
@@ -821,9 +749,8 @@ const DeviceSelectorOption = ({ disabled, id, label, onChange, name, selected, d
821
749
  'str-video__device-settings__option--disabled': disabled,
822
750
  }), htmlFor: id, children: [jsx("input", { type: "radio", name: name, onChange: onChange, value: value, id: id, checked: selected, defaultChecked: defaultChecked, disabled: disabled }), label] }));
823
751
  };
824
- const DeviceSelector = (props) => {
825
- const { devices = [], selectedDeviceId: selectedDeviceFromProps, title, onChange, } = props;
826
- const inputGroupName = title.replace(' ', '-').toLowerCase();
752
+ const DeviceSelectorList = (props) => {
753
+ const { devices = [], selectedDeviceId: selectedDeviceFromProps, title, type, onChange, } = props;
827
754
  // sometimes the browser (Chrome) will report the system-default device
828
755
  // with an id of 'default'. In case when it doesn't, we'll select the first
829
756
  // available device.
@@ -832,46 +759,71 @@ const DeviceSelector = (props) => {
832
759
  !devices.find((d) => d.deviceId === selectedDeviceId)) {
833
760
  selectedDeviceId = devices[0].deviceId;
834
761
  }
835
- return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [jsx("div", { className: "str-video__device-settings__device-selector-title", children: title }), !devices.length ? (jsx(DeviceSelectorOption, { id: `${inputGroupName}--default`, label: "Default", name: inputGroupName, defaultChecked: true, value: "default" })) : (devices.map((device) => {
836
- return (jsx(DeviceSelectorOption, { id: `${inputGroupName}--${device.deviceId}`, value: device.deviceId, label: device.label, onChange: (e) => {
762
+ return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [title && (jsx("div", { className: "str-video__device-settings__device-selector-title", children: title })), !devices.length ? (jsx(DeviceSelectorOption, { id: `${type}--default`, label: "Default", name: type, defaultChecked: true, value: "default" })) : (devices.map((device) => {
763
+ return (jsx(DeviceSelectorOption, { id: `${type}--${device.deviceId}`, value: device.deviceId, label: device.label, onChange: (e) => {
837
764
  onChange?.(e.target.value);
838
- }, name: inputGroupName, selected: device.deviceId === selectedDeviceId || devices.length === 1 }, device.deviceId));
765
+ }, name: type, selected: device.deviceId === selectedDeviceId || devices.length === 1 }, device.deviceId));
839
766
  }))] }));
840
767
  };
768
+ const DeviceSelectorDropdown = (props) => {
769
+ const { devices = [], selectedDeviceId: selectedDeviceFromProps, title, onChange, icon, } = props;
770
+ // sometimes the browser (Chrome) will report the system-default device
771
+ // with an id of 'default'. In case when it doesn't, we'll select the first
772
+ // available device.
773
+ let selectedDeviceId = selectedDeviceFromProps;
774
+ if (devices.length > 0 &&
775
+ !devices.find((d) => d.deviceId === selectedDeviceId)) {
776
+ selectedDeviceId = devices[0].deviceId;
777
+ }
778
+ const selectedIndex = devices.findIndex((d) => d.deviceId === selectedDeviceId);
779
+ const handleSelect = useCallback((index) => {
780
+ onChange?.(devices[index].deviceId);
781
+ }, [devices, onChange]);
782
+ return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [jsx("div", { className: "str-video__device-settings__device-selector-title", children: title }), jsx(DropDownSelect, { icon: icon, defaultSelectedIndex: selectedIndex, defaultSelectedLabel: devices[selectedIndex]?.label, handleSelect: handleSelect, children: devices.map((device) => {
783
+ return (jsx(DropDownSelectOption, { icon: icon, label: device.label, selected: device.deviceId === selectedDeviceId || devices.length === 1 }, device.deviceId));
784
+ }) })] }));
785
+ };
786
+ const DeviceSelector = (props) => {
787
+ const { visualType = 'list', icon, placeholder, ...rest } = props;
788
+ if (visualType === 'list') {
789
+ return jsx(DeviceSelectorList, { ...rest });
790
+ }
791
+ return (jsx(DeviceSelectorDropdown, { ...rest, icon: icon, placeholder: placeholder }));
792
+ };
841
793
 
842
- const DeviceSelectorAudioInput = ({ title, }) => {
843
- const { t } = useI18n();
794
+ const DeviceSelectorAudioInput = ({ title, visualType, }) => {
844
795
  const { useMicrophoneState } = useCallStateHooks();
845
796
  const { microphone, selectedDevice, devices } = useMicrophoneState();
846
- return (jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
797
+ return (jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, type: "audioinput", onChange: async (deviceId) => {
847
798
  await microphone.select(deviceId);
848
- }, title: title || t('Select a Mic') }));
799
+ }, title: title, visualType: visualType, icon: "mic" }));
849
800
  };
850
- const DeviceSelectorAudioOutput = ({ title, }) => {
851
- const { t } = useI18n();
801
+ const DeviceSelectorAudioOutput = ({ title, visualType, }) => {
852
802
  const { useSpeakerState } = useCallStateHooks();
853
803
  const { speaker, selectedDevice, devices, isDeviceSelectionSupported } = useSpeakerState();
854
804
  if (!isDeviceSelectionSupported)
855
805
  return null;
856
- return (jsx(DeviceSelector, { devices: devices, selectedDeviceId: selectedDevice, onChange: (deviceId) => {
806
+ return (jsx(DeviceSelector, { devices: devices, type: "audiooutput", selectedDeviceId: selectedDevice, onChange: (deviceId) => {
857
807
  speaker.select(deviceId);
858
- }, title: title || t('Select Speakers') }));
808
+ }, title: title, visualType: visualType, icon: "speaker" }));
859
809
  };
860
810
 
861
- const DeviceSelectorVideo = ({ title }) => {
862
- const { t } = useI18n();
811
+ const DeviceSelectorVideo = ({ title, visualType, }) => {
863
812
  const { useCameraState } = useCallStateHooks();
864
813
  const { camera, devices, selectedDevice } = useCameraState();
865
- return (jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
814
+ return (jsx(DeviceSelector, { devices: devices || [], type: "videoinput", selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
866
815
  await camera.select(deviceId);
867
- }, title: title || t('Select a Camera') }));
816
+ }, title: title, visualType: visualType, icon: "camera" }));
868
817
  };
869
818
 
870
- const DeviceSettings = () => {
871
- return (jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleMenuButton, children: jsx(Menu, {}) }));
819
+ const DeviceSettings = ({ visualType = MenuVisualType.MENU, }) => {
820
+ return (jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleDeviceSettingsMenuButton, visualType: visualType, children: jsx(Menu, {}) }));
872
821
  };
873
- const Menu = () => (jsxs("div", { className: "str-video__device-settings", children: [jsx(DeviceSelectorVideo, {}), jsx(DeviceSelectorAudioInput, {}), jsx(DeviceSelectorAudioOutput, {})] }));
874
- const ToggleMenuButton = forwardRef(({ menuShown }, ref) => {
822
+ const Menu = () => {
823
+ const { t } = useI18n();
824
+ return (jsxs("div", { className: "str-video__device-settings", children: [jsx(DeviceSelectorVideo, { title: t('Select a Camera') }), jsx(DeviceSelectorAudioInput, { title: t('Select a Mic') }), jsx(DeviceSelectorAudioOutput, { title: t('Select Speakers') })] }));
825
+ };
826
+ const ToggleDeviceSettingsMenuButton = forwardRef(function ToggleDeviceSettingsMenuButton({ menuShown }, ref) {
875
827
  const { t } = useI18n();
876
828
  return (jsx(IconButton, { className: clsx('str-video__device-settings__button', {
877
829
  'str-video__device-settings__button--active': menuShown,
@@ -879,53 +831,103 @@ const ToggleMenuButton = forwardRef(({ menuShown }, ref) => {
879
831
  });
880
832
 
881
833
  const ToggleAudioPreviewButton = (props) => {
834
+ const { caption, Menu, menuPlacement, ...restCompositeButtonProps } = props;
882
835
  const { t } = useI18n();
883
- const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
884
836
  const { useMicrophoneState } = useCallStateHooks();
885
- const { microphone, isMute } = useMicrophoneState();
886
- return (jsx(CompositeButton, { Menu: Menu, active: isMute, caption: caption || t('Mic'), children: jsx(IconButton, { icon: !isMute ? 'mic' : 'mic-off', onClick: () => microphone.toggle() }) }));
837
+ const { microphone, isMute, hasBrowserPermission } = useMicrophoneState();
838
+ return (jsxs(CompositeButton, { active: isMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", title: !hasBrowserPermission
839
+ ? t('Check your browser audio permissions')
840
+ : caption || t('Mic'), disabled: !hasBrowserPermission, "data-testid": isMute ? 'preview-audio-unmute-button' : 'preview-audio-mute-button', onClick: () => microphone.toggle(), Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, children: [jsx(Icon, { icon: !isMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" }))] }));
887
841
  };
888
842
  const ToggleAudioPublishingButton = (props) => {
889
843
  const { t } = useI18n();
890
- const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
844
+ const { caption, Menu = jsx(DeviceSelectorAudioInput, { visualType: "list" }), menuPlacement = 'top', ...restCompositeButtonProps } = props;
891
845
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(OwnCapability.SEND_AUDIO);
892
846
  const { useMicrophoneState } = useCallStateHooks();
893
- const { microphone, isMute } = useMicrophoneState();
894
- return (jsx(Restricted, { requiredGrants: [OwnCapability.SEND_AUDIO], children: jsx(PermissionNotification, { permission: OwnCapability.SEND_AUDIO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now speak.'), messageAwaitingApproval: t('Awaiting for an approval to speak.'), messageRevoked: t('You can no longer speak.'), children: jsx(CompositeButton, { Menu: Menu, active: isMute, caption: caption, children: jsx(IconButton, { icon: isMute ? 'mic-off' : 'mic', onClick: async () => {
895
- if (!hasPermission) {
896
- await requestPermission();
897
- }
898
- else {
899
- await microphone.toggle();
900
- }
901
- } }) }) }) }));
847
+ const { microphone, isMute, hasBrowserPermission } = useMicrophoneState();
848
+ return (jsx(Restricted, { requiredGrants: [OwnCapability.SEND_AUDIO], children: jsx(PermissionNotification, { permission: OwnCapability.SEND_AUDIO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now speak.'), messageAwaitingApproval: t('Awaiting for an approval to speak.'), messageRevoked: t('You can no longer speak.'), children: jsxs(CompositeButton, { active: isMute, caption: caption, title: !hasPermission
849
+ ? t('You have no permission to share your audio')
850
+ : !hasBrowserPermission
851
+ ? t('Check your browser mic permissions')
852
+ : caption || t('Mic'), variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": isMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: async () => {
853
+ if (!hasPermission) {
854
+ await requestPermission();
855
+ }
856
+ else {
857
+ await microphone.toggle();
858
+ }
859
+ }, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, children: [jsx(Icon, { icon: isMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }));
902
860
  };
903
861
 
904
862
  const ToggleVideoPreviewButton = (props) => {
863
+ const { caption, ...restCompositeButtonProps } = props;
905
864
  const { t } = useI18n();
906
- const { caption = t('Video'), Menu = DeviceSelectorVideo } = props;
907
865
  const { useCameraState } = useCallStateHooks();
908
- const { camera, isMute } = useCameraState();
909
- return (jsx(CompositeButton, { Menu: Menu, active: isMute, caption: caption, children: jsx(IconButton, { icon: !isMute ? 'camera' : 'camera-off', onClick: () => camera.toggle() }) }));
866
+ const { camera, isMute, hasBrowserPermission } = useCameraState();
867
+ return (jsxs(CompositeButton, { active: isMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), title: !hasBrowserPermission
868
+ ? t('Check your browser video permissions')
869
+ : caption || t('Video'), variant: "secondary", "data-testid": isMute ? 'preview-video-unmute-button' : 'preview-video-mute-button', onClick: () => camera.toggle(), disabled: !hasBrowserPermission, ...restCompositeButtonProps, children: [jsx(Icon, { icon: !isMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" }))] }));
910
870
  };
911
871
  const ToggleVideoPublishingButton = (props) => {
912
872
  const { t } = useI18n();
913
- const { caption = t('Video'), Menu = DeviceSelectorVideo } = props;
873
+ const { caption, Menu = jsx(DeviceSelectorVideo, { visualType: "list" }), menuPlacement = 'top', ...restCompositeButtonProps } = props;
914
874
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(OwnCapability.SEND_VIDEO);
915
- const { useCameraState } = useCallStateHooks();
916
- const { camera, isMute } = useCameraState();
917
- return (jsx(Restricted, { requiredGrants: [OwnCapability.SEND_VIDEO], children: jsx(PermissionNotification, { permission: OwnCapability.SEND_VIDEO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your video.'), messageAwaitingApproval: t('Awaiting for an approval to share your video.'), messageRevoked: t('You can no longer share your video.'), children: jsx(CompositeButton, { Menu: Menu, active: isMute, caption: caption, children: jsx(IconButton, { icon: isMute ? 'camera-off' : 'camera', onClick: async () => {
918
- if (!hasPermission) {
919
- await requestPermission();
920
- }
921
- else {
922
- await camera.toggle();
923
- }
924
- } }) }) }) }));
875
+ const { useCameraState, useCallSettings } = useCallStateHooks();
876
+ const { camera, isMute, hasBrowserPermission } = useCameraState();
877
+ const callSettings = useCallSettings();
878
+ const isPublishingVideoAllowed = callSettings?.video.enabled;
879
+ return (jsx(Restricted, { requiredGrants: [OwnCapability.SEND_VIDEO], children: jsx(PermissionNotification, { permission: OwnCapability.SEND_VIDEO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your video.'), messageAwaitingApproval: t('Awaiting for an approval to share your video.'), messageRevoked: t('You can no longer share your video.'), children: jsxs(CompositeButton, { active: isMute, caption: caption, variant: "secondary", title: !hasPermission
880
+ ? t('You have no permission to share your video')
881
+ : !hasBrowserPermission
882
+ ? t('Check your browser video permissions')
883
+ : !isPublishingVideoAllowed
884
+ ? t('Video publishing is disabled by the system')
885
+ : caption || t('Video'), disabled: !hasBrowserPermission || !hasPermission || !isPublishingVideoAllowed, "data-testid": isMute ? 'video-unmute-button' : 'video-mute-button', onClick: async () => {
886
+ if (!hasPermission) {
887
+ await requestPermission();
888
+ }
889
+ else {
890
+ await camera.toggle();
891
+ }
892
+ }, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, children: [jsx(Icon, { icon: isMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
893
+ !hasPermission ||
894
+ !isPublishingVideoAllowed) && (jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }));
895
+ };
896
+
897
+ const EndCallMenu = (props) => {
898
+ const { onLeave, onEnd } = props;
899
+ const { t } = useI18n();
900
+ return (jsxs("div", { className: "str-video__end-call__confirmation", children: [jsxs("button", { className: "str-video__button str-video__end-call__leave", type: "button", "data-testid": "leave-call-button", onClick: onLeave, children: [jsx(Icon, { className: "str-video__button__icon str-video__end-call__leave-icon", icon: "logout" }), t('Leave call')] }), jsx(Restricted, { requiredGrants: [OwnCapability.END_CALL], children: jsxs("button", { className: "str-video__button str-video__end-call__end", type: "button", "data-testid": "end-call-for-all-button", onClick: onEnd, children: [jsx(Icon, { className: "str-video__button__icon str-video__end-call__end-icon", icon: "call-end" }), t('End call for all')] }) })] }));
901
+ };
902
+ const CancelCallToggleMenuButton = forwardRef(function CancelCallToggleMenuButton(props, ref) {
903
+ const { t } = useI18n();
904
+ return (jsx(IconButton, { icon: "call-end", variant: "danger", title: t('Leave call'), "data-testid": "leave-call-button", ref: ref }));
905
+ });
906
+ const CancelCallConfirmButton = ({ onClick, onLeave, }) => {
907
+ const call = useCall();
908
+ const handleLeave = useCallback(async (e) => {
909
+ if (onClick) {
910
+ onClick(e);
911
+ }
912
+ else if (call) {
913
+ await call.leave();
914
+ onLeave?.();
915
+ }
916
+ }, [onClick, onLeave, call]);
917
+ const handleEndCall = useCallback(async (e) => {
918
+ if (onClick) {
919
+ onClick(e);
920
+ }
921
+ else if (call) {
922
+ await call.endCall();
923
+ onLeave?.();
924
+ }
925
+ }, [onClick, onLeave, call]);
926
+ return (jsx(MenuToggle, { placement: "top-start", ToggleButton: CancelCallToggleMenuButton, children: jsx(EndCallMenu, { onEnd: handleEndCall, onLeave: handleLeave }) }));
925
927
  };
926
-
927
928
  const CancelCallButton = ({ disabled, onClick, onLeave, }) => {
928
929
  const call = useCall();
930
+ const { t } = useI18n();
929
931
  const handleClick = useCallback(async (e) => {
930
932
  if (onClick) {
931
933
  onClick(e);
@@ -935,81 +937,202 @@ const CancelCallButton = ({ disabled, onClick, onLeave, }) => {
935
937
  onLeave?.();
936
938
  }
937
939
  }, [onClick, onLeave, call]);
938
- return (jsx(IconButton, { disabled: disabled, icon: "call-end", variant: "danger", onClick: handleClick }));
940
+ return (jsx(IconButton, { disabled: disabled, icon: "call-end", variant: "danger", title: t('Leave call'), "data-testid": "cancel-call-button", onClick: handleClick }));
939
941
  };
940
942
 
941
- const CallControls = ({ onLeave }) => (jsxs("div", { className: "str-video__call-controls", children: [jsx(RecordCallButton, {}), jsx(CallStatsButton, {}), jsx(ScreenShareButton, {}), jsx(SpeakingWhileMutedNotification, { children: jsx(ToggleAudioPublishingButton, {}) }), jsx(ToggleVideoPublishingButton, {}), jsx(CancelCallButton, { onLeave: onLeave })] }));
943
+ const CallControls = ({ onLeave }) => (jsxs("div", { className: "str-video__call-controls", children: [jsx(RecordCallButton, {}), jsx(ReactionsButton, {}), jsx(ScreenShareButton, {}), jsx(SpeakingWhileMutedNotification, { children: jsx(ToggleAudioPublishingButton, {}) }), jsx(ToggleVideoPublishingButton, {}), jsx(CancelCallButton, { onLeave: onLeave })] }));
942
944
 
943
- const defaultEmojiReactionMap = {
944
- ':like:': '👍',
945
- ':raise-hand:': '✋',
946
- ':fireworks:': '🎉',
947
- ':dislike:': '👎',
948
- ':heart:': '❤️',
949
- ':smile:': '😀',
945
+ Chart.register(CategoryScale, LinearScale, LineElement, PointElement);
946
+ const CallStatsLatencyChart = (props) => {
947
+ const { values } = props;
948
+ let max = 0;
949
+ const data = {
950
+ labels: values.map((point) => {
951
+ const date = new Date(point.x * 1000);
952
+ return `${date.getHours()}:${date.getMinutes()}`;
953
+ }),
954
+ datasets: [
955
+ {
956
+ data: values.map((point) => {
957
+ const { y } = point;
958
+ max = Math.max(max, y);
959
+ return point;
960
+ }),
961
+ borderColor: '#00e2a1',
962
+ backgroundColor: '#00e2a1',
963
+ },
964
+ ],
965
+ };
966
+ const options = useMemo(() => {
967
+ return {
968
+ maintainAspectRatio: false,
969
+ animation: {
970
+ duration: 0,
971
+ },
972
+ elements: {
973
+ line: {
974
+ borderWidth: 1,
975
+ },
976
+ point: {
977
+ radius: 2,
978
+ },
979
+ },
980
+ scales: {
981
+ y: {
982
+ position: 'right',
983
+ stacked: true,
984
+ min: 0,
985
+ max: Math.max(180, Math.ceil((max + 10) / 10) * 10),
986
+ grid: {
987
+ display: true,
988
+ color: '#979ca0',
989
+ },
990
+ ticks: {
991
+ stepSize: 30,
992
+ },
993
+ },
994
+ x: {
995
+ grid: {
996
+ display: false,
997
+ },
998
+ ticks: {
999
+ display: false,
1000
+ },
1001
+ },
1002
+ },
1003
+ };
1004
+ }, [max]);
1005
+ return (jsx("div", { className: "str-video__call-stats-line-chart-container", children: jsx(Line, { options: options, data: data, className: "str-video__call-stats__latencychart" }) }));
1006
+ };
1007
+
1008
+ var Statuses;
1009
+ (function (Statuses) {
1010
+ Statuses["GOOD"] = "Good";
1011
+ Statuses["OK"] = "Ok";
1012
+ Statuses["BAD"] = "Bad";
1013
+ })(Statuses || (Statuses = {}));
1014
+ const statsStatus = ({ value, lowBound, highBound, }) => {
1015
+ if (value <= lowBound) {
1016
+ return Statuses.GOOD;
1017
+ }
1018
+ if (value >= lowBound && value <= highBound) {
1019
+ return Statuses.OK;
1020
+ }
1021
+ if (value >= highBound) {
1022
+ return Statuses.BAD;
1023
+ }
1024
+ return Statuses.GOOD;
950
1025
  };
951
- const Reaction = ({ participant: { reaction, sessionId }, hideAfterTimeoutInMs = 5500, emojiReactionMap = defaultEmojiReactionMap, }) => {
952
- const call = useCall();
1026
+ const CallStats = (props) => {
1027
+ const { latencyLowBound = 75, latencyHighBound = 400 } = props;
1028
+ const [latencyBuffer, setLatencyBuffer] = useState(() => {
1029
+ const now = Date.now();
1030
+ return Array.from({ length: 20 }, (_, i) => ({ x: now + i, y: 0 }));
1031
+ });
1032
+ const { t } = useI18n();
1033
+ const [publishBitrate, setPublishBitrate] = useState('-');
1034
+ const [subscribeBitrate, setSubscribeBitrate] = useState('-');
1035
+ const previousStats = useRef();
1036
+ const { useCallStatsReport } = useCallStateHooks();
1037
+ const callStatsReport = useCallStatsReport();
953
1038
  useEffect(() => {
954
- if (!call || !reaction)
1039
+ if (!callStatsReport)
955
1040
  return;
956
- const timeoutId = setTimeout(() => {
957
- call.resetReaction(sessionId);
958
- }, hideAfterTimeoutInMs);
959
- return () => {
960
- clearTimeout(timeoutId);
961
- };
962
- }, [call, hideAfterTimeoutInMs, reaction, sessionId]);
963
- if (!reaction)
964
- return null;
965
- const { emoji_code: emojiCode } = reaction;
966
- return (jsx("div", { className: "str-video__reaction", children: jsx("span", { className: "str-video__reaction__emoji", children: emojiCode && emojiReactionMap[emojiCode] }) }));
1041
+ if (!previousStats.current) {
1042
+ previousStats.current = callStatsReport;
1043
+ return;
1044
+ }
1045
+ const previousCallStatsReport = previousStats.current;
1046
+ setPublishBitrate(() => {
1047
+ return calculatePublishBitrate(previousCallStatsReport, callStatsReport);
1048
+ });
1049
+ setSubscribeBitrate(() => {
1050
+ return calculateSubscribeBitrate(previousCallStatsReport, callStatsReport);
1051
+ });
1052
+ setLatencyBuffer((latencyBuf) => {
1053
+ const newLatencyBuffer = latencyBuf.slice(-19);
1054
+ newLatencyBuffer.push({
1055
+ x: callStatsReport.timestamp,
1056
+ y: callStatsReport.publisherStats.averageRoundTripTimeInMs,
1057
+ });
1058
+ return newLatencyBuffer;
1059
+ });
1060
+ previousStats.current = callStatsReport;
1061
+ }, [callStatsReport]);
1062
+ const latencyComparison = {
1063
+ lowBound: latencyLowBound,
1064
+ highBound: latencyHighBound,
1065
+ value: callStatsReport?.publisherStats.averageRoundTripTimeInMs || 0,
1066
+ };
1067
+ return (jsx("div", { className: "str-video__call-stats", children: callStatsReport && (jsxs(Fragment, { children: [jsxs("div", { className: "str-video__call-stats__header", children: [jsxs("h3", { className: "str-video__call-stats__heading", children: [jsx(Icon, { className: "str-video__call-stats__icon", icon: "call-latency" }), t('Call Latency')] }), jsx("p", { className: "str-video__call-stats__description", children: t('Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.') })] }), jsx("div", { className: "str-video__call-stats__latencychart", children: jsx(CallStatsLatencyChart, { values: latencyBuffer }) }), jsxs("div", { className: "str-video__call-stats__header", children: [jsxs("h3", { className: "str-video__call-stats__heading", children: [jsx(Icon, { className: "str-video__call-stats__icon", icon: "network-quality" }), t('Call performance')] }), jsx("p", { className: "str-video__call-stats__description", children: t('Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.') })] }), jsxs("div", { className: "str-video__call-stats__card-container", children: [jsx(StatCard, { label: "Region", value: callStatsReport.datacenter }), jsx(StatCard, { label: "Latency", value: `${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.`, comparison: latencyComparison }), jsx(StatCard, { label: "Receive jitter", value: `${callStatsReport.subscriberStats.averageJitterInMs} ms.`, comparison: {
1068
+ ...latencyComparison,
1069
+ value: callStatsReport.subscriberStats.averageJitterInMs,
1070
+ } }), jsx(StatCard, { label: "Publish jitter", value: `${callStatsReport.publisherStats.averageJitterInMs} ms.`, comparison: {
1071
+ ...latencyComparison,
1072
+ value: callStatsReport.publisherStats.averageJitterInMs,
1073
+ } }), jsx(StatCard, { label: "Publish resolution", value: toFrameSize(callStatsReport.publisherStats) }), jsx(StatCard, { label: "Publish quality drop reason", value: callStatsReport.publisherStats.qualityLimitationReasons }), jsx(StatCard, { label: "Receiving resolution", value: toFrameSize(callStatsReport.subscriberStats) }), jsx(StatCard, { label: "Receive quality drop reason", value: callStatsReport.subscriberStats.qualityLimitationReasons }), jsx(StatCard, { label: "Publish bitrate", value: publishBitrate }), jsx(StatCard, { label: "Receiving bitrate", value: subscribeBitrate })] })] })) }));
1074
+ };
1075
+ const StatCardExplanation = (props) => {
1076
+ const { description } = props;
1077
+ const [isOpen, setIsOpen] = useState(false);
1078
+ const { refs, floatingStyles, context } = useFloating({
1079
+ open: isOpen,
1080
+ onOpenChange: setIsOpen,
1081
+ });
1082
+ const hover = useHover(context);
1083
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover]);
1084
+ return (jsxs(Fragment, { children: [jsx("div", { className: "str-video__call-explanation", ref: refs.setReference, ...getReferenceProps(), children: jsx(Icon, { className: "str-video__call-explanation__icon", icon: "info" }) }), isOpen && (jsx("div", { className: "str-video__call-explanation__description", ref: refs.setFloating, style: floatingStyles, ...getFloatingProps(), children: description }))] }));
967
1085
  };
968
-
969
- const defaultReactions = [
970
- {
971
- type: 'reaction',
972
- emoji_code: ':like:',
973
- },
974
- {
975
- // TODO OL: use `prompt` type?
976
- type: 'raised-hand',
977
- emoji_code: ':raise-hand:',
978
- },
979
- {
980
- type: 'reaction',
981
- emoji_code: ':fireworks:',
982
- },
983
- {
984
- type: 'reaction',
985
- emoji_code: ':dislike:',
986
- },
987
- {
988
- type: 'reaction',
989
- emoji_code: ':heart:',
990
- },
991
- {
992
- type: 'reaction',
993
- emoji_code: ':smile:',
994
- },
995
- ];
996
- const ReactionsButton = ({ reactions = defaultReactions, }) => {
1086
+ const StatsTag = ({ children, status = Statuses.GOOD, }) => {
1087
+ return (jsx("div", { className: clsx('str-video__call-stats__tag', {
1088
+ 'str-video__call-stats__tag--good': status === Statuses.GOOD,
1089
+ 'str-video__call-stats__tag--ok': status === Statuses.OK,
1090
+ 'str-video__call-stats__tag--bad': status === Statuses.BAD,
1091
+ }), children: jsx("div", { className: "str-video__call-stats__tag__text", children: children }) }));
1092
+ };
1093
+ const StatCard = (props) => {
1094
+ const { label, value, description, comparison } = props;
997
1095
  const { t } = useI18n();
998
- return (jsx(Restricted, { requiredGrants: [OwnCapability.CREATE_REACTION], children: jsx(CompositeButton, { active: false, caption: t('Reactions'), menuPlacement: "top-start", Menu: jsx(DefaultReactionsMenu, { reactions: reactions }), children: jsx(IconButton, { icon: "reactions", title: t('Reactions'), onClick: () => {
999
- console.log('Reactions');
1000
- } }) }) }));
1096
+ const status = comparison ? statsStatus(comparison) : undefined;
1097
+ return (jsxs("div", { className: "str-video__call-stats__card", children: [jsxs("div", { className: "str-video__call-stats__card-content", children: [jsxs("div", { className: "str-video__call-stats__card-label", children: [label, description && jsx(StatCardExplanation, { description: description })] }), jsx("div", { className: "str-video__call-stats__card-value", children: value })] }), comparison && status && jsx(StatsTag, { status: status, children: t(status) })] }));
1001
1098
  };
1002
- const DefaultReactionsMenu = ({ reactions, }) => {
1003
- const call = useCall();
1004
- return (jsx("div", { className: "str-video__reactions-menu", children: reactions.map((reaction) => (jsx("button", { type: "button", className: "str-video__reactions-menu__button", onClick: () => {
1005
- call?.sendReaction(reaction);
1006
- }, children: reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code] }, reaction.emoji_code))) }));
1099
+ const toFrameSize = (stats) => {
1100
+ const { highestFrameWidth: w, highestFrameHeight: h, highestFramesPerSecond: fps, } = stats;
1101
+ let size = `-`;
1102
+ if (w && h) {
1103
+ size = `${w}x${h}`;
1104
+ if (fps) {
1105
+ size += `@${fps}fps.`;
1106
+ }
1107
+ }
1108
+ return size;
1109
+ };
1110
+ const calculatePublishBitrate = (previousCallStatsReport, callStatsReport) => {
1111
+ const { publisherStats: { totalBytesSent: previousTotalBytesSent, timestamp: previousTimestamp, }, } = previousCallStatsReport;
1112
+ const { publisherStats: { totalBytesSent, timestamp }, } = callStatsReport;
1113
+ const bytesSent = totalBytesSent - previousTotalBytesSent;
1114
+ const timeElapsed = timestamp - previousTimestamp;
1115
+ return `${((bytesSent * 8) / timeElapsed).toFixed(2)} kbps`;
1116
+ };
1117
+ const calculateSubscribeBitrate = (previousCallStatsReport, callStatsReport) => {
1118
+ const { subscriberStats: { totalBytesReceived: previousTotalBytesReceived, timestamp: previousTimestamp, }, } = previousCallStatsReport;
1119
+ const { subscriberStats: { totalBytesReceived, timestamp }, } = callStatsReport;
1120
+ const bytesReceived = totalBytesReceived - previousTotalBytesReceived;
1121
+ const timeElapsed = timestamp - previousTimestamp;
1122
+ return `${((bytesReceived * 8) / timeElapsed).toFixed(2)} kbps`;
1007
1123
  };
1008
1124
 
1125
+ const CallStatsButton = () => (jsx(MenuToggle, { placement: "top-end", ToggleButton: ToggleMenuButton, children: jsx(CallStats, {}) }));
1126
+ const ToggleMenuButton = forwardRef(function ToggleMenuButton(props, ref) {
1127
+ const { t } = useI18n();
1128
+ const { caption, menuShown } = props;
1129
+ return (jsx(CompositeButton, { ref: ref, active: menuShown, caption: caption, title: caption || t('Statistics'), "data-testid": "stats-button", children: jsx(Icon, { icon: "stats" }) }));
1130
+ });
1131
+
1009
1132
  const ToggleAudioOutputButton = (props) => {
1010
1133
  const { t } = useI18n();
1011
- const { caption = t('Speakers'), Menu = DeviceSelectorAudioOutput } = props;
1012
- return (jsx(CompositeButton, { Menu: Menu, caption: caption, children: jsx(IconButton, { icon: "speaker" }) }));
1134
+ const { caption, Menu } = props;
1135
+ return (jsx(CompositeButton, { Menu: Menu, caption: caption, title: caption || t('Speakers'), "data-testid": "audio-output-button", children: jsx(Icon, { icon: "speaker" }) }));
1013
1136
  };
1014
1137
 
1015
1138
  const BlockedUserListing = ({ data }) => {
@@ -1031,146 +1154,76 @@ const CallParticipantListHeader = ({ onClose, }) => {
1031
1154
  const participants = useParticipants();
1032
1155
  const anonymousParticipantCount = useAnonymousParticipantCount();
1033
1156
  const { t } = useI18n();
1034
- return (jsxs("div", { className: "str-video__participant-list-header", children: [jsxs("div", { className: "str-video__participant-list-header__title", children: [t('Participants'), ' ', jsxs("span", { className: "str-video__participant-list-header__title-count", children: ["(", participants.length, ")"] }), anonymousParticipantCount > 0 && (jsx("span", { className: "str-video__participant-list-header__title-anonymous", children: t('Anonymous', { count: anonymousParticipantCount }) }))] }), jsx("button", { onClick: onClose, className: "str-video__participant-list-header__close-button", children: jsx("span", { className: "str-video__participant-list-header__close-button--icon" }) })] }));
1157
+ return (jsxs("div", { className: "str-video__participant-list-header", children: [jsxs("div", { className: "str-video__participant-list-header__title", children: [t('Participants'), ' ', jsxs("span", { className: "str-video__participant-list-header__title-count", children: ["[", participants.length, "]"] }), anonymousParticipantCount > 0 && (jsx("span", { className: "str-video__participant-list-header__title-anonymous", children: t('Anonymous', { count: anonymousParticipantCount }) }))] }), jsx(IconButton, { onClick: onClose, className: "str-video__participant-list-header__close-button", icon: "close" })] }));
1035
1158
  };
1036
1159
 
1037
- const CallParticipantListingItem = ({ participant, DisplayName = DefaultDisplayName, }) => {
1038
- const isAudioOn = participant.publishedTracks.includes(SfuModels.TrackType.AUDIO);
1039
- const isVideoOn = participant.publishedTracks.includes(SfuModels.TrackType.VIDEO);
1040
- const isPinned = !!participant.pin;
1041
- const { t } = useI18n();
1042
- return (jsxs("div", { className: "str-video__participant-listing-item", children: [jsx(DisplayName, { participant: participant }), jsxs("div", { className: "str-video__participant-listing-item__media-indicator-group", children: [jsx(MediaIndicator, { title: isAudioOn ? t('Microphone on') : t('Microphone off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isAudioOn ? 'mic' : 'mic-off'}`) }), jsx(MediaIndicator, { title: isVideoOn ? t('Camera on') : t('Camera off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isVideoOn ? 'camera' : 'camera-off'}`) }), isPinned && (jsx(MediaIndicator, { title: t('Pinned'), className: clsx('str-video__participant-listing-item__icon', 'str-video__participant-listing-item__icon-pinned') })), jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$2, children: jsx(ParticipantActionsContextMenu, { participant: participant }) })] })] }));
1043
- };
1044
- const MediaIndicator = (props) => (jsx(WithTooltip, { ...props }));
1045
- // todo: implement display device flag
1046
- const DefaultDisplayName = ({ participant }) => {
1047
- const connectedUser = useConnectedUser();
1048
- const { t } = useI18n();
1049
- const meFlag = participant.userId === connectedUser?.id ? t('Me') : '';
1050
- const nameOrId = participant.name || participant.userId || t('Unknown');
1051
- let displayName;
1052
- if (!participant.name) {
1053
- displayName = meFlag || nameOrId || t('Unknown');
1054
- }
1055
- else if (meFlag) {
1056
- displayName = `${nameOrId} (${meFlag})`;
1057
- }
1058
- else {
1059
- displayName = nameOrId;
1060
- }
1061
- return (jsx(WithTooltip, { className: "str-video__participant-listing-item__display-name", title: displayName, children: displayName }));
1062
- };
1063
- const ToggleButton$2 = forwardRef((props, ref) => {
1064
- return jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
1065
- });
1066
- const ParticipantActionsContextMenu = ({ participant, participantViewElement, videoElement, }) => {
1067
- const [fullscreenModeOn, setFullscreenModeOn] = useState(!!document.fullscreenElement);
1068
- const [pictureInPictureElement, setPictureInPictureElement] = useState(document.pictureInPictureElement);
1069
- const call = useCall();
1070
- const { t } = useI18n();
1071
- const { pin, publishedTracks, sessionId, userId } = participant;
1072
- const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
1073
- const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
1074
- const hasScreenShare = publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
1075
- const hasScreenShareAudio = publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE_AUDIO);
1076
- const blockUser = () => call?.blockUser(userId);
1077
- const muteAudio = () => call?.muteUser(userId, 'audio');
1078
- const muteVideo = () => call?.muteUser(userId, 'video');
1079
- const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
1080
- const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
1081
- const grantPermission = (permission) => () => {
1082
- call?.updateUserPermissions({
1083
- user_id: userId,
1084
- grant_permissions: [permission],
1085
- });
1086
- };
1087
- const revokePermission = (permission) => () => {
1088
- call?.updateUserPermissions({
1089
- user_id: userId,
1090
- revoke_permissions: [permission],
1091
- });
1092
- };
1093
- const toggleParticipantPinnedAt = () => {
1094
- if (pin) {
1095
- call?.unpin(sessionId);
1096
- }
1097
- else {
1098
- call?.pin(sessionId);
1099
- }
1100
- };
1101
- const pinForEveryone = () => {
1102
- call
1103
- ?.pinForEveryone({
1104
- user_id: userId,
1105
- session_id: sessionId,
1106
- })
1107
- .catch((err) => {
1108
- console.error(`Failed to pin participant ${userId}`, err);
1109
- });
1110
- };
1111
- const unpinForEveryone = () => {
1112
- call
1113
- ?.unpinForEveryone({
1114
- user_id: userId,
1115
- session_id: sessionId,
1116
- })
1117
- .catch((err) => {
1118
- console.error(`Failed to unpin participant ${userId}`, err);
1119
- });
1120
- };
1121
- const toggleFullscreenMode = () => {
1122
- if (!fullscreenModeOn) {
1123
- return participantViewElement
1124
- ?.requestFullscreen()
1125
- .then(() => setFullscreenModeOn(true))
1126
- .catch(console.error);
1127
- }
1128
- document
1129
- .exitFullscreen()
1130
- .catch(console.error)
1131
- .finally(() => setFullscreenModeOn(false));
1132
- };
1133
- useEffect(() => {
1134
- // handles the case when fullscreen mode is toggled externally,
1135
- // e.g., by pressing ESC key or some other keyboard shortcut
1136
- const handleFullscreenChange = () => {
1137
- setFullscreenModeOn(!!document.fullscreenElement);
1138
- };
1139
- document.addEventListener('fullscreenchange', handleFullscreenChange);
1140
- return () => {
1141
- document.removeEventListener('fullscreenchange', handleFullscreenChange);
1142
- };
1143
- }, []);
1160
+ const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
1161
+ const { refs, x, y, strategy } = useFloatingUIPreset({
1162
+ placement: tooltipPlacement,
1163
+ strategy: 'absolute',
1164
+ });
1144
1165
  useEffect(() => {
1145
- if (!videoElement)
1146
- return;
1147
- const handlePictureInPicture = () => {
1148
- setPictureInPictureElement(document.pictureInPictureElement);
1149
- };
1150
- videoElement.addEventListener('enterpictureinpicture', handlePictureInPicture);
1151
- videoElement.addEventListener('leavepictureinpicture', handlePictureInPicture);
1152
- return () => {
1153
- videoElement.removeEventListener('enterpictureinpicture', handlePictureInPicture);
1154
- videoElement.removeEventListener('leavepictureinpicture', handlePictureInPicture);
1155
- };
1156
- }, [videoElement]);
1157
- const togglePictureInPicture = () => {
1158
- if (videoElement && pictureInPictureElement !== videoElement) {
1159
- return videoElement
1160
- .requestPictureInPicture()
1161
- .catch(console.error);
1162
- }
1163
- document.exitPictureInPicture().catch(console.error);
1164
- };
1165
- return (jsxs(GenericMenu, { children: [jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPinnedAt, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxs(Restricted, { requiredGrants: [OwnCapability.PIN_FOR_EVERYONE], children: [jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsx(Restricted, { requiredGrants: [OwnCapability.BLOCK_USERS], children: jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxs(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], children: [jsxs(GenericMenuButtonItem, { disabled: !hasVideo, onClick: muteVideo, children: [jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] }), jsxs(GenericMenuButtonItem, { disabled: !hasScreenShare, onClick: muteScreenShare, children: [jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] }), jsxs(GenericMenuButtonItem, { disabled: !hasAudio, onClick: muteAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute audio')] }), jsxs(GenericMenuButtonItem, { disabled: !hasScreenShareAudio, onClick: muteScreenShareAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] })] }), participantViewElement && (jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
1166
- direction: fullscreenModeOn ? t('Leave') : t('Enter'),
1167
- }) })), videoElement && document.pictureInPictureEnabled && (jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
1168
- direction: pictureInPictureElement === videoElement
1169
- ? t('Leave')
1170
- : t('Enter'),
1171
- }) })), jsxs(Restricted, { requiredGrants: [OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
1166
+ refs.setReference(referenceElement);
1167
+ }, [referenceElement, refs]);
1168
+ if (!visible)
1169
+ return null;
1170
+ return (jsx("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
1171
+ position: strategy,
1172
+ top: y ?? 0,
1173
+ left: x ?? 0,
1174
+ overflowY: 'auto',
1175
+ }, children: children }));
1176
+ };
1177
+
1178
+ const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
1179
+ const [tooltipVisible, setTooltipVisible] = useState(false);
1180
+ const handleMouseEnter = useCallback((e) => {
1181
+ setTooltipVisible(true);
1182
+ onMouseEnter?.(e);
1183
+ }, [onMouseEnter]);
1184
+ const handleMouseLeave = useCallback((e) => {
1185
+ setTooltipVisible(false);
1186
+ onMouseLeave?.(e);
1187
+ }, [onMouseLeave]);
1188
+ return { handleMouseEnter, handleMouseLeave, tooltipVisible };
1172
1189
  };
1173
1190
 
1191
+ // todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
1192
+ const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, ...props }) => {
1193
+ const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
1194
+ const [tooltipAnchor, setTooltipAnchor] = useState(null);
1195
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
1196
+ };
1197
+
1198
+ const CallParticipantListingItem = ({ participant, DisplayName = DefaultDisplayName, }) => {
1199
+ const isAudioOn = participant.publishedTracks.includes(SfuModels.TrackType.AUDIO);
1200
+ const isVideoOn = participant.publishedTracks.includes(SfuModels.TrackType.VIDEO);
1201
+ const isPinned = !!participant.pin;
1202
+ const { t } = useI18n();
1203
+ return (jsxs("div", { className: "str-video__participant-listing-item", children: [jsx(Avatar, { name: participant.name, imageSrc: participant.image }), jsx(DisplayName, { participant: participant }), jsxs("div", { className: "str-video__participant-listing-item__media-indicator-group", children: [jsx(MediaIndicator, { title: isAudioOn ? t('Microphone on') : t('Microphone off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isAudioOn ? 'mic' : 'mic-off'}`) }), jsx(MediaIndicator, { title: isVideoOn ? t('Camera on') : t('Camera off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isVideoOn ? 'camera' : 'camera-off'}`) }), isPinned && (jsx(MediaIndicator, { title: t('Pinned'), className: clsx('str-video__participant-listing-item__icon', 'str-video__participant-listing-item__icon-pinned') })), jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$2, children: jsx(ParticipantViewContext.Provider, { value: { participant, trackType: 'none' }, children: jsx(ParticipantActionsContextMenu, {}) }) })] })] }));
1204
+ };
1205
+ const MediaIndicator = (props) => (jsx(WithTooltip, { ...props }));
1206
+ const DefaultDisplayName = ({ participant }) => {
1207
+ const connectedUser = useConnectedUser();
1208
+ const { t } = useI18n();
1209
+ const meFlag = participant.userId === connectedUser?.id ? t('Me') : '';
1210
+ const nameOrId = participant.name || participant.userId || t('Unknown');
1211
+ let displayName;
1212
+ if (!participant.name) {
1213
+ displayName = meFlag || nameOrId || t('Unknown');
1214
+ }
1215
+ else if (meFlag) {
1216
+ displayName = `${nameOrId} (${meFlag})`;
1217
+ }
1218
+ else {
1219
+ displayName = nameOrId;
1220
+ }
1221
+ return (jsx(WithTooltip, { className: "str-video__participant-listing-item__display-name", title: displayName, children: displayName }));
1222
+ };
1223
+ const ToggleButton$2 = forwardRef(function ToggleButton(props, ref) {
1224
+ return jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
1225
+ });
1226
+
1174
1227
  const CallParticipantListing = ({ data, }) => (jsx("div", { className: "str-video__participant-listing", children: data.map((participant) => (jsx(CallParticipantListingItem, { participant: participant }, participant.sessionId))) }));
1175
1228
 
1176
1229
  const EmptyParticipantSearchList = () => {
@@ -1211,8 +1264,11 @@ const useSearch = ({ debounceInterval, searchFn, searchQuery = '', }) => {
1211
1264
  const [searchResults, setSearchResults] = useState([]);
1212
1265
  const [searchQueryInProgress, setSearchQueryInProgress] = useState(false);
1213
1266
  useEffect(() => {
1214
- if (!searchQuery.length)
1215
- return setSearchResults([]);
1267
+ if (!searchQuery.length) {
1268
+ setSearchQueryInProgress(false);
1269
+ setSearchResults([]);
1270
+ return;
1271
+ }
1216
1272
  setSearchQueryInProgress(true);
1217
1273
  const timeout = setTimeout(async () => {
1218
1274
  try {
@@ -1245,14 +1301,14 @@ const CallParticipantsList = ({ onClose, activeUsersSearchFn, blockedUsersSearch
1245
1301
  const [searchQuery, setSearchQuery] = useState('');
1246
1302
  const [userListType, setUserListType] = useState('active');
1247
1303
  const exitSearch = useCallback(() => setSearchQuery(''), []);
1248
- return (jsxs("div", { className: "str-video__participant-list", children: [jsx(CallParticipantListHeader, { onClose: onClose }), jsx(SearchInput, { value: searchQuery, onChange: ({ currentTarget }) => setSearchQuery(currentTarget.value), exitSearch: exitSearch, isActive: !!searchQuery }), jsx(CallParticipantListContentHeader, { userListType: userListType, setUserListType: setUserListType }), jsxs("div", { className: "str-video__participant-list__content", children: [userListType === 'active' && (jsx(ActiveUsersSearchResults, { searchQuery: searchQuery, activeUsersSearchFn: activeUsersSearchFn, debounceSearchInterval: debounceSearchInterval })), userListType === 'blocked' && (jsx(BlockedUsersSearchResults, { searchQuery: searchQuery, blockedUsersSearchFn: blockedUsersSearchFn, debounceSearchInterval: debounceSearchInterval }))] }), jsx("div", { className: "str-video__participant-list__footer", children: jsx(CopyToClipboardButtonWithPopup, { Button: InviteLinkButton, copyValue: typeof window !== 'undefined' ? window.location.href : '' }) })] }));
1304
+ return (jsxs("div", { className: "str-video__participant-list", children: [jsx(CallParticipantListHeader, { onClose: onClose }), jsx(SearchInput, { value: searchQuery, onChange: ({ currentTarget }) => setSearchQuery(currentTarget.value), exitSearch: exitSearch, isActive: !!searchQuery }), jsx(CallParticipantListContentHeader, { userListType: userListType, setUserListType: setUserListType }), jsxs("div", { className: "str-video__participant-list__content", children: [userListType === 'active' && (jsx(ActiveUsersSearchResults, { searchQuery: searchQuery, activeUsersSearchFn: activeUsersSearchFn, debounceSearchInterval: debounceSearchInterval })), userListType === 'blocked' && (jsx(BlockedUsersSearchResults, { searchQuery: searchQuery, blockedUsersSearchFn: blockedUsersSearchFn, debounceSearchInterval: debounceSearchInterval }))] })] }));
1249
1305
  };
1250
1306
  const CallParticipantListContentHeader = ({ userListType, setUserListType, }) => {
1251
1307
  const call = useCall();
1252
1308
  const muteAll = () => {
1253
1309
  call?.muteAllUsers('audio');
1254
1310
  };
1255
- return (jsxs("div", { className: "str-video__participant-list__content-header", children: [jsxs("div", { className: "str-video__participant-list__content-header-title", children: [jsx("span", { children: UserListTypes[userListType] }), userListType === 'active' && (jsx(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], hasPermissionsOnly: true, children: jsx(TextButton, { onClick: muteAll, children: "Mute all" }) }))] }), jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$1, children: jsx(GenericMenu, { children: Object.keys(UserListTypes).map((lt) => (jsx(GenericMenuButtonItem, { "aria-selected": lt === userListType, onClick: () => setUserListType(lt), children: UserListTypes[lt] }, lt))) }) })] }));
1311
+ return (jsxs("div", { className: "str-video__participant-list__content-header", children: [jsx("div", { className: "str-video__participant-list__content-header-title", children: userListType === 'active' && (jsx(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], hasPermissionsOnly: true, children: jsx(TextButton, { onClick: muteAll, children: "Mute all" }) })) }), jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$1, children: jsx(GenericMenu, { children: Object.keys(UserListTypes).map((lt) => (jsx(GenericMenuButtonItem, { "aria-selected": lt === userListType, onClick: () => setUserListType(lt), children: UserListTypes[lt] }, lt))) }) })] }));
1256
1312
  };
1257
1313
  const ActiveUsersSearchResults = ({ searchQuery, activeUsersSearchFn: activeUsersSearchFnFromProps, debounceSearchInterval = DEFAULT_DEBOUNCE_SEARCH_INTERVAL, }) => {
1258
1314
  const { useParticipants } = useCallStateHooks();
@@ -1286,10 +1342,9 @@ const BlockedUsersSearchResults = ({ blockedUsersSearchFn: blockedUsersSearchFnF
1286
1342
  });
1287
1343
  return (jsx(SearchResults, { EmptySearchResultComponent: EmptyParticipantSearchList, LoadingIndicator: LoadingIndicator, searchQueryInProgress: searchQueryInProgress, searchResults: searchQuery ? searchResults : blockedUsers, SearchResultList: BlockedUserListing }));
1288
1344
  };
1289
- const ToggleButton$1 = forwardRef((props, ref) => {
1345
+ const ToggleButton$1 = forwardRef(function ToggleButton(props, ref) {
1290
1346
  return jsx(IconButton, { enabled: props.menuShown, icon: "filter", ref: ref });
1291
1347
  });
1292
- const InviteLinkButton = forwardRef(({ className, ...props }, ref) => (jsxs("button", { ...props, className: clsx('str-video__invite-link-button', className), ref: ref, children: [jsx("div", { className: "str-video__invite-participant-icon" }), jsx("div", { className: "str-video__invite-link-button__text", children: "Invite Link" })] })));
1293
1348
 
1294
1349
  const CallPreview = (props) => {
1295
1350
  const { className, style } = props;
@@ -1309,15 +1364,17 @@ const CallPreview = (props) => {
1309
1364
  };
1310
1365
 
1311
1366
  const CallRecordingListHeader = ({ callRecordings, onRefresh, }) => {
1312
- return (jsxs("div", { className: "str-video__call-recording-list__header", children: [jsxs("div", { className: "str-video__call-recording-list__title", children: [jsx("span", { children: "Call Recordings" }), callRecordings.length ? jsxs("span", { children: ["(", callRecordings.length, ")"] }) : null] }), jsx(IconButton, { icon: "refresh", title: "Refresh", onClick: onRefresh })] }));
1367
+ const { t } = useI18n();
1368
+ return (jsxs("div", { className: "str-video__call-recording-list__header", children: [jsxs("div", { className: "str-video__call-recording-list__title", children: [jsx("span", { children: t('Call Recordings') }), callRecordings.length ? jsxs("span", { children: ["(", callRecordings.length, ")"] }) : null] }), onRefresh && (jsx(IconButton, { icon: "refresh", title: t('Refresh'), onClick: onRefresh }))] }));
1313
1369
  };
1314
1370
 
1371
+ const dateFormat = (date) => {
1372
+ const format = new Date(date);
1373
+ return format.toTimeString().split(' ')[0];
1374
+ };
1315
1375
  const CallRecordingListItem = ({ recording, }) => {
1316
- return (jsxs("div", { className: "str-video__call-recording-list-item", children: [jsx("div", { className: "str-video__call-recording-list-item__info", children: jsx("div", { className: "str-video__call-recording-list-item__created", children: new Date(recording.end_time).toLocaleString() }) }), jsxs("div", { className: "str-video__call-recording-list-item__actions", children: [jsx("a", { className: clsx('str-video__call-recording-list-item__action-button', 'str-video__call-recording-list-item__action-button--download'), role: "button", href: recording.url, download: recording.filename, title: "Download the recording", children: jsx("span", { className: clsx('str-video__call-recording-list-item__action-button-icon', 'str-video__download-button--icon') }) }), jsx(CopyToClipboardButtonWithPopup, { Button: CopyUrlButton, copyValue: recording.url })] })] }));
1376
+ return (jsxs("li", { className: "str-video__call-recording-list__item", children: [jsx("div", { className: "str-video__call-recording-list__table-cell str-video__call-recording-list__filename", children: recording.filename }), jsx("div", { className: "str-video__call-recording-list__table-cell str-video__call-recording-list__time", children: dateFormat(recording.start_time) }), jsx("div", { className: "str-video__call-recording-list__table-cell str-video__call-recording-list__time", children: dateFormat(recording.end_time) }), jsx("div", { className: "str-video__call-recording-list__table-cell str-video__call-recording-list__download", children: jsx("a", { className: clsx('str-video__call-recording-list-item__action-button', 'str-video__call-recording-list-item__action-button--download'), role: "button", href: recording.url, download: recording.filename, title: "Download the recording", children: jsx(Icon, { icon: "download" }) }) })] }));
1317
1377
  };
1318
- const CopyUrlButton = forwardRef((props, ref) => {
1319
- return (jsx("button", { ...props, className: clsx('str-video__call-recording-list-item__action-button', 'str-video__call-recording-list-item__action-button--copy-link'), ref: ref, title: "Copy the recording link", children: jsx("span", { className: clsx('str-video__call-recording-list-item__action-button-icon', 'str-video__copy-button--icon') }) }));
1320
- });
1321
1378
 
1322
1379
  const EmptyCallRecordingListing = () => {
1323
1380
  return (jsxs("div", { className: "str-video__call-recording-list__listing str-video__call-recording-list__listing--empty", children: [jsx("div", { className: "str-video__call-recording-list__listing--icon-empty" }), jsx("p", { className: "str-video__call-recording-list__listing--text-empty", children: "No recordings available" })] }));
@@ -1328,7 +1385,7 @@ const LoadingCallRecordingListing = ({ callRecordings, }) => {
1328
1385
  };
1329
1386
 
1330
1387
  const CallRecordingList = ({ callRecordings, CallRecordingListHeader: CallRecordingListHeader$1 = CallRecordingListHeader, CallRecordingListItem: CallRecordingListItem$1 = CallRecordingListItem, EmptyCallRecordingList = EmptyCallRecordingListing, loading, LoadingCallRecordingList = LoadingCallRecordingListing, onRefresh, }) => {
1331
- return (jsxs("div", { className: "str-video__call-recording-list", children: [jsx(CallRecordingListHeader$1, { callRecordings: callRecordings, onRefresh: onRefresh }), jsx("div", { className: "str-video__call-recording-list__listing", children: loading ? (jsx(LoadingCallRecordingList, { callRecordings: callRecordings })) : callRecordings.length ? (callRecordings.map((recording) => (jsx(CallRecordingListItem$1, { recording: recording }, recording.filename)))) : (jsx(EmptyCallRecordingList, {})) })] }));
1388
+ return (jsxs("div", { className: "str-video__call-recording-list", children: [jsx(CallRecordingListHeader$1, { callRecordings: callRecordings, onRefresh: onRefresh }), jsx("div", { className: "str-video__call-recording-list__listing", children: loading ? (jsx(LoadingCallRecordingList, { callRecordings: callRecordings })) : callRecordings.length ? (jsxs(Fragment, { children: [jsx("ul", { className: "str-video__call-recording-list__list", children: jsxs("li", { className: "str-video__call-recording-list__item", children: [jsx("div", { className: "str-video__call-recording-list__filename", children: "Name" }), jsx("div", { className: "str-video__call-recording-list__time", children: "Start time" }), jsx("div", { className: "str-video__call-recording-list__time", children: "End time" }), jsx("div", { className: "str-video__call-recording-list__download" })] }) }), jsx("ul", { className: "str-video__call-recording-list__list", children: callRecordings.map((recording) => (jsx(CallRecordingListItem$1, { recording: recording }, recording.filename))) })] })) : (jsx(EmptyCallRecordingList, {})) })] }));
1332
1389
  };
1333
1390
 
1334
1391
  const RingingCallControls = () => {
@@ -1394,7 +1451,7 @@ const byNameOrId = (a, b) => {
1394
1451
  };
1395
1452
  const PermissionRequests = () => {
1396
1453
  const call = useCall();
1397
- const { useLocalParticipant } = useCallStateHooks();
1454
+ const { useLocalParticipant, useHasPermissions } = useCallStateHooks();
1398
1455
  const localParticipant = useLocalParticipant();
1399
1456
  const [expanded, setExpanded] = useState(false);
1400
1457
  const [permissionRequests, setPermissionRequests] = useState([]);
@@ -1403,16 +1460,11 @@ const PermissionRequests = () => {
1403
1460
  useEffect(() => {
1404
1461
  if (!call || !canUpdateCallPermissions)
1405
1462
  return;
1406
- const unsubscribe = call.on('call.permission_request', (event) => {
1407
- if (event.type !== 'call.permission_request')
1408
- return;
1463
+ return call.on('call.permission_request', (event) => {
1409
1464
  if (event.user.id !== localUserId) {
1410
1465
  setPermissionRequests((requests) => [...requests, event].sort((a, b) => byNameOrId(a.user, b.user)));
1411
1466
  }
1412
1467
  });
1413
- return () => {
1414
- unsubscribe();
1415
- };
1416
1468
  }, [call, canUpdateCallPermissions, localUserId]);
1417
1469
  const handleUpdatePermission = (request, type) => {
1418
1470
  return async () => {
@@ -1444,7 +1496,7 @@ const PermissionRequests = () => {
1444
1496
  overflowY: 'auto',
1445
1497
  }, permissionRequests: permissionRequests, handleUpdatePermission: handleUpdatePermission }))] }));
1446
1498
  };
1447
- const PermissionRequestList = forwardRef((props, ref) => {
1499
+ const PermissionRequestList = forwardRef(function PermissionRequestList(props, ref) {
1448
1500
  const { permissionRequests, handleUpdatePermission, ...rest } = props;
1449
1501
  const { t } = useI18n();
1450
1502
  return (jsx("div", { className: "str-video__permission-requests-list", ref: ref, ...rest, children: permissionRequests.map((request, reqIndex) => {
@@ -1480,6 +1532,124 @@ const StreamTheme = ({ as: Component = 'div', className, children, ...props }) =
1480
1532
  return (jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
1481
1533
  };
1482
1534
 
1535
+ const DefaultVideoPlaceholder = forwardRef(function DefaultVideoPlaceholder({ participant, style }, ref) {
1536
+ const { t } = useI18n();
1537
+ const [error, setError] = useState(false);
1538
+ const name = participant.name || participant.userId;
1539
+ return (jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
1540
+ (name ? (jsx(InitialsFallback, { name: name })) : (jsx("div", { className: "str-video__video-placeholder__no-video-label", children: t('Video is disabled') }))), participant.image && !error && (jsx("img", { onError: () => setError(true), alt: "video-placeholder", className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
1541
+ });
1542
+ const InitialsFallback = (props) => {
1543
+ const { name } = props;
1544
+ const initials = name
1545
+ .split(' ')
1546
+ .slice(0, 2)
1547
+ .map((n) => n[0])
1548
+ .join('');
1549
+ return (jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: initials }));
1550
+ };
1551
+
1552
+ const Video$1 = ({ trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
1553
+ const { sessionId, videoStream, screenShareStream, publishedTracks, viewportVisibilityState, isLocalParticipant, userId, } = participant;
1554
+ const call = useCall();
1555
+ const [videoElement, setVideoElement] = useState(null);
1556
+ // start with true, will flip once the video starts playing
1557
+ const [isVideoPaused, setIsVideoPaused] = useState(true);
1558
+ const [isWideMode, setIsWideMode] = useState(true);
1559
+ const stream = trackType === 'videoTrack'
1560
+ ? videoStream
1561
+ : trackType === 'screenShareTrack'
1562
+ ? screenShareStream
1563
+ : undefined;
1564
+ useLayoutEffect(() => {
1565
+ if (!call || !videoElement || trackType === 'none')
1566
+ return;
1567
+ const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
1568
+ return () => {
1569
+ cleanup?.();
1570
+ };
1571
+ }, [call, trackType, sessionId, videoElement]);
1572
+ useEffect(() => {
1573
+ if (!stream || !videoElement)
1574
+ return;
1575
+ const [track] = stream.getVideoTracks();
1576
+ if (!track)
1577
+ return;
1578
+ const handlePlayPause = () => {
1579
+ setIsVideoPaused(videoElement.paused);
1580
+ const { width = 0, height = 0 } = track.getSettings();
1581
+ setIsWideMode(width >= height);
1582
+ };
1583
+ // playback may have started before we had a chance to
1584
+ // attach the 'play/pause' event listener, so we set the state
1585
+ // here to make sure it's in sync
1586
+ setIsVideoPaused(videoElement.paused);
1587
+ videoElement.addEventListener('play', handlePlayPause);
1588
+ videoElement.addEventListener('pause', handlePlayPause);
1589
+ track.addEventListener('unmute', handlePlayPause);
1590
+ return () => {
1591
+ videoElement.removeEventListener('play', handlePlayPause);
1592
+ videoElement.removeEventListener('pause', handlePlayPause);
1593
+ track.removeEventListener('unmute', handlePlayPause);
1594
+ // reset the 'pause' state once we unmount the video element
1595
+ setIsVideoPaused(true);
1596
+ };
1597
+ }, [stream, videoElement]);
1598
+ if (!call)
1599
+ return null;
1600
+ const isPublishingTrack = trackType === 'videoTrack'
1601
+ ? publishedTracks.includes(SfuModels.TrackType.VIDEO)
1602
+ : trackType === 'screenShareTrack'
1603
+ ? publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE)
1604
+ : false;
1605
+ const isInvisible = trackType === 'none' ||
1606
+ viewportVisibilityState?.[trackType] === VisibilityState.INVISIBLE;
1607
+ const hasNoVideoOrInvisible = !isPublishingTrack || isInvisible;
1608
+ const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
1609
+ const isScreenShareTrack = trackType === 'screenShareTrack';
1610
+ return (jsxs(Fragment, { children: [!hasNoVideoOrInvisible && (jsx("video", { ...rest, className: clsx('str-video__video', className, {
1611
+ 'str-video__video--not-playing': isVideoPaused,
1612
+ 'str-video__video--tall': !isWideMode,
1613
+ 'str-video__video--mirror': mirrorVideo,
1614
+ 'str-video__video--screen-share': isScreenShareTrack,
1615
+ }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
1616
+ setVideoElement(element);
1617
+ refs?.setVideoElement?.(element);
1618
+ } })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
1619
+ };
1620
+
1621
+ /**
1622
+ * @description Extends video element with `stream` property
1623
+ * (`srcObject`) to reactively handle stream changes
1624
+ */
1625
+ const BaseVideo = forwardRef(function BaseVideo({ stream, ...rest }, ref) {
1626
+ const [videoElement, setVideoElement] = useState(null);
1627
+ useEffect(() => {
1628
+ if (!videoElement || !stream)
1629
+ return;
1630
+ if (stream === videoElement.srcObject)
1631
+ return;
1632
+ videoElement.srcObject = stream;
1633
+ if (Browsers.isSafari() || Browsers.isFirefox()) {
1634
+ // Firefox and Safari have some timing issue
1635
+ setTimeout(() => {
1636
+ videoElement.srcObject = stream;
1637
+ videoElement.play().catch((e) => {
1638
+ console.error(`Failed to play stream`, e);
1639
+ });
1640
+ }, 0);
1641
+ }
1642
+ return () => {
1643
+ videoElement.pause();
1644
+ videoElement.srcObject = null;
1645
+ };
1646
+ }, [stream, videoElement]);
1647
+ return (jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
1648
+ applyElementToRef(ref, element);
1649
+ setVideoElement(element);
1650
+ } }));
1651
+ });
1652
+
1483
1653
  const DefaultDisabledVideoPreview = () => {
1484
1654
  const { t } = useI18n();
1485
1655
  return (jsx("div", { className: "str_video__video-preview__disabled-video-preview", children: t('Video is disabled') }));
@@ -1508,10 +1678,124 @@ const VideoPreview = ({ className, mirror = true, DisabledVideoPreview = Default
1508
1678
  return (jsx("div", { className: clsx('str-video__video-preview-container', className), children: contents }));
1509
1679
  };
1510
1680
 
1511
- const ParticipantViewContext = createContext(undefined);
1512
- const useParticipantViewContext = () => useContext(ParticipantViewContext);
1681
+ const ParticipantActionsContextMenu = () => {
1682
+ const { participant, participantViewElement, videoElement } = useParticipantViewContext();
1683
+ const [fullscreenModeOn, setFullscreenModeOn] = useState(!!document.fullscreenElement);
1684
+ const [pictureInPictureElement, setPictureInPictureElement] = useState(document.pictureInPictureElement);
1685
+ const call = useCall();
1686
+ const { t } = useI18n();
1687
+ const { pin, publishedTracks, sessionId, userId } = participant;
1688
+ const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
1689
+ const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
1690
+ const hasScreenShare = publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
1691
+ const hasScreenShareAudio = publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE_AUDIO);
1692
+ const blockUser = () => call?.blockUser(userId);
1693
+ const muteAudio = () => call?.muteUser(userId, 'audio');
1694
+ const muteVideo = () => call?.muteUser(userId, 'video');
1695
+ const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
1696
+ const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
1697
+ const grantPermission = (permission) => () => {
1698
+ call?.updateUserPermissions({
1699
+ user_id: userId,
1700
+ grant_permissions: [permission],
1701
+ });
1702
+ };
1703
+ const revokePermission = (permission) => () => {
1704
+ call?.updateUserPermissions({
1705
+ user_id: userId,
1706
+ revoke_permissions: [permission],
1707
+ });
1708
+ };
1709
+ const toggleParticipantPin = () => {
1710
+ if (pin) {
1711
+ call?.unpin(sessionId);
1712
+ }
1713
+ else {
1714
+ call?.pin(sessionId);
1715
+ }
1716
+ };
1717
+ const pinForEveryone = () => {
1718
+ call
1719
+ ?.pinForEveryone({
1720
+ user_id: userId,
1721
+ session_id: sessionId,
1722
+ })
1723
+ .catch((err) => {
1724
+ console.error(`Failed to pin participant ${userId}`, err);
1725
+ });
1726
+ };
1727
+ const unpinForEveryone = () => {
1728
+ call
1729
+ ?.unpinForEveryone({
1730
+ user_id: userId,
1731
+ session_id: sessionId,
1732
+ })
1733
+ .catch((err) => {
1734
+ console.error(`Failed to unpin participant ${userId}`, err);
1735
+ });
1736
+ };
1737
+ const toggleFullscreenMode = () => {
1738
+ if (!fullscreenModeOn) {
1739
+ return participantViewElement?.requestFullscreen().catch(console.error);
1740
+ }
1741
+ return document.exitFullscreen().catch(console.error);
1742
+ };
1743
+ useEffect(() => {
1744
+ // handles the case when fullscreen mode is toggled externally,
1745
+ // e.g., by pressing ESC key or some other keyboard shortcut
1746
+ const handleFullscreenChange = () => {
1747
+ setFullscreenModeOn(!!document.fullscreenElement);
1748
+ };
1749
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
1750
+ return () => {
1751
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
1752
+ };
1753
+ }, []);
1754
+ useEffect(() => {
1755
+ if (!videoElement)
1756
+ return;
1757
+ const handlePiP = () => {
1758
+ setPictureInPictureElement(document.pictureInPictureElement);
1759
+ };
1760
+ videoElement.addEventListener('enterpictureinpicture', handlePiP);
1761
+ videoElement.addEventListener('leavepictureinpicture', handlePiP);
1762
+ return () => {
1763
+ videoElement.removeEventListener('enterpictureinpicture', handlePiP);
1764
+ videoElement.removeEventListener('leavepictureinpicture', handlePiP);
1765
+ };
1766
+ }, [videoElement]);
1767
+ const togglePictureInPicture = () => {
1768
+ if (videoElement && pictureInPictureElement !== videoElement) {
1769
+ return videoElement
1770
+ .requestPictureInPicture()
1771
+ .catch(console.error);
1772
+ }
1773
+ return document.exitPictureInPicture().catch(console.error);
1774
+ };
1775
+ const { close } = useMenuContext() || {};
1776
+ return (jsxs(GenericMenu, { onItemClick: close, children: [jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxs(Restricted, { requiredGrants: [OwnCapability.PIN_FOR_EVERYONE], children: [jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsx(Restricted, { requiredGrants: [OwnCapability.BLOCK_USERS], children: jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxs(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], children: [hasVideo && (jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShare && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudio && (jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudio && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
1777
+ direction: fullscreenModeOn ? t('Leave') : t('Enter'),
1778
+ }) })), videoElement && document.pictureInPictureEnabled && (jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
1779
+ direction: pictureInPictureElement === videoElement
1780
+ ? t('Leave')
1781
+ : t('Enter'),
1782
+ }) })), jsxs(Restricted, { requiredGrants: [OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
1783
+ };
1784
+
1785
+ const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
1786
+ const call = useCall();
1787
+ const manager = propsDynascaleManager ?? call?.dynascaleManager;
1788
+ useEffect(() => {
1789
+ if (!trackedElement || !manager || !call || trackType === 'none')
1790
+ return;
1791
+ const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
1792
+ return () => {
1793
+ unobserve();
1794
+ };
1795
+ }, [trackedElement, manager, call, sessionId, trackType]);
1796
+ };
1513
1797
 
1514
- const ToggleButton = forwardRef((props, ref) => {
1798
+ const ToggleButton = forwardRef(function ToggleButton(props, ref) {
1515
1799
  return jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
1516
1800
  });
1517
1801
  const DefaultScreenShareOverlay = () => {
@@ -1522,8 +1806,8 @@ const DefaultScreenShareOverlay = () => {
1522
1806
  };
1523
1807
  return (jsxs("div", { className: "str-video__screen-share-overlay", children: [jsx(Icon, { icon: "screen-share-off" }), jsx("span", { className: "str-video__screen-share-overlay__title", children: t('You are presenting your screen') }), jsxs("button", { onClick: stopScreenShare, className: "str-video__screen-share-overlay__button", children: [jsx(Icon, { icon: "close" }), " ", t('Stop Screen Sharing')] })] }));
1524
1808
  };
1525
- const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'bottom-end', showMenuButton = true, }) => {
1526
- const { participant, participantViewElement, trackType, videoElement } = useParticipantViewContext();
1809
+ const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'bottom-start', showMenuButton = true, ParticipantActionsContextMenu: ParticipantActionsContextMenu$1 = ParticipantActionsContextMenu, }) => {
1810
+ const { participant, trackType } = useParticipantViewContext();
1527
1811
  const { publishedTracks } = participant;
1528
1812
  const hasScreenShare = publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
1529
1813
  if (participant.isLocalParticipant &&
@@ -1531,11 +1815,11 @@ const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'b
1531
1815
  trackType === 'screenShareTrack') {
1532
1816
  return (jsxs(Fragment, { children: [jsx(DefaultScreenShareOverlay, {}), jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
1533
1817
  }
1534
- return (jsxs(Fragment, { children: [showMenuButton && (jsx(MenuToggle, { strategy: "fixed", placement: menuPlacement, ToggleButton: ToggleButton, children: jsx(ParticipantActionsContextMenu, { participantViewElement: participantViewElement, participant: participant, videoElement: videoElement }) })), jsx(Reaction, { participant: participant }), jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
1818
+ return (jsxs(Fragment, { children: [showMenuButton && (jsx(MenuToggle, { strategy: "fixed", placement: menuPlacement, ToggleButton: ToggleButton, children: jsx(ParticipantActionsContextMenu$1, {}) })), jsx(Reaction, { participant: participant }), jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
1535
1819
  };
1536
1820
  const ParticipantDetails = ({ indicatorsVisible = true, }) => {
1537
1821
  const { participant } = useParticipantViewContext();
1538
- const { isDominantSpeaker, isLocalParticipant, connectionQuality, publishedTracks, pin, sessionId, name, userId, } = participant;
1822
+ const { isLocalParticipant, connectionQuality, publishedTracks, pin, sessionId, name, userId, } = participant;
1539
1823
  const call = useCall();
1540
1824
  const { t } = useI18n();
1541
1825
  const connectionQualityAsString = !!connectionQuality &&
@@ -1543,13 +1827,18 @@ const ParticipantDetails = ({ indicatorsVisible = true, }) => {
1543
1827
  const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
1544
1828
  const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
1545
1829
  const canUnpin = !!pin && pin.isLocalPin;
1546
- return (jsx("div", { className: "str-video__participant-details", children: jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && isDominantSpeaker && (jsx("span", { className: "str-video__participant-details__name--dominant_speaker", title: t('Dominant speaker') })), indicatorsVisible && (jsx(Notification, { isVisible: isLocalParticipant &&
1547
- connectionQuality === SfuModels.ConnectionQuality.POOR, message: t('Poor connection quality'), children: connectionQualityAsString && (jsx("span", { className: clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) })), indicatorsVisible && !hasAudio && (jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideo && (jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && canUnpin && (
1548
- // TODO: remove this monstrosity once we have a proper design
1549
- jsx("span", { title: t('Unpin'), onClick: () => call?.unpin(sessionId), style: { cursor: 'pointer' }, className: "str-video__participant-details__name--pinned" }))] }) }));
1830
+ return (jsxs(Fragment, { children: [jsx("div", { className: "str-video__participant-details", children: jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && !hasAudio && (jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideo && (jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && canUnpin && (
1831
+ // TODO: remove this monstrosity once we have a proper design
1832
+ jsx("span", { title: t('Unpin'), onClick: () => call?.unpin(sessionId), className: "str-video__participant-details__name--pinned" })), indicatorsVisible && jsx(SpeechIndicator, {})] }) }), indicatorsVisible && (jsx(Notification, { isVisible: isLocalParticipant &&
1833
+ connectionQuality === SfuModels.ConnectionQuality.POOR, message: t('Poor connection quality'), children: connectionQualityAsString && (jsx("span", { className: clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) }))] }));
1834
+ };
1835
+ const SpeechIndicator = () => {
1836
+ const { participant } = useParticipantViewContext();
1837
+ const { isSpeaking, isDominantSpeaker } = participant;
1838
+ return (jsxs("span", { className: clsx('str-video__speech-indicator', isSpeaking && 'str-video__speech-indicator--speaking', isDominantSpeaker && 'str-video__speech-indicator--dominant'), children: [jsx("span", { className: "str-video__speech-indicator__bar" }), jsx("span", { className: "str-video__speech-indicator__bar" }), jsx("span", { className: "str-video__speech-indicator__bar" })] }));
1550
1839
  };
1551
1840
 
1552
- const ParticipantView = forwardRef(({ participant, trackType = 'videoTrack', muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) => {
1841
+ const ParticipantView = forwardRef(function ParticipantView({ participant, trackType = 'videoTrack', muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) {
1553
1842
  const { isLocalParticipant, isSpeaking, isDominantSpeaker, publishedTracks, sessionId, } = participant;
1554
1843
  const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
1555
1844
  const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
@@ -1602,11 +1891,13 @@ var Speakers = "Speakers";
1602
1891
  var Video = "Video";
1603
1892
  var Live = "Live";
1604
1893
  var Reactions = "Reactions";
1894
+ var Statistics = "Statistics";
1605
1895
  var Invite = "Invite";
1606
1896
  var Join = "Join";
1607
1897
  var You = "You";
1608
1898
  var Me = "Me";
1609
1899
  var Unknown = "Unknown";
1900
+ var Refresh = "Refresh";
1610
1901
  var Allow = "Allow";
1611
1902
  var Revoke = "Revoke";
1612
1903
  var Dismiss = "Dismiss";
@@ -1642,6 +1933,7 @@ var en = {
1642
1933
  "Waiting for recording to start...": "Waiting for recording to start...",
1643
1934
  "Record call": "Record call",
1644
1935
  Reactions: Reactions,
1936
+ Statistics: Statistics,
1645
1937
  "You can now share your screen.": "You can now share your screen.",
1646
1938
  "Awaiting for an approval to share screen.": "Awaiting for an approval to share screen.",
1647
1939
  "You can no longer share your screen.": "You can no longer share your screen.",
@@ -1655,6 +1947,12 @@ var en = {
1655
1947
  Me: Me,
1656
1948
  Unknown: Unknown,
1657
1949
  "Toggle device menu": "Toggle device menu",
1950
+ "Call Recordings": "Call Recordings",
1951
+ Refresh: Refresh,
1952
+ "Check your browser video permissions": "Check your browser video permissions",
1953
+ "Video publishing is disabled by the system": "Video publishing is disabled by the system",
1954
+ "You have no permission to share your video": "You have no permission to share your video",
1955
+ "You have no permission to share your audio": "You have no permission to share your audio",
1658
1956
  "You are presenting your screen": "You are presenting your screen",
1659
1957
  "Stop Screen Sharing": "Stop Screen Sharing",
1660
1958
  Allow: Allow,
@@ -1684,6 +1982,8 @@ var en = {
1684
1982
  "Disable screen sharing": "Disable screen sharing",
1685
1983
  Enter: Enter,
1686
1984
  Leave: Leave,
1985
+ "Leave call": "Leave call",
1986
+ "End call for all": "End call for all",
1687
1987
  "{{ direction }} fullscreen": "{{ direction }} fullscreen",
1688
1988
  "{{ direction }} picture-in-picture": "{{ direction }} picture-in-picture",
1689
1989
  "Dominant Speaker": "Dominant Speaker",
@@ -1977,7 +2277,7 @@ const VerticalScrollButtons = ({ scrollWrapper, }) => {
1977
2277
  };
1978
2278
  const hasScreenShare = (p) => !!p?.publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
1979
2279
 
1980
- const [major, minor, patch] = ("0.4.26" ).split('.');
2280
+ const [major, minor, patch] = ("0.5.0" ).split('.');
1981
2281
  setSdkInfo({
1982
2282
  type: SfuModels.SdkType.REACT,
1983
2283
  major,
@@ -1985,5 +2285,5 @@ setSdkInfo({
1985
2285
  patch,
1986
2286
  });
1987
2287
 
1988
- export { AcceptCallButton, Audio, Avatar, AvatarFallback, BaseVideo, CallControls, CallParticipantListing, CallParticipantListingItem, CallParticipantsList, CallPreview, CallRecordingList, CallRecordingListHeader, CallRecordingListItem, CallStats, CallStatsButton, CallStatsLatencyChart, CancelCallButton, CompositeButton, CopyToClipboardButton, CopyToClipboardButtonWithPopup, DefaultParticipantViewUI, DefaultReactionsMenu, DefaultScreenShareOverlay, DefaultVideoPlaceholder, DeviceSelector, DeviceSelectorAudioInput, DeviceSelectorAudioOutput, DeviceSelectorVideo, DeviceSettings, EmptyCallRecordingListing, GenericMenu, GenericMenuButtonItem, Icon, IconButton, LivestreamLayout, LoadingCallRecordingListing, LoadingIndicator, MenuToggle, Notification, PaginatedGridLayout, ParticipantActionsContextMenu, ParticipantDetails, ParticipantView, ParticipantViewContext, ParticipantsAudio, PermissionNotification, PermissionRequestList, PermissionRequests, ReactionsButton, RecordCallButton, RingingCall, RingingCallControls, ScreenShareButton, SearchInput, SearchResults, SpeakerLayout, SpeakingWhileMutedNotification, StatCard, StreamCall, StreamTheme, StreamVideo, TextButton, ToggleAudioOutputButton, ToggleAudioPreviewButton, ToggleAudioPublishingButton, ToggleVideoPreviewButton, ToggleVideoPublishingButton, Tooltip, Video$1 as Video, VideoPreview, WithTooltip, defaultReactions, translations, useHorizontalScrollPosition, useParticipantViewContext, usePersistedDevicePreferences, useRequestPermission, useTrackElementVisibility, useVerticalScrollPosition };
2288
+ export { AcceptCallButton, Audio, Avatar, AvatarFallback, BaseVideo, CallControls, CallParticipantListing, CallParticipantListingItem, CallParticipantsList, CallPreview, CallRecordingList, CallRecordingListHeader, CallRecordingListItem, CallStats, CallStatsButton, CallStatsLatencyChart, CancelCallButton, CancelCallConfirmButton, CompositeButton, DefaultParticipantViewUI, DefaultReactionsMenu, DefaultScreenShareOverlay, DefaultVideoPlaceholder, DeviceSelector, DeviceSelectorAudioInput, DeviceSelectorAudioOutput, DeviceSelectorVideo, DeviceSettings, DropDownSelect, DropDownSelectOption, EmptyCallRecordingListing, GenericMenu, GenericMenuButtonItem, Icon, IconButton, LivestreamLayout, LoadingCallRecordingListing, LoadingIndicator, MenuToggle, MenuVisualType, Notification, PaginatedGridLayout, ParticipantActionsContextMenu, ParticipantDetails, ParticipantView, ParticipantViewContext, ParticipantsAudio, PermissionNotification, PermissionRequestList, PermissionRequests, ReactionsButton, RecordCallButton, RecordCallConfirmationButton, RecordingInProgressNotification, RingingCall, RingingCallControls, ScreenShareButton, SearchInput, SearchResults, SpeakerLayout, SpeakingWhileMutedNotification, SpeechIndicator, StatCard, StatCardExplanation, StatsTag, Statuses, StreamCall, StreamTheme, StreamVideo, TextButton, ToggleAudioOutputButton, ToggleAudioPreviewButton, ToggleAudioPublishingButton, ToggleVideoPreviewButton, ToggleVideoPublishingButton, Tooltip, Video$1 as Video, VideoPreview, WithTooltip, defaultReactions, translations, useHorizontalScrollPosition, useMenuContext, useParticipantViewContext, usePersistedDevicePreferences, useRequestPermission, useTrackElementVisibility, useVerticalScrollPosition };
1989
2289
  //# sourceMappingURL=index.es.js.map