@stream-io/video-react-sdk 1.0.6 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/css/styles.css +3 -3
  3. package/dist/index.cjs.js +173 -123
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.es.js +174 -124
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/src/components/Button/CompositeButton.d.ts +1 -0
  8. package/dist/src/components/CallControls/CancelCallButton.d.ts +2 -1
  9. package/dist/src/components/CallControls/ScreenShareButton.d.ts +3 -2
  10. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -2
  11. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -2
  12. package/dist/src/components/Menu/MenuToggle.d.ts +2 -1
  13. package/dist/src/components/Tooltip/WithTooltip.d.ts +4 -2
  14. package/dist/src/utilities/callControlHandler.d.ts +16 -0
  15. package/package.json +4 -4
  16. package/src/components/Button/CompositeButton.tsx +3 -0
  17. package/src/components/CallControls/CallControls.tsx +3 -3
  18. package/src/components/CallControls/CancelCallButton.tsx +12 -8
  19. package/src/components/CallControls/ReactionsButton.tsx +14 -9
  20. package/src/components/CallControls/RecordCallButton.tsx +21 -15
  21. package/src/components/CallControls/ScreenShareButton.tsx +34 -26
  22. package/src/components/CallControls/ToggleAudioButton.tsx +84 -56
  23. package/src/components/CallControls/ToggleVideoButton.tsx +87 -59
  24. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +10 -9
  25. package/src/components/DeviceSettings/DeviceSelector.tsx +4 -0
  26. package/src/components/Menu/MenuToggle.tsx +9 -0
  27. package/src/components/Tooltip/WithTooltip.tsx +7 -2
  28. package/src/core/components/Audio/ParticipantsAudio.tsx +10 -13
  29. package/src/core/components/CallLayout/LivestreamLayout.tsx +5 -7
  30. package/src/core/components/CallLayout/SpeakerLayout.tsx +3 -5
  31. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +12 -12
  32. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +16 -14
  33. package/src/core/components/ParticipantView/ParticipantView.tsx +12 -17
  34. package/src/core/components/Video/Video.tsx +4 -4
  35. package/src/utilities/callControlHandler.ts +43 -0
package/dist/index.cjs.js CHANGED
@@ -30,11 +30,11 @@ const ParticipantsAudio = (props) => {
30
30
  return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: participants.map((participant) => {
31
31
  if (participant.isLocalParticipant)
32
32
  return null;
33
- const { publishedTracks, audioStream, screenShareAudioStream, sessionId, } = participant;
34
- const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
35
- const audioTrackElement = hasAudio && audioStream && (jsxRuntime.jsx(Audio, { ...audioProps, trackType: "audioTrack", participant: participant }));
36
- const hasScreenShareAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
37
- const screenShareAudioTrackElement = hasScreenShareAudio &&
33
+ const { audioStream, screenShareAudioStream, sessionId } = participant;
34
+ const hasAudioTrack = videoClient.hasAudio(participant);
35
+ const audioTrackElement = hasAudioTrack && audioStream && (jsxRuntime.jsx(Audio, { ...audioProps, trackType: "audioTrack", participant: participant }));
36
+ const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
37
+ const screenShareAudioTrackElement = hasScreenShareAudioTrack &&
38
38
  screenShareAudioStream && (jsxRuntime.jsx(Audio, { ...audioProps, trackType: "screenShareAudioTrack", participant: participant }));
39
39
  return (jsxRuntime.jsxs(react.Fragment, { children: [audioTrackElement, screenShareAudioTrackElement] }, sessionId));
40
40
  }) }));
@@ -516,8 +516,10 @@ const MenuPortal = ({ children, refs, }) => {
516
516
  const portalId = react.useMemo(() => `str-video-portal-${Math.random().toString(36).substring(2, 9)}`, []);
517
517
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { id: portalId, className: "str-video__portal" }), jsxRuntime.jsx(react$1.FloatingOverlay, { children: jsxRuntime.jsx(react$1.FloatingPortal, { id: portalId, children: jsxRuntime.jsx("div", { className: "str-video__portal-content", ref: refs.setFloating, children: children }) }) })] }));
518
518
  };
519
- const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', offset, visualType = exports.MenuVisualType.MENU, children, }) => {
519
+ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', offset, visualType = exports.MenuVisualType.MENU, children, onToggle, }) => {
520
520
  const [menuShown, setMenuShown] = react.useState(false);
521
+ const toggleHandler = react.useRef(onToggle);
522
+ toggleHandler.current = onToggle;
521
523
  const { floating, domReference, refs, x, y } = useFloatingUIPreset({
522
524
  placement,
523
525
  strategy,
@@ -527,9 +529,11 @@ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolut
527
529
  const handleClick = (event) => {
528
530
  if (!floating && domReference?.contains(event.target)) {
529
531
  setMenuShown(true);
532
+ toggleHandler.current?.(true);
530
533
  }
531
534
  else if (floating && !floating?.contains(event.target)) {
532
535
  setMenuShown(false);
536
+ toggleHandler.current?.(false);
533
537
  }
534
538
  };
535
539
  const handleKeyDown = (event) => {
@@ -537,6 +541,7 @@ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolut
537
541
  !event.altKey &&
538
542
  !event.ctrlKey) {
539
543
  setMenuShown(false);
544
+ toggleHandler.current?.(false);
540
545
  }
541
546
  };
542
547
  document?.addEventListener('click', handleClick, { capture: true });
@@ -551,7 +556,7 @@ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolut
551
556
  top: y ?? 0,
552
557
  left: x ?? 0,
553
558
  overflowY: 'auto',
554
- }, children: children })) : null })), jsxRuntime.jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
559
+ }, role: "menu", children: children })) : null })), jsxRuntime.jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
555
560
  };
556
561
 
557
562
  const GenericMenu = ({ children, onItemClick, }) => {
@@ -600,7 +605,7 @@ const applyElementToRef = (ref, element) => {
600
605
  ref.current = element;
601
606
  };
602
607
 
603
- const CompositeButton = react.forwardRef(function CompositeButton({ caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, ...restButtonProps }, ref) {
608
+ const CompositeButton = react.forwardRef(function CompositeButton({ caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
604
609
  return (jsxRuntime.jsxs("div", { className: clsx('str-video__composite-button', className, {
605
610
  'str-video__composite-button--caption': caption,
606
611
  'str-video__composite-button--menu': Menu,
@@ -611,7 +616,7 @@ const CompositeButton = react.forwardRef(function CompositeButton({ caption, chi
611
616
  }), children: [jsxRuntime.jsx("button", { type: "button", className: "str-video__composite-button__button", onClick: (e) => {
612
617
  e.preventDefault();
613
618
  onClick?.(e);
614
- }, ...restButtonProps, children: children }), Menu && (jsxRuntime.jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
619
+ }, ...restButtonProps, children: children }), Menu && (jsxRuntime.jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
615
620
  });
616
621
  const DefaultToggleMenuButton = react.forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
617
622
  return (jsxRuntime.jsx(IconButton, { className: clsx('str-video__menu-toggle-button', {
@@ -710,6 +715,45 @@ const LoadingIndicator = ({ className, type = 'spinner', text, tooltip, }) => {
710
715
  return (jsxRuntime.jsxs("div", { className: clsx('str-video__loading-indicator', className), title: tooltip, children: [jsxRuntime.jsx("div", { className: clsx('str-video__loading-indicator__icon', type) }), text && jsxRuntime.jsx("p", { className: "str-video__loading-indicator-text", children: text })] }));
711
716
  };
712
717
 
718
+ const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
719
+ const { refs, x, y, strategy } = useFloatingUIPreset({
720
+ placement: tooltipPlacement,
721
+ strategy: 'absolute',
722
+ });
723
+ react.useEffect(() => {
724
+ refs.setReference(referenceElement);
725
+ }, [referenceElement, refs]);
726
+ if (!visible)
727
+ return null;
728
+ return (jsxRuntime.jsx("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
729
+ position: strategy,
730
+ top: y ?? 0,
731
+ left: x ?? 0,
732
+ overflowY: 'auto',
733
+ }, children: children }));
734
+ };
735
+
736
+ const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
737
+ const [tooltipVisible, setTooltipVisible] = react.useState(false);
738
+ const handleMouseEnter = react.useCallback((e) => {
739
+ setTooltipVisible(true);
740
+ onMouseEnter?.(e);
741
+ }, [onMouseEnter]);
742
+ const handleMouseLeave = react.useCallback((e) => {
743
+ setTooltipVisible(false);
744
+ onMouseLeave?.(e);
745
+ }, [onMouseLeave]);
746
+ return { handleMouseEnter, handleMouseLeave, tooltipVisible };
747
+ };
748
+
749
+ // todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
750
+ const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, tooltipDisabled, ...props }) => {
751
+ const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
752
+ const [tooltipAnchor, setTooltipAnchor] = react.useState(null);
753
+ const tooltipActuallyVisible = !tooltipDisabled && Boolean(title) && tooltipVisible;
754
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipActuallyVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsxRuntime.jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
755
+ };
756
+
713
757
  const RecordEndConfirmation = () => {
714
758
  const { t } = videoReactBindings.useI18n();
715
759
  const { toggleCallRecording, isAwaitingResponse } = useToggleCallRecording();
@@ -728,15 +772,18 @@ const RecordCallConfirmationButton = ({ caption, }) => {
728
772
  videoClient.OwnCapability.STOP_RECORD_CALL,
729
773
  ], children: jsxRuntime.jsx(MenuToggle, { ToggleButton: ToggleEndRecordingMenuButton, visualType: exports.MenuVisualType.PORTAL, children: jsxRuntime.jsx(RecordEndConfirmation, {}) }) }));
730
774
  }
775
+ const title = isAwaitingResponse
776
+ ? t('Waiting for recording to start...')
777
+ : caption ?? t('Record call');
731
778
  return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [
732
779
  videoClient.OwnCapability.START_RECORD_CALL,
733
780
  videoClient.OwnCapability.STOP_RECORD_CALL,
734
- ], children: jsxRuntime.jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, title: caption || t('Record call'), variant: "secondary", "data-testid": "recording-start-button", onClick: isAwaitingResponse ? undefined : toggleCallRecording, children: isAwaitingResponse ? (jsxRuntime.jsx(LoadingIndicator, { tooltip: t('Waiting for recording to start...') })) : (jsxRuntime.jsx(Icon, { icon: "recording-off" })) }) }));
781
+ ], children: jsxRuntime.jsx(WithTooltip, { title: title, children: jsxRuntime.jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, variant: "secondary", "data-testid": "recording-start-button", onClick: isAwaitingResponse ? undefined : toggleCallRecording, children: isAwaitingResponse ? (jsxRuntime.jsx(LoadingIndicator, {})) : (jsxRuntime.jsx(Icon, { icon: "recording-off" })) }) }) }));
735
782
  };
736
783
  const RecordCallButton = ({ caption }) => {
737
784
  const { t } = videoReactBindings.useI18n();
738
785
  const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = useToggleCallRecording();
739
- let title = caption || t('Record call');
786
+ let title = caption ?? t('Record call');
740
787
  if (isAwaitingResponse) {
741
788
  title = isCallRecordingInProgress
742
789
  ? t('Waiting for recording to stop...')
@@ -808,18 +855,48 @@ const ReactionsButton = ({ reactions = defaultReactions, }) => {
808
855
  };
809
856
  const ToggleReactionsMenuButton = react.forwardRef(function ToggleReactionsMenuButton({ menuShown }, ref) {
810
857
  const { t } = videoReactBindings.useI18n();
811
- return (jsxRuntime.jsx(CompositeButton, { ref: ref, active: menuShown, variant: "primary", title: t('Reactions'), children: jsxRuntime.jsx(Icon, { icon: "reactions" }) }));
858
+ return (jsxRuntime.jsx(WithTooltip, { title: t('Reactions'), tooltipDisabled: menuShown, children: jsxRuntime.jsx(CompositeButton, { ref: ref, active: menuShown, variant: "primary", children: jsxRuntime.jsx(Icon, { icon: "reactions" }) }) }));
812
859
  });
813
860
  const DefaultReactionsMenu = ({ reactions, layout = 'horizontal', }) => {
814
861
  const call = videoReactBindings.useCall();
862
+ const { close } = useMenuContext();
815
863
  return (jsxRuntime.jsx("div", { className: clsx('str-video__reactions-menu', {
816
864
  'str-video__reactions-menu--horizontal': layout === 'horizontal',
817
865
  'str-video__reactions-menu--vertical': layout === 'vertical',
818
866
  }), children: reactions.map((reaction) => (jsxRuntime.jsx("button", { type: "button", className: "str-video__reactions-menu__button", onClick: () => {
819
867
  call?.sendReaction(reaction);
868
+ close?.();
820
869
  }, children: reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code] }, reaction.emoji_code))) }));
821
870
  };
822
871
 
872
+ /**
873
+ * Wraps an event handler, silencing and logging exceptions (excluding the NotAllowedError
874
+ * DOMException, which is a normal situation handled by the SDK)
875
+ *
876
+ * @param props component props, including the onError callback
877
+ * @param handler event handler to wrap
878
+ */
879
+ const createCallControlHandler = (props, handler) => {
880
+ const logger = videoClient.getLogger(['react-sdk']);
881
+ return async () => {
882
+ try {
883
+ await handler();
884
+ }
885
+ catch (error) {
886
+ if (props.onError) {
887
+ props.onError(error);
888
+ return;
889
+ }
890
+ if (!isNotAllowedError(error)) {
891
+ logger('error', 'Call control handler failed', error);
892
+ }
893
+ }
894
+ };
895
+ };
896
+ function isNotAllowedError(error) {
897
+ return error instanceof DOMException && error.name === 'NotAllowedError';
898
+ }
899
+
823
900
  const ScreenShareButton = (props) => {
824
901
  const { t } = videoReactBindings.useI18n();
825
902
  const { caption } = props;
@@ -832,16 +909,17 @@ const ScreenShareButton = (props) => {
832
909
  const amIScreenSharing = !optimisticIsMute;
833
910
  const disableScreenShareButton = !amIScreenSharing &&
834
911
  (isSomeoneScreenSharing || isScreenSharingAllowed === false);
835
- return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SCREENSHARE], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SCREENSHARE, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your screen.'), messageAwaitingApproval: t('Awaiting for an approval to share screen.'), messageRevoked: t('You can no longer share your screen.'), children: jsxRuntime.jsx(CompositeButton, { active: isSomeoneScreenSharing || amIScreenSharing, caption: caption, title: caption || t('Share screen'), variant: "primary", "data-testid": isSomeoneScreenSharing
836
- ? 'screen-share-stop-button'
837
- : 'screen-share-start-button', disabled: disableScreenShareButton, onClick: async () => {
838
- if (!hasPermission) {
839
- await requestPermission();
840
- }
841
- else {
842
- await screenShare.toggle();
843
- }
844
- }, children: jsxRuntime.jsx(Icon, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off' }) }) }) }));
912
+ const handleClick = createCallControlHandler(props, async () => {
913
+ if (!hasPermission) {
914
+ await requestPermission();
915
+ }
916
+ else {
917
+ await screenShare.toggle();
918
+ }
919
+ });
920
+ return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SCREENSHARE], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SCREENSHARE, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your screen.'), messageAwaitingApproval: t('Awaiting for an approval to share screen.'), messageRevoked: t('You can no longer share your screen.'), children: jsxRuntime.jsx(WithTooltip, { title: caption ?? t('Share screen'), children: jsxRuntime.jsx(CompositeButton, { active: isSomeoneScreenSharing || amIScreenSharing, caption: caption, variant: "primary", "data-testid": isSomeoneScreenSharing
921
+ ? 'screen-share-stop-button'
922
+ : 'screen-share-start-button', disabled: disableScreenShareButton, onClick: handleClick, children: jsxRuntime.jsx(Icon, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off' }) }) }) }) }));
845
923
  };
846
924
 
847
925
  const SelectContext = react.createContext({});
@@ -923,6 +1001,7 @@ const DeviceSelectorOption = ({ disabled, id, label, onChange, name, selected, d
923
1001
  };
924
1002
  const DeviceSelectorList = (props) => {
925
1003
  const { devices = [], selectedDeviceId: selectedDeviceFromProps, title, type, onChange, } = props;
1004
+ const { close } = useMenuContext();
926
1005
  // sometimes the browser (Chrome) will report the system-default device
927
1006
  // with an id of 'default'. In case when it doesn't, we'll select the first
928
1007
  // available device.
@@ -934,6 +1013,7 @@ const DeviceSelectorList = (props) => {
934
1013
  return (jsxRuntime.jsxs("div", { className: "str-video__device-settings__device-kind", children: [title && (jsxRuntime.jsx("div", { className: "str-video__device-settings__device-selector-title", children: title })), !devices.length ? (jsxRuntime.jsx(DeviceSelectorOption, { id: `${type}--default`, label: "Default", name: type, defaultChecked: true, value: "default" })) : (devices.map((device) => {
935
1014
  return (jsxRuntime.jsx(DeviceSelectorOption, { id: `${type}--${device.deviceId}`, value: device.deviceId, label: device.label, onChange: (e) => {
936
1015
  onChange?.(e.target.value);
1016
+ close?.();
937
1017
  }, name: type, selected: device.deviceId === selectedDeviceId || devices.length === 1 }, device.deviceId));
938
1018
  }))] }));
939
1019
  };
@@ -1007,11 +1087,13 @@ const ToggleAudioPreviewButton = (props) => {
1007
1087
  const { t } = videoReactBindings.useI18n();
1008
1088
  const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
1009
1089
  const { microphone, optimisticIsMute, hasBrowserPermission } = useMicrophoneState();
1010
- return (jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", title: !hasBrowserPermission
1090
+ const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1091
+ const handleClick = createCallControlHandler(props, () => microphone.toggle());
1092
+ return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1011
1093
  ? t('Check your browser audio permissions')
1012
- : caption || t('Mic'), disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1013
- ? 'preview-audio-unmute-button'
1014
- : 'preview-audio-mute-button', onClick: () => microphone.toggle(), Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" }))] }));
1094
+ : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1095
+ ? 'preview-audio-unmute-button'
1096
+ : 'preview-audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, onMenuToggle: (shown) => setTooltipDisabled(shown), ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" }))] }) }));
1015
1097
  };
1016
1098
  const ToggleAudioPublishingButton = (props) => {
1017
1099
  const { t } = videoReactBindings.useI18n();
@@ -1019,18 +1101,20 @@ const ToggleAudioPublishingButton = (props) => {
1019
1101
  const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SEND_AUDIO);
1020
1102
  const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
1021
1103
  const { microphone, optimisticIsMute, hasBrowserPermission } = useMicrophoneState();
1022
- return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_AUDIO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now speak.'), messageAwaitingApproval: t('Awaiting for an approval to speak.'), messageRevoked: t('You can no longer speak.'), children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, title: !hasPermission
1104
+ const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1105
+ const handleClick = createCallControlHandler(props, async () => {
1106
+ if (!hasPermission) {
1107
+ await requestPermission();
1108
+ }
1109
+ else {
1110
+ await microphone.toggle();
1111
+ }
1112
+ });
1113
+ return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_AUDIO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now speak.'), messageAwaitingApproval: t('Awaiting for an approval to speak.'), messageRevoked: t('You can no longer speak.'), children: jsxRuntime.jsx(WithTooltip, { title: !hasPermission
1023
1114
  ? t('You have no permission to share your audio')
1024
1115
  : !hasBrowserPermission
1025
1116
  ? t('Check your browser mic permissions')
1026
- : caption || t('Mic'), variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: async () => {
1027
- if (!hasPermission) {
1028
- await requestPermission();
1029
- }
1030
- else {
1031
- await microphone.toggle();
1032
- }
1033
- }, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }));
1117
+ : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, onMenuToggle: (shown) => setTooltipDisabled(shown), ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }) }));
1034
1118
  };
1035
1119
 
1036
1120
  const ToggleVideoPreviewButton = (props) => {
@@ -1038,11 +1122,13 @@ const ToggleVideoPreviewButton = (props) => {
1038
1122
  const { t } = videoReactBindings.useI18n();
1039
1123
  const { useCameraState } = videoReactBindings.useCallStateHooks();
1040
1124
  const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
1041
- return (jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), title: !hasBrowserPermission
1125
+ const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1126
+ const handleClick = createCallControlHandler(props, () => camera.toggle());
1127
+ return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1042
1128
  ? t('Check your browser video permissions')
1043
- : caption || t('Video'), variant: "secondary", "data-testid": optimisticIsMute
1044
- ? 'preview-video-unmute-button'
1045
- : 'preview-video-mute-button', onClick: () => camera.toggle(), disabled: !hasBrowserPermission, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" }))] }));
1129
+ : caption ?? t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1130
+ ? 'preview-video-unmute-button'
1131
+ : 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission, Menu: Menu, menuPlacement: menuPlacement, onMenuToggle: (shown) => setTooltipDisabled(shown), ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: !optimisticIsMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" }))] }) }));
1046
1132
  };
1047
1133
  const ToggleVideoPublishingButton = (props) => {
1048
1134
  const { t } = videoReactBindings.useI18n();
@@ -1052,22 +1138,26 @@ const ToggleVideoPublishingButton = (props) => {
1052
1138
  const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
1053
1139
  const callSettings = useCallSettings();
1054
1140
  const isPublishingVideoAllowed = callSettings?.video.enabled;
1055
- return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_VIDEO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your video.'), messageAwaitingApproval: t('Awaiting for an approval to share your video.'), messageRevoked: t('You can no longer share your video.'), children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", title: !hasPermission
1141
+ const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
1142
+ const handleClick = createCallControlHandler(props, async () => {
1143
+ if (!hasPermission) {
1144
+ await requestPermission();
1145
+ }
1146
+ else {
1147
+ await camera.toggle();
1148
+ }
1149
+ });
1150
+ return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_VIDEO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your video.'), messageAwaitingApproval: t('Awaiting for an approval to share your video.'), messageRevoked: t('You can no longer share your video.'), children: jsxRuntime.jsx(WithTooltip, { title: !hasPermission
1056
1151
  ? t('You have no permission to share your video')
1057
1152
  : !hasBrowserPermission
1058
1153
  ? t('Check your browser video permissions')
1059
1154
  : !isPublishingVideoAllowed
1060
1155
  ? t('Video publishing is disabled by the system')
1061
- : caption || t('Video'), disabled: !hasBrowserPermission || !hasPermission || !isPublishingVideoAllowed, "data-testid": optimisticIsMute ? 'video-unmute-button' : 'video-mute-button', onClick: async () => {
1062
- if (!hasPermission) {
1063
- await requestPermission();
1064
- }
1065
- else {
1066
- await camera.toggle();
1067
- }
1068
- }, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
1156
+ : caption || t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
1069
1157
  !hasPermission ||
1070
- !isPublishingVideoAllowed) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }));
1158
+ !isPublishingVideoAllowed, "data-testid": optimisticIsMute ? 'video-unmute-button' : 'video-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, onMenuToggle: (shown) => setTooltipDisabled(shown), ...restCompositeButtonProps, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
1159
+ !hasPermission ||
1160
+ !isPublishingVideoAllowed) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" }))] }) }) }) }));
1071
1161
  };
1072
1162
 
1073
1163
  const EndCallMenu = (props) => {
@@ -1077,7 +1167,7 @@ const EndCallMenu = (props) => {
1077
1167
  };
1078
1168
  const CancelCallToggleMenuButton = react.forwardRef(function CancelCallToggleMenuButton(props, ref) {
1079
1169
  const { t } = videoReactBindings.useI18n();
1080
- return (jsxRuntime.jsx(IconButton, { icon: "call-end", variant: "danger", title: t('Leave call'), "data-testid": "leave-call-button", ref: ref }));
1170
+ return (jsxRuntime.jsx(WithTooltip, { title: t('Leave call'), children: jsxRuntime.jsx(IconButton, { icon: "call-end", variant: "danger", "data-testid": "leave-call-button", ref: ref }) }));
1081
1171
  });
1082
1172
  const CancelCallConfirmButton = ({ onClick, onLeave, }) => {
1083
1173
  const call = videoReactBindings.useCall();
@@ -1101,7 +1191,7 @@ const CancelCallConfirmButton = ({ onClick, onLeave, }) => {
1101
1191
  }, [onClick, onLeave, call]);
1102
1192
  return (jsxRuntime.jsx(MenuToggle, { placement: "top-start", ToggleButton: CancelCallToggleMenuButton, children: jsxRuntime.jsx(EndCallMenu, { onEnd: handleEndCall, onLeave: handleLeave }) }));
1103
1193
  };
1104
- const CancelCallButton = ({ disabled, onClick, onLeave, }) => {
1194
+ const CancelCallButton = ({ disabled, caption, onClick, onLeave, }) => {
1105
1195
  const call = videoReactBindings.useCall();
1106
1196
  const { t } = videoReactBindings.useI18n();
1107
1197
  const handleClick = react.useCallback(async (e) => {
@@ -1113,10 +1203,10 @@ const CancelCallButton = ({ disabled, onClick, onLeave, }) => {
1113
1203
  onLeave?.();
1114
1204
  }
1115
1205
  }, [onClick, onLeave, call]);
1116
- return (jsxRuntime.jsx(IconButton, { disabled: disabled, icon: "call-end", variant: "danger", title: t('Leave call'), "data-testid": "cancel-call-button", onClick: handleClick }));
1206
+ return (jsxRuntime.jsx(IconButton, { disabled: disabled, icon: "call-end", variant: "danger", title: caption ?? t('Leave call'), "data-testid": "cancel-call-button", onClick: handleClick }));
1117
1207
  };
1118
1208
 
1119
- const CallControls = ({ onLeave }) => (jsxRuntime.jsxs("div", { className: "str-video__call-controls", children: [jsxRuntime.jsx(RecordCallButton, {}), jsxRuntime.jsx(ReactionsButton, {}), jsxRuntime.jsx(ScreenShareButton, {}), jsxRuntime.jsx(SpeakingWhileMutedNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, {}) }), jsxRuntime.jsx(ToggleVideoPublishingButton, {}), jsxRuntime.jsx(CancelCallButton, { onLeave: onLeave })] }));
1209
+ const CallControls = ({ onLeave }) => (jsxRuntime.jsxs("div", { className: "str-video__call-controls", children: [jsxRuntime.jsx(SpeakingWhileMutedNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, {}) }), jsxRuntime.jsx(ToggleVideoPublishingButton, {}), jsxRuntime.jsx(ReactionsButton, {}), jsxRuntime.jsx(ScreenShareButton, {}), jsxRuntime.jsx(RecordCallButton, {}), jsxRuntime.jsx(CancelCallButton, { onLeave: onLeave })] }));
1120
1210
 
1121
1211
  chart_js.Chart.register(chart_js.CategoryScale, chart_js.LinearScale, chart_js.LineElement, chart_js.PointElement);
1122
1212
  const CallStatsLatencyChart = (props) => {
@@ -1333,50 +1423,12 @@ const CallParticipantListHeader = ({ onClose, }) => {
1333
1423
  return (jsxRuntime.jsxs("div", { className: "str-video__participant-list-header", children: [jsxRuntime.jsxs("div", { className: "str-video__participant-list-header__title", children: [t('Participants'), ' ', jsxRuntime.jsxs("span", { className: "str-video__participant-list-header__title-count", children: ["[", participants.length, "]"] }), anonymousParticipantCount > 0 && (jsxRuntime.jsx("span", { className: "str-video__participant-list-header__title-anonymous", children: t('Anonymous', { count: anonymousParticipantCount }) }))] }), jsxRuntime.jsx(IconButton, { onClick: onClose, className: "str-video__participant-list-header__close-button", icon: "close" })] }));
1334
1424
  };
1335
1425
 
1336
- const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
1337
- const { refs, x, y, strategy } = useFloatingUIPreset({
1338
- placement: tooltipPlacement,
1339
- strategy: 'absolute',
1340
- });
1341
- react.useEffect(() => {
1342
- refs.setReference(referenceElement);
1343
- }, [referenceElement, refs]);
1344
- if (!visible)
1345
- return null;
1346
- return (jsxRuntime.jsx("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
1347
- position: strategy,
1348
- top: y ?? 0,
1349
- left: x ?? 0,
1350
- overflowY: 'auto',
1351
- }, children: children }));
1352
- };
1353
-
1354
- const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
1355
- const [tooltipVisible, setTooltipVisible] = react.useState(false);
1356
- const handleMouseEnter = react.useCallback((e) => {
1357
- setTooltipVisible(true);
1358
- onMouseEnter?.(e);
1359
- }, [onMouseEnter]);
1360
- const handleMouseLeave = react.useCallback((e) => {
1361
- setTooltipVisible(false);
1362
- onMouseLeave?.(e);
1363
- }, [onMouseLeave]);
1364
- return { handleMouseEnter, handleMouseLeave, tooltipVisible };
1365
- };
1366
-
1367
- // todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
1368
- const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, ...props }) => {
1369
- const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
1370
- const [tooltipAnchor, setTooltipAnchor] = react.useState(null);
1371
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsxRuntime.jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
1372
- };
1373
-
1374
1426
  const CallParticipantListingItem = ({ participant, DisplayName = DefaultDisplayName, }) => {
1375
- const isAudioOn = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1376
- const isVideoOn = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
1377
- const isPinned = !!participant.pin;
1427
+ const isAudioOn = videoClient.hasAudio(participant);
1428
+ const isVideoOn = videoClient.hasVideo(participant);
1429
+ const isPinnedOn = videoClient.isPinned(participant);
1378
1430
  const { t } = videoReactBindings.useI18n();
1379
- return (jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item", children: [jsxRuntime.jsx(Avatar, { name: participant.name, imageSrc: participant.image }), jsxRuntime.jsx(DisplayName, { participant: participant }), jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item__media-indicator-group", children: [jsxRuntime.jsx(MediaIndicator, { title: isAudioOn ? t('Microphone on') : t('Microphone off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isAudioOn ? 'mic' : 'mic-off'}`) }), jsxRuntime.jsx(MediaIndicator, { title: isVideoOn ? t('Camera on') : t('Camera off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isVideoOn ? 'camera' : 'camera-off'}`) }), isPinned && (jsxRuntime.jsx(MediaIndicator, { title: t('Pinned'), className: clsx('str-video__participant-listing-item__icon', 'str-video__participant-listing-item__icon-pinned') })), jsxRuntime.jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$2, children: jsxRuntime.jsx(ParticipantViewContext.Provider, { value: { participant, trackType: 'none' }, children: jsxRuntime.jsx(ParticipantActionsContextMenu, {}) }) })] })] }));
1431
+ return (jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item", children: [jsxRuntime.jsx(Avatar, { name: participant.name, imageSrc: participant.image }), jsxRuntime.jsx(DisplayName, { participant: participant }), jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item__media-indicator-group", children: [jsxRuntime.jsx(MediaIndicator, { title: isAudioOn ? t('Microphone on') : t('Microphone off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isAudioOn ? 'mic' : 'mic-off'}`) }), jsxRuntime.jsx(MediaIndicator, { title: isVideoOn ? t('Camera on') : t('Camera off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isVideoOn ? 'camera' : 'camera-off'}`) }), isPinnedOn && (jsxRuntime.jsx(MediaIndicator, { title: t('Pinned'), className: clsx('str-video__participant-listing-item__icon', 'str-video__participant-listing-item__icon-pinned') })), jsxRuntime.jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$2, children: jsxRuntime.jsx(ParticipantViewContext.Provider, { value: { participant, trackType: 'none' }, children: jsxRuntime.jsx(ParticipantActionsContextMenu, {}) }) })] })] }));
1380
1432
  };
1381
1433
  const MediaIndicator = (props) => (jsxRuntime.jsx(WithTooltip, { ...props }));
1382
1434
  const DefaultDisplayName = ({ participant }) => {
@@ -1787,7 +1839,7 @@ const InitialsFallback = (props) => {
1787
1839
  };
1788
1840
 
1789
1841
  const Video$1 = ({ trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
1790
- const { sessionId, videoStream, screenShareStream, publishedTracks, viewportVisibilityState, isLocalParticipant, userId, } = participant;
1842
+ const { sessionId, videoStream, screenShareStream, viewportVisibilityState, isLocalParticipant, userId, } = participant;
1791
1843
  const call = videoReactBindings.useCall();
1792
1844
  const [videoElement, setVideoElement] = react.useState(null);
1793
1845
  // start with true, will flip once the video starts playing
@@ -1835,9 +1887,9 @@ const Video$1 = ({ trackType, participant, className, VideoPlaceholder = Default
1835
1887
  if (!call)
1836
1888
  return null;
1837
1889
  const isPublishingTrack = trackType === 'videoTrack'
1838
- ? publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO)
1890
+ ? videoClient.hasVideo(participant)
1839
1891
  : trackType === 'screenShareTrack'
1840
- ? publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE)
1892
+ ? videoClient.hasScreenShare(participant)
1841
1893
  : false;
1842
1894
  const isInvisible = trackType === 'none' ||
1843
1895
  viewportVisibilityState?.[trackType] === videoClient.VisibilityState.INVISIBLE;
@@ -1921,11 +1973,11 @@ const ParticipantActionsContextMenu = () => {
1921
1973
  const [pictureInPictureElement, setPictureInPictureElement] = react.useState(document.pictureInPictureElement);
1922
1974
  const call = videoReactBindings.useCall();
1923
1975
  const { t } = videoReactBindings.useI18n();
1924
- const { pin, publishedTracks, sessionId, userId } = participant;
1925
- const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1926
- const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
1927
- const hasScreenShare = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
1928
- const hasScreenShareAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
1976
+ const { pin, sessionId, userId } = participant;
1977
+ const hasAudioTrack = videoClient.hasAudio(participant);
1978
+ const hasVideoTrack = videoClient.hasVideo(participant);
1979
+ const hasScreenShareTrack = videoClient.hasScreenShare(participant);
1980
+ const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
1929
1981
  const blockUser = () => call?.blockUser(userId);
1930
1982
  const muteAudio = () => call?.muteUser(userId, 'audio');
1931
1983
  const muteVideo = () => call?.muteUser(userId, 'video');
@@ -2010,7 +2062,7 @@ const ParticipantActionsContextMenu = () => {
2010
2062
  return document.exitPictureInPicture().catch(console.error);
2011
2063
  };
2012
2064
  const { close } = useMenuContext() || {};
2013
- return (jsxRuntime.jsxs(GenericMenu, { onItemClick: close, children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.PIN_FOR_EVERYONE], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsxRuntime.jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [hasVideo && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShare && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudio && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudio && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
2065
+ return (jsxRuntime.jsxs(GenericMenu, { onItemClick: close, children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.PIN_FOR_EVERYONE], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsxRuntime.jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
2014
2066
  direction: fullscreenModeOn ? t('Leave') : t('Enter'),
2015
2067
  }) })), videoElement && document.pictureInPictureEnabled && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
2016
2068
  direction: pictureInPictureElement === videoElement
@@ -2045,10 +2097,9 @@ const DefaultScreenShareOverlay = () => {
2045
2097
  };
2046
2098
  const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'bottom-start', showMenuButton = true, ParticipantActionsContextMenu: ParticipantActionsContextMenu$1 = ParticipantActionsContextMenu, }) => {
2047
2099
  const { participant, trackType } = useParticipantViewContext();
2048
- const { publishedTracks } = participant;
2049
- const hasScreenShare = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2100
+ const isScreenSharing = videoClient.hasScreenShare(participant);
2050
2101
  if (participant.isLocalParticipant &&
2051
- hasScreenShare &&
2102
+ isScreenSharing &&
2052
2103
  trackType === 'screenShareTrack') {
2053
2104
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DefaultScreenShareOverlay, {}), jsxRuntime.jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
2054
2105
  }
@@ -2056,15 +2107,15 @@ const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'b
2056
2107
  };
2057
2108
  const ParticipantDetails = ({ indicatorsVisible = true, }) => {
2058
2109
  const { participant } = useParticipantViewContext();
2059
- const { isLocalParticipant, connectionQuality, publishedTracks, pin, sessionId, name, userId, } = participant;
2110
+ const { isLocalParticipant, connectionQuality, pin, sessionId, name, userId, } = participant;
2060
2111
  const call = videoReactBindings.useCall();
2061
2112
  const { t } = videoReactBindings.useI18n();
2062
2113
  const connectionQualityAsString = !!connectionQuality &&
2063
2114
  videoClient.SfuModels.ConnectionQuality[connectionQuality].toLowerCase();
2064
- const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
2065
- const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
2115
+ const hasAudioTrack = videoClient.hasAudio(participant);
2116
+ const hasVideoTrack = videoClient.hasVideo(participant);
2066
2117
  const canUnpin = !!pin && pin.isLocalPin;
2067
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "str-video__participant-details", children: jsxRuntime.jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && !hasAudio && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideo && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && canUnpin && (
2118
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "str-video__participant-details", children: jsxRuntime.jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && !hasAudioTrack && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideoTrack && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && canUnpin && (
2068
2119
  // TODO: remove this monstrosity once we have a proper design
2069
2120
  jsxRuntime.jsx("span", { title: t('Unpin'), onClick: () => call?.unpin(sessionId), className: "str-video__participant-details__name--pinned" })), indicatorsVisible && jsxRuntime.jsx(SpeechIndicator, {})] }) }), indicatorsVisible && (jsxRuntime.jsx(Notification, { isVisible: isLocalParticipant &&
2070
2121
  connectionQuality === videoClient.SfuModels.ConnectionQuality.POOR, message: t('Poor connection quality'), children: connectionQualityAsString && (jsxRuntime.jsx("span", { className: clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) }))] }));
@@ -2076,10 +2127,10 @@ const SpeechIndicator = () => {
2076
2127
  };
2077
2128
 
2078
2129
  const ParticipantView = react.forwardRef(function ParticipantView({ participant, trackType = 'videoTrack', muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) {
2079
- const { isLocalParticipant, isSpeaking, isDominantSpeaker, publishedTracks, sessionId, } = participant;
2080
- const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
2081
- const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
2082
- const hasScreenShareAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
2130
+ const { isLocalParticipant, isSpeaking, isDominantSpeaker, sessionId } = participant;
2131
+ const hasAudioTrack = videoClient.hasAudio(participant);
2132
+ const hasVideoTrack = videoClient.hasVideo(participant);
2133
+ const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
2083
2134
  const [trackedElement, setTrackedElement] = react.useState(null);
2084
2135
  const [contextVideoElement, setContextVideoElement] = react.useState(null);
2085
2136
  const [contextVideoPlaceholderElement, setContextVideoPlaceholderElement] = react.useState(null);
@@ -2115,7 +2166,7 @@ const ParticipantView = react.forwardRef(function ParticipantView({ participant,
2115
2166
  return (jsxRuntime.jsx("div", { "data-testid": "participant-view", ref: (element) => {
2116
2167
  applyElementToRef(ref, element);
2117
2168
  setTrackedElement(element);
2118
- }, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideo && 'str-video__participant-view--no-video', !hasAudio && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudio && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudio && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2169
+ }, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideoTrack && 'str-video__participant-view--no-video', !hasAudioTrack && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2119
2170
  });
2120
2171
 
2121
2172
  // re-exporting the StreamCallProvider as StreamCall
@@ -2284,12 +2335,13 @@ const loggedIn = (a, b) => {
2284
2335
  const LivestreamLayout = (props) => {
2285
2336
  const { useParticipants, useRemoteParticipants, useHasOngoingScreenShare } = videoReactBindings.useCallStateHooks();
2286
2337
  const call = videoReactBindings.useCall();
2287
- const [currentSpeaker, ...otherParticipants] = useParticipants();
2338
+ const participants = useParticipants();
2339
+ const [currentSpeaker] = participants;
2288
2340
  const remoteParticipants = useRemoteParticipants();
2289
2341
  const hasOngoingScreenShare = useHasOngoingScreenShare();
2290
2342
  const presenter = hasOngoingScreenShare
2291
- ? hasScreenShare$1(currentSpeaker) && currentSpeaker
2292
- : otherParticipants.find(hasScreenShare$1);
2343
+ ? participants.find(videoClient.hasScreenShare)
2344
+ : undefined;
2293
2345
  usePaginatedLayoutSortPreset(call);
2294
2346
  const Overlay = (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName }));
2295
2347
  const { floatingParticipantProps } = props;
@@ -2303,7 +2355,6 @@ const LivestreamLayout = (props) => {
2303
2355
  clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: FloatingParticipantOverlay || Overlay, muteAudio // audio is rendered by ParticipantsAudio
2304
2356
  : true }))] }));
2305
2357
  };
2306
- const hasScreenShare$1 = (p) => !!p?.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2307
2358
  const ParticipantOverlay = (props) => {
2308
2359
  const { enableFullScreen = true, showParticipantCount = true, showDuration = true, showLiveBadge = true, showSpeakerName = false, } = props;
2309
2360
  const { participant } = useParticipantViewContext();
@@ -2467,7 +2518,7 @@ const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, Par
2467
2518
  const [participantsBarWrapperElement, setParticipantsBarWrapperElement] = react.useState(null);
2468
2519
  const [participantsBarElement, setParticipantsBarElement] = react.useState(null);
2469
2520
  const [buttonsWrapperElement, setButtonsWrapperElement] = react.useState(null);
2470
- const isSpeakerScreenSharing = hasScreenShare(participantInSpotlight);
2521
+ const isSpeakerScreenSharing = participantInSpotlight && videoClient.hasScreenShare(participantInSpotlight);
2471
2522
  const hardLimit = useCalculateHardLimit(buttonsWrapperElement, participantsBarElement, participantsBarLimit);
2472
2523
  const isVertical = participantsBarPosition === 'left' || participantsBarPosition === 'right';
2473
2524
  const isHorizontal = participantsBarPosition === 'top' || participantsBarPosition === 'bottom';
@@ -2515,9 +2566,8 @@ const VerticalScrollButtons = ({ scrollWrapper, }) => {
2515
2566
  };
2516
2567
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [scrollPosition && scrollPosition !== 'top' && (jsxRuntime.jsx(IconButton, { onClick: scrollTopClickHandler, icon: "caret-up", className: "str-video__speaker-layout__participants-bar--button-top" })), scrollPosition && scrollPosition !== 'bottom' && (jsxRuntime.jsx(IconButton, { onClick: scrollBottomClickHandler, icon: "caret-down", className: "str-video__speaker-layout__participants-bar--button-bottom" }))] }));
2517
2568
  };
2518
- const hasScreenShare = (p) => !!p?.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2519
2569
 
2520
- const [major, minor, patch] = ("1.0.6" ).split('.');
2570
+ const [major, minor, patch] = ("1.0.8" ).split('.');
2521
2571
  videoClient.setSdkInfo({
2522
2572
  type: videoClient.SfuModels.SdkType.REACT,
2523
2573
  major,