@stream-io/video-react-sdk 0.4.25 → 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 +301 -236
  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,6 +1,6 @@
1
1
  import { SpeakingWhileMutedNotification } from '../Notification';
2
2
  import { RecordCallButton } from './RecordCallButton';
3
- import { CallStatsButton } from './CallStatsButton';
3
+ import { ReactionsButton } from './ReactionsButton';
4
4
  import { ScreenShareButton } from './ScreenShareButton';
5
5
  import { ToggleAudioPublishingButton } from './ToggleAudioButton';
6
6
  import { ToggleVideoPublishingButton } from './ToggleVideoButton';
@@ -13,7 +13,7 @@ export type CallControlsProps = {
13
13
  export const CallControls = ({ onLeave }: CallControlsProps) => (
14
14
  <div className="str-video__call-controls">
15
15
  <RecordCallButton />
16
- <CallStatsButton />
16
+ <ReactionsButton />
17
17
  <ScreenShareButton />
18
18
  <SpeakingWhileMutedNotification>
19
19
  <ToggleAudioPublishingButton />
@@ -1,8 +1,14 @@
1
1
  import { forwardRef } from 'react';
2
+ import { useI18n } from '@stream-io/video-react-bindings';
2
3
 
3
4
  import { CallStats } from '../CallStats';
4
- import { CompositeButton, IconButton } from '../Button/';
5
+ import { CompositeButton } from '../Button/';
5
6
  import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
7
+ import { Icon } from '../Icon';
8
+
9
+ export type CallStatsButtonProps = {
10
+ caption?: string;
11
+ };
6
12
 
7
13
  export const CallStatsButton = () => (
8
14
  <MenuToggle placement="top-end" ToggleButton={ToggleMenuButton}>
@@ -12,9 +18,20 @@ export const CallStatsButton = () => (
12
18
 
13
19
  const ToggleMenuButton = forwardRef<
14
20
  HTMLDivElement,
15
- ToggleMenuButtonProps<HTMLDivElement>
16
- >(({ menuShown }, ref) => (
17
- <CompositeButton ref={ref} active={menuShown} caption={'Stats'}>
18
- <IconButton icon="stats" title="Statistics" />
19
- </CompositeButton>
20
- ));
21
+ ToggleMenuButtonProps<HTMLDivElement> & CallStatsButtonProps
22
+ >(function ToggleMenuButton(props, ref) {
23
+ const { t } = useI18n();
24
+ const { caption, menuShown } = props;
25
+
26
+ return (
27
+ <CompositeButton
28
+ ref={ref}
29
+ active={menuShown}
30
+ caption={caption}
31
+ title={caption || t('Statistics')}
32
+ data-testid="stats-button"
33
+ >
34
+ <Icon icon="stats" />
35
+ </CompositeButton>
36
+ );
37
+ });
@@ -1,6 +1,65 @@
1
- import { MouseEventHandler, useCallback } from 'react';
2
- import { IconButton } from '../Button/';
3
- import { useCall } from '@stream-io/video-react-bindings';
1
+ import { forwardRef, MouseEventHandler, useCallback } from 'react';
2
+ import { OwnCapability } from '@stream-io/video-client';
3
+ import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
4
+
5
+ import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
6
+
7
+ import { IconButton } from '../Button';
8
+ import { Icon } from '../Icon';
9
+
10
+ const EndCallMenu = (props: {
11
+ onLeave: MouseEventHandler<HTMLButtonElement>;
12
+ onEnd: MouseEventHandler<HTMLButtonElement>;
13
+ }) => {
14
+ const { onLeave, onEnd } = props;
15
+ const { t } = useI18n();
16
+ return (
17
+ <div className="str-video__end-call__confirmation">
18
+ <button
19
+ className="str-video__button str-video__end-call__leave"
20
+ type="button"
21
+ data-testid="leave-call-button"
22
+ onClick={onLeave}
23
+ >
24
+ <Icon
25
+ className="str-video__button__icon str-video__end-call__leave-icon"
26
+ icon="logout"
27
+ />
28
+ {t('Leave call')}
29
+ </button>
30
+ <Restricted requiredGrants={[OwnCapability.END_CALL]}>
31
+ <button
32
+ className="str-video__button str-video__end-call__end"
33
+ type="button"
34
+ data-testid="end-call-for-all-button"
35
+ onClick={onEnd}
36
+ >
37
+ <Icon
38
+ className="str-video__button__icon str-video__end-call__end-icon"
39
+ icon="call-end"
40
+ />
41
+ {t('End call for all')}
42
+ </button>
43
+ </Restricted>
44
+ </div>
45
+ );
46
+ };
47
+
48
+ const CancelCallToggleMenuButton = forwardRef<
49
+ HTMLButtonElement,
50
+ ToggleMenuButtonProps
51
+ >(function CancelCallToggleMenuButton(props, ref) {
52
+ const { t } = useI18n();
53
+ return (
54
+ <IconButton
55
+ icon="call-end"
56
+ variant="danger"
57
+ title={t('Leave call')}
58
+ data-testid="leave-call-button"
59
+ ref={ref}
60
+ />
61
+ );
62
+ });
4
63
 
5
64
  export type CancelCallButtonProps = {
6
65
  disabled?: boolean;
@@ -8,12 +67,50 @@ export type CancelCallButtonProps = {
8
67
  onLeave?: () => void;
9
68
  };
10
69
 
70
+ export const CancelCallConfirmButton = ({
71
+ onClick,
72
+ onLeave,
73
+ }: CancelCallButtonProps) => {
74
+ const call = useCall();
75
+
76
+ const handleLeave: MouseEventHandler<HTMLButtonElement> = useCallback(
77
+ async (e) => {
78
+ if (onClick) {
79
+ onClick(e);
80
+ } else if (call) {
81
+ await call.leave();
82
+ onLeave?.();
83
+ }
84
+ },
85
+ [onClick, onLeave, call],
86
+ );
87
+
88
+ const handleEndCall: MouseEventHandler<HTMLButtonElement> = useCallback(
89
+ async (e) => {
90
+ if (onClick) {
91
+ onClick(e);
92
+ } else if (call) {
93
+ await call.endCall();
94
+ onLeave?.();
95
+ }
96
+ },
97
+ [onClick, onLeave, call],
98
+ );
99
+
100
+ return (
101
+ <MenuToggle placement="top-start" ToggleButton={CancelCallToggleMenuButton}>
102
+ <EndCallMenu onEnd={handleEndCall} onLeave={handleLeave} />
103
+ </MenuToggle>
104
+ );
105
+ };
106
+
11
107
  export const CancelCallButton = ({
12
108
  disabled,
13
109
  onClick,
14
110
  onLeave,
15
111
  }: CancelCallButtonProps) => {
16
112
  const call = useCall();
113
+ const { t } = useI18n();
17
114
  const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback(
18
115
  async (e) => {
19
116
  if (onClick) {
@@ -30,6 +127,8 @@ export const CancelCallButton = ({
30
127
  disabled={disabled}
31
128
  icon="call-end"
32
129
  variant="danger"
130
+ title={t('Leave call')}
131
+ data-testid="cancel-call-button"
33
132
  onClick={handleClick}
34
133
  />
35
134
  );
@@ -1,8 +1,13 @@
1
+ import { forwardRef } from 'react';
2
+ import clsx from 'clsx';
3
+
1
4
  import { OwnCapability, StreamReaction } from '@stream-io/video-client';
2
5
  import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
3
6
 
4
- import { CompositeButton, IconButton } from '../Button';
7
+ import { MenuToggle, MenuVisualType, ToggleMenuButtonProps } from '../Menu';
8
+ import { CompositeButton } from '../Button';
5
9
  import { defaultEmojiReactionMap } from '../Reaction';
10
+ import { Icon } from '../Icon';
6
11
 
7
12
  export const defaultReactions: StreamReaction[] = [
8
13
  {
@@ -39,38 +44,53 @@ export interface ReactionsButtonProps {
39
44
  export const ReactionsButton = ({
40
45
  reactions = defaultReactions,
41
46
  }: ReactionsButtonProps) => {
42
- const { t } = useI18n();
43
-
44
47
  return (
45
48
  <Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
46
- <CompositeButton
47
- active={false}
48
- caption={t('Reactions')}
49
- menuPlacement="top-start"
50
- Menu={<DefaultReactionsMenu reactions={reactions} />}
49
+ <MenuToggle
50
+ placement="top"
51
+ ToggleButton={ToggleReactionsMenuButton}
52
+ visualType={MenuVisualType.MENU}
51
53
  >
52
- <IconButton
53
- icon="reactions"
54
- title={t('Reactions')}
55
- onClick={() => {
56
- console.log('Reactions');
57
- }}
58
- />
59
- </CompositeButton>
54
+ <DefaultReactionsMenu reactions={reactions} />
55
+ </MenuToggle>
60
56
  </Restricted>
61
57
  );
62
58
  };
63
59
 
60
+ const ToggleReactionsMenuButton = forwardRef<
61
+ HTMLDivElement,
62
+ ToggleMenuButtonProps
63
+ >(function ToggleReactionsMenuButton({ menuShown }, ref) {
64
+ const { t } = useI18n();
65
+ return (
66
+ <CompositeButton
67
+ ref={ref}
68
+ active={menuShown}
69
+ variant="primary"
70
+ title={t('Reactions')}
71
+ >
72
+ <Icon icon="reactions" />
73
+ </CompositeButton>
74
+ );
75
+ });
76
+
64
77
  export interface DefaultReactionsMenuProps {
65
78
  reactions: StreamReaction[];
79
+ layout?: 'horizontal' | 'vertical';
66
80
  }
67
81
 
68
82
  export const DefaultReactionsMenu = ({
69
83
  reactions,
84
+ layout = 'horizontal',
70
85
  }: DefaultReactionsMenuProps) => {
71
86
  const call = useCall();
72
87
  return (
73
- <div className="str-video__reactions-menu">
88
+ <div
89
+ className={clsx('str-video__reactions-menu', {
90
+ 'str-video__reactions-menu--horizontal': layout === 'horizontal',
91
+ 'str-video__reactions-menu--vertical': layout === 'vertical',
92
+ })}
93
+ >
74
94
  {reactions.map((reaction) => (
75
95
  <button
76
96
  key={reaction.emoji_code}
@@ -1,22 +1,94 @@
1
+ import { forwardRef } from 'react';
2
+
1
3
  import { OwnCapability } from '@stream-io/video-client';
2
- import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
3
- import { CompositeButton, IconButton } from '../Button/';
4
+ import { Restricted, useI18n } from '@stream-io/video-react-bindings';
5
+ import { CompositeButton } from '../Button/';
6
+ import { Icon } from '../Icon';
7
+ import {
8
+ MenuToggle,
9
+ MenuVisualType,
10
+ ToggleMenuButtonProps,
11
+ useMenuContext,
12
+ } from '../Menu';
4
13
  import { LoadingIndicator } from '../LoadingIndicator';
5
- import { useToggleCallRecording } from '../../hooks/useToggleCallRecording';
14
+ import { useToggleCallRecording } from '../../hooks';
6
15
 
7
16
  export type RecordCallButtonProps = {
8
17
  caption?: string;
9
18
  };
10
19
 
11
- export const RecordCallButton = ({
12
- caption = 'Record',
13
- }: RecordCallButtonProps) => {
14
- const call = useCall();
20
+ const RecordEndConfirmation = () => {
21
+ const { t } = useI18n();
22
+ const { toggleCallRecording, isAwaitingResponse } = useToggleCallRecording();
23
+
24
+ const { close } = useMenuContext();
25
+
26
+ return (
27
+ <div className="str-video__end-recording__confirmation">
28
+ <div className="str-video__end-recording__header">
29
+ <Icon icon="recording-on" />
30
+ <h2 className="str-video__end-recording__heading">
31
+ {t('End recording')}
32
+ </h2>
33
+ </div>
34
+ <p className="str-video__end-recording__description">
35
+ {t('Are you sure you want end the recording?')}
36
+ </p>
37
+ <div className="str-video__end-recording__actions">
38
+ <CompositeButton variant="secondary" onClick={close}>
39
+ {t('Cancel')}
40
+ </CompositeButton>
41
+ <CompositeButton variant="primary" onClick={toggleCallRecording}>
42
+ {isAwaitingResponse ? <LoadingIndicator /> : t('End recording')}
43
+ </CompositeButton>
44
+ </div>
45
+ </div>
46
+ );
47
+ };
48
+
49
+ const ToggleEndRecordingMenuButton = forwardRef<
50
+ HTMLDivElement,
51
+ ToggleMenuButtonProps
52
+ >(function ToggleEndRecordingMenuButton(props, ref) {
53
+ return (
54
+ <CompositeButton
55
+ ref={ref}
56
+ active={true}
57
+ variant="secondary"
58
+ data-testid="recording-stop-button"
59
+ >
60
+ <Icon icon="recording-off" />
61
+ </CompositeButton>
62
+ );
63
+ });
15
64
 
65
+ export const RecordCallConfirmationButton = ({
66
+ caption,
67
+ }: {
68
+ caption?: string;
69
+ }) => {
16
70
  const { t } = useI18n();
17
71
  const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } =
18
72
  useToggleCallRecording();
19
73
 
74
+ if (isCallRecordingInProgress) {
75
+ return (
76
+ <Restricted
77
+ requiredGrants={[
78
+ OwnCapability.START_RECORD_CALL,
79
+ OwnCapability.STOP_RECORD_CALL,
80
+ ]}
81
+ >
82
+ <MenuToggle
83
+ ToggleButton={ToggleEndRecordingMenuButton}
84
+ visualType={MenuVisualType.PORTAL}
85
+ >
86
+ <RecordEndConfirmation />
87
+ </MenuToggle>
88
+ </Restricted>
89
+ );
90
+ }
91
+
20
92
  return (
21
93
  <Restricted
22
94
  requiredGrants={[
@@ -24,23 +96,61 @@ export const RecordCallButton = ({
24
96
  OwnCapability.STOP_RECORD_CALL,
25
97
  ]}
26
98
  >
27
- <CompositeButton active={isCallRecordingInProgress} caption={caption}>
99
+ <CompositeButton
100
+ active={isCallRecordingInProgress}
101
+ caption={caption}
102
+ title={caption || t('Record call')}
103
+ variant="secondary"
104
+ data-testid="recording-start-button"
105
+ onClick={isAwaitingResponse ? undefined : toggleCallRecording}
106
+ >
28
107
  {isAwaitingResponse ? (
29
- <LoadingIndicator
30
- tooltip={
31
- isCallRecordingInProgress
32
- ? t('Waiting for recording to stop...')
33
- : t('Waiting for recording to start...')
34
- }
35
- />
108
+ <LoadingIndicator tooltip={t('Waiting for recording to start...')} />
109
+ ) : (
110
+ <Icon icon="recording-off" />
111
+ )}
112
+ </CompositeButton>
113
+ </Restricted>
114
+ );
115
+ };
116
+
117
+ export const RecordCallButton = ({ caption }: RecordCallButtonProps) => {
118
+ const { t } = useI18n();
119
+ const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } =
120
+ useToggleCallRecording();
121
+
122
+ let title = caption || t('Record call');
123
+
124
+ if (isAwaitingResponse) {
125
+ title = isCallRecordingInProgress
126
+ ? t('Waiting for recording to stop...')
127
+ : t('Waiting for recording to start...');
128
+ }
129
+
130
+ return (
131
+ <Restricted
132
+ requiredGrants={[
133
+ OwnCapability.START_RECORD_CALL,
134
+ OwnCapability.STOP_RECORD_CALL,
135
+ ]}
136
+ >
137
+ <CompositeButton
138
+ active={isCallRecordingInProgress}
139
+ caption={caption}
140
+ variant="secondary"
141
+ data-testid={
142
+ isCallRecordingInProgress
143
+ ? 'recording-stop-button'
144
+ : 'recording-start-button'
145
+ }
146
+ title={title}
147
+ onClick={isAwaitingResponse ? undefined : toggleCallRecording}
148
+ >
149
+ {isAwaitingResponse ? (
150
+ <LoadingIndicator />
36
151
  ) : (
37
- <IconButton
38
- // FIXME OL: sort out this ambiguity
39
- enabled={!!call}
40
- disabled={!call}
152
+ <Icon
41
153
  icon={isCallRecordingInProgress ? 'recording-on' : 'recording-off'}
42
- title={t('Record call')}
43
- onClick={toggleCallRecording}
44
154
  />
45
155
  )}
46
156
  </CompositeButton>
@@ -4,9 +4,10 @@ import {
4
4
  useCallStateHooks,
5
5
  useI18n,
6
6
  } from '@stream-io/video-react-bindings';
7
- import { CompositeButton, IconButton } from '../Button/';
7
+ import { CompositeButton } from '../Button/';
8
8
  import { PermissionNotification } from '../Notification';
9
9
  import { useRequestPermission } from '../../hooks';
10
+ import { Icon } from '../Icon';
10
11
 
11
12
  export type ScreenShareButtonProps = {
12
13
  caption?: string;
@@ -14,16 +15,20 @@ export type ScreenShareButtonProps = {
14
15
 
15
16
  export const ScreenShareButton = (props: ScreenShareButtonProps) => {
16
17
  const { t } = useI18n();
17
- const { caption = t('Screen Share') } = props;
18
+ const { caption } = props;
18
19
 
19
- const { useHasOngoingScreenShare, useScreenShareState } = useCallStateHooks();
20
+ const { useHasOngoingScreenShare, useScreenShareState, useCallSettings } =
21
+ useCallStateHooks();
20
22
  const isSomeoneScreenSharing = useHasOngoingScreenShare();
21
23
  const { hasPermission, requestPermission, isAwaitingPermission } =
22
24
  useRequestPermission(OwnCapability.SCREENSHARE);
23
25
 
26
+ const callSettings = useCallSettings();
27
+ const isScreenSharingAllowed = callSettings?.screensharing.enabled;
28
+
24
29
  const { screenShare, isMute: amIScreenSharing } = useScreenShareState();
25
30
  const disableScreenShareButton = amIScreenSharing
26
- ? isSomeoneScreenSharing
31
+ ? isSomeoneScreenSharing || isScreenSharingAllowed === false
27
32
  : false;
28
33
  return (
29
34
  <Restricted requiredGrants={[OwnCapability.SCREENSHARE]}>
@@ -34,20 +39,29 @@ export const ScreenShareButton = (props: ScreenShareButtonProps) => {
34
39
  messageAwaitingApproval={t('Awaiting for an approval to share screen.')}
35
40
  messageRevoked={t('You can no longer share your screen.')}
36
41
  >
37
- <CompositeButton active={isSomeoneScreenSharing} caption={caption}>
38
- <IconButton
42
+ <CompositeButton
43
+ active={isSomeoneScreenSharing}
44
+ caption={caption}
45
+ title={caption || t('Share screen')}
46
+ variant="primary"
47
+ data-testid={
48
+ isSomeoneScreenSharing
49
+ ? 'screen-share-stop-button'
50
+ : 'screen-share-start-button'
51
+ }
52
+ disabled={disableScreenShareButton}
53
+ onClick={async () => {
54
+ if (!hasPermission) {
55
+ await requestPermission();
56
+ } else {
57
+ await screenShare.toggle();
58
+ }
59
+ }}
60
+ >
61
+ <Icon
39
62
  icon={
40
63
  isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off'
41
64
  }
42
- title={t('Share screen')}
43
- disabled={disableScreenShareButton}
44
- onClick={async () => {
45
- if (!hasPermission) {
46
- await requestPermission();
47
- } else {
48
- await screenShare.toggle();
49
- }
50
- }}
51
65
  />
52
66
  </CompositeButton>
53
67
  </PermissionNotification>
@@ -1,55 +1,82 @@
1
- import { ComponentType } from 'react';
2
1
  import { OwnCapability } from '@stream-io/video-client';
3
2
  import {
4
3
  Restricted,
5
4
  useCallStateHooks,
6
5
  useI18n,
7
6
  } from '@stream-io/video-react-bindings';
7
+ import clsx from 'clsx';
8
+ import { CompositeButton, IconButtonWithMenuProps } from '../Button';
8
9
  import { DeviceSelectorAudioInput } from '../DeviceSettings';
9
- import { CompositeButton, IconButton } from '../Button';
10
10
  import { PermissionNotification } from '../Notification';
11
11
  import { useRequestPermission } from '../../hooks';
12
+ import { Icon } from '../Icon';
12
13
 
13
- export type ToggleAudioPreviewButtonProps = {
14
- caption?: string;
15
- Menu?: ComponentType;
16
- };
14
+ export type ToggleAudioPreviewButtonProps = Pick<
15
+ IconButtonWithMenuProps,
16
+ 'caption' | 'Menu' | 'menuPlacement'
17
+ >;
17
18
 
18
19
  export const ToggleAudioPreviewButton = (
19
20
  props: ToggleAudioPreviewButtonProps,
20
21
  ) => {
22
+ const { caption, Menu, menuPlacement, ...restCompositeButtonProps } = props;
21
23
  const { t } = useI18n();
22
- const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
23
-
24
24
  const { useMicrophoneState } = useCallStateHooks();
25
- const { microphone, isMute } = useMicrophoneState();
25
+ const { microphone, isMute, hasBrowserPermission } = useMicrophoneState();
26
26
 
27
27
  return (
28
- <CompositeButton Menu={Menu} active={isMute} caption={caption || t('Mic')}>
29
- <IconButton
30
- icon={!isMute ? 'mic' : 'mic-off'}
31
- onClick={() => microphone.toggle()}
32
- />
28
+ <CompositeButton
29
+ active={isMute}
30
+ caption={caption}
31
+ className={clsx(!hasBrowserPermission && 'str-video__device-unavailable')}
32
+ variant="secondary"
33
+ title={
34
+ !hasBrowserPermission
35
+ ? t('Check your browser audio permissions')
36
+ : caption || t('Mic')
37
+ }
38
+ disabled={!hasBrowserPermission}
39
+ data-testid={
40
+ isMute ? 'preview-audio-unmute-button' : 'preview-audio-mute-button'
41
+ }
42
+ onClick={() => microphone.toggle()}
43
+ Menu={Menu}
44
+ menuPlacement={menuPlacement}
45
+ {...restCompositeButtonProps}
46
+ >
47
+ <Icon icon={!isMute ? 'mic' : 'mic-off'} />
48
+ {!hasBrowserPermission && (
49
+ <span
50
+ className="str-video__no-media-permission"
51
+ title={t('Check your browser audio permissions')}
52
+ children="!"
53
+ />
54
+ )}
33
55
  </CompositeButton>
34
56
  );
35
57
  };
36
58
 
37
- export type ToggleAudioPublishingButtonProps = {
38
- caption?: string;
39
- Menu?: ComponentType;
40
- };
59
+ export type ToggleAudioPublishingButtonProps = Pick<
60
+ IconButtonWithMenuProps,
61
+ 'caption' | 'Menu' | 'menuPlacement'
62
+ >;
41
63
 
42
64
  export const ToggleAudioPublishingButton = (
43
65
  props: ToggleAudioPublishingButtonProps,
44
66
  ) => {
45
67
  const { t } = useI18n();
46
- const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
68
+ const {
69
+ caption,
70
+ Menu = <DeviceSelectorAudioInput visualType="list" />,
71
+ menuPlacement = 'top',
72
+ ...restCompositeButtonProps
73
+ } = props;
47
74
 
48
75
  const { hasPermission, requestPermission, isAwaitingPermission } =
49
76
  useRequestPermission(OwnCapability.SEND_AUDIO);
50
77
 
51
78
  const { useMicrophoneState } = useCallStateHooks();
52
- const { microphone, isMute } = useMicrophoneState();
79
+ const { microphone, isMute, hasBrowserPermission } = useMicrophoneState();
53
80
 
54
81
  return (
55
82
  <Restricted requiredGrants={[OwnCapability.SEND_AUDIO]}>
@@ -60,17 +87,35 @@ export const ToggleAudioPublishingButton = (
60
87
  messageAwaitingApproval={t('Awaiting for an approval to speak.')}
61
88
  messageRevoked={t('You can no longer speak.')}
62
89
  >
63
- <CompositeButton Menu={Menu} active={isMute} caption={caption}>
64
- <IconButton
65
- icon={isMute ? 'mic-off' : 'mic'}
66
- onClick={async () => {
67
- if (!hasPermission) {
68
- await requestPermission();
69
- } else {
70
- await microphone.toggle();
71
- }
72
- }}
73
- />
90
+ <CompositeButton
91
+ active={isMute}
92
+ caption={caption}
93
+ title={
94
+ !hasPermission
95
+ ? t('You have no permission to share your audio')
96
+ : !hasBrowserPermission
97
+ ? t('Check your browser mic permissions')
98
+ : caption || t('Mic')
99
+ }
100
+ variant="secondary"
101
+ disabled={!hasBrowserPermission || !hasPermission}
102
+ data-testid={isMute ? 'audio-unmute-button' : 'audio-mute-button'}
103
+ onClick={async () => {
104
+ if (!hasPermission) {
105
+ await requestPermission();
106
+ } else {
107
+ await microphone.toggle();
108
+ }
109
+ }}
110
+ Menu={Menu}
111
+ menuPlacement={menuPlacement}
112
+ menuOffset={16}
113
+ {...restCompositeButtonProps}
114
+ >
115
+ <Icon icon={isMute ? 'mic-off' : 'mic'} />
116
+ {(!hasBrowserPermission || !hasPermission) && (
117
+ <span className="str-video__no-media-permission">!</span>
118
+ )}
74
119
  </CompositeButton>
75
120
  </PermissionNotification>
76
121
  </Restricted>