@stream-io/video-react-sdk 1.0.7 → 1.0.8
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 +10 -0
- package/dist/css/styles.css +3 -3
- package/dist/index.cjs.js +139 -87
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +140 -88
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/Button/CompositeButton.d.ts +1 -0
- package/dist/src/components/CallControls/CancelCallButton.d.ts +2 -1
- package/dist/src/components/CallControls/ScreenShareButton.d.ts +3 -2
- package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -2
- package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -2
- package/dist/src/components/Menu/MenuToggle.d.ts +2 -1
- package/dist/src/components/Tooltip/WithTooltip.d.ts +4 -2
- package/dist/src/utilities/callControlHandler.d.ts +16 -0
- package/package.json +2 -2
- package/src/components/Button/CompositeButton.tsx +3 -0
- package/src/components/CallControls/CallControls.tsx +3 -3
- package/src/components/CallControls/CancelCallButton.tsx +12 -8
- package/src/components/CallControls/ReactionsButton.tsx +14 -9
- package/src/components/CallControls/RecordCallButton.tsx +21 -15
- package/src/components/CallControls/ScreenShareButton.tsx +34 -26
- package/src/components/CallControls/ToggleAudioButton.tsx +84 -56
- package/src/components/CallControls/ToggleVideoButton.tsx +87 -59
- package/src/components/DeviceSettings/DeviceSelector.tsx +4 -0
- package/src/components/Menu/MenuToggle.tsx +9 -0
- package/src/components/Tooltip/WithTooltip.tsx +7 -2
- package/src/utilities/callControlHandler.ts +43 -0
|
@@ -10,10 +10,18 @@ import { DeviceSelectorVideo } from '../DeviceSettings';
|
|
|
10
10
|
import { PermissionNotification } from '../Notification';
|
|
11
11
|
import { useRequestPermission } from '../../hooks';
|
|
12
12
|
import { Icon } from '../Icon';
|
|
13
|
+
import { WithTooltip } from '../Tooltip';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
PropsWithErrorHandler,
|
|
17
|
+
createCallControlHandler,
|
|
18
|
+
} from '../../utilities/callControlHandler';
|
|
13
19
|
|
|
14
|
-
export type ToggleVideoPreviewButtonProps =
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
export type ToggleVideoPreviewButtonProps = PropsWithErrorHandler<
|
|
21
|
+
Pick<
|
|
22
|
+
IconButtonWithMenuProps,
|
|
23
|
+
'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
|
|
24
|
+
>
|
|
17
25
|
>;
|
|
18
26
|
|
|
19
27
|
export const ToggleVideoPreviewButton = (
|
|
@@ -28,44 +36,55 @@ export const ToggleVideoPreviewButton = (
|
|
|
28
36
|
const { t } = useI18n();
|
|
29
37
|
const { useCameraState } = useCallStateHooks();
|
|
30
38
|
const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
|
|
39
|
+
const [tooltipDisabled, setTooltipDisabled] = useState(false);
|
|
40
|
+
const handleClick = createCallControlHandler(props, () => camera.toggle());
|
|
31
41
|
|
|
32
42
|
return (
|
|
33
|
-
<
|
|
34
|
-
active={optimisticIsMute}
|
|
35
|
-
caption={caption}
|
|
36
|
-
className={clsx(!hasBrowserPermission && 'str-video__device-unavailable')}
|
|
43
|
+
<WithTooltip
|
|
37
44
|
title={
|
|
38
45
|
!hasBrowserPermission
|
|
39
46
|
? t('Check your browser video permissions')
|
|
40
|
-
: caption
|
|
41
|
-
}
|
|
42
|
-
variant="secondary"
|
|
43
|
-
data-testid={
|
|
44
|
-
optimisticIsMute
|
|
45
|
-
? 'preview-video-unmute-button'
|
|
46
|
-
: 'preview-video-mute-button'
|
|
47
|
+
: caption ?? t('Video')
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
disabled={!hasBrowserPermission}
|
|
50
|
-
Menu={Menu}
|
|
51
|
-
menuPlacement={menuPlacement}
|
|
52
|
-
{...restCompositeButtonProps}
|
|
49
|
+
tooltipDisabled={tooltipDisabled}
|
|
53
50
|
>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
<CompositeButton
|
|
52
|
+
active={optimisticIsMute}
|
|
53
|
+
caption={caption}
|
|
54
|
+
className={clsx(
|
|
55
|
+
!hasBrowserPermission && 'str-video__device-unavailable',
|
|
56
|
+
)}
|
|
57
|
+
variant="secondary"
|
|
58
|
+
data-testid={
|
|
59
|
+
optimisticIsMute
|
|
60
|
+
? 'preview-video-unmute-button'
|
|
61
|
+
: 'preview-video-mute-button'
|
|
62
|
+
}
|
|
63
|
+
onClick={handleClick}
|
|
64
|
+
disabled={!hasBrowserPermission}
|
|
65
|
+
Menu={Menu}
|
|
66
|
+
menuPlacement={menuPlacement}
|
|
67
|
+
onMenuToggle={(shown) => setTooltipDisabled(shown)}
|
|
68
|
+
{...restCompositeButtonProps}
|
|
69
|
+
>
|
|
70
|
+
<Icon icon={!optimisticIsMute ? 'camera' : 'camera-off'} />
|
|
71
|
+
{!hasBrowserPermission && (
|
|
72
|
+
<span
|
|
73
|
+
className="str-video__no-media-permission"
|
|
74
|
+
title={t('Check your browser video permissions')}
|
|
75
|
+
children="!"
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</CompositeButton>
|
|
79
|
+
</WithTooltip>
|
|
63
80
|
);
|
|
64
81
|
};
|
|
65
82
|
|
|
66
|
-
type ToggleVideoPublishingButtonProps =
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
type ToggleVideoPublishingButtonProps = PropsWithErrorHandler<
|
|
84
|
+
Pick<
|
|
85
|
+
IconButtonWithMenuProps,
|
|
86
|
+
'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
|
|
87
|
+
>
|
|
69
88
|
>;
|
|
70
89
|
|
|
71
90
|
export const ToggleVideoPublishingButton = (
|
|
@@ -86,6 +105,14 @@ export const ToggleVideoPublishingButton = (
|
|
|
86
105
|
const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
|
|
87
106
|
const callSettings = useCallSettings();
|
|
88
107
|
const isPublishingVideoAllowed = callSettings?.video.enabled;
|
|
108
|
+
const [tooltipDisabled, setTooltipDisabled] = useState(false);
|
|
109
|
+
const handleClick = createCallControlHandler(props, async () => {
|
|
110
|
+
if (!hasPermission) {
|
|
111
|
+
await requestPermission();
|
|
112
|
+
} else {
|
|
113
|
+
await camera.toggle();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
89
116
|
|
|
90
117
|
return (
|
|
91
118
|
<Restricted requiredGrants={[OwnCapability.SEND_VIDEO]}>
|
|
@@ -98,10 +125,7 @@ export const ToggleVideoPublishingButton = (
|
|
|
98
125
|
)}
|
|
99
126
|
messageRevoked={t('You can no longer share your video.')}
|
|
100
127
|
>
|
|
101
|
-
<
|
|
102
|
-
active={optimisticIsMute}
|
|
103
|
-
caption={caption}
|
|
104
|
-
variant="secondary"
|
|
128
|
+
<WithTooltip
|
|
105
129
|
title={
|
|
106
130
|
!hasPermission
|
|
107
131
|
? t('You have no permission to share your video')
|
|
@@ -111,31 +135,35 @@ export const ToggleVideoPublishingButton = (
|
|
|
111
135
|
? t('Video publishing is disabled by the system')
|
|
112
136
|
: caption || t('Video')
|
|
113
137
|
}
|
|
114
|
-
|
|
115
|
-
!hasBrowserPermission || !hasPermission || !isPublishingVideoAllowed
|
|
116
|
-
}
|
|
117
|
-
data-testid={
|
|
118
|
-
optimisticIsMute ? 'video-unmute-button' : 'video-mute-button'
|
|
119
|
-
}
|
|
120
|
-
onClick={async () => {
|
|
121
|
-
if (!hasPermission) {
|
|
122
|
-
await requestPermission();
|
|
123
|
-
} else {
|
|
124
|
-
await camera.toggle();
|
|
125
|
-
}
|
|
126
|
-
}}
|
|
127
|
-
Menu={Menu}
|
|
128
|
-
menuPlacement={menuPlacement}
|
|
129
|
-
menuOffset={16}
|
|
130
|
-
{...restCompositeButtonProps}
|
|
138
|
+
tooltipDisabled={tooltipDisabled}
|
|
131
139
|
>
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
<CompositeButton
|
|
141
|
+
active={optimisticIsMute}
|
|
142
|
+
caption={caption}
|
|
143
|
+
variant="secondary"
|
|
144
|
+
disabled={
|
|
145
|
+
!hasBrowserPermission ||
|
|
146
|
+
!hasPermission ||
|
|
147
|
+
!isPublishingVideoAllowed
|
|
148
|
+
}
|
|
149
|
+
data-testid={
|
|
150
|
+
optimisticIsMute ? 'video-unmute-button' : 'video-mute-button'
|
|
151
|
+
}
|
|
152
|
+
onClick={handleClick}
|
|
153
|
+
Menu={Menu}
|
|
154
|
+
menuPlacement={menuPlacement}
|
|
155
|
+
menuOffset={16}
|
|
156
|
+
onMenuToggle={(shown) => setTooltipDisabled(shown)}
|
|
157
|
+
{...restCompositeButtonProps}
|
|
158
|
+
>
|
|
159
|
+
<Icon icon={optimisticIsMute ? 'camera-off' : 'camera'} />
|
|
160
|
+
{(!hasBrowserPermission ||
|
|
161
|
+
!hasPermission ||
|
|
162
|
+
!isPublishingVideoAllowed) && (
|
|
163
|
+
<span className="str-video__no-media-permission">!</span>
|
|
164
|
+
)}
|
|
165
|
+
</CompositeButton>
|
|
166
|
+
</WithTooltip>
|
|
139
167
|
</PermissionNotification>
|
|
140
168
|
</Restricted>
|
|
141
169
|
);
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import { ChangeEventHandler, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect';
|
|
5
|
+
import { useMenuContext } from '../Menu';
|
|
5
6
|
|
|
6
7
|
type DeviceSelectorOptionProps = {
|
|
7
8
|
id: string;
|
|
@@ -64,6 +65,8 @@ const DeviceSelectorList = (props: {
|
|
|
64
65
|
onChange,
|
|
65
66
|
} = props;
|
|
66
67
|
|
|
68
|
+
const { close } = useMenuContext();
|
|
69
|
+
|
|
67
70
|
// sometimes the browser (Chrome) will report the system-default device
|
|
68
71
|
// with an id of 'default'. In case when it doesn't, we'll select the first
|
|
69
72
|
// available device.
|
|
@@ -100,6 +103,7 @@ const DeviceSelectorList = (props: {
|
|
|
100
103
|
key={device.deviceId}
|
|
101
104
|
onChange={(e) => {
|
|
102
105
|
onChange?.(e.target.value);
|
|
106
|
+
close?.();
|
|
103
107
|
}}
|
|
104
108
|
name={type}
|
|
105
109
|
selected={
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import {
|
|
@@ -34,6 +35,7 @@ export type MenuToggleProps<E extends HTMLElement> = PropsWithChildren<{
|
|
|
34
35
|
strategy?: Strategy;
|
|
35
36
|
offset?: number;
|
|
36
37
|
visualType?: MenuVisualType;
|
|
38
|
+
onToggle?: (menuShown: boolean) => void;
|
|
37
39
|
}>;
|
|
38
40
|
|
|
39
41
|
export type MenuContextValue = {
|
|
@@ -84,8 +86,11 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
84
86
|
offset,
|
|
85
87
|
visualType = MenuVisualType.MENU,
|
|
86
88
|
children,
|
|
89
|
+
onToggle,
|
|
87
90
|
}: MenuToggleProps<E>) => {
|
|
88
91
|
const [menuShown, setMenuShown] = useState(false);
|
|
92
|
+
const toggleHandler = useRef(onToggle);
|
|
93
|
+
toggleHandler.current = onToggle;
|
|
89
94
|
|
|
90
95
|
const { floating, domReference, refs, x, y } = useFloatingUIPreset({
|
|
91
96
|
placement,
|
|
@@ -97,8 +102,10 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
97
102
|
const handleClick = (event: MouseEvent) => {
|
|
98
103
|
if (!floating && domReference?.contains(event.target as Node)) {
|
|
99
104
|
setMenuShown(true);
|
|
105
|
+
toggleHandler.current?.(true);
|
|
100
106
|
} else if (floating && !floating?.contains(event.target as Node)) {
|
|
101
107
|
setMenuShown(false);
|
|
108
|
+
toggleHandler.current?.(false);
|
|
102
109
|
}
|
|
103
110
|
};
|
|
104
111
|
|
|
@@ -109,6 +116,7 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
109
116
|
!event.ctrlKey
|
|
110
117
|
) {
|
|
111
118
|
setMenuShown(false);
|
|
119
|
+
toggleHandler.current?.(false);
|
|
112
120
|
}
|
|
113
121
|
};
|
|
114
122
|
document?.addEventListener('click', handleClick, { capture: true });
|
|
@@ -135,6 +143,7 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
135
143
|
left: x ?? 0,
|
|
136
144
|
overflowY: 'auto',
|
|
137
145
|
}}
|
|
146
|
+
role="menu"
|
|
138
147
|
children={children}
|
|
139
148
|
/>
|
|
140
149
|
) : null}
|
|
@@ -3,13 +3,16 @@ import { Tooltip, TooltipProps } from './Tooltip';
|
|
|
3
3
|
import { useEnterLeaveHandlers } from './hooks';
|
|
4
4
|
|
|
5
5
|
type WithPopupProps = ComponentProps<'div'> &
|
|
6
|
-
Omit<TooltipProps<HTMLDivElement>, 'referenceElement'
|
|
6
|
+
Omit<TooltipProps<HTMLDivElement>, 'referenceElement' | 'children'> & {
|
|
7
|
+
tooltipDisabled?: boolean;
|
|
8
|
+
};
|
|
7
9
|
|
|
8
10
|
// todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
|
|
9
11
|
export const WithTooltip = ({
|
|
10
12
|
title,
|
|
11
13
|
tooltipClassName,
|
|
12
14
|
tooltipPlacement,
|
|
15
|
+
tooltipDisabled,
|
|
13
16
|
...props
|
|
14
17
|
}: WithPopupProps) => {
|
|
15
18
|
const { handleMouseEnter, handleMouseLeave, tooltipVisible } =
|
|
@@ -17,12 +20,14 @@ export const WithTooltip = ({
|
|
|
17
20
|
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLDivElement | null>(
|
|
18
21
|
null,
|
|
19
22
|
);
|
|
23
|
+
const tooltipActuallyVisible =
|
|
24
|
+
!tooltipDisabled && Boolean(title) && tooltipVisible;
|
|
20
25
|
|
|
21
26
|
return (
|
|
22
27
|
<>
|
|
23
28
|
<Tooltip
|
|
24
29
|
referenceElement={tooltipAnchor}
|
|
25
|
-
visible={
|
|
30
|
+
visible={tooltipActuallyVisible}
|
|
26
31
|
tooltipClassName={tooltipClassName}
|
|
27
32
|
tooltipPlacement={tooltipPlacement}
|
|
28
33
|
>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getLogger } from '@stream-io/video-client';
|
|
2
|
+
|
|
3
|
+
export type PropsWithErrorHandler<T = unknown> = T & {
|
|
4
|
+
/**
|
|
5
|
+
* Will be called if the call control action failed with an error (e.g. user didn't grant a
|
|
6
|
+
* browser permission to enable a media device). If no callback is provided, just logs the error.
|
|
7
|
+
* @param error Exception which caused call control action to fail
|
|
8
|
+
*/
|
|
9
|
+
onError?: (error: unknown) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps an event handler, silencing and logging exceptions (excluding the NotAllowedError
|
|
14
|
+
* DOMException, which is a normal situation handled by the SDK)
|
|
15
|
+
*
|
|
16
|
+
* @param props component props, including the onError callback
|
|
17
|
+
* @param handler event handler to wrap
|
|
18
|
+
*/
|
|
19
|
+
export const createCallControlHandler = (
|
|
20
|
+
props: PropsWithErrorHandler,
|
|
21
|
+
handler: () => Promise<void>,
|
|
22
|
+
): (() => Promise<void>) => {
|
|
23
|
+
const logger = getLogger(['react-sdk']);
|
|
24
|
+
|
|
25
|
+
return async () => {
|
|
26
|
+
try {
|
|
27
|
+
await handler();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (props.onError) {
|
|
30
|
+
props.onError(error);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!isNotAllowedError(error)) {
|
|
35
|
+
logger('error', 'Call control handler failed', error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function isNotAllowedError(error: unknown): error is DOMException {
|
|
42
|
+
return error instanceof DOMException && error.name === 'NotAllowedError';
|
|
43
|
+
}
|