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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +297 -238
  2. package/README.md +5 -5
  3. package/dist/css/styles.css +952 -481
  4. package/dist/css/styles.css.map +1 -1
  5. package/dist/index.cjs.js +946 -639
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.es.js +939 -639
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/components/Button/CompositeButton.d.ts +9 -11
  10. package/dist/src/components/Button/index.d.ts +0 -1
  11. package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
  12. package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
  13. package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
  14. package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
  15. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
  16. package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
  17. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
  18. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
  19. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
  20. package/dist/src/components/CallStats/CallStats.d.ts +25 -2
  21. package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
  22. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
  23. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
  24. package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
  25. package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
  26. package/dist/src/components/DropdownSelect/index.d.ts +1 -0
  27. package/dist/src/components/Icon/Icon.d.ts +2 -1
  28. package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
  29. package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
  30. package/dist/src/components/Notification/Notification.d.ts +1 -0
  31. package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
  32. package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
  33. package/dist/src/components/Notification/index.d.ts +1 -0
  34. package/dist/src/components/index.d.ts +2 -0
  35. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
  36. package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
  37. package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
  38. package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
  39. package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
  40. package/dist/src/translations/index.d.ts +9 -0
  41. package/package.json +7 -9
  42. package/src/components/Button/CompositeButton.tsx +78 -26
  43. package/src/components/Button/IconButton.tsx +22 -21
  44. package/src/components/Button/index.ts +0 -1
  45. package/src/components/CallControls/AcceptCallButton.tsx +1 -0
  46. package/src/components/CallControls/CallControls.tsx +2 -2
  47. package/src/components/CallControls/CallStatsButton.tsx +24 -7
  48. package/src/components/CallControls/CancelCallButton.tsx +102 -3
  49. package/src/components/CallControls/ReactionsButton.tsx +37 -17
  50. package/src/components/CallControls/RecordCallButton.tsx +131 -21
  51. package/src/components/CallControls/ScreenShareButton.tsx +29 -15
  52. package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
  53. package/src/components/CallControls/ToggleAudioOutputButton.tsx +14 -10
  54. package/src/components/CallControls/ToggleVideoButton.tsx +83 -33
  55. package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
  56. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
  57. package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
  58. package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
  59. package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
  60. package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
  61. package/src/components/CallStats/CallStats.tsx +167 -10
  62. package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
  63. package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
  64. package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
  65. package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
  66. package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
  67. package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
  68. package/src/components/DropdownSelect/index.ts +1 -0
  69. package/src/components/Icon/Icon.tsx +7 -2
  70. package/src/components/Menu/GenericMenu.tsx +25 -3
  71. package/src/components/Menu/MenuToggle.tsx +79 -14
  72. package/src/components/Notification/Notification.tsx +8 -0
  73. package/src/components/Notification/PermissionNotification.tsx +2 -1
  74. package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
  75. package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
  76. package/src/components/Notification/index.ts +1 -0
  77. package/src/components/Permissions/PermissionRequests.tsx +9 -21
  78. package/src/components/Search/hooks/useSearch.ts +5 -1
  79. package/src/components/index.ts +2 -0
  80. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
  81. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
  82. package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
  83. package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
  84. package/src/core/components/ParticipantView/index.ts +1 -0
  85. package/src/core/components/Video/BaseVideo.tsx +1 -1
  86. package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
  87. package/src/hooks/useFloatingUIPreset.ts +3 -2
  88. package/src/hooks/useRequestPermission.ts +2 -1
  89. package/src/translations/en.json +9 -0
  90. package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
  91. package/src/components/Button/CopyToClipboardButton.tsx +0 -129
@@ -1,14 +1,15 @@
1
- import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
1
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
2
2
  import { DeviceSelector } from './DeviceSelector';
3
3
 
4
4
  export type DeviceSelectorAudioInputProps = {
5
5
  title?: string;
6
+ visualType?: 'list' | 'dropdown';
6
7
  };
7
8
 
8
9
  export const DeviceSelectorAudioInput = ({
9
10
  title,
11
+ visualType,
10
12
  }: DeviceSelectorAudioInputProps) => {
11
- const { t } = useI18n();
12
13
  const { useMicrophoneState } = useCallStateHooks();
13
14
  const { microphone, selectedDevice, devices } = useMicrophoneState();
14
15
 
@@ -16,22 +17,26 @@ export const DeviceSelectorAudioInput = ({
16
17
  <DeviceSelector
17
18
  devices={devices || []}
18
19
  selectedDeviceId={selectedDevice}
20
+ type="audioinput"
19
21
  onChange={async (deviceId) => {
20
22
  await microphone.select(deviceId);
21
23
  }}
22
- title={title || t('Select a Mic')}
24
+ title={title}
25
+ visualType={visualType}
26
+ icon="mic"
23
27
  />
24
28
  );
25
29
  };
26
30
 
27
31
  export type DeviceSelectorAudioOutputProps = {
28
32
  title?: string;
33
+ visualType?: 'list' | 'dropdown';
29
34
  };
30
35
 
31
36
  export const DeviceSelectorAudioOutput = ({
32
37
  title,
38
+ visualType,
33
39
  }: DeviceSelectorAudioOutputProps) => {
34
- const { t } = useI18n();
35
40
  const { useSpeakerState } = useCallStateHooks();
36
41
  const { speaker, selectedDevice, devices, isDeviceSelectionSupported } =
37
42
  useSpeakerState();
@@ -41,11 +46,14 @@ export const DeviceSelectorAudioOutput = ({
41
46
  return (
42
47
  <DeviceSelector
43
48
  devices={devices}
49
+ type="audiooutput"
44
50
  selectedDeviceId={selectedDevice}
45
51
  onChange={(deviceId) => {
46
52
  speaker.select(deviceId);
47
53
  }}
48
- title={title || t('Select Speakers')}
54
+ title={title}
55
+ visualType={visualType}
56
+ icon="speaker"
49
57
  />
50
58
  );
51
59
  };
@@ -1,23 +1,29 @@
1
1
  import { DeviceSelector } from './DeviceSelector';
2
- import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
2
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
3
3
 
4
4
  export type DeviceSelectorVideoProps = {
5
5
  title?: string;
6
+ visualType?: 'list' | 'dropdown';
6
7
  };
7
8
 
8
- export const DeviceSelectorVideo = ({ title }: DeviceSelectorVideoProps) => {
9
- const { t } = useI18n();
9
+ export const DeviceSelectorVideo = ({
10
+ title,
11
+ visualType,
12
+ }: DeviceSelectorVideoProps) => {
10
13
  const { useCameraState } = useCallStateHooks();
11
14
  const { camera, devices, selectedDevice } = useCameraState();
12
15
 
13
16
  return (
14
17
  <DeviceSelector
15
18
  devices={devices || []}
19
+ type="videoinput"
16
20
  selectedDeviceId={selectedDevice}
17
21
  onChange={async (deviceId) => {
18
22
  await camera.select(deviceId);
19
23
  }}
20
- title={title || t('Select a Camera')}
24
+ title={title}
25
+ visualType={visualType}
26
+ icon="camera"
21
27
  />
22
28
  );
23
29
  };
@@ -1,44 +1,56 @@
1
1
  import { forwardRef } from 'react';
2
2
  import { useI18n } from '@stream-io/video-react-bindings';
3
- import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
3
+ import clsx from 'clsx';
4
+ import { MenuToggle, MenuVisualType, ToggleMenuButtonProps } from '../Menu';
4
5
  import {
5
6
  DeviceSelectorAudioInput,
6
7
  DeviceSelectorAudioOutput,
7
8
  } from './DeviceSelectorAudio';
8
9
  import { DeviceSelectorVideo } from './DeviceSelectorVideo';
9
10
  import { IconButton } from '../Button';
10
- import clsx from 'clsx';
11
11
 
12
- export const DeviceSettings = () => {
12
+ export type DeviceSettingsProps = {
13
+ visualType?: MenuVisualType;
14
+ };
15
+
16
+ export const DeviceSettings = ({
17
+ visualType = MenuVisualType.MENU,
18
+ }: DeviceSettingsProps) => {
13
19
  return (
14
- <MenuToggle placement="bottom-end" ToggleButton={ToggleMenuButton}>
20
+ <MenuToggle
21
+ placement="bottom-end"
22
+ ToggleButton={ToggleDeviceSettingsMenuButton}
23
+ visualType={visualType}
24
+ >
15
25
  <Menu />
16
26
  </MenuToggle>
17
27
  );
18
28
  };
19
29
 
20
- const Menu = () => (
21
- <div className="str-video__device-settings">
22
- <DeviceSelectorVideo />
23
- <DeviceSelectorAudioInput />
24
- <DeviceSelectorAudioOutput />
25
- </div>
26
- );
27
-
28
- const ToggleMenuButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
29
- ({ menuShown }, ref) => {
30
- const { t } = useI18n();
30
+ const Menu = () => {
31
+ const { t } = useI18n();
32
+ return (
33
+ <div className="str-video__device-settings">
34
+ <DeviceSelectorVideo title={t('Select a Camera')} />
35
+ <DeviceSelectorAudioInput title={t('Select a Mic')} />
36
+ <DeviceSelectorAudioOutput title={t('Select Speakers')} />
37
+ </div>
38
+ );
39
+ };
31
40
 
32
- return (
33
- <IconButton
34
- className={clsx('str-video__device-settings__button', {
35
- 'str-video__device-settings__button--active': menuShown,
36
- })}
37
- title={t('Toggle device menu')}
38
- icon="device-settings"
39
- ref={ref}
40
- // tabindex={0}
41
- />
42
- );
43
- },
44
- );
41
+ const ToggleDeviceSettingsMenuButton = forwardRef<
42
+ HTMLButtonElement,
43
+ ToggleMenuButtonProps
44
+ >(function ToggleDeviceSettingsMenuButton({ menuShown }, ref) {
45
+ const { t } = useI18n();
46
+ return (
47
+ <IconButton
48
+ className={clsx('str-video__device-settings__button', {
49
+ 'str-video__device-settings__button--active': menuShown,
50
+ })}
51
+ title={t('Toggle device menu')}
52
+ icon="device-settings"
53
+ ref={ref}
54
+ />
55
+ );
56
+ });
@@ -0,0 +1,214 @@
1
+ import {
2
+ createContext,
3
+ ReactElement,
4
+ ReactNode,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import clsx from 'clsx';
12
+ import {
13
+ autoUpdate,
14
+ flip,
15
+ FloatingFocusManager,
16
+ FloatingList,
17
+ useClick,
18
+ useDismiss,
19
+ useFloating,
20
+ useInteractions,
21
+ useListItem,
22
+ useListNavigation,
23
+ useRole,
24
+ useTypeahead,
25
+ } from '@floating-ui/react';
26
+
27
+ import { Icon } from '../Icon';
28
+
29
+ interface SelectContextValue {
30
+ activeIndex: number | null;
31
+ selectedIndex: number | null;
32
+ getItemProps: ReturnType<typeof useInteractions>['getItemProps'];
33
+ handleSelect: (index: number | null) => void;
34
+ }
35
+
36
+ const SelectContext = createContext<SelectContextValue>(
37
+ {} as SelectContextValue,
38
+ );
39
+
40
+ const Select = (props: {
41
+ children: ReactNode;
42
+ icon?: string;
43
+ defaultSelectedLabel: string;
44
+ defaultSelectedIndex: number;
45
+ handleSelect: (index: number) => void;
46
+ }) => {
47
+ const {
48
+ children,
49
+ icon,
50
+ defaultSelectedLabel,
51
+ defaultSelectedIndex,
52
+ handleSelect: handleSelectProp,
53
+ } = props;
54
+ const [isOpen, setIsOpen] = useState(false);
55
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
56
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(
57
+ defaultSelectedIndex,
58
+ );
59
+ const [selectedLabel, setSelectedLabel] = useState<string | null>(
60
+ defaultSelectedLabel,
61
+ );
62
+
63
+ const { refs, context } = useFloating({
64
+ placement: 'bottom-start',
65
+ open: isOpen,
66
+ onOpenChange: setIsOpen,
67
+ whileElementsMounted: autoUpdate,
68
+ middleware: [flip()],
69
+ });
70
+
71
+ const elementsRef = useRef<Array<HTMLElement | null>>([]);
72
+ const labelsRef = useRef<Array<string | null>>([]);
73
+
74
+ const handleSelect = useCallback(
75
+ (index: number | null) => {
76
+ setSelectedIndex(index);
77
+ handleSelectProp(index || 0);
78
+ setIsOpen(false);
79
+ if (index !== null) {
80
+ setSelectedLabel(labelsRef.current[index]);
81
+ }
82
+ },
83
+ [handleSelectProp],
84
+ );
85
+
86
+ const handleTypeaheadMatch = (index: number | null) => {
87
+ if (isOpen) {
88
+ setActiveIndex(index);
89
+ } else {
90
+ handleSelect(index);
91
+ }
92
+ };
93
+
94
+ const listNav = useListNavigation(context, {
95
+ listRef: elementsRef,
96
+ activeIndex,
97
+ selectedIndex,
98
+ onNavigate: setActiveIndex,
99
+ });
100
+ const typeahead = useTypeahead(context, {
101
+ listRef: labelsRef,
102
+ activeIndex,
103
+ selectedIndex,
104
+ onMatch: handleTypeaheadMatch,
105
+ });
106
+ const click = useClick(context);
107
+ const dismiss = useDismiss(context);
108
+ const role = useRole(context, { role: 'listbox' });
109
+
110
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
111
+ [listNav, typeahead, click, dismiss, role],
112
+ );
113
+
114
+ const selectContext = useMemo(
115
+ () => ({
116
+ activeIndex,
117
+ selectedIndex,
118
+ getItemProps,
119
+ handleSelect,
120
+ }),
121
+ [activeIndex, selectedIndex, getItemProps, handleSelect],
122
+ );
123
+
124
+ return (
125
+ <div className="str-video__dropdown">
126
+ <div
127
+ className="str-video__dropdown-selected"
128
+ ref={refs.setReference}
129
+ tabIndex={0}
130
+ {...getReferenceProps()}
131
+ >
132
+ <label className="str-video__dropdown-selected__label">
133
+ {icon && (
134
+ <Icon className="str-video__dropdown-selected__icon" icon={icon} />
135
+ )}
136
+ {selectedLabel}
137
+ </label>
138
+ <Icon
139
+ className="str-video__dropdown-selected__chevron"
140
+ icon={isOpen ? 'chevron-up' : 'chevron-down'}
141
+ />
142
+ </div>
143
+ <SelectContext.Provider value={selectContext}>
144
+ {isOpen && (
145
+ <FloatingFocusManager context={context} modal={false}>
146
+ <div
147
+ className="str-video__dropdown-list"
148
+ ref={refs.setFloating}
149
+ {...getFloatingProps()}
150
+ >
151
+ <FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
152
+ {children}
153
+ </FloatingList>
154
+ </div>
155
+ </FloatingFocusManager>
156
+ )}
157
+ </SelectContext.Provider>
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export type DropDownSelectOptionProps = {
163
+ label: string;
164
+ selected?: boolean;
165
+ icon: string;
166
+ };
167
+
168
+ export const DropDownSelectOption = (props: DropDownSelectOptionProps) => {
169
+ const { selected, label, icon } = props;
170
+ const { getItemProps, handleSelect } = useContext(SelectContext);
171
+ const { ref, index } = useListItem();
172
+ return (
173
+ <div
174
+ className={clsx('str-video__dropdown-option', {
175
+ 'str-video__dropdown-option--selected': selected,
176
+ })}
177
+ ref={ref}
178
+ {...getItemProps({
179
+ onClick: () => handleSelect(index),
180
+ })}
181
+ >
182
+ <Icon className="str-video__dropdown-icon" icon={icon} />
183
+ <span className="str-video__dropdown-label">{label}</span>
184
+ </div>
185
+ );
186
+ };
187
+
188
+ export const DropDownSelect = (props: {
189
+ icon?: string;
190
+ defaultSelectedLabel: string;
191
+ defaultSelectedIndex: number;
192
+ handleSelect: (index: number) => void;
193
+ children:
194
+ | ReactElement<DropDownSelectOptionProps>
195
+ | ReactElement<DropDownSelectOptionProps>[];
196
+ }) => {
197
+ const {
198
+ children,
199
+ icon,
200
+ handleSelect,
201
+ defaultSelectedLabel,
202
+ defaultSelectedIndex,
203
+ } = props;
204
+ return (
205
+ <Select
206
+ icon={icon}
207
+ handleSelect={handleSelect}
208
+ defaultSelectedIndex={defaultSelectedIndex}
209
+ defaultSelectedLabel={defaultSelectedLabel}
210
+ >
211
+ {children}
212
+ </Select>
213
+ );
214
+ };
@@ -0,0 +1 @@
1
+ export * from './DropdownSelect';
@@ -2,10 +2,15 @@ import clsx from 'clsx';
2
2
 
3
3
  export type IconProps = {
4
4
  icon: string;
5
+ className?: string;
5
6
  };
6
7
 
7
- export const Icon = ({ icon }: IconProps) => (
8
+ export const Icon = ({ className, icon }: IconProps) => (
8
9
  <span
9
- className={clsx('str-video__icon', icon && `str-video__icon--${icon}`)}
10
+ className={clsx(
11
+ 'str-video__icon',
12
+ icon && `str-video__icon--${icon}`,
13
+ className,
14
+ )}
10
15
  />
11
16
  );
@@ -1,7 +1,29 @@
1
- import { ComponentProps, PropsWithChildren } from 'react';
1
+ import { ComponentProps, MouseEvent, PropsWithChildren, useRef } from 'react';
2
2
 
3
- export const GenericMenu = ({ children }: PropsWithChildren) => {
4
- return <ul className="str-video__generic-menu">{children}</ul>;
3
+ export const GenericMenu = ({
4
+ children,
5
+ onItemClick,
6
+ }: PropsWithChildren<{
7
+ onItemClick?: (e: MouseEvent) => void;
8
+ }>) => {
9
+ const ref = useRef<HTMLUListElement>(null);
10
+ return (
11
+ <ul
12
+ className="str-video__generic-menu"
13
+ ref={ref}
14
+ onClick={(e) => {
15
+ if (
16
+ onItemClick &&
17
+ e.target !== ref.current &&
18
+ ref.current?.contains(e.target as Node)
19
+ ) {
20
+ onItemClick(e);
21
+ }
22
+ }}
23
+ >
24
+ {children}
25
+ </ul>
26
+ );
5
27
  };
6
28
 
7
29
  export const GenericMenuButtonItem = ({
@@ -1,11 +1,20 @@
1
1
  import {
2
2
  ComponentType,
3
+ createContext,
4
+ ForwardedRef,
3
5
  PropsWithChildren,
6
+ useContext,
4
7
  useEffect,
8
+ useMemo,
5
9
  useState,
6
- ForwardedRef,
7
10
  } from 'react';
8
- import { Placement, Strategy } from '@floating-ui/react';
11
+ import {
12
+ FloatingOverlay,
13
+ FloatingPortal,
14
+ Placement,
15
+ Strategy,
16
+ UseFloatingReturn,
17
+ } from '@floating-ui/react';
9
18
 
10
19
  import { useFloatingUIPreset } from '../../hooks';
11
20
 
@@ -14,16 +23,66 @@ export type ToggleMenuButtonProps<E extends HTMLElement = HTMLButtonElement> = {
14
23
  ref: ForwardedRef<E>;
15
24
  };
16
25
 
26
+ export enum MenuVisualType {
27
+ PORTAL = 'portal',
28
+ MENU = 'menu',
29
+ }
30
+
17
31
  export type MenuToggleProps<E extends HTMLElement> = PropsWithChildren<{
18
32
  ToggleButton: ComponentType<ToggleMenuButtonProps<E>>;
19
33
  placement?: Placement;
20
34
  strategy?: Strategy;
35
+ offset?: number;
36
+ visualType?: MenuVisualType;
21
37
  }>;
22
38
 
39
+ export type MenuContextValue = {
40
+ close?: () => void;
41
+ };
42
+
43
+ /**
44
+ * Used to provide utility APIs to the components rendered inside the portal.
45
+ */
46
+ const MenuContext = createContext<MenuContextValue>({});
47
+
48
+ /**
49
+ * Access to the closes MenuContext.
50
+ */
51
+ export const useMenuContext = (): MenuContextValue => {
52
+ return useContext(MenuContext);
53
+ };
54
+
55
+ const MenuPortal = ({
56
+ children,
57
+ refs,
58
+ }: PropsWithChildren<{
59
+ refs: UseFloatingReturn['refs'];
60
+ }>) => {
61
+ const portalId = useMemo(
62
+ () => `str-video-portal-${Math.random().toString(36).substring(2, 9)}`,
63
+ [],
64
+ );
65
+
66
+ return (
67
+ <>
68
+ <div id={portalId} className="str-video__portal" />
69
+ <FloatingOverlay>
70
+ <FloatingPortal id={portalId}>
71
+ <div className="str-video__portal-content" ref={refs.setFloating}>
72
+ {children}
73
+ </div>
74
+ </FloatingPortal>
75
+ </FloatingOverlay>
76
+ </>
77
+ );
78
+ };
79
+
23
80
  export const MenuToggle = <E extends HTMLElement>({
24
81
  ToggleButton,
25
82
  placement = 'top-start',
26
83
  strategy = 'absolute',
84
+ offset,
85
+ visualType = MenuVisualType.MENU,
27
86
  children,
28
87
  }: MenuToggleProps<E>) => {
29
88
  const [menuShown, setMenuShown] = useState(false);
@@ -31,6 +90,7 @@ export const MenuToggle = <E extends HTMLElement>({
31
90
  const { floating, domReference, refs, x, y } = useFloatingUIPreset({
32
91
  placement,
33
92
  strategy,
93
+ offset,
34
94
  });
35
95
 
36
96
  useEffect(() => {
@@ -62,18 +122,23 @@ export const MenuToggle = <E extends HTMLElement>({
62
122
  return (
63
123
  <>
64
124
  {menuShown && (
65
- <div
66
- className="str-video__menu-container"
67
- ref={refs.setFloating}
68
- style={{
69
- position: strategy,
70
- top: y ?? 0,
71
- left: x ?? 0,
72
- overflowY: 'auto',
73
- }}
74
- >
75
- {children}
76
- </div>
125
+ <MenuContext.Provider value={{ close: () => setMenuShown(false) }}>
126
+ {visualType === MenuVisualType.PORTAL ? (
127
+ <MenuPortal refs={refs} children={children} />
128
+ ) : visualType === MenuVisualType.MENU ? (
129
+ <div
130
+ className="str-video__menu-container"
131
+ ref={refs.setFloating}
132
+ style={{
133
+ position: strategy,
134
+ top: y ?? 0,
135
+ left: x ?? 0,
136
+ overflowY: 'auto',
137
+ }}
138
+ children={children}
139
+ />
140
+ ) : null}
141
+ </MenuContext.Provider>
77
142
  )}
78
143
  <ToggleButton menuShown={menuShown} ref={refs.setReference} />
79
144
  </>
@@ -10,6 +10,7 @@ export type NotificationProps = {
10
10
  resetIsVisible?: () => void;
11
11
  placement?: Placement;
12
12
  iconClassName?: string | null;
13
+ close?: () => void;
13
14
  };
14
15
 
15
16
  export const Notification = (props: PropsWithChildren<NotificationProps>) => {
@@ -21,6 +22,7 @@ export const Notification = (props: PropsWithChildren<NotificationProps>) => {
21
22
  resetIsVisible,
22
23
  placement = 'top',
23
24
  iconClassName = 'str-video__notification__icon',
25
+ close,
24
26
  } = props;
25
27
 
26
28
  const { refs, x, y, strategy } = useFloatingUIPreset({
@@ -53,6 +55,12 @@ export const Notification = (props: PropsWithChildren<NotificationProps>) => {
53
55
  >
54
56
  {iconClassName && <i className={iconClassName} />}
55
57
  <span className="str-video__notification__message">{message}</span>
58
+ {close ? (
59
+ <i
60
+ className="str-video__icon str-video__icon--close str-video__notification__close"
61
+ onClick={close}
62
+ />
63
+ ) : null}
56
64
  </div>
57
65
  )}
58
66
  {children}
@@ -7,7 +7,7 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
- import { useHasPermissions } from '@stream-io/video-react-bindings';
10
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
11
11
 
12
12
  export type PermissionNotificationProps = PropsWithChildren<{
13
13
  /**
@@ -55,6 +55,7 @@ export const PermissionNotification = (props: PermissionNotificationProps) => {
55
55
  visibilityTimeout = 3500,
56
56
  children,
57
57
  } = props;
58
+ const { useHasPermissions } = useCallStateHooks();
58
59
  const hasPermission = useHasPermissions(permission);
59
60
  const prevHasPermission = useRef(hasPermission);
60
61
  const [showNotification, setShowNotification] = useState<
@@ -0,0 +1,40 @@
1
+ import { PropsWithChildren, useEffect, useState } from 'react';
2
+ import { useI18n } from '@stream-io/video-react-bindings';
3
+ import { useToggleCallRecording } from '../../hooks';
4
+ import { Notification } from './Notification';
5
+
6
+ export type RecordingInProgressNotificationProps = {
7
+ text?: string;
8
+ };
9
+
10
+ export const RecordingInProgressNotification = ({
11
+ children,
12
+ text,
13
+ }: PropsWithChildren<RecordingInProgressNotificationProps>) => {
14
+ const { t } = useI18n();
15
+ const { isCallRecordingInProgress } = useToggleCallRecording();
16
+
17
+ const [isVisible, setVisible] = useState(false);
18
+
19
+ const message = text ?? t('Recording in progress...');
20
+
21
+ useEffect(() => {
22
+ if (isCallRecordingInProgress) {
23
+ setVisible(true);
24
+ } else {
25
+ setVisible(false);
26
+ }
27
+ }, [isCallRecordingInProgress]);
28
+
29
+ return (
30
+ <Notification
31
+ message={message}
32
+ iconClassName="str-video__icon str-video__icon--recording-on"
33
+ isVisible={isVisible}
34
+ placement="top-start"
35
+ close={() => setVisible(false)}
36
+ >
37
+ {children}
38
+ </Notification>
39
+ );
40
+ };