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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +304 -238
  2. package/README.md +5 -5
  3. package/dist/css/styles.css +952 -481
  4. package/dist/css/styles.css.map +1 -1
  5. package/dist/index.cjs.js +946 -639
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.es.js +939 -639
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/components/Button/CompositeButton.d.ts +9 -11
  10. package/dist/src/components/Button/index.d.ts +0 -1
  11. package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
  12. package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
  13. package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
  14. package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
  15. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
  16. package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
  17. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
  18. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
  19. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
  20. package/dist/src/components/CallStats/CallStats.d.ts +25 -2
  21. package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
  22. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
  23. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
  24. package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
  25. package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
  26. package/dist/src/components/DropdownSelect/index.d.ts +1 -0
  27. package/dist/src/components/Icon/Icon.d.ts +2 -1
  28. package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
  29. package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
  30. package/dist/src/components/Notification/Notification.d.ts +1 -0
  31. package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
  32. package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
  33. package/dist/src/components/Notification/index.d.ts +1 -0
  34. package/dist/src/components/index.d.ts +2 -0
  35. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
  36. package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
  37. package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
  38. package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
  39. package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
  40. package/dist/src/translations/index.d.ts +9 -0
  41. package/package.json +7 -9
  42. package/src/components/Button/CompositeButton.tsx +78 -26
  43. package/src/components/Button/IconButton.tsx +22 -21
  44. package/src/components/Button/index.ts +0 -1
  45. package/src/components/CallControls/AcceptCallButton.tsx +1 -0
  46. package/src/components/CallControls/CallControls.tsx +2 -2
  47. package/src/components/CallControls/CallStatsButton.tsx +24 -7
  48. package/src/components/CallControls/CancelCallButton.tsx +102 -3
  49. package/src/components/CallControls/ReactionsButton.tsx +37 -17
  50. package/src/components/CallControls/RecordCallButton.tsx +131 -21
  51. package/src/components/CallControls/ScreenShareButton.tsx +29 -15
  52. package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
  53. package/src/components/CallControls/ToggleAudioOutputButton.tsx +20 -10
  54. package/src/components/CallControls/ToggleVideoButton.tsx +90 -33
  55. package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
  56. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
  57. package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
  58. package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
  59. package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
  60. package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
  61. package/src/components/CallStats/CallStats.tsx +167 -10
  62. package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
  63. package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
  64. package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
  65. package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
  66. package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
  67. package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
  68. package/src/components/DropdownSelect/index.ts +1 -0
  69. package/src/components/Icon/Icon.tsx +7 -2
  70. package/src/components/Menu/GenericMenu.tsx +25 -3
  71. package/src/components/Menu/MenuToggle.tsx +79 -14
  72. package/src/components/Notification/Notification.tsx +8 -0
  73. package/src/components/Notification/PermissionNotification.tsx +2 -1
  74. package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
  75. package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
  76. package/src/components/Notification/index.ts +1 -0
  77. package/src/components/Permissions/PermissionRequests.tsx +9 -21
  78. package/src/components/Search/hooks/useSearch.ts +5 -1
  79. package/src/components/index.ts +2 -0
  80. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
  81. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
  82. package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
  83. package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
  84. package/src/core/components/ParticipantView/index.ts +1 -0
  85. package/src/core/components/Video/BaseVideo.tsx +1 -1
  86. package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
  87. package/src/hooks/useFloatingUIPreset.ts +3 -2
  88. package/src/hooks/useRequestPermission.ts +2 -1
  89. package/src/translations/en.json +9 -0
  90. package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
  91. package/src/components/Button/CopyToClipboardButton.tsx +0 -129
@@ -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 message={message} isVisible={isSpeakingWhileMuted}>
26
+ <Notification
27
+ message={message}
28
+ isVisible={isSpeakingWhileMuted}
29
+ placement={placement || 'top-start'}
30
+ >
23
31
  {children}
24
32
  </Notification>
25
33
  );
@@ -1,3 +1,4 @@
1
1
  export * from './Notification';
2
2
  export * from './PermissionNotification';
3
3
  export * from './SpeakingWhileMutedNotification';
4
+ export * from './RecordingInProgressNotification';
@@ -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
- const unsubscribe = call.on(
57
- 'call.permission_request',
58
- (event: StreamVideoEvent) => {
59
- if (event.type !== 'call.permission_request') return;
60
- if (event.user.id !== localUserId) {
61
- setPermissionRequests((requests) =>
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) return setSearchResults([]);
28
+ if (!searchQuery.length) {
29
+ setSearchQueryInProgress(false);
30
+ setSearchResults([]);
31
+ return;
32
+ }
29
33
 
30
34
  setSearchQueryInProgress(true);
31
35
 
@@ -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-end',
69
+ menuPlacement = 'bottom-start',
66
70
  showMenuButton = true,
71
+ ParticipantActionsContextMenu = DefaultParticipantActionsContextMenu,
67
72
  }: DefaultParticipantViewUIProps) => {
68
- const { participant, participantViewElement, trackType, videoElement } =
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
- <div className="str-video__participant-details">
137
- <span className="str-video__participant-details__name">
138
- {name || userId}
139
- {indicatorsVisible && isDominantSpeaker && (
140
- <span
141
- className="str-video__participant-details__name--dominant_speaker"
142
- title={t('Dominant speaker')}
143
- />
144
- )}
145
- {indicatorsVisible && (
146
- <Notification
147
- isVisible={
148
- isLocalParticipant &&
149
- connectionQuality === SfuModels.ConnectionQuality.POOR
150
- }
151
- message={t('Poor connection quality')}
152
- >
153
- {connectionQualityAsString && (
154
- <span
155
- className={clsx(
156
- 'str-video__participant-details__connection-quality',
157
- `str-video__participant-details__connection-quality--${connectionQualityAsString}`,
158
- )}
159
- title={connectionQualityAsString}
160
- />
161
- )}
162
- </Notification>
163
- )}
164
- {indicatorsVisible && !hasAudio && (
165
- <span className="str-video__participant-details__name--audio-muted" />
166
- )}
167
- {indicatorsVisible && !hasVideo && (
168
- <span className="str-video__participant-details__name--video-muted" />
169
- )}
170
- {indicatorsVisible && canUnpin && (
171
- // TODO: remove this monstrosity once we have a proper design
172
- <span
173
- title={t('Unpin')}
174
- onClick={() => call?.unpin(sessionId)}
175
- style={{ cursor: 'pointer' }}
176
- className="str-video__participant-details__name--pinned"
177
- />
178
- )}
179
- </span>
180
- </div>
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: HTMLDivElement | null;
8
- videoElement: HTMLVideoElement | null;
9
- videoPlaceholderElement: HTMLDivElement | null;
7
+ participantViewElement?: HTMLDivElement | null;
8
+ videoElement?: HTMLVideoElement | null;
9
+ videoPlaceholderElement?: HTMLDivElement | null;
10
10
  };
11
11
 
12
12
  export const ParticipantViewContext = createContext<
@@ -1,3 +1,4 @@
1
+ export * from './ParticipantActionsContextMenu';
1
2
  export * from './ParticipantView';
2
3
  export * from './ParticipantViewContext';
3
4
  export * from './DefaultParticipantViewUI';
@@ -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
- <div className="str-video__video-placeholder__initials-fallback">
21
- <div>{name[0]}</div>
22
- </div>
20
+ <InitialsFallback name={name} />
23
21
  ) : (
24
- <div>{t('Video is disabled')}</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
- }: Pick<UseFloatingData, 'placement' | 'strategy'>) => {
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(10),
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, useHasPermissions } from '@stream-io/video-react-bindings';
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