mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
refactor(voice): flatten participant context menu and extract stream menus (#2112)
This commit is contained in:
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"
|
||||
/>
|
||||
)}
|
||||
|
||||
+10
-18
@@ -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);
|
||||
}, []);
|
||||
|
||||
+12
-59
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user