@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.
@@ -10,5 +10,6 @@ export type IconButtonWithMenuProps<E extends HTMLElement = HTMLButtonElement> =
10
10
  menuOffset?: number;
11
11
  ToggleMenuButton?: ComponentType<ToggleMenuButtonProps<E>>;
12
12
  variant?: 'primary' | 'secondary';
13
+ onMenuToggle?: (menuShown: boolean) => void;
13
14
  }> & ComponentProps<'button'>;
14
15
  export declare const CompositeButton: import("react").ForwardRefExoticComponent<Omit<IconButtonWithMenuProps<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,8 +1,9 @@
1
1
  import { MouseEventHandler } from 'react';
2
2
  export type CancelCallButtonProps = {
3
3
  disabled?: boolean;
4
+ caption?: string;
4
5
  onClick?: MouseEventHandler<HTMLButtonElement>;
5
6
  onLeave?: () => void;
6
7
  };
7
8
  export declare const CancelCallConfirmButton: ({ onClick, onLeave, }: CancelCallButtonProps) => import("react/jsx-runtime").JSX.Element;
8
- export declare const CancelCallButton: ({ disabled, onClick, onLeave, }: CancelCallButtonProps) => import("react/jsx-runtime").JSX.Element;
9
+ export declare const CancelCallButton: ({ disabled, caption, onClick, onLeave, }: CancelCallButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,5 @@
1
- export type ScreenShareButtonProps = {
1
+ import { PropsWithErrorHandler } from '../../utilities/callControlHandler';
2
+ export type ScreenShareButtonProps = PropsWithErrorHandler<{
2
3
  caption?: string;
3
- };
4
+ }>;
4
5
  export declare const ScreenShareButton: (props: ScreenShareButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { IconButtonWithMenuProps } from '../Button';
2
- export type ToggleAudioPreviewButtonProps = Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement'>;
2
+ import { PropsWithErrorHandler } from '../../utilities/callControlHandler';
3
+ export type ToggleAudioPreviewButtonProps = PropsWithErrorHandler<Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'>>;
3
4
  export declare const ToggleAudioPreviewButton: (props: ToggleAudioPreviewButtonProps) => import("react/jsx-runtime").JSX.Element;
4
- export type ToggleAudioPublishingButtonProps = Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement'>;
5
+ export type ToggleAudioPublishingButtonProps = PropsWithErrorHandler<Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'>>;
5
6
  export declare const ToggleAudioPublishingButton: (props: ToggleAudioPublishingButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { IconButtonWithMenuProps } from '../Button/';
2
- export type ToggleVideoPreviewButtonProps = Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement'>;
2
+ import { PropsWithErrorHandler } from '../../utilities/callControlHandler';
3
+ export type ToggleVideoPreviewButtonProps = PropsWithErrorHandler<Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'>>;
3
4
  export declare const ToggleVideoPreviewButton: (props: ToggleVideoPreviewButtonProps) => import("react/jsx-runtime").JSX.Element;
4
- type ToggleVideoPublishingButtonProps = Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement'>;
5
+ type ToggleVideoPublishingButtonProps = PropsWithErrorHandler<Pick<IconButtonWithMenuProps, 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'>>;
5
6
  export declare const ToggleVideoPublishingButton: (props: ToggleVideoPublishingButtonProps) => import("react/jsx-runtime").JSX.Element;
6
7
  export {};
@@ -14,6 +14,7 @@ export type MenuToggleProps<E extends HTMLElement> = PropsWithChildren<{
14
14
  strategy?: Strategy;
15
15
  offset?: number;
16
16
  visualType?: MenuVisualType;
17
+ onToggle?: (menuShown: boolean) => void;
17
18
  }>;
18
19
  export type MenuContextValue = {
19
20
  close?: () => void;
@@ -22,4 +23,4 @@ export type MenuContextValue = {
22
23
  * Access to the closes MenuContext.
23
24
  */
24
25
  export declare const useMenuContext: () => MenuContextValue;
25
- export declare const MenuToggle: <E extends HTMLElement>({ ToggleButton, placement, strategy, offset, visualType, children, }: MenuToggleProps<E>) => import("react/jsx-runtime").JSX.Element;
26
+ export declare const MenuToggle: <E extends HTMLElement>({ ToggleButton, placement, strategy, offset, visualType, children, onToggle, }: MenuToggleProps<E>) => import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,7 @@
1
1
  import { ComponentProps } from 'react';
2
2
  import { TooltipProps } from './Tooltip';
3
- type WithPopupProps = ComponentProps<'div'> & Omit<TooltipProps<HTMLDivElement>, 'referenceElement'>;
4
- export declare const WithTooltip: ({ title, tooltipClassName, tooltipPlacement, ...props }: WithPopupProps) => import("react/jsx-runtime").JSX.Element;
3
+ type WithPopupProps = ComponentProps<'div'> & Omit<TooltipProps<HTMLDivElement>, 'referenceElement' | 'children'> & {
4
+ tooltipDisabled?: boolean;
5
+ };
6
+ export declare const WithTooltip: ({ title, tooltipClassName, tooltipPlacement, tooltipDisabled, ...props }: WithPopupProps) => import("react/jsx-runtime").JSX.Element;
5
7
  export {};
@@ -0,0 +1,16 @@
1
+ export type PropsWithErrorHandler<T = unknown> = T & {
2
+ /**
3
+ * Will be called if the call control action failed with an error (e.g. user didn't grant a
4
+ * browser permission to enable a media device). If no callback is provided, just logs the error.
5
+ * @param error Exception which caused call control action to fail
6
+ */
7
+ onError?: (error: unknown) => void;
8
+ };
9
+ /**
10
+ * Wraps an event handler, silencing and logging exceptions (excluding the NotAllowedError
11
+ * DOMException, which is a normal situation handled by the SDK)
12
+ *
13
+ * @param props component props, including the onError callback
14
+ * @param handler event handler to wrap
15
+ */
16
+ export declare const createCallControlHandler: (props: PropsWithErrorHandler, handler: () => Promise<void>) => (() => Promise<void>);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.es.js",
@@ -45,7 +45,7 @@
45
45
  "@rollup/plugin-replace": "^5.0.5",
46
46
  "@rollup/plugin-typescript": "^11.1.6",
47
47
  "@stream-io/audio-filters-web": "^0.1.0",
48
- "@stream-io/video-styling": "^1.0.2",
48
+ "@stream-io/video-styling": "^1.0.3",
49
49
  "react": "^18.2.0",
50
50
  "react-dom": "^18.2.0",
51
51
  "rimraf": "^5.0.5",
@@ -22,6 +22,7 @@ export type IconButtonWithMenuProps<E extends HTMLElement = HTMLButtonElement> =
22
22
  menuOffset?: number;
23
23
  ToggleMenuButton?: ComponentType<ToggleMenuButtonProps<E>>;
24
24
  variant?: 'primary' | 'secondary';
25
+ onMenuToggle?: (menuShown: boolean) => void;
25
26
  }> &
26
27
  ComponentProps<'button'>;
27
28
 
@@ -41,6 +42,7 @@ export const CompositeButton = forwardRef<
41
42
  ToggleMenuButton = DefaultToggleMenuButton,
42
43
  variant,
43
44
  onClick,
45
+ onMenuToggle,
44
46
  ...restButtonProps
45
47
  },
46
48
  ref,
@@ -79,6 +81,7 @@ export const CompositeButton = forwardRef<
79
81
  offset={menuOffset}
80
82
  placement={menuPlacement}
81
83
  ToggleButton={ToggleMenuButton}
84
+ onToggle={onMenuToggle}
82
85
  >
83
86
  {isComponentType(Menu) ? <Menu /> : Menu}
84
87
  </MenuToggle>
@@ -12,13 +12,13 @@ export type CallControlsProps = {
12
12
 
13
13
  export const CallControls = ({ onLeave }: CallControlsProps) => (
14
14
  <div className="str-video__call-controls">
15
- <RecordCallButton />
16
- <ReactionsButton />
17
- <ScreenShareButton />
18
15
  <SpeakingWhileMutedNotification>
19
16
  <ToggleAudioPublishingButton />
20
17
  </SpeakingWhileMutedNotification>
21
18
  <ToggleVideoPublishingButton />
19
+ <ReactionsButton />
20
+ <ScreenShareButton />
21
+ <RecordCallButton />
22
22
  <CancelCallButton onLeave={onLeave} />
23
23
  </div>
24
24
  );
@@ -6,6 +6,7 @@ import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
6
6
 
7
7
  import { IconButton } from '../Button';
8
8
  import { Icon } from '../Icon';
9
+ import { WithTooltip } from '../Tooltip';
9
10
 
10
11
  const EndCallMenu = (props: {
11
12
  onLeave: MouseEventHandler<HTMLButtonElement>;
@@ -51,18 +52,20 @@ const CancelCallToggleMenuButton = forwardRef<
51
52
  >(function CancelCallToggleMenuButton(props, ref) {
52
53
  const { t } = useI18n();
53
54
  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
- />
55
+ <WithTooltip title={t('Leave call')}>
56
+ <IconButton
57
+ icon="call-end"
58
+ variant="danger"
59
+ data-testid="leave-call-button"
60
+ ref={ref}
61
+ />
62
+ </WithTooltip>
61
63
  );
62
64
  });
63
65
 
64
66
  export type CancelCallButtonProps = {
65
67
  disabled?: boolean;
68
+ caption?: string;
66
69
  onClick?: MouseEventHandler<HTMLButtonElement>;
67
70
  onLeave?: () => void;
68
71
  };
@@ -106,6 +109,7 @@ export const CancelCallConfirmButton = ({
106
109
 
107
110
  export const CancelCallButton = ({
108
111
  disabled,
112
+ caption,
109
113
  onClick,
110
114
  onLeave,
111
115
  }: CancelCallButtonProps) => {
@@ -127,7 +131,7 @@ export const CancelCallButton = ({
127
131
  disabled={disabled}
128
132
  icon="call-end"
129
133
  variant="danger"
130
- title={t('Leave call')}
134
+ title={caption ?? t('Leave call')}
131
135
  data-testid="cancel-call-button"
132
136
  onClick={handleClick}
133
137
  />
@@ -4,10 +4,16 @@ import clsx from 'clsx';
4
4
  import { OwnCapability, StreamReaction } from '@stream-io/video-client';
5
5
  import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
6
6
 
7
- import { MenuToggle, MenuVisualType, ToggleMenuButtonProps } from '../Menu';
7
+ import {
8
+ MenuToggle,
9
+ MenuVisualType,
10
+ ToggleMenuButtonProps,
11
+ useMenuContext,
12
+ } from '../Menu';
8
13
  import { CompositeButton } from '../Button';
9
14
  import { defaultEmojiReactionMap } from '../Reaction';
10
15
  import { Icon } from '../Icon';
16
+ import { WithTooltip } from '../Tooltip';
11
17
 
12
18
  export const defaultReactions: StreamReaction[] = [
13
19
  {
@@ -63,14 +69,11 @@ const ToggleReactionsMenuButton = forwardRef<
63
69
  >(function ToggleReactionsMenuButton({ menuShown }, ref) {
64
70
  const { t } = useI18n();
65
71
  return (
66
- <CompositeButton
67
- ref={ref}
68
- active={menuShown}
69
- variant="primary"
70
- title={t('Reactions')}
71
- >
72
- <Icon icon="reactions" />
73
- </CompositeButton>
72
+ <WithTooltip title={t('Reactions')} tooltipDisabled={menuShown}>
73
+ <CompositeButton ref={ref} active={menuShown} variant="primary">
74
+ <Icon icon="reactions" />
75
+ </CompositeButton>
76
+ </WithTooltip>
74
77
  );
75
78
  });
76
79
 
@@ -84,6 +87,7 @@ export const DefaultReactionsMenu = ({
84
87
  layout = 'horizontal',
85
88
  }: DefaultReactionsMenuProps) => {
86
89
  const call = useCall();
90
+ const { close } = useMenuContext();
87
91
  return (
88
92
  <div
89
93
  className={clsx('str-video__reactions-menu', {
@@ -98,6 +102,7 @@ export const DefaultReactionsMenu = ({
98
102
  className="str-video__reactions-menu__button"
99
103
  onClick={() => {
100
104
  call?.sendReaction(reaction);
105
+ close?.();
101
106
  }}
102
107
  >
103
108
  {reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code]}
@@ -12,6 +12,7 @@ import {
12
12
  } from '../Menu';
13
13
  import { LoadingIndicator } from '../LoadingIndicator';
14
14
  import { useToggleCallRecording } from '../../hooks';
15
+ import { WithTooltip } from '../Tooltip';
15
16
 
16
17
  export type RecordCallButtonProps = {
17
18
  caption?: string;
@@ -89,6 +90,10 @@ export const RecordCallConfirmationButton = ({
89
90
  );
90
91
  }
91
92
 
93
+ const title = isAwaitingResponse
94
+ ? t('Waiting for recording to start...')
95
+ : caption ?? t('Record call');
96
+
92
97
  return (
93
98
  <Restricted
94
99
  requiredGrants={[
@@ -96,20 +101,21 @@ export const RecordCallConfirmationButton = ({
96
101
  OwnCapability.STOP_RECORD_CALL,
97
102
  ]}
98
103
  >
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
- >
107
- {isAwaitingResponse ? (
108
- <LoadingIndicator tooltip={t('Waiting for recording to start...')} />
109
- ) : (
110
- <Icon icon="recording-off" />
111
- )}
112
- </CompositeButton>
104
+ <WithTooltip title={title}>
105
+ <CompositeButton
106
+ active={isCallRecordingInProgress}
107
+ caption={caption}
108
+ variant="secondary"
109
+ data-testid="recording-start-button"
110
+ onClick={isAwaitingResponse ? undefined : toggleCallRecording}
111
+ >
112
+ {isAwaitingResponse ? (
113
+ <LoadingIndicator />
114
+ ) : (
115
+ <Icon icon="recording-off" />
116
+ )}
117
+ </CompositeButton>
118
+ </WithTooltip>
113
119
  </Restricted>
114
120
  );
115
121
  };
@@ -119,7 +125,7 @@ export const RecordCallButton = ({ caption }: RecordCallButtonProps) => {
119
125
  const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } =
120
126
  useToggleCallRecording();
121
127
 
122
- let title = caption || t('Record call');
128
+ let title = caption ?? t('Record call');
123
129
 
124
130
  if (isAwaitingResponse) {
125
131
  title = isCallRecordingInProgress
@@ -8,10 +8,15 @@ import { CompositeButton } from '../Button/';
8
8
  import { PermissionNotification } from '../Notification';
9
9
  import { useRequestPermission } from '../../hooks';
10
10
  import { Icon } from '../Icon';
11
+ import { WithTooltip } from '../Tooltip';
12
+ import {
13
+ PropsWithErrorHandler,
14
+ createCallControlHandler,
15
+ } from '../../utilities/callControlHandler';
11
16
 
12
- export type ScreenShareButtonProps = {
17
+ export type ScreenShareButtonProps = PropsWithErrorHandler<{
13
18
  caption?: string;
14
- };
19
+ }>;
15
20
 
16
21
  export const ScreenShareButton = (props: ScreenShareButtonProps) => {
17
22
  const { t } = useI18n();
@@ -31,6 +36,14 @@ export const ScreenShareButton = (props: ScreenShareButtonProps) => {
31
36
  const disableScreenShareButton =
32
37
  !amIScreenSharing &&
33
38
  (isSomeoneScreenSharing || isScreenSharingAllowed === false);
39
+ const handleClick = createCallControlHandler(props, async () => {
40
+ if (!hasPermission) {
41
+ await requestPermission();
42
+ } else {
43
+ await screenShare.toggle();
44
+ }
45
+ });
46
+
34
47
  return (
35
48
  <Restricted requiredGrants={[OwnCapability.SCREENSHARE]}>
36
49
  <PermissionNotification
@@ -40,31 +53,26 @@ export const ScreenShareButton = (props: ScreenShareButtonProps) => {
40
53
  messageAwaitingApproval={t('Awaiting for an approval to share screen.')}
41
54
  messageRevoked={t('You can no longer share your screen.')}
42
55
  >
43
- <CompositeButton
44
- active={isSomeoneScreenSharing || amIScreenSharing}
45
- caption={caption}
46
- title={caption || t('Share screen')}
47
- variant="primary"
48
- data-testid={
49
- isSomeoneScreenSharing
50
- ? 'screen-share-stop-button'
51
- : 'screen-share-start-button'
52
- }
53
- disabled={disableScreenShareButton}
54
- onClick={async () => {
55
- if (!hasPermission) {
56
- await requestPermission();
57
- } else {
58
- await screenShare.toggle();
59
- }
60
- }}
61
- >
62
- <Icon
63
- icon={
64
- isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off'
56
+ <WithTooltip title={caption ?? t('Share screen')}>
57
+ <CompositeButton
58
+ active={isSomeoneScreenSharing || amIScreenSharing}
59
+ caption={caption}
60
+ variant="primary"
61
+ data-testid={
62
+ isSomeoneScreenSharing
63
+ ? 'screen-share-stop-button'
64
+ : 'screen-share-start-button'
65
65
  }
66
- />
67
- </CompositeButton>
66
+ disabled={disableScreenShareButton}
67
+ onClick={handleClick}
68
+ >
69
+ <Icon
70
+ icon={
71
+ isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off'
72
+ }
73
+ />
74
+ </CompositeButton>
75
+ </WithTooltip>
68
76
  </PermissionNotification>
69
77
  </Restricted>
70
78
  );
@@ -10,10 +10,18 @@ import { DeviceSelectorAudioInput } 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 ToggleAudioPreviewButtonProps = Pick<
15
- IconButtonWithMenuProps,
16
- 'caption' | 'Menu' | 'menuPlacement'
20
+ export type ToggleAudioPreviewButtonProps = PropsWithErrorHandler<
21
+ Pick<
22
+ IconButtonWithMenuProps,
23
+ 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
24
+ >
17
25
  >;
18
26
 
19
27
  export const ToggleAudioPreviewButton = (
@@ -24,44 +32,57 @@ export const ToggleAudioPreviewButton = (
24
32
  const { useMicrophoneState } = useCallStateHooks();
25
33
  const { microphone, optimisticIsMute, hasBrowserPermission } =
26
34
  useMicrophoneState();
35
+ const [tooltipDisabled, setTooltipDisabled] = useState(false);
36
+ const handleClick = createCallControlHandler(props, () =>
37
+ microphone.toggle(),
38
+ );
27
39
 
28
40
  return (
29
- <CompositeButton
30
- active={optimisticIsMute}
31
- caption={caption}
32
- className={clsx(!hasBrowserPermission && 'str-video__device-unavailable')}
33
- variant="secondary"
41
+ <WithTooltip
34
42
  title={
35
43
  !hasBrowserPermission
36
44
  ? t('Check your browser audio permissions')
37
- : caption || t('Mic')
45
+ : caption ?? t('Mic')
38
46
  }
39
- disabled={!hasBrowserPermission}
40
- data-testid={
41
- optimisticIsMute
42
- ? 'preview-audio-unmute-button'
43
- : 'preview-audio-mute-button'
44
- }
45
- onClick={() => microphone.toggle()}
46
- Menu={Menu}
47
- menuPlacement={menuPlacement}
48
- {...restCompositeButtonProps}
47
+ tooltipDisabled={tooltipDisabled}
49
48
  >
50
- <Icon icon={!optimisticIsMute ? 'mic' : 'mic-off'} />
51
- {!hasBrowserPermission && (
52
- <span
53
- className="str-video__no-media-permission"
54
- title={t('Check your browser audio permissions')}
55
- children="!"
56
- />
57
- )}
58
- </CompositeButton>
49
+ <CompositeButton
50
+ active={optimisticIsMute}
51
+ caption={caption}
52
+ className={clsx(
53
+ !hasBrowserPermission && 'str-video__device-unavailable',
54
+ )}
55
+ variant="secondary"
56
+ disabled={!hasBrowserPermission}
57
+ data-testid={
58
+ optimisticIsMute
59
+ ? 'preview-audio-unmute-button'
60
+ : 'preview-audio-mute-button'
61
+ }
62
+ onClick={handleClick}
63
+ Menu={Menu}
64
+ menuPlacement={menuPlacement}
65
+ onMenuToggle={(shown) => setTooltipDisabled(shown)}
66
+ {...restCompositeButtonProps}
67
+ >
68
+ <Icon icon={!optimisticIsMute ? 'mic' : 'mic-off'} />
69
+ {!hasBrowserPermission && (
70
+ <span
71
+ className="str-video__no-media-permission"
72
+ title={t('Check your browser audio permissions')}
73
+ children="!"
74
+ />
75
+ )}
76
+ </CompositeButton>
77
+ </WithTooltip>
59
78
  );
60
79
  };
61
80
 
62
- export type ToggleAudioPublishingButtonProps = Pick<
63
- IconButtonWithMenuProps,
64
- 'caption' | 'Menu' | 'menuPlacement'
81
+ export type ToggleAudioPublishingButtonProps = PropsWithErrorHandler<
82
+ Pick<
83
+ IconButtonWithMenuProps,
84
+ 'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
85
+ >
65
86
  >;
66
87
 
67
88
  export const ToggleAudioPublishingButton = (
@@ -81,6 +102,14 @@ export const ToggleAudioPublishingButton = (
81
102
  const { useMicrophoneState } = useCallStateHooks();
82
103
  const { microphone, optimisticIsMute, hasBrowserPermission } =
83
104
  useMicrophoneState();
105
+ const [tooltipDisabled, setTooltipDisabled] = useState(false);
106
+ const handleClick = createCallControlHandler(props, async () => {
107
+ if (!hasPermission) {
108
+ await requestPermission();
109
+ } else {
110
+ await microphone.toggle();
111
+ }
112
+ });
84
113
 
85
114
  return (
86
115
  <Restricted requiredGrants={[OwnCapability.SEND_AUDIO]}>
@@ -91,38 +120,37 @@ export const ToggleAudioPublishingButton = (
91
120
  messageAwaitingApproval={t('Awaiting for an approval to speak.')}
92
121
  messageRevoked={t('You can no longer speak.')}
93
122
  >
94
- <CompositeButton
95
- active={optimisticIsMute}
96
- caption={caption}
123
+ <WithTooltip
97
124
  title={
98
125
  !hasPermission
99
126
  ? t('You have no permission to share your audio')
100
127
  : !hasBrowserPermission
101
128
  ? t('Check your browser mic permissions')
102
- : caption || t('Mic')
129
+ : caption ?? t('Mic')
103
130
  }
104
- variant="secondary"
105
- disabled={!hasBrowserPermission || !hasPermission}
106
- data-testid={
107
- optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button'
108
- }
109
- onClick={async () => {
110
- if (!hasPermission) {
111
- await requestPermission();
112
- } else {
113
- await microphone.toggle();
114
- }
115
- }}
116
- Menu={Menu}
117
- menuPlacement={menuPlacement}
118
- menuOffset={16}
119
- {...restCompositeButtonProps}
131
+ tooltipDisabled={tooltipDisabled}
120
132
  >
121
- <Icon icon={optimisticIsMute ? 'mic-off' : 'mic'} />
122
- {(!hasBrowserPermission || !hasPermission) && (
123
- <span className="str-video__no-media-permission">!</span>
124
- )}
125
- </CompositeButton>
133
+ <CompositeButton
134
+ active={optimisticIsMute}
135
+ caption={caption}
136
+ variant="secondary"
137
+ disabled={!hasBrowserPermission || !hasPermission}
138
+ data-testid={
139
+ optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button'
140
+ }
141
+ onClick={handleClick}
142
+ Menu={Menu}
143
+ menuPlacement={menuPlacement}
144
+ menuOffset={16}
145
+ onMenuToggle={(shown) => setTooltipDisabled(shown)}
146
+ {...restCompositeButtonProps}
147
+ >
148
+ <Icon icon={optimisticIsMute ? 'mic-off' : 'mic'} />
149
+ {(!hasBrowserPermission || !hasPermission) && (
150
+ <span className="str-video__no-media-permission">!</span>
151
+ )}
152
+ </CompositeButton>
153
+ </WithTooltip>
126
154
  </PermissionNotification>
127
155
  </Restricted>
128
156
  );