@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,22 +1,26 @@
|
|
|
1
|
-
import { CompositeButton,
|
|
2
|
-
import { DeviceSelectorAudioOutput } from '../DeviceSettings';
|
|
1
|
+
import { CompositeButton, IconButtonWithMenuProps } from '../Button';
|
|
3
2
|
import { useI18n } from '@stream-io/video-react-bindings';
|
|
4
|
-
import {
|
|
3
|
+
import { Icon } from '../Icon';
|
|
5
4
|
|
|
6
|
-
export type ToggleAudioOutputButtonProps =
|
|
7
|
-
|
|
8
|
-
Menu
|
|
9
|
-
|
|
5
|
+
export type ToggleAudioOutputButtonProps = Pick<
|
|
6
|
+
IconButtonWithMenuProps,
|
|
7
|
+
'caption' | 'Menu' | 'menuPlacement'
|
|
8
|
+
>;
|
|
10
9
|
|
|
11
10
|
export const ToggleAudioOutputButton = (
|
|
12
11
|
props: ToggleAudioOutputButtonProps,
|
|
13
12
|
) => {
|
|
14
13
|
const { t } = useI18n();
|
|
15
|
-
const { caption
|
|
14
|
+
const { caption, Menu } = props;
|
|
16
15
|
|
|
17
16
|
return (
|
|
18
|
-
<CompositeButton
|
|
19
|
-
|
|
17
|
+
<CompositeButton
|
|
18
|
+
Menu={Menu}
|
|
19
|
+
caption={caption}
|
|
20
|
+
title={caption || t('Speakers')}
|
|
21
|
+
data-testid="audio-output-button"
|
|
22
|
+
>
|
|
23
|
+
<Icon icon="speaker" />
|
|
20
24
|
</CompositeButton>
|
|
21
25
|
);
|
|
22
26
|
};
|
|
@@ -1,56 +1,82 @@
|
|
|
1
|
-
import { ComponentType } from 'react';
|
|
2
1
|
import {
|
|
3
2
|
Restricted,
|
|
4
3
|
useCallStateHooks,
|
|
5
4
|
useI18n,
|
|
6
5
|
} from '@stream-io/video-react-bindings';
|
|
7
|
-
|
|
6
|
+
import clsx from 'clsx';
|
|
8
7
|
import { OwnCapability } from '@stream-io/video-client';
|
|
9
|
-
import { CompositeButton,
|
|
8
|
+
import { CompositeButton, IconButtonWithMenuProps } from '../Button/';
|
|
10
9
|
import { DeviceSelectorVideo } from '../DeviceSettings';
|
|
11
10
|
import { PermissionNotification } from '../Notification';
|
|
12
11
|
import { useRequestPermission } from '../../hooks';
|
|
12
|
+
import { Icon } from '../Icon';
|
|
13
13
|
|
|
14
|
-
export type ToggleVideoPreviewButtonProps =
|
|
15
|
-
|
|
16
|
-
Menu
|
|
17
|
-
|
|
14
|
+
export type ToggleVideoPreviewButtonProps = Pick<
|
|
15
|
+
IconButtonWithMenuProps,
|
|
16
|
+
'caption' | 'Menu' | 'menuPlacement'
|
|
17
|
+
>;
|
|
18
18
|
|
|
19
19
|
export const ToggleVideoPreviewButton = (
|
|
20
20
|
props: ToggleVideoPreviewButtonProps,
|
|
21
21
|
) => {
|
|
22
|
+
const { caption, ...restCompositeButtonProps } = props;
|
|
22
23
|
const { t } = useI18n();
|
|
23
|
-
const { caption = t('Video'), Menu = DeviceSelectorVideo } = props;
|
|
24
|
-
|
|
25
24
|
const { useCameraState } = useCallStateHooks();
|
|
26
|
-
const { camera, isMute } = useCameraState();
|
|
25
|
+
const { camera, isMute, hasBrowserPermission } = useCameraState();
|
|
27
26
|
|
|
28
27
|
return (
|
|
29
|
-
<CompositeButton
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
<CompositeButton
|
|
29
|
+
active={isMute}
|
|
30
|
+
caption={caption}
|
|
31
|
+
className={clsx(!hasBrowserPermission && 'str-video__device-unavailable')}
|
|
32
|
+
title={
|
|
33
|
+
!hasBrowserPermission
|
|
34
|
+
? t('Check your browser video permissions')
|
|
35
|
+
: caption || t('Video')
|
|
36
|
+
}
|
|
37
|
+
variant="secondary"
|
|
38
|
+
data-testid={
|
|
39
|
+
isMute ? 'preview-video-unmute-button' : 'preview-video-mute-button'
|
|
40
|
+
}
|
|
41
|
+
onClick={() => camera.toggle()}
|
|
42
|
+
disabled={!hasBrowserPermission}
|
|
43
|
+
{...restCompositeButtonProps}
|
|
44
|
+
>
|
|
45
|
+
<Icon icon={!isMute ? 'camera' : 'camera-off'} />
|
|
46
|
+
{!hasBrowserPermission && (
|
|
47
|
+
<span
|
|
48
|
+
className="str-video__no-media-permission"
|
|
49
|
+
title={t('Check your browser video permissions')}
|
|
50
|
+
children="!"
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
34
53
|
</CompositeButton>
|
|
35
54
|
);
|
|
36
55
|
};
|
|
37
56
|
|
|
38
|
-
type ToggleVideoPublishingButtonProps =
|
|
39
|
-
|
|
40
|
-
Menu
|
|
41
|
-
|
|
57
|
+
type ToggleVideoPublishingButtonProps = Pick<
|
|
58
|
+
IconButtonWithMenuProps,
|
|
59
|
+
'caption' | 'Menu' | 'menuPlacement'
|
|
60
|
+
>;
|
|
42
61
|
|
|
43
62
|
export const ToggleVideoPublishingButton = (
|
|
44
63
|
props: ToggleVideoPublishingButtonProps,
|
|
45
64
|
) => {
|
|
46
65
|
const { t } = useI18n();
|
|
47
|
-
const {
|
|
66
|
+
const {
|
|
67
|
+
caption,
|
|
68
|
+
Menu = <DeviceSelectorVideo visualType="list" />,
|
|
69
|
+
menuPlacement = 'top',
|
|
70
|
+
...restCompositeButtonProps
|
|
71
|
+
} = props;
|
|
48
72
|
|
|
49
73
|
const { hasPermission, requestPermission, isAwaitingPermission } =
|
|
50
74
|
useRequestPermission(OwnCapability.SEND_VIDEO);
|
|
51
75
|
|
|
52
|
-
const { useCameraState } = useCallStateHooks();
|
|
53
|
-
const { camera, isMute } = useCameraState();
|
|
76
|
+
const { useCameraState, useCallSettings } = useCallStateHooks();
|
|
77
|
+
const { camera, isMute, hasBrowserPermission } = useCameraState();
|
|
78
|
+
const callSettings = useCallSettings();
|
|
79
|
+
const isPublishingVideoAllowed = callSettings?.video.enabled;
|
|
54
80
|
|
|
55
81
|
return (
|
|
56
82
|
<Restricted requiredGrants={[OwnCapability.SEND_VIDEO]}>
|
|
@@ -63,17 +89,41 @@ export const ToggleVideoPublishingButton = (
|
|
|
63
89
|
)}
|
|
64
90
|
messageRevoked={t('You can no longer share your video.')}
|
|
65
91
|
>
|
|
66
|
-
<CompositeButton
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
<CompositeButton
|
|
93
|
+
active={isMute}
|
|
94
|
+
caption={caption}
|
|
95
|
+
variant="secondary"
|
|
96
|
+
title={
|
|
97
|
+
!hasPermission
|
|
98
|
+
? t('You have no permission to share your video')
|
|
99
|
+
: !hasBrowserPermission
|
|
100
|
+
? t('Check your browser video permissions')
|
|
101
|
+
: !isPublishingVideoAllowed
|
|
102
|
+
? t('Video publishing is disabled by the system')
|
|
103
|
+
: caption || t('Video')
|
|
104
|
+
}
|
|
105
|
+
disabled={
|
|
106
|
+
!hasBrowserPermission || !hasPermission || !isPublishingVideoAllowed
|
|
107
|
+
}
|
|
108
|
+
data-testid={isMute ? 'video-unmute-button' : 'video-mute-button'}
|
|
109
|
+
onClick={async () => {
|
|
110
|
+
if (!hasPermission) {
|
|
111
|
+
await requestPermission();
|
|
112
|
+
} else {
|
|
113
|
+
await camera.toggle();
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
Menu={Menu}
|
|
117
|
+
menuPlacement={menuPlacement}
|
|
118
|
+
menuOffset={16}
|
|
119
|
+
{...restCompositeButtonProps}
|
|
120
|
+
>
|
|
121
|
+
<Icon icon={isMute ? 'camera-off' : 'camera'} />
|
|
122
|
+
{(!hasBrowserPermission ||
|
|
123
|
+
!hasPermission ||
|
|
124
|
+
!isPublishingVideoAllowed) && (
|
|
125
|
+
<span className="str-video__no-media-permission">!</span>
|
|
126
|
+
)}
|
|
77
127
|
</CompositeButton>
|
|
78
128
|
</PermissionNotification>
|
|
79
129
|
</Restricted>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
2
2
|
|
|
3
|
+
import { IconButton } from '../Button';
|
|
4
|
+
|
|
3
5
|
export type CallParticipantListHeaderProps = {
|
|
4
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Click event listener function to be invoked to dismiss / hide the CallParticipantsList from the UI.
|
|
8
|
+
*/
|
|
5
9
|
onClose: () => void;
|
|
6
10
|
};
|
|
7
11
|
|
|
@@ -18,7 +22,7 @@ export const CallParticipantListHeader = ({
|
|
|
18
22
|
<div className="str-video__participant-list-header__title">
|
|
19
23
|
{t('Participants')}{' '}
|
|
20
24
|
<span className="str-video__participant-list-header__title-count">
|
|
21
|
-
|
|
25
|
+
[{participants.length}]
|
|
22
26
|
</span>
|
|
23
27
|
{anonymousParticipantCount > 0 && (
|
|
24
28
|
<span className="str-video__participant-list-header__title-anonymous">
|
|
@@ -26,12 +30,11 @@ export const CallParticipantListHeader = ({
|
|
|
26
30
|
</span>
|
|
27
31
|
)}
|
|
28
32
|
</div>
|
|
29
|
-
<
|
|
33
|
+
<IconButton
|
|
30
34
|
onClick={onClose}
|
|
31
35
|
className="str-video__participant-list-header__close-button"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</button>
|
|
36
|
+
icon="close"
|
|
37
|
+
/>
|
|
35
38
|
</div>
|
|
36
39
|
);
|
|
37
40
|
};
|
|
@@ -1,31 +1,15 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
forwardRef,
|
|
6
|
-
useEffect,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
9
|
-
import {
|
|
10
|
-
Restricted,
|
|
11
|
-
useCall,
|
|
12
|
-
useConnectedUser,
|
|
13
|
-
useI18n,
|
|
14
|
-
} from '@stream-io/video-react-bindings';
|
|
15
|
-
import {
|
|
16
|
-
OwnCapability,
|
|
17
|
-
SfuModels,
|
|
18
|
-
StreamVideoParticipant,
|
|
19
|
-
} from '@stream-io/video-client';
|
|
2
|
+
import { ComponentProps, ComponentType, forwardRef } from 'react';
|
|
3
|
+
import { useConnectedUser, useI18n } from '@stream-io/video-react-bindings';
|
|
4
|
+
import { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
|
|
20
5
|
import { IconButton } from '../Button';
|
|
21
|
-
import {
|
|
22
|
-
GenericMenu,
|
|
23
|
-
GenericMenuButtonItem,
|
|
24
|
-
MenuToggle,
|
|
25
|
-
ToggleMenuButtonProps,
|
|
26
|
-
} from '../Menu';
|
|
6
|
+
import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
|
|
27
7
|
import { WithTooltip } from '../Tooltip';
|
|
28
|
-
import {
|
|
8
|
+
import { Avatar } from '../Avatar';
|
|
9
|
+
import {
|
|
10
|
+
ParticipantActionsContextMenu,
|
|
11
|
+
ParticipantViewContext,
|
|
12
|
+
} from '../../core/';
|
|
29
13
|
|
|
30
14
|
type CallParticipantListingItemProps = {
|
|
31
15
|
/** Participant object be rendered */
|
|
@@ -49,6 +33,7 @@ export const CallParticipantListingItem = ({
|
|
|
49
33
|
|
|
50
34
|
return (
|
|
51
35
|
<div className="str-video__participant-listing-item">
|
|
36
|
+
<Avatar name={participant.name} imageSrc={participant.image} />
|
|
52
37
|
<DisplayName participant={participant} />
|
|
53
38
|
<div className="str-video__participant-listing-item__media-indicator-group">
|
|
54
39
|
<MediaIndicator
|
|
@@ -80,7 +65,11 @@ export const CallParticipantListingItem = ({
|
|
|
80
65
|
)}
|
|
81
66
|
|
|
82
67
|
<MenuToggle placement="bottom-end" ToggleButton={ToggleButton}>
|
|
83
|
-
<
|
|
68
|
+
<ParticipantViewContext.Provider
|
|
69
|
+
value={{ participant, trackType: 'none' }}
|
|
70
|
+
>
|
|
71
|
+
<ParticipantActionsContextMenu />
|
|
72
|
+
</ParticipantViewContext.Provider>
|
|
84
73
|
</MenuToggle>
|
|
85
74
|
</div>
|
|
86
75
|
</div>
|
|
@@ -95,7 +84,7 @@ type DisplayNameProps = {
|
|
|
95
84
|
/** Participant object that provides the data from which display name can be generated */
|
|
96
85
|
participant: StreamVideoParticipant;
|
|
97
86
|
};
|
|
98
|
-
|
|
87
|
+
|
|
99
88
|
const DefaultDisplayName = ({ participant }: DisplayNameProps) => {
|
|
100
89
|
const connectedUser = useConnectedUser();
|
|
101
90
|
const { t } = useI18n();
|
|
@@ -122,260 +111,7 @@ const DefaultDisplayName = ({ participant }: DisplayNameProps) => {
|
|
|
122
111
|
};
|
|
123
112
|
|
|
124
113
|
const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
|
|
125
|
-
(props, ref)
|
|
114
|
+
function ToggleButton(props, ref) {
|
|
126
115
|
return <IconButton enabled={props.menuShown} icon="ellipsis" ref={ref} />;
|
|
127
116
|
},
|
|
128
117
|
);
|
|
129
|
-
|
|
130
|
-
export const ParticipantActionsContextMenu = ({
|
|
131
|
-
participant,
|
|
132
|
-
participantViewElement,
|
|
133
|
-
videoElement,
|
|
134
|
-
}: {
|
|
135
|
-
participant: StreamVideoParticipant;
|
|
136
|
-
participantViewElement?: HTMLDivElement | null;
|
|
137
|
-
videoElement?: HTMLVideoElement | null;
|
|
138
|
-
}) => {
|
|
139
|
-
const [fullscreenModeOn, setFullscreenModeOn] = useState(
|
|
140
|
-
!!document.fullscreenElement,
|
|
141
|
-
);
|
|
142
|
-
const [pictureInPictureElement, setPictureInPictureElement] = useState(
|
|
143
|
-
document.pictureInPictureElement,
|
|
144
|
-
);
|
|
145
|
-
const call = useCall();
|
|
146
|
-
const { t } = useI18n();
|
|
147
|
-
|
|
148
|
-
const { pin, publishedTracks, sessionId, userId } = participant;
|
|
149
|
-
|
|
150
|
-
const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
|
|
151
|
-
const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
|
|
152
|
-
const hasScreenShare = publishedTracks.includes(
|
|
153
|
-
SfuModels.TrackType.SCREEN_SHARE,
|
|
154
|
-
);
|
|
155
|
-
const hasScreenShareAudio = publishedTracks.includes(
|
|
156
|
-
SfuModels.TrackType.SCREEN_SHARE_AUDIO,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const blockUser = () => call?.blockUser(userId);
|
|
160
|
-
const muteAudio = () => call?.muteUser(userId, 'audio');
|
|
161
|
-
const muteVideo = () => call?.muteUser(userId, 'video');
|
|
162
|
-
const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
|
|
163
|
-
const muteScreenShareAudio = () =>
|
|
164
|
-
call?.muteUser(userId, 'screenshare_audio');
|
|
165
|
-
|
|
166
|
-
const grantPermission = (permission: string) => () => {
|
|
167
|
-
call?.updateUserPermissions({
|
|
168
|
-
user_id: userId,
|
|
169
|
-
grant_permissions: [permission],
|
|
170
|
-
});
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const revokePermission = (permission: string) => () => {
|
|
174
|
-
call?.updateUserPermissions({
|
|
175
|
-
user_id: userId,
|
|
176
|
-
revoke_permissions: [permission],
|
|
177
|
-
});
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const toggleParticipantPinnedAt = () => {
|
|
181
|
-
if (pin) {
|
|
182
|
-
call?.unpin(sessionId);
|
|
183
|
-
} else {
|
|
184
|
-
call?.pin(sessionId);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const pinForEveryone = () => {
|
|
189
|
-
call
|
|
190
|
-
?.pinForEveryone({
|
|
191
|
-
user_id: userId,
|
|
192
|
-
session_id: sessionId,
|
|
193
|
-
})
|
|
194
|
-
.catch((err) => {
|
|
195
|
-
console.error(`Failed to pin participant ${userId}`, err);
|
|
196
|
-
});
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const unpinForEveryone = () => {
|
|
200
|
-
call
|
|
201
|
-
?.unpinForEveryone({
|
|
202
|
-
user_id: userId,
|
|
203
|
-
session_id: sessionId,
|
|
204
|
-
})
|
|
205
|
-
.catch((err) => {
|
|
206
|
-
console.error(`Failed to unpin participant ${userId}`, err);
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const toggleFullscreenMode = () => {
|
|
211
|
-
if (!fullscreenModeOn) {
|
|
212
|
-
return participantViewElement
|
|
213
|
-
?.requestFullscreen()
|
|
214
|
-
.then(() => setFullscreenModeOn(true))
|
|
215
|
-
.catch(console.error);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
document
|
|
219
|
-
.exitFullscreen()
|
|
220
|
-
.catch(console.error)
|
|
221
|
-
.finally(() => setFullscreenModeOn(false));
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
// handles the case when fullscreen mode is toggled externally,
|
|
226
|
-
// e.g., by pressing ESC key or some other keyboard shortcut
|
|
227
|
-
const handleFullscreenChange = () => {
|
|
228
|
-
setFullscreenModeOn(!!document.fullscreenElement);
|
|
229
|
-
};
|
|
230
|
-
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
231
|
-
return () => {
|
|
232
|
-
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
233
|
-
};
|
|
234
|
-
}, []);
|
|
235
|
-
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
if (!videoElement) return;
|
|
238
|
-
|
|
239
|
-
const handlePictureInPicture = () => {
|
|
240
|
-
setPictureInPictureElement(document.pictureInPictureElement);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
videoElement.addEventListener(
|
|
244
|
-
'enterpictureinpicture',
|
|
245
|
-
handlePictureInPicture,
|
|
246
|
-
);
|
|
247
|
-
videoElement.addEventListener(
|
|
248
|
-
'leavepictureinpicture',
|
|
249
|
-
handlePictureInPicture,
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
return () => {
|
|
253
|
-
videoElement.removeEventListener(
|
|
254
|
-
'enterpictureinpicture',
|
|
255
|
-
handlePictureInPicture,
|
|
256
|
-
);
|
|
257
|
-
videoElement.removeEventListener(
|
|
258
|
-
'leavepictureinpicture',
|
|
259
|
-
handlePictureInPicture,
|
|
260
|
-
);
|
|
261
|
-
};
|
|
262
|
-
}, [videoElement]);
|
|
263
|
-
|
|
264
|
-
const togglePictureInPicture = () => {
|
|
265
|
-
if (videoElement && pictureInPictureElement !== videoElement) {
|
|
266
|
-
return videoElement
|
|
267
|
-
.requestPictureInPicture()
|
|
268
|
-
.catch(console.error) as Promise<void>;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
document.exitPictureInPicture().catch(console.error);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
return (
|
|
275
|
-
<GenericMenu>
|
|
276
|
-
<GenericMenuButtonItem
|
|
277
|
-
onClick={toggleParticipantPinnedAt}
|
|
278
|
-
disabled={pin && !pin.isLocalPin}
|
|
279
|
-
>
|
|
280
|
-
<Icon icon="pin" />
|
|
281
|
-
{pin ? t('Unpin') : t('Pin')}
|
|
282
|
-
</GenericMenuButtonItem>
|
|
283
|
-
<Restricted requiredGrants={[OwnCapability.PIN_FOR_EVERYONE]}>
|
|
284
|
-
<GenericMenuButtonItem
|
|
285
|
-
onClick={pinForEveryone}
|
|
286
|
-
disabled={pin && !pin.isLocalPin}
|
|
287
|
-
>
|
|
288
|
-
<Icon icon="pin" />
|
|
289
|
-
{t('Pin for everyone')}
|
|
290
|
-
</GenericMenuButtonItem>
|
|
291
|
-
<GenericMenuButtonItem
|
|
292
|
-
onClick={unpinForEveryone}
|
|
293
|
-
disabled={!pin || pin.isLocalPin}
|
|
294
|
-
>
|
|
295
|
-
<Icon icon="pin" />
|
|
296
|
-
{t('Unpin for everyone')}
|
|
297
|
-
</GenericMenuButtonItem>
|
|
298
|
-
</Restricted>
|
|
299
|
-
<Restricted requiredGrants={[OwnCapability.BLOCK_USERS]}>
|
|
300
|
-
<GenericMenuButtonItem onClick={blockUser}>
|
|
301
|
-
<Icon icon="not-allowed" />
|
|
302
|
-
{t('Block')}
|
|
303
|
-
</GenericMenuButtonItem>
|
|
304
|
-
</Restricted>
|
|
305
|
-
<Restricted requiredGrants={[OwnCapability.MUTE_USERS]}>
|
|
306
|
-
<GenericMenuButtonItem disabled={!hasVideo} onClick={muteVideo}>
|
|
307
|
-
<Icon icon="camera-off-outline" />
|
|
308
|
-
{t('Turn off video')}
|
|
309
|
-
</GenericMenuButtonItem>
|
|
310
|
-
<GenericMenuButtonItem
|
|
311
|
-
disabled={!hasScreenShare}
|
|
312
|
-
onClick={muteScreenShare}
|
|
313
|
-
>
|
|
314
|
-
<Icon icon="screen-share-off" />
|
|
315
|
-
{t('Turn off screen share')}
|
|
316
|
-
</GenericMenuButtonItem>
|
|
317
|
-
<GenericMenuButtonItem disabled={!hasAudio} onClick={muteAudio}>
|
|
318
|
-
<Icon icon="no-audio" />
|
|
319
|
-
{t('Mute audio')}
|
|
320
|
-
</GenericMenuButtonItem>
|
|
321
|
-
<GenericMenuButtonItem
|
|
322
|
-
disabled={!hasScreenShareAudio}
|
|
323
|
-
onClick={muteScreenShareAudio}
|
|
324
|
-
>
|
|
325
|
-
<Icon icon="no-audio" />
|
|
326
|
-
{t('Mute screen share audio')}
|
|
327
|
-
</GenericMenuButtonItem>
|
|
328
|
-
</Restricted>
|
|
329
|
-
{participantViewElement && (
|
|
330
|
-
<GenericMenuButtonItem onClick={toggleFullscreenMode}>
|
|
331
|
-
{t('{{ direction }} fullscreen', {
|
|
332
|
-
direction: fullscreenModeOn ? t('Leave') : t('Enter'),
|
|
333
|
-
})}
|
|
334
|
-
</GenericMenuButtonItem>
|
|
335
|
-
)}
|
|
336
|
-
{videoElement && document.pictureInPictureEnabled && (
|
|
337
|
-
<GenericMenuButtonItem onClick={togglePictureInPicture}>
|
|
338
|
-
{t('{{ direction }} picture-in-picture', {
|
|
339
|
-
direction:
|
|
340
|
-
pictureInPictureElement === videoElement
|
|
341
|
-
? t('Leave')
|
|
342
|
-
: t('Enter'),
|
|
343
|
-
})}
|
|
344
|
-
</GenericMenuButtonItem>
|
|
345
|
-
)}
|
|
346
|
-
<Restricted requiredGrants={[OwnCapability.UPDATE_CALL_PERMISSIONS]}>
|
|
347
|
-
<GenericMenuButtonItem
|
|
348
|
-
onClick={grantPermission(OwnCapability.SEND_AUDIO)}
|
|
349
|
-
>
|
|
350
|
-
{t('Allow audio')}
|
|
351
|
-
</GenericMenuButtonItem>
|
|
352
|
-
<GenericMenuButtonItem
|
|
353
|
-
onClick={grantPermission(OwnCapability.SEND_VIDEO)}
|
|
354
|
-
>
|
|
355
|
-
{t('Allow video')}
|
|
356
|
-
</GenericMenuButtonItem>
|
|
357
|
-
<GenericMenuButtonItem
|
|
358
|
-
onClick={grantPermission(OwnCapability.SCREENSHARE)}
|
|
359
|
-
>
|
|
360
|
-
{t('Allow screen sharing')}
|
|
361
|
-
</GenericMenuButtonItem>
|
|
362
|
-
|
|
363
|
-
<GenericMenuButtonItem
|
|
364
|
-
onClick={revokePermission(OwnCapability.SEND_AUDIO)}
|
|
365
|
-
>
|
|
366
|
-
{t('Disable audio')}
|
|
367
|
-
</GenericMenuButtonItem>
|
|
368
|
-
<GenericMenuButtonItem
|
|
369
|
-
onClick={revokePermission(OwnCapability.SEND_VIDEO)}
|
|
370
|
-
>
|
|
371
|
-
{t('Disable video')}
|
|
372
|
-
</GenericMenuButtonItem>
|
|
373
|
-
<GenericMenuButtonItem
|
|
374
|
-
onClick={revokePermission(OwnCapability.SCREENSHARE)}
|
|
375
|
-
>
|
|
376
|
-
{t('Disable screen sharing')}
|
|
377
|
-
</GenericMenuButtonItem>
|
|
378
|
-
</Restricted>
|
|
379
|
-
</GenericMenu>
|
|
380
|
-
);
|
|
381
|
-
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ComponentProps,
|
|
3
2
|
Dispatch,
|
|
4
|
-
ForwardedRef,
|
|
5
3
|
forwardRef,
|
|
6
4
|
SetStateAction,
|
|
7
5
|
useCallback,
|
|
@@ -17,14 +15,9 @@ import {
|
|
|
17
15
|
OwnCapability,
|
|
18
16
|
StreamVideoParticipant,
|
|
19
17
|
} from '@stream-io/video-client';
|
|
20
|
-
import clsx from 'clsx';
|
|
21
18
|
|
|
22
19
|
import { BlockedUserListing } from './BlockedUserListing';
|
|
23
|
-
import {
|
|
24
|
-
CopyToClipboardButtonWithPopup,
|
|
25
|
-
IconButton,
|
|
26
|
-
TextButton,
|
|
27
|
-
} from '../Button';
|
|
20
|
+
import { IconButton, TextButton } from '../Button';
|
|
28
21
|
import { CallParticipantListHeader } from './CallParticipantListHeader';
|
|
29
22
|
import { CallParticipantListing } from './CallParticipantListing';
|
|
30
23
|
import { EmptyParticipantSearchList } from './EmptyParticipantSearchList';
|
|
@@ -97,12 +90,6 @@ export const CallParticipantsList = ({
|
|
|
97
90
|
/>
|
|
98
91
|
)}
|
|
99
92
|
</div>
|
|
100
|
-
<div className="str-video__participant-list__footer">
|
|
101
|
-
<CopyToClipboardButtonWithPopup
|
|
102
|
-
Button={InviteLinkButton}
|
|
103
|
-
copyValue={typeof window !== 'undefined' ? window.location.href : ''}
|
|
104
|
-
/>
|
|
105
|
-
</div>
|
|
106
93
|
</div>
|
|
107
94
|
);
|
|
108
95
|
};
|
|
@@ -123,7 +110,6 @@ const CallParticipantListContentHeader = ({
|
|
|
123
110
|
return (
|
|
124
111
|
<div className="str-video__participant-list__content-header">
|
|
125
112
|
<div className="str-video__participant-list__content-header-title">
|
|
126
|
-
<span>{UserListTypes[userListType]}</span>
|
|
127
113
|
{userListType === 'active' && (
|
|
128
114
|
<Restricted
|
|
129
115
|
requiredGrants={[OwnCapability.MUTE_USERS]}
|
|
@@ -234,23 +220,7 @@ const BlockedUsersSearchResults = ({
|
|
|
234
220
|
};
|
|
235
221
|
|
|
236
222
|
const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
|
|
237
|
-
(props, ref)
|
|
223
|
+
function ToggleButton(props, ref) {
|
|
238
224
|
return <IconButton enabled={props.menuShown} icon="filter" ref={ref} />;
|
|
239
225
|
},
|
|
240
226
|
);
|
|
241
|
-
|
|
242
|
-
const InviteLinkButton = forwardRef(
|
|
243
|
-
(
|
|
244
|
-
{ className, ...props }: ComponentProps<'button'>,
|
|
245
|
-
ref: ForwardedRef<HTMLButtonElement>,
|
|
246
|
-
) => (
|
|
247
|
-
<button
|
|
248
|
-
{...props}
|
|
249
|
-
className={clsx('str-video__invite-link-button', className)}
|
|
250
|
-
ref={ref}
|
|
251
|
-
>
|
|
252
|
-
<div className="str-video__invite-participant-icon" />
|
|
253
|
-
<div className="str-video__invite-link-button__text">Invite Link</div>
|
|
254
|
-
</button>
|
|
255
|
-
),
|
|
256
|
-
);
|
|
@@ -51,12 +51,30 @@ export const CallRecordingList = ({
|
|
|
51
51
|
{loading ? (
|
|
52
52
|
<LoadingCallRecordingList callRecordings={callRecordings} />
|
|
53
53
|
) : callRecordings.length ? (
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
<>
|
|
55
|
+
<ul className="str-video__call-recording-list__list">
|
|
56
|
+
<li className="str-video__call-recording-list__item">
|
|
57
|
+
<div className="str-video__call-recording-list__filename">
|
|
58
|
+
Name
|
|
59
|
+
</div>
|
|
60
|
+
<div className="str-video__call-recording-list__time">
|
|
61
|
+
Start time
|
|
62
|
+
</div>
|
|
63
|
+
<div className="str-video__call-recording-list__time">
|
|
64
|
+
End time
|
|
65
|
+
</div>
|
|
66
|
+
<div className="str-video__call-recording-list__download"></div>
|
|
67
|
+
</li>
|
|
68
|
+
</ul>
|
|
69
|
+
<ul className="str-video__call-recording-list__list">
|
|
70
|
+
{callRecordings.map((recording) => (
|
|
71
|
+
<CallRecordingListItem
|
|
72
|
+
recording={recording}
|
|
73
|
+
key={recording.filename}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</ul>
|
|
77
|
+
</>
|
|
60
78
|
) : (
|
|
61
79
|
<EmptyCallRecordingList />
|
|
62
80
|
)}
|