@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.
- package/CHANGELOG.md +297 -238
- package/README.md +5 -5
- package/dist/css/styles.css +952 -481
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +946 -639
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +939 -639
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/Button/CompositeButton.d.ts +9 -11
- package/dist/src/components/Button/index.d.ts +0 -1
- package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
- package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
- package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
- package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
- package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
- package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
- package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
- package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
- package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
- package/dist/src/components/CallStats/CallStats.d.ts +25 -2
- package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
- package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
- package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
- package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
- package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
- package/dist/src/components/DropdownSelect/index.d.ts +1 -0
- package/dist/src/components/Icon/Icon.d.ts +2 -1
- package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
- package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
- package/dist/src/components/Notification/Notification.d.ts +1 -0
- package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
- package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
- package/dist/src/components/Notification/index.d.ts +1 -0
- package/dist/src/components/index.d.ts +2 -0
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
- package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
- package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
- package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
- package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
- package/dist/src/translations/index.d.ts +9 -0
- package/package.json +7 -9
- package/src/components/Button/CompositeButton.tsx +78 -26
- package/src/components/Button/IconButton.tsx +22 -21
- package/src/components/Button/index.ts +0 -1
- package/src/components/CallControls/AcceptCallButton.tsx +1 -0
- package/src/components/CallControls/CallControls.tsx +2 -2
- package/src/components/CallControls/CallStatsButton.tsx +24 -7
- package/src/components/CallControls/CancelCallButton.tsx +102 -3
- package/src/components/CallControls/ReactionsButton.tsx +37 -17
- package/src/components/CallControls/RecordCallButton.tsx +131 -21
- package/src/components/CallControls/ScreenShareButton.tsx +29 -15
- package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
- package/src/components/CallControls/ToggleAudioOutputButton.tsx +14 -10
- package/src/components/CallControls/ToggleVideoButton.tsx +83 -33
- package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
- package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
- package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
- package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
- package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
- package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
- package/src/components/CallStats/CallStats.tsx +167 -10
- package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
- package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
- package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
- package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
- package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
- package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
- package/src/components/DropdownSelect/index.ts +1 -0
- package/src/components/Icon/Icon.tsx +7 -2
- package/src/components/Menu/GenericMenu.tsx +25 -3
- package/src/components/Menu/MenuToggle.tsx +79 -14
- package/src/components/Notification/Notification.tsx +8 -0
- package/src/components/Notification/PermissionNotification.tsx +2 -1
- package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
- package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
- package/src/components/Notification/index.ts +1 -0
- package/src/components/Permissions/PermissionRequests.tsx +9 -21
- package/src/components/Search/hooks/useSearch.ts +5 -1
- package/src/components/index.ts +2 -0
- package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
- package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
- package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
- package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
- package/src/core/components/ParticipantView/index.ts +1 -0
- package/src/core/components/Video/BaseVideo.tsx +1 -1
- package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
- package/src/hooks/useFloatingUIPreset.ts +3 -2
- package/src/hooks/useRequestPermission.ts +2 -1
- package/src/translations/en.json +9 -0
- package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
- package/src/components/Button/CopyToClipboardButton.tsx +0 -129
package/dist/index.es.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { SfuModels,
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
|
42
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
const GenericMenu = ({ children }) => {
|
|
502
|
-
|
|
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
|
|
522
|
-
return
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
550
|
-
const
|
|
551
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
677
|
-
const
|
|
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:
|
|
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
|
|
693
|
-
const {
|
|
694
|
-
let
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
return (jsx(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
|
788
|
-
const {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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,
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
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: `${
|
|
836
|
-
return (jsx(DeviceSelectorOption, { id: `${
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
816
|
+
}, title: title, visualType: visualType, icon: "camera" }));
|
|
868
817
|
};
|
|
869
818
|
|
|
870
|
-
const DeviceSettings = () => {
|
|
871
|
-
return (jsx(MenuToggle, { placement: "bottom-end", ToggleButton:
|
|
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 = () =>
|
|
874
|
-
const
|
|
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 (
|
|
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 =
|
|
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:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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(
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
|
952
|
-
const
|
|
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 (!
|
|
1039
|
+
if (!callStatsReport)
|
|
955
1040
|
return;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
999
|
-
|
|
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
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
|
1012
|
-
return (jsx(CompositeButton, { Menu: Menu, caption: caption, children: jsx(
|
|
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: ["
|
|
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
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
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 }))] })
|
|
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: [
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
1512
|
-
const
|
|
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-
|
|
1526
|
-
const { participant,
|
|
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, {
|
|
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 {
|
|
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 &&
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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.
|
|
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,
|
|
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
|