refactor(voice): flatten participant context menu and extract stream menus (#2112)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-30 03:02:30 +02:00
1 parent 3b1eb56713
commit 03641f622f
25 files changed
+1787 -565

No files matched your search

@@ -0,0 +1,123 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Logger} from '@app/features/platform/utils/AppLogger';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
import {
openScreenShareSourceSwitcherModal,
type ScreenSharePickerTab,
} from '@app/features/voice/components/modals/ScreenSharePickerModal';
import {StreamSettingsMenuContent} from '@app/features/voice/components/StreamSettingsMenuContent';
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
import type {DisplayShareEnvironment} from '@app/features/voice/utils/ScreenShareEnvironment';
import type {StreamSettingsShareContext} from '@app/features/voice/utils/StreamSettingsUpdatePolicy';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {MonitorPlayIcon, StopCircleIcon} from '@phosphor-icons/react';
import type React from 'react';
const STOP_STREAMING_DESCRIPTOR = msg({
message: 'Stop Streaming',
comment: 'Danger action that stops the active screen share.',
});
const CHANGE_STREAM_DESCRIPTOR = msg({
message: 'Change Stream',
comment: 'Action that opens the source picker for the active screen share.',
});
const logger = new Logger('ActiveScreenShareMenu');
export interface ActiveScreenShareMenuProps {
onClose: () => void;
displayShareEnvironment: DisplayShareEnvironment;
shareContext: StreamSettingsShareContext;
shareContextResolved: boolean;
iconClassName?: string;
additionalActions?: React.ReactNode;
showLiveSettings?: boolean;
tail?: React.ReactNode;
}
export async function stopActiveScreenShare(): Promise<void> {
await MediaEngine.setScreenShareEnabled(false);
}
function getSourceSwitcherTab(shareContext: StreamSettingsShareContext): ScreenSharePickerTab {
if (shareContext === 'device') return 'devices';
if (shareContext === 'app') return 'apps';
return 'displays';
}
export async function changeActiveScreenShare(shareContext: StreamSettingsShareContext = 'display'): Promise<void> {
await openScreenShareSourceSwitcherModal({initialTab: getSourceSwitcherTab(shareContext)});
}
export const ActiveScreenShareMenu: React.FC<ActiveScreenShareMenuProps> = ({
onClose,
displayShareEnvironment,
shareContext,
shareContextResolved,
iconClassName,
additionalActions,
showLiveSettings = true,
tail,
}) => {
const {i18n} = useLingui();
const isWeb = displayShareEnvironment === 'web';
const openChangeStream = (nextShareContext: StreamSettingsShareContext) => {
onClose();
void changeActiveScreenShare(nextShareContext).catch((error) => {
logger.error('Failed to change active screen share source', error);
});
};
const handleChangeStream = () => openChangeStream(shareContext);
return (
<>
<MenuGroup data-flx="voice.active-screen-share-menu.actions">
<MenuItem
icon={
<StopCircleIcon
weight="fill"
className={iconClassName}
data-flx="voice.active-screen-share-menu.stop-circle-icon"
/>
}
danger
onClick={() => {
onClose();
void stopActiveScreenShare().catch((error) => {
logger.error('Failed to stop active screen share', error);
});
}}
data-flx="voice.active-screen-share-menu.stop"
>
{i18n._(STOP_STREAMING_DESCRIPTOR)}
</MenuItem>
<MenuItem
icon={
<MonitorPlayIcon
weight="fill"
className={iconClassName}
data-flx="voice.active-screen-share-menu.monitor-play-icon"
/>
}
onClick={handleChangeStream}
data-flx="voice.active-screen-share-menu.change"
>
{i18n._(CHANGE_STREAM_DESCRIPTOR)}
</MenuItem>
{additionalActions}
{showLiveSettings && !isWeb && (
<StreamSettingsMenuContent
applyToLiveStream
variant="compactLive"
displayShareEnvironment={displayShareEnvironment}
shareContext={shareContext}
shareContextResolved={shareContextResolved}
data-flx="voice.active-screen-share-menu.live-settings"
/>
)}
</MenuGroup>
{tail}
</>
);
};
@@ -149,6 +149,14 @@ const ADAPTIVE_QUALITY_ACTIVE_DESCRIPTOR = msg({
comment:
'Inline adaptive-quality status in the stream settings menu. Shows the current automatically lowered resolution and frame rate.',
});
const STREAM_QUALITY_DESCRIPTOR = msg({
message: 'Stream Quality',
comment: 'Compact submenu label for changing the resolution and frame rate of an active stream.',
});
const SHARE_STREAM_AUDIO_DESCRIPTOR = msg({
message: 'Share Stream Audio',
comment: 'Toggle label for sharing audio with an active stream.',
});
const logger = new Logger('StreamSettingsMenuContent');
const SCREEN_SHARE_AUDIO_SOURCE = VoiceTrackSource.ScreenShareAudio as Track.Source;
@@ -329,6 +337,7 @@ interface StreamSettingsMenuContentProps {
shareContext?: StreamSettingsShareContext;
shareContextResolved?: boolean;
displayShareEnvironment: DisplayShareEnvironment;
variant?: 'full' | 'compactLive';
}
export const StreamSettingsMenuContent = observer(
@@ -337,6 +346,7 @@ export const StreamSettingsMenuContent = observer(
shareContext = 'display',
shareContextResolved = true,
displayShareEnvironment,
variant = 'full',
}: StreamSettingsMenuContentProps) => {
const {i18n} = useLingui();
useMediaEngineVersion();
@@ -348,6 +358,18 @@ export const StreamSettingsMenuContent = observer(
const currentMode = VoiceSettings.getStreamingMode();
const currentResolution = VoiceSettings.getScreenshareResolution();
const currentFrameRate = resolveScreenShareFrameRate(VoiceSettings.getVideoFrameRate());
const effectiveMode = normaliseStreamingModeForContext(currentMode, getScreenShareContext(shareContext));
const effectiveResolution = normaliseResolutionForContext(
currentResolution,
getScreenShareContext(shareContext),
hasHigherVideoQuality,
);
const effectiveQuality = resolveStreamingModeSettings(
effectiveMode,
effectiveResolution,
currentFrameRate,
hasHigherVideoQuality,
);
const captureAudioEnabled = isAppShare
? VoiceSettings.getShareAppAudio()
: isDeviceShare
@@ -552,6 +574,117 @@ export const StreamSettingsMenuContent = observer(
const selectedAudioDeviceLabel = selectedAudioDevice
? formatVoiceAudioDeviceLabel(i18n, selectedAudioDevice, i18n._(UNNAMED_INPUT_DESCRIPTOR))
: i18n._(SYSTEM_DEFAULT_DESCRIPTOR);
if (variant === 'compactLive') {
const selectCompactResolution = (option: Option<ScreenshareResolution>) => {
if (option.isPremium && !hasHigherVideoQuality) {
if (showPremiumFeatures) PremiumModalCommands.open();
return;
}
if (currentMode === 'custom' && effectiveQuality.resolution === option.value) return;
VoiceSettingsCommands.update({
streamingMode: 'custom',
screenshareResolution: option.value,
videoFrameRate: effectiveQuality.frameRate,
});
runApply();
};
const selectCompactFrameRate = (option: Option<SupportedScreenShareFrameRate>) => {
if (option.isPremium && !hasHigherVideoQuality) {
if (showPremiumFeatures) PremiumModalCommands.open();
return;
}
if (currentMode === 'custom' && effectiveQuality.frameRate === option.value) return;
VoiceSettingsCommands.update({
streamingMode: 'custom',
screenshareResolution: effectiveQuality.resolution,
videoFrameRate: option.value,
});
runApply();
};
return (
<>
<MenuItemSubmenu
label={i18n._(STREAM_QUALITY_DESCRIPTOR)}
render={() => (
<>
<MenuGroup data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-group">
<MenuGroupLabel data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-label">
{i18n._(FRAMERATE_DESCRIPTOR)}
</MenuGroupLabel>
{frameRateOptions.map((option) => {
const premiumLocked = showPremiumFeatures && option.isPremium && !hasHigherVideoQuality;
return (
<MenuItemRadio
key={option.value}
selected={effectiveQuality.frameRate === option.value}
onSelect={() => selectCompactFrameRate(option)}
data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-option"
>
<span
className={styles.row}
data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-row"
>
<span
className={styles.rowLabel}
data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-row-label"
>
{option.label}
</span>
{premiumLocked && (
<PremiumBadge data-flx="voice.stream-settings-menu-content.compact-live.frame-rate-premium-badge" />
)}
</span>
</MenuItemRadio>
);
})}
</MenuGroup>
<MenuGroup data-flx="voice.stream-settings-menu-content.compact-live.resolution-group">
<MenuGroupLabel data-flx="voice.stream-settings-menu-content.compact-live.resolution-label">
{i18n._(RESOLUTION_DESCRIPTOR)}
</MenuGroupLabel>
{resolutionOptions.map((option) => {
const premiumLocked = showPremiumFeatures && option.isPremium && !hasHigherVideoQuality;
return (
<MenuItemRadio
key={option.value}
selected={effectiveQuality.resolution === option.value}
onSelect={() => selectCompactResolution(option)}
data-flx="voice.stream-settings-menu-content.compact-live.resolution-option"
>
<span
className={styles.row}
data-flx="voice.stream-settings-menu-content.compact-live.resolution-row"
>
<span
className={styles.rowLabel}
data-flx="voice.stream-settings-menu-content.compact-live.resolution-row-label"
>
{option.label}
</span>
{premiumLocked && (
<PremiumBadge data-flx="voice.stream-settings-menu-content.compact-live.resolution-premium-badge" />
)}
</span>
</MenuItemRadio>
);
})}
</MenuGroup>
</>
)}
data-flx="voice.stream-settings-menu-content.compact-live.quality-submenu"
/>
{audioMenuState.control.value === 'toggle' && (
<CheckboxItem
checked={audioMenuState.control.checked}
onCheckedChange={handleCaptureAudioToggle}
data-flx="voice.stream-settings-menu-content.compact-live.share-audio"
>
{i18n._(SHARE_STREAM_AUDIO_DESCRIPTOR)}
</CheckboxItem>
)}
</>
);
}
return (
<>
<MenuGroup data-flx="voice.stream-settings-menu-content.menu-group">
@@ -247,6 +247,8 @@ const SpectatorRow = observer(function SpectatorRow({
onClose={onClose}
guildId={guildId}
connectionId={connectionId}
surface="stream-spectator-list"
source={{kind: 'participant'}}
data-flx="voice.stream-spectators-popout.handle-context-menu.voice-participant-context-menu"
/>
));
@@ -19,6 +19,7 @@ import {TooltipWithKeybind} from '@app/features/ui/keybind_hint/KeybindHint';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {isDesktop} from '@app/features/ui/utils/NativeUtils';
import {ActiveScreenShareMenu} from '@app/features/voice/components/ActiveScreenShareMenu';
import {
VoiceAudioSettingsBottomSheet,
VoiceCameraSettingsBottomSheet,
@@ -30,7 +31,6 @@ import {
} from '@app/features/voice/components/modals/CameraPreviewModal';
import {
openScreenSharePickerModal,
openScreenShareSourceSwitcherModal,
preloadScreenSharePickerSources,
} from '@app/features/voice/components/modals/ScreenSharePickerModal';
import {getStreamKey} from '@app/features/voice/components/StreamKeys';
@@ -105,10 +105,6 @@ import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
const CHANGE_SOURCE_DESCRIPTOR = msg({
message: 'Change source',
comment: 'Voice control menu action to choose a different screen-share source.',
});
const END_SCREEN_SHARE_DESCRIPTOR = msg({
message: 'End screen share',
comment: 'Voice control menu action to stop sharing the screen.',
@@ -353,56 +349,10 @@ const VoiceControlBarInner = observer(function VoiceControlBarInner() {
}
}, [localParticipant, isCameraEnabled, isConnected]);
const renderScreenShareMenu = useCallback(
({onClose}: {onClose: () => void}) => (
<>
{isScreenShareEnabled && (
<MenuGroup data-flx="voice.voice-control-bar.render-screen-share-menu.menu-group">
<MenuItem
icon={
<MonitorPlayIcon
weight="fill"
className={styles.icon}
data-flx="voice.voice-control-bar.render-screen-share-menu.icon"
/>
}
onClick={async () => {
onClose();
try {
await openScreenShareSourceSwitcherModal();
} catch (error) {
logger.error('Failed to open screen share source switcher:', error);
}
}}
data-flx="voice.voice-control-bar.render-screen-share-menu.menu-item.close"
>
{i18n._(CHANGE_SOURCE_DESCRIPTOR)}
</MenuItem>
<MenuItem
icon={
<MonitorPlayIcon
weight="fill"
className={styles.icon}
data-flx="voice.voice-control-bar.render-screen-share-menu.icon--2"
/>
}
danger
onClick={async () => {
onClose();
await MediaEngine.setScreenShareEnabled(false);
}}
data-flx="voice.voice-control-bar.render-screen-share-menu.menu-item.close--2"
>
{i18n._(END_SCREEN_SHARE_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
)}
<StreamSettingsMenuContent
applyToLiveStream={isScreenShareEnabled}
displayShareEnvironment={displayShareEnvironment}
shareContext={ActiveScreenShareSource.getSourceId()?.startsWith('window:') ? 'app' : 'display'}
shareContextResolved={ActiveScreenShareSource.getSourceId() != null}
data-flx="voice.voice-control-bar.render-screen-share-menu.stream-settings-menu-content"
/>
({onClose}: {onClose: () => void}) => {
const shareContext = ActiveScreenShareSource.getSourceId()?.startsWith('window:') ? 'app' : 'display';
const shareContextResolved = ActiveScreenShareSource.getSourceId() != null;
const screenShareSettingsMenu = (
<MenuGroup data-flx="voice.voice-control-bar.render-screen-share-menu.menu-group--2">
<MenuItem
icon={
@@ -418,8 +368,33 @@ const VoiceControlBarInner = observer(function VoiceControlBarInner() {
{i18n._(VOICE_SCREEN_SHARE_SETTINGS_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
</>
),
);
if (isScreenShareEnabled) {
return (
<ActiveScreenShareMenu
onClose={onClose}
displayShareEnvironment={displayShareEnvironment}
shareContext={shareContext}
shareContextResolved={shareContextResolved}
iconClassName={styles.icon}
tail={screenShareSettingsMenu}
data-flx="voice.voice-control-bar.render-screen-share-menu.active-screen-share-menu"
/>
);
}
return (
<>
<StreamSettingsMenuContent
applyToLiveStream={false}
displayShareEnvironment={displayShareEnvironment}
shareContext={shareContext}
shareContextResolved={shareContextResolved}
data-flx="voice.voice-control-bar.render-screen-share-menu.stream-settings-menu-content"
/>
{screenShareSettingsMenu}
</>
);
},
[displayShareEnvironment, isScreenShareEnabled, i18n],
);
const openScreenShareMenu = useCallback(
@@ -279,6 +279,8 @@ function VoiceParticipantPopoutRow({entry, guildId, channelId}: VoiceParticipant
onClose={onClose}
guildId={guildId ?? undefined}
connectionId={entry.connectionId}
surface="participant-avatar-list"
source={{kind: 'participant'}}
data-flx="voice.voice-participant-avatar-list.handle-context-menu.voice-participant-context-menu"
/>
));
@@ -414,6 +416,8 @@ export const VoiceParticipantSpeakingAvatarStack: React.FC<VoiceParticipantSpeak
onClose={onClose}
guildId={guildId ?? undefined}
connectionId={entry.connectionId}
surface="participant-avatar-list"
source={{kind: 'participant'}}
data-flx="voice.voice-participant-avatar-list.handle-user-context-menu.voice-participant-context-menu"
/>
));
@@ -563,6 +567,8 @@ export const VoiceParticipantWrappedAvatarList: React.FC<VoiceParticipantWrapped
onClose={onClose}
guildId={guildId ?? undefined}
connectionId={entry.connectionId}
surface="participant-avatar-list"
source={{kind: 'participant'}}
data-flx="voice.voice-participant-avatar-list.voice-participant-wrapped-avatar-list.handle-context-menu.voice-participant-context-menu"
/>
));
@@ -5,6 +5,7 @@ import {WATCH_STREAM_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDes
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import Permission from '@app/features/permissions/state/Permission';
import {dimColor} from '@app/features/theme/utils/ColorUtils';
import type {VoiceParticipantMenuSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
import {VoiceParticipantContextMenu} from '@app/features/ui/action_menu/VoiceParticipantContextMenu';
import {Button} from '@app/features/ui/button/Button';
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
@@ -624,6 +625,43 @@ const VoiceParticipantTileInner = observer(function VoiceParticipantTileInner({
other: 'Keep watching # streams and add this one',
},
);
const participantMenuSource = useMemo<VoiceParticipantMenuSource>(() => {
if (!isScreenShare) {
return isCameraTile && isCameraPublicationActive ? {kind: 'camera'} : {kind: 'participant'};
}
if (isOwnScreenShare) {
return {kind: 'screen-share', streamKey, state: {kind: 'own'}};
}
if (isWatching) {
return {
kind: 'screen-share',
streamKey,
state: {kind: 'remote-watched', hasAudio: hasScreenShareAudio, onStopWatching: stopWatching},
};
}
return {
kind: 'screen-share',
streamKey,
state: {
kind: 'remote-unwatched',
onWatch: () => {
startWatching();
VoiceCallLayoutCommands.setPinnedParticipant(identity, VoiceTrackSource.ScreenShare);
},
},
};
}, [
hasScreenShareAudio,
identity,
isCameraPublicationActive,
isCameraTile,
isOwnScreenShare,
isScreenShare,
isWatching,
startWatching,
stopWatching,
streamKey,
]);
const handleContextMenu = useCallback(
(event: React.MouseEvent | MouseEvent) => {
if (!participantUser) return;
@@ -635,13 +673,9 @@ const VoiceParticipantTileInner = observer(function VoiceParticipantTileInner({
onClose={onClose}
guildId={guildId}
connectionId={connectionId}
surface="call-tile"
source={participantMenuSource}
isGroupedItem={isGroupedItem}
streamKey={streamKey}
isScreenShare={isScreenShare}
isWatching={isWatching}
hasScreenShareAudio={hasScreenShareAudio}
isOwnScreenShare={isOwnScreenShare}
onStopWatching={stopWatching}
hiddenConnectionCount={groupHiddenCount}
deviceConnectionCount={groupDeviceConnectionCount}
isDeviceGroupExpanded={tileGroup?.isExpanded ?? false}
@@ -656,12 +690,7 @@ const VoiceParticipantTileInner = observer(function VoiceParticipantTileInner({
guildId,
connectionId,
isCurrentUser,
streamKey,
isScreenShare,
isWatching,
hasScreenShareAudio,
isOwnScreenShare,
stopWatching,
participantMenuSource,
groupHiddenCount,
groupDeviceConnectionCount,
tileGroup,
@@ -1395,13 +1424,9 @@ const VoiceParticipantTileInner = observer(function VoiceParticipantTileInner({
participant={connectionParticipant}
guildId={guildId}
connectionId={connectionId}
surface="call-tile"
source={participantMenuSource}
isConnectionItem
streamKey={streamKey}
isScreenShare={isScreenShare}
isWatching={isWatching}
hasScreenShareAudio={hasScreenShareAudio}
isOwnScreenShare={isOwnScreenShare}
onStopWatching={stopWatching}
data-flx="voice.voice-participant-tile.voice-participant-tile-inner.voice-participant-bottom-sheet"
/>
)}
@@ -1,6 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {useVoiceParticipantMenuData} from '@app/features/ui/action_menu/items/VoiceParticipantMenuData';
import type {
VoiceParticipantMenuSource,
VoiceParticipantMenuSurface,
} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
import {MenuBottomSheet} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet';
import type {User} from '@app/features/user/models/User';
import {observer} from 'mobx-react-lite';
@@ -12,15 +16,11 @@ interface VoiceParticipantBottomSheetProps {
user: User;
guildId?: string;
connectionId?: string;
surface: VoiceParticipantMenuSurface;
source: VoiceParticipantMenuSource;
isConnectionItem?: boolean;
isParentGroupedItem?: boolean;
participant?: unknown;
streamKey?: string;
isScreenShare?: boolean;
isWatching?: boolean;
hasScreenShareAudio?: boolean;
isOwnScreenShare?: boolean;
onStopWatching?: () => void;
}
export const VoiceParticipantBottomSheet: React.FC<VoiceParticipantBottomSheetProps> = observer(
@@ -30,27 +30,19 @@ export const VoiceParticipantBottomSheet: React.FC<VoiceParticipantBottomSheetPr
user,
guildId,
connectionId,
surface,
source,
isConnectionItem = false,
isParentGroupedItem = false,
streamKey,
isScreenShare = false,
isWatching = false,
hasScreenShareAudio = false,
isOwnScreenShare = false,
onStopWatching,
}) => {
const {groups} = useVoiceParticipantMenuData({
user,
guildId,
connectionId,
surface,
source,
isGroupedItem: isConnectionItem,
isParentGroupedItem,
streamKey,
isScreenShare,
isWatching,
hasScreenShareAudio,
isOwnScreenShare,
onStopWatching,
onClose,
});
return (
@@ -527,16 +527,18 @@ const ScreenSharePreviewInfoModal = observer(() => {
);
});
export function openScreenSharePreviewPrivacyModal(): void {
ModalCommands.push(
ModalCommands.modal(() => (
<ScreenSharePreviewInfoModal data-flx="voice.screen-share-picker-modal.open-preview-privacy-modal.screen-share-preview-info-modal" />
)),
);
}
const ScreenSharePreviewFooterNotice = observer(() => {
const {i18n} = useLingui();
const previewsEnabled = !PrivacyPreferences.getDisableStreamPreviews();
const openInfoModal = useCallback(() => {
ModalCommands.push(
ModalCommands.modal(() => (
<ScreenSharePreviewInfoModal data-flx="voice.screen-share-picker-modal.open-info-modal.screen-share-preview-info-modal" />
)),
);
}, []);
const openInfoModal = useCallback(openScreenSharePreviewPrivacyModal, []);
const handleDisable = useCallback(() => {
PrivacyPreferences.setDisableStreamPreviews(true);
}, []);
@@ -3,24 +3,21 @@
import {TURN_OFF_CAMERA_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import Permission from '@app/features/permissions/state/Permission';
import NativePermission from '@app/features/permissions/system/state/NativePermission';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {isDesktop} from '@app/features/ui/utils/NativeUtils';
import {ActiveScreenShareMenu} from '@app/features/voice/components/ActiveScreenShareMenu';
import {
CameraPreviewModalInRoom,
CameraPreviewModalStandalone,
} from '@app/features/voice/components/modals/CameraPreviewModal';
import {
openScreenSharePickerModal,
openScreenShareSourceSwitcherModal,
preloadScreenSharePickerSources,
} from '@app/features/voice/components/modals/ScreenSharePickerModal';
import {StreamSettingsMenuContent} from '@app/features/voice/components/StreamSettingsMenuContent';
import styles from '@app/features/voice/components/VoiceConnectionStatus.module.css';
import {VoiceCameraSettingsMenu} from '@app/features/voice/components/VoiceSettingsMenus';
import {selectLocalParticipantControlsViewState} from '@app/features/voice/components/voice_connection_status/LocalParticipantControlsStateMachine';
@@ -45,14 +42,6 @@ import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import {type MouseEvent as ReactMouseEvent, useCallback} from 'react';
const CHANGE_SOURCE_DESCRIPTOR = msg({
message: 'Change source',
comment: 'Tooltip / button label in the screen-share status pill. Opens the picker to change what is being shared.',
});
const END_SCREEN_SHARE_DESCRIPTOR = msg({
message: 'End screen share',
comment: 'Button label that stops the current screen share.',
});
const WAITING_FOR_CONNECTION_DESCRIPTOR = msg({
message: 'Waiting for connection...',
comment:
@@ -146,56 +135,20 @@ export const LocalParticipantControls = observer(() => {
if (!isConnected || !isScreenShareEnabled) return;
event.preventDefault();
event.stopPropagation();
const shareContext = ActiveScreenShareSource.getSourceId()?.startsWith('window:') ? 'app' : 'display';
const shareContextResolved = ActiveScreenShareSource.getSourceId() != null;
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
<>
<MenuGroup data-flx="voice.voice-connection-status.open-screen-share-menu.menu-group">
<MenuItem
icon={
<MonitorPlayIcon
weight="fill"
className={styles.icon}
data-flx="voice.voice-connection-status.open-screen-share-menu.icon"
/>
}
onClick={async () => {
onClose();
try {
await openScreenShareSourceSwitcherModal();
} catch (error) {
logger.error('Failed to open screen share source switcher:', error);
}
}}
data-flx="voice.voice-connection-status.open-screen-share-menu.menu-item.close"
>
{i18n._(CHANGE_SOURCE_DESCRIPTOR)}
</MenuItem>
<MenuItem
icon={
<MonitorPlayIcon
weight="fill"
className={styles.icon}
data-flx="voice.voice-connection-status.open-screen-share-menu.icon--2"
/>
}
danger
onClick={async () => {
onClose();
await MediaEngine.setScreenShareEnabled(false);
}}
data-flx="voice.voice-connection-status.open-screen-share-menu.menu-item.close--2"
>
{i18n._(END_SCREEN_SHARE_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
<StreamSettingsMenuContent
displayShareEnvironment={displayShareEnvironment}
shareContext={ActiveScreenShareSource.getSourceId()?.startsWith('window:') ? 'app' : 'display'}
data-flx="voice.voice-connection-status.open-screen-share-menu.stream-settings-menu-content"
/>
</>
<ActiveScreenShareMenu
onClose={onClose}
displayShareEnvironment={displayShareEnvironment}
shareContext={shareContext}
shareContextResolved={shareContextResolved}
iconClassName={styles.icon}
data-flx="voice.voice-connection-status.open-screen-share-menu.active-screen-share-menu"
/>
));
},
[displayShareEnvironment, isConnected, isScreenShareEnabled, i18n],
[displayShareEnvironment, isConnected, isScreenShareEnabled],
);
const handleScreenShare = useCallback(async () => {
if (!isConnected) return;
@@ -13,7 +13,7 @@ export const VOICE_TILE_POPOUT_DEFAULT_HEIGHT = 480;
export const VOICE_CALL_POPOUT_DEFAULT_WIDTH = 960;
export const VOICE_CALL_POPOUT_DEFAULT_HEIGHT = 600;
export type VoiceTilePopoutSource = 'camera' | 'screen_share';
export type VoiceTilePopoutSource = 'user' | 'camera' | 'screen_share';
export interface VoiceTilePopoutDescriptor {
kind: 'tile';