@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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { Placement } from '@floating-ui/react';
|
|
3
|
+
|
|
2
4
|
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
3
5
|
import { Notification } from './Notification';
|
|
4
6
|
|
|
@@ -7,11 +9,13 @@ export type SpeakingWhileMutedNotificationProps = {
|
|
|
7
9
|
* Text message displayed by the notification.
|
|
8
10
|
*/
|
|
9
11
|
text?: string;
|
|
12
|
+
placement?: Placement;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export const SpeakingWhileMutedNotification = ({
|
|
13
16
|
children,
|
|
14
17
|
text,
|
|
18
|
+
placement,
|
|
15
19
|
}: PropsWithChildren<SpeakingWhileMutedNotificationProps>) => {
|
|
16
20
|
const { useMicrophoneState } = useCallStateHooks();
|
|
17
21
|
const { isSpeakingWhileMuted } = useMicrophoneState();
|
|
@@ -19,7 +23,11 @@ export const SpeakingWhileMutedNotification = ({
|
|
|
19
23
|
|
|
20
24
|
const message = text ?? t('You are muted. Unmute to speak.');
|
|
21
25
|
return (
|
|
22
|
-
<Notification
|
|
26
|
+
<Notification
|
|
27
|
+
message={message}
|
|
28
|
+
isVisible={isSpeakingWhileMuted}
|
|
29
|
+
placement={placement || 'top-start'}
|
|
30
|
+
>
|
|
23
31
|
{children}
|
|
24
32
|
</Notification>
|
|
25
33
|
);
|
|
@@ -10,14 +10,12 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
OwnCapability,
|
|
12
12
|
PermissionRequestEvent,
|
|
13
|
-
StreamVideoEvent,
|
|
14
13
|
UserResponse,
|
|
15
14
|
} from '@stream-io/video-client';
|
|
16
15
|
import {
|
|
17
16
|
TranslatorFunction,
|
|
18
17
|
useCall,
|
|
19
18
|
useCallStateHooks,
|
|
20
|
-
useHasPermissions,
|
|
21
19
|
useI18n,
|
|
22
20
|
} from '@stream-io/video-react-bindings';
|
|
23
21
|
import clsx from 'clsx';
|
|
@@ -39,7 +37,7 @@ type HandleUpdatePermission = (
|
|
|
39
37
|
|
|
40
38
|
export const PermissionRequests = () => {
|
|
41
39
|
const call = useCall();
|
|
42
|
-
const { useLocalParticipant } = useCallStateHooks();
|
|
40
|
+
const { useLocalParticipant, useHasPermissions } = useCallStateHooks();
|
|
43
41
|
const localParticipant = useLocalParticipant();
|
|
44
42
|
const [expanded, setExpanded] = useState(false);
|
|
45
43
|
const [permissionRequests, setPermissionRequests] = useState<
|
|
@@ -52,23 +50,13 @@ export const PermissionRequests = () => {
|
|
|
52
50
|
const localUserId = localParticipant?.userId;
|
|
53
51
|
useEffect(() => {
|
|
54
52
|
if (!call || !canUpdateCallPermissions) return;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[...requests, event as PermissionRequestEvent].sort((a, b) =>
|
|
63
|
-
byNameOrId(a.user, b.user),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
return () => {
|
|
70
|
-
unsubscribe();
|
|
71
|
-
};
|
|
53
|
+
return call.on('call.permission_request', (event) => {
|
|
54
|
+
if (event.user.id !== localUserId) {
|
|
55
|
+
setPermissionRequests((requests) =>
|
|
56
|
+
[...requests, event].sort((a, b) => byNameOrId(a.user, b.user)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
72
60
|
}, [call, canUpdateCallPermissions, localUserId]);
|
|
73
61
|
|
|
74
62
|
const handleUpdatePermission: HandleUpdatePermission = (request, type) => {
|
|
@@ -138,7 +126,7 @@ export type PermissionRequestListProps = ComponentProps<'div'> & {
|
|
|
138
126
|
export const PermissionRequestList = forwardRef<
|
|
139
127
|
HTMLDivElement,
|
|
140
128
|
PermissionRequestListProps
|
|
141
|
-
>((props, ref)
|
|
129
|
+
>(function PermissionRequestList(props, ref) {
|
|
142
130
|
const { permissionRequests, handleUpdatePermission, ...rest } = props;
|
|
143
131
|
|
|
144
132
|
const { t } = useI18n();
|
|
@@ -25,7 +25,11 @@ export const useSearch = <T>({
|
|
|
25
25
|
const [searchQueryInProgress, setSearchQueryInProgress] = useState(false);
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
|
-
if (!searchQuery.length)
|
|
28
|
+
if (!searchQuery.length) {
|
|
29
|
+
setSearchQueryInProgress(false);
|
|
30
|
+
setSearchResults([]);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
setSearchQueryInProgress(true);
|
|
31
35
|
|
package/src/components/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ export * from './Button';
|
|
|
3
3
|
export * from './CallControls';
|
|
4
4
|
export * from './CallParticipantsList';
|
|
5
5
|
export * from './CallPreview';
|
|
6
|
+
export * from './CallStats';
|
|
6
7
|
export * from './CallRecordingList';
|
|
7
8
|
export * from './CallStats';
|
|
8
9
|
export * from './DeviceSettings';
|
|
10
|
+
export * from './DropdownSelect';
|
|
9
11
|
export * from './Icon';
|
|
10
12
|
export * from './LoadingIndicator';
|
|
11
13
|
export * from './Menu';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef } from 'react';
|
|
1
|
+
import { ComponentType, forwardRef } from 'react';
|
|
2
2
|
import { Placement } from '@floating-ui/react';
|
|
3
3
|
import { SfuModels } from '@stream-io/video-client';
|
|
4
4
|
import { useCall, useI18n } from '@stream-io/video-react-bindings';
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
IconButton,
|
|
10
10
|
MenuToggle,
|
|
11
11
|
Notification,
|
|
12
|
-
ParticipantActionsContextMenu,
|
|
13
12
|
ToggleMenuButtonProps,
|
|
14
13
|
} from '../../../components';
|
|
14
|
+
import { ParticipantActionsContextMenu as DefaultParticipantActionsContextMenu } from './ParticipantActionsContextMenu';
|
|
15
15
|
import { Reaction } from '../../../components/Reaction';
|
|
16
16
|
import { useParticipantViewContext } from './ParticipantViewContext';
|
|
17
17
|
|
|
@@ -28,10 +28,14 @@ export type DefaultParticipantViewUIProps = {
|
|
|
28
28
|
* Option to show/hide menu button component
|
|
29
29
|
*/
|
|
30
30
|
showMenuButton?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Custom component to render the context menu
|
|
33
|
+
*/
|
|
34
|
+
ParticipantActionsContextMenu?: ComponentType;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
|
|
34
|
-
(props, ref)
|
|
38
|
+
function ToggleButton(props, ref) {
|
|
35
39
|
return <IconButton enabled={props.menuShown} icon="ellipsis" ref={ref} />;
|
|
36
40
|
},
|
|
37
41
|
);
|
|
@@ -62,11 +66,11 @@ export const DefaultScreenShareOverlay = () => {
|
|
|
62
66
|
|
|
63
67
|
export const DefaultParticipantViewUI = ({
|
|
64
68
|
indicatorsVisible = true,
|
|
65
|
-
menuPlacement = 'bottom-
|
|
69
|
+
menuPlacement = 'bottom-start',
|
|
66
70
|
showMenuButton = true,
|
|
71
|
+
ParticipantActionsContextMenu = DefaultParticipantActionsContextMenu,
|
|
67
72
|
}: DefaultParticipantViewUIProps) => {
|
|
68
|
-
const { participant,
|
|
69
|
-
useParticipantViewContext();
|
|
73
|
+
const { participant, trackType } = useParticipantViewContext();
|
|
70
74
|
const { publishedTracks } = participant;
|
|
71
75
|
|
|
72
76
|
const hasScreenShare = publishedTracks.includes(
|
|
@@ -94,11 +98,7 @@ export const DefaultParticipantViewUI = ({
|
|
|
94
98
|
placement={menuPlacement}
|
|
95
99
|
ToggleButton={ToggleButton}
|
|
96
100
|
>
|
|
97
|
-
<ParticipantActionsContextMenu
|
|
98
|
-
participantViewElement={participantViewElement}
|
|
99
|
-
participant={participant}
|
|
100
|
-
videoElement={videoElement}
|
|
101
|
-
/>
|
|
101
|
+
<ParticipantActionsContextMenu />
|
|
102
102
|
</MenuToggle>
|
|
103
103
|
)}
|
|
104
104
|
<Reaction participant={participant} />
|
|
@@ -112,7 +112,6 @@ export const ParticipantDetails = ({
|
|
|
112
112
|
}: Pick<DefaultParticipantViewUIProps, 'indicatorsVisible'>) => {
|
|
113
113
|
const { participant } = useParticipantViewContext();
|
|
114
114
|
const {
|
|
115
|
-
isDominantSpeaker,
|
|
116
115
|
isLocalParticipant,
|
|
117
116
|
connectionQuality,
|
|
118
117
|
publishedTracks,
|
|
@@ -133,50 +132,65 @@ export const ParticipantDetails = ({
|
|
|
133
132
|
const canUnpin = !!pin && pin.isLocalPin;
|
|
134
133
|
|
|
135
134
|
return (
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
135
|
+
<>
|
|
136
|
+
<div className="str-video__participant-details">
|
|
137
|
+
<span className="str-video__participant-details__name">
|
|
138
|
+
{name || userId}
|
|
139
|
+
|
|
140
|
+
{indicatorsVisible && !hasAudio && (
|
|
141
|
+
<span className="str-video__participant-details__name--audio-muted" />
|
|
142
|
+
)}
|
|
143
|
+
{indicatorsVisible && !hasVideo && (
|
|
144
|
+
<span className="str-video__participant-details__name--video-muted" />
|
|
145
|
+
)}
|
|
146
|
+
{indicatorsVisible && canUnpin && (
|
|
147
|
+
// TODO: remove this monstrosity once we have a proper design
|
|
148
|
+
<span
|
|
149
|
+
title={t('Unpin')}
|
|
150
|
+
onClick={() => call?.unpin(sessionId)}
|
|
151
|
+
className="str-video__participant-details__name--pinned"
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
{indicatorsVisible && <SpeechIndicator />}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
{indicatorsVisible && (
|
|
158
|
+
<Notification
|
|
159
|
+
isVisible={
|
|
160
|
+
isLocalParticipant &&
|
|
161
|
+
connectionQuality === SfuModels.ConnectionQuality.POOR
|
|
162
|
+
}
|
|
163
|
+
message={t('Poor connection quality')}
|
|
164
|
+
>
|
|
165
|
+
{connectionQualityAsString && (
|
|
166
|
+
<span
|
|
167
|
+
className={clsx(
|
|
168
|
+
'str-video__participant-details__connection-quality',
|
|
169
|
+
`str-video__participant-details__connection-quality--${connectionQualityAsString}`,
|
|
170
|
+
)}
|
|
171
|
+
title={connectionQualityAsString}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</Notification>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const SpeechIndicator = () => {
|
|
181
|
+
const { participant } = useParticipantViewContext();
|
|
182
|
+
const { isSpeaking, isDominantSpeaker } = participant;
|
|
183
|
+
return (
|
|
184
|
+
<span
|
|
185
|
+
className={clsx(
|
|
186
|
+
'str-video__speech-indicator',
|
|
187
|
+
isSpeaking && 'str-video__speech-indicator--speaking',
|
|
188
|
+
isDominantSpeaker && 'str-video__speech-indicator--dominant',
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
<span className="str-video__speech-indicator__bar" />
|
|
192
|
+
<span className="str-video__speech-indicator__bar" />
|
|
193
|
+
<span className="str-video__speech-indicator__bar" />
|
|
194
|
+
</span>
|
|
181
195
|
);
|
|
182
196
|
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
|
|
3
|
+
import { OwnCapability, SfuModels } from '@stream-io/video-client';
|
|
4
|
+
import { useParticipantViewContext } from './ParticipantViewContext';
|
|
5
|
+
import {
|
|
6
|
+
GenericMenu,
|
|
7
|
+
GenericMenuButtonItem,
|
|
8
|
+
Icon,
|
|
9
|
+
useMenuContext,
|
|
10
|
+
} from '../../../components';
|
|
11
|
+
|
|
12
|
+
export const ParticipantActionsContextMenu = () => {
|
|
13
|
+
const { participant, participantViewElement, videoElement } =
|
|
14
|
+
useParticipantViewContext();
|
|
15
|
+
const [fullscreenModeOn, setFullscreenModeOn] = useState(
|
|
16
|
+
!!document.fullscreenElement,
|
|
17
|
+
);
|
|
18
|
+
const [pictureInPictureElement, setPictureInPictureElement] = useState(
|
|
19
|
+
document.pictureInPictureElement,
|
|
20
|
+
);
|
|
21
|
+
const call = useCall();
|
|
22
|
+
const { t } = useI18n();
|
|
23
|
+
|
|
24
|
+
const { pin, publishedTracks, sessionId, userId } = participant;
|
|
25
|
+
|
|
26
|
+
const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
|
|
27
|
+
const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
|
|
28
|
+
const hasScreenShare = publishedTracks.includes(
|
|
29
|
+
SfuModels.TrackType.SCREEN_SHARE,
|
|
30
|
+
);
|
|
31
|
+
const hasScreenShareAudio = publishedTracks.includes(
|
|
32
|
+
SfuModels.TrackType.SCREEN_SHARE_AUDIO,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const blockUser = () => call?.blockUser(userId);
|
|
36
|
+
const muteAudio = () => call?.muteUser(userId, 'audio');
|
|
37
|
+
const muteVideo = () => call?.muteUser(userId, 'video');
|
|
38
|
+
const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
|
|
39
|
+
const muteScreenShareAudio = () =>
|
|
40
|
+
call?.muteUser(userId, 'screenshare_audio');
|
|
41
|
+
|
|
42
|
+
const grantPermission = (permission: string) => () => {
|
|
43
|
+
call?.updateUserPermissions({
|
|
44
|
+
user_id: userId,
|
|
45
|
+
grant_permissions: [permission],
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const revokePermission = (permission: string) => () => {
|
|
50
|
+
call?.updateUserPermissions({
|
|
51
|
+
user_id: userId,
|
|
52
|
+
revoke_permissions: [permission],
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const toggleParticipantPin = () => {
|
|
57
|
+
if (pin) {
|
|
58
|
+
call?.unpin(sessionId);
|
|
59
|
+
} else {
|
|
60
|
+
call?.pin(sessionId);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const pinForEveryone = () => {
|
|
65
|
+
call
|
|
66
|
+
?.pinForEveryone({
|
|
67
|
+
user_id: userId,
|
|
68
|
+
session_id: sessionId,
|
|
69
|
+
})
|
|
70
|
+
.catch((err) => {
|
|
71
|
+
console.error(`Failed to pin participant ${userId}`, err);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const unpinForEveryone = () => {
|
|
76
|
+
call
|
|
77
|
+
?.unpinForEveryone({
|
|
78
|
+
user_id: userId,
|
|
79
|
+
session_id: sessionId,
|
|
80
|
+
})
|
|
81
|
+
.catch((err) => {
|
|
82
|
+
console.error(`Failed to unpin participant ${userId}`, err);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const toggleFullscreenMode = () => {
|
|
87
|
+
if (!fullscreenModeOn) {
|
|
88
|
+
return participantViewElement?.requestFullscreen().catch(console.error);
|
|
89
|
+
}
|
|
90
|
+
return document.exitFullscreen().catch(console.error);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
// handles the case when fullscreen mode is toggled externally,
|
|
95
|
+
// e.g., by pressing ESC key or some other keyboard shortcut
|
|
96
|
+
const handleFullscreenChange = () => {
|
|
97
|
+
setFullscreenModeOn(!!document.fullscreenElement);
|
|
98
|
+
};
|
|
99
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
100
|
+
return () => {
|
|
101
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!videoElement) return;
|
|
107
|
+
|
|
108
|
+
const handlePiP = () => {
|
|
109
|
+
setPictureInPictureElement(document.pictureInPictureElement);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
videoElement.addEventListener('enterpictureinpicture', handlePiP);
|
|
113
|
+
videoElement.addEventListener('leavepictureinpicture', handlePiP);
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
videoElement.removeEventListener('enterpictureinpicture', handlePiP);
|
|
117
|
+
videoElement.removeEventListener('leavepictureinpicture', handlePiP);
|
|
118
|
+
};
|
|
119
|
+
}, [videoElement]);
|
|
120
|
+
|
|
121
|
+
const togglePictureInPicture = () => {
|
|
122
|
+
if (videoElement && pictureInPictureElement !== videoElement) {
|
|
123
|
+
return videoElement
|
|
124
|
+
.requestPictureInPicture()
|
|
125
|
+
.catch(console.error) as Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return document.exitPictureInPicture().catch(console.error);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const { close } = useMenuContext() || {};
|
|
132
|
+
return (
|
|
133
|
+
<GenericMenu onItemClick={close}>
|
|
134
|
+
<GenericMenuButtonItem
|
|
135
|
+
onClick={toggleParticipantPin}
|
|
136
|
+
disabled={pin && !pin.isLocalPin}
|
|
137
|
+
>
|
|
138
|
+
<Icon icon="pin" />
|
|
139
|
+
{pin ? t('Unpin') : t('Pin')}
|
|
140
|
+
</GenericMenuButtonItem>
|
|
141
|
+
<Restricted requiredGrants={[OwnCapability.PIN_FOR_EVERYONE]}>
|
|
142
|
+
<GenericMenuButtonItem
|
|
143
|
+
onClick={pinForEveryone}
|
|
144
|
+
disabled={pin && !pin.isLocalPin}
|
|
145
|
+
>
|
|
146
|
+
<Icon icon="pin" />
|
|
147
|
+
{t('Pin for everyone')}
|
|
148
|
+
</GenericMenuButtonItem>
|
|
149
|
+
<GenericMenuButtonItem
|
|
150
|
+
onClick={unpinForEveryone}
|
|
151
|
+
disabled={!pin || pin.isLocalPin}
|
|
152
|
+
>
|
|
153
|
+
<Icon icon="pin" />
|
|
154
|
+
{t('Unpin for everyone')}
|
|
155
|
+
</GenericMenuButtonItem>
|
|
156
|
+
</Restricted>
|
|
157
|
+
<Restricted requiredGrants={[OwnCapability.BLOCK_USERS]}>
|
|
158
|
+
<GenericMenuButtonItem onClick={blockUser}>
|
|
159
|
+
<Icon icon="not-allowed" />
|
|
160
|
+
{t('Block')}
|
|
161
|
+
</GenericMenuButtonItem>
|
|
162
|
+
</Restricted>
|
|
163
|
+
<Restricted requiredGrants={[OwnCapability.MUTE_USERS]}>
|
|
164
|
+
{hasVideo && (
|
|
165
|
+
<GenericMenuButtonItem onClick={muteVideo}>
|
|
166
|
+
<Icon icon="camera-off-outline" />
|
|
167
|
+
{t('Turn off video')}
|
|
168
|
+
</GenericMenuButtonItem>
|
|
169
|
+
)}
|
|
170
|
+
{hasScreenShare && (
|
|
171
|
+
<GenericMenuButtonItem onClick={muteScreenShare}>
|
|
172
|
+
<Icon icon="screen-share-off" />
|
|
173
|
+
{t('Turn off screen share')}
|
|
174
|
+
</GenericMenuButtonItem>
|
|
175
|
+
)}
|
|
176
|
+
{hasAudio && (
|
|
177
|
+
<GenericMenuButtonItem onClick={muteAudio}>
|
|
178
|
+
<Icon icon="no-audio" />
|
|
179
|
+
{t('Mute audio')}
|
|
180
|
+
</GenericMenuButtonItem>
|
|
181
|
+
)}
|
|
182
|
+
{hasScreenShareAudio && (
|
|
183
|
+
<GenericMenuButtonItem onClick={muteScreenShareAudio}>
|
|
184
|
+
<Icon icon="no-audio" />
|
|
185
|
+
{t('Mute screen share audio')}
|
|
186
|
+
</GenericMenuButtonItem>
|
|
187
|
+
)}
|
|
188
|
+
</Restricted>
|
|
189
|
+
{participantViewElement && (
|
|
190
|
+
<GenericMenuButtonItem onClick={toggleFullscreenMode}>
|
|
191
|
+
{t('{{ direction }} fullscreen', {
|
|
192
|
+
direction: fullscreenModeOn ? t('Leave') : t('Enter'),
|
|
193
|
+
})}
|
|
194
|
+
</GenericMenuButtonItem>
|
|
195
|
+
)}
|
|
196
|
+
{videoElement && document.pictureInPictureEnabled && (
|
|
197
|
+
<GenericMenuButtonItem onClick={togglePictureInPicture}>
|
|
198
|
+
{t('{{ direction }} picture-in-picture', {
|
|
199
|
+
direction:
|
|
200
|
+
pictureInPictureElement === videoElement
|
|
201
|
+
? t('Leave')
|
|
202
|
+
: t('Enter'),
|
|
203
|
+
})}
|
|
204
|
+
</GenericMenuButtonItem>
|
|
205
|
+
)}
|
|
206
|
+
<Restricted requiredGrants={[OwnCapability.UPDATE_CALL_PERMISSIONS]}>
|
|
207
|
+
<GenericMenuButtonItem
|
|
208
|
+
onClick={grantPermission(OwnCapability.SEND_AUDIO)}
|
|
209
|
+
>
|
|
210
|
+
{t('Allow audio')}
|
|
211
|
+
</GenericMenuButtonItem>
|
|
212
|
+
<GenericMenuButtonItem
|
|
213
|
+
onClick={grantPermission(OwnCapability.SEND_VIDEO)}
|
|
214
|
+
>
|
|
215
|
+
{t('Allow video')}
|
|
216
|
+
</GenericMenuButtonItem>
|
|
217
|
+
<GenericMenuButtonItem
|
|
218
|
+
onClick={grantPermission(OwnCapability.SCREENSHARE)}
|
|
219
|
+
>
|
|
220
|
+
{t('Allow screen sharing')}
|
|
221
|
+
</GenericMenuButtonItem>
|
|
222
|
+
|
|
223
|
+
<GenericMenuButtonItem
|
|
224
|
+
onClick={revokePermission(OwnCapability.SEND_AUDIO)}
|
|
225
|
+
>
|
|
226
|
+
{t('Disable audio')}
|
|
227
|
+
</GenericMenuButtonItem>
|
|
228
|
+
<GenericMenuButtonItem
|
|
229
|
+
onClick={revokePermission(OwnCapability.SEND_VIDEO)}
|
|
230
|
+
>
|
|
231
|
+
{t('Disable video')}
|
|
232
|
+
</GenericMenuButtonItem>
|
|
233
|
+
<GenericMenuButtonItem
|
|
234
|
+
onClick={revokePermission(OwnCapability.SCREENSHARE)}
|
|
235
|
+
>
|
|
236
|
+
{t('Disable screen sharing')}
|
|
237
|
+
</GenericMenuButtonItem>
|
|
238
|
+
</Restricted>
|
|
239
|
+
</GenericMenu>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
@@ -62,7 +62,7 @@ export type ParticipantViewProps = {
|
|
|
62
62
|
} & Pick<VideoProps, 'VideoPlaceholder'>;
|
|
63
63
|
|
|
64
64
|
export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
65
|
-
(
|
|
65
|
+
function ParticipantView(
|
|
66
66
|
{
|
|
67
67
|
participant,
|
|
68
68
|
trackType = 'videoTrack',
|
|
@@ -73,7 +73,7 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
|
73
73
|
ParticipantViewUI = DefaultParticipantViewUI as ComponentType,
|
|
74
74
|
},
|
|
75
75
|
ref,
|
|
76
|
-
)
|
|
76
|
+
) {
|
|
77
77
|
const {
|
|
78
78
|
isLocalParticipant,
|
|
79
79
|
isSpeaking,
|
|
@@ -4,9 +4,9 @@ import { ParticipantViewProps } from './ParticipantView';
|
|
|
4
4
|
export type ParticipantViewContextValue = Required<
|
|
5
5
|
Pick<ParticipantViewProps, 'participant' | 'trackType'>
|
|
6
6
|
> & {
|
|
7
|
-
participantViewElement
|
|
8
|
-
videoElement
|
|
9
|
-
videoPlaceholderElement
|
|
7
|
+
participantViewElement?: HTMLDivElement | null;
|
|
8
|
+
videoElement?: HTMLVideoElement | null;
|
|
9
|
+
videoPlaceholderElement?: HTMLDivElement | null;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export const ParticipantViewContext = createContext<
|
|
@@ -12,7 +12,7 @@ export type BaseVideoProps = ComponentPropsWithRef<'video'> & {
|
|
|
12
12
|
* (`srcObject`) to reactively handle stream changes
|
|
13
13
|
*/
|
|
14
14
|
export const BaseVideo = forwardRef<HTMLVideoElement, BaseVideoProps>(
|
|
15
|
-
({ stream, ...rest }, ref)
|
|
15
|
+
function BaseVideo({ stream, ...rest }, ref) {
|
|
16
16
|
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
17
17
|
null,
|
|
18
18
|
);
|
|
@@ -9,7 +9,7 @@ export type VideoPlaceholderProps = {
|
|
|
9
9
|
export const DefaultVideoPlaceholder = forwardRef<
|
|
10
10
|
HTMLDivElement,
|
|
11
11
|
VideoPlaceholderProps
|
|
12
|
-
>(({ participant, style }, ref)
|
|
12
|
+
>(function DefaultVideoPlaceholder({ participant, style }, ref) {
|
|
13
13
|
const { t } = useI18n();
|
|
14
14
|
const [error, setError] = useState(false);
|
|
15
15
|
const name = participant.name || participant.userId;
|
|
@@ -17,11 +17,11 @@ export const DefaultVideoPlaceholder = forwardRef<
|
|
|
17
17
|
<div className="str-video__video-placeholder" style={style} ref={ref}>
|
|
18
18
|
{(!participant.image || error) &&
|
|
19
19
|
(name ? (
|
|
20
|
-
<
|
|
21
|
-
<div>{name[0]}</div>
|
|
22
|
-
</div>
|
|
20
|
+
<InitialsFallback name={name} />
|
|
23
21
|
) : (
|
|
24
|
-
<div
|
|
22
|
+
<div className="str-video__video-placeholder__no-video-label">
|
|
23
|
+
{t('Video is disabled')}
|
|
24
|
+
</div>
|
|
25
25
|
))}
|
|
26
26
|
{participant.image && !error && (
|
|
27
27
|
<img
|
|
@@ -34,3 +34,17 @@ export const DefaultVideoPlaceholder = forwardRef<
|
|
|
34
34
|
</div>
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
const InitialsFallback = (props: { name: string }) => {
|
|
39
|
+
const { name } = props;
|
|
40
|
+
const initials = name
|
|
41
|
+
.split(' ')
|
|
42
|
+
.slice(0, 2)
|
|
43
|
+
.map((n) => n[0])
|
|
44
|
+
.join('');
|
|
45
|
+
return (
|
|
46
|
+
<div className="str-video__video-placeholder__initials-fallback">
|
|
47
|
+
{initials}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
export const useFloatingUIPreset = ({
|
|
13
13
|
placement,
|
|
14
14
|
strategy,
|
|
15
|
-
|
|
15
|
+
offset: offsetInPx = 10,
|
|
16
|
+
}: Pick<UseFloatingData, 'placement' | 'strategy'> & { offset?: number }) => {
|
|
16
17
|
const {
|
|
17
18
|
refs,
|
|
18
19
|
x,
|
|
@@ -23,7 +24,7 @@ export const useFloatingUIPreset = ({
|
|
|
23
24
|
placement,
|
|
24
25
|
strategy,
|
|
25
26
|
middleware: [
|
|
26
|
-
offset(
|
|
27
|
+
offset(offsetInPx),
|
|
27
28
|
shift(),
|
|
28
29
|
flip(),
|
|
29
30
|
size({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { OwnCapability } from '@stream-io/video-client';
|
|
3
|
-
import { useCall,
|
|
3
|
+
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
4
4
|
|
|
5
5
|
export const useRequestPermission = (permission: OwnCapability) => {
|
|
6
6
|
const call = useCall();
|
|
7
|
+
const { useHasPermissions } = useCallStateHooks();
|
|
7
8
|
const hasPermission = useHasPermissions(permission);
|
|
8
9
|
const [isAwaitingPermission, setIsAwaitingPermission] = useState(false); // TODO: load with possibly pending state
|
|
9
10
|
|