initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,594 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {useLingui} from '@lingui/react/macro';
import {
CopyIcon,
EyeIcon,
EyeSlashIcon,
GearIcon,
MicrophoneSlashIcon,
PhoneXIcon,
ProjectorScreenIcon,
SpeakerSlashIcon,
VideoCameraSlashIcon,
VideoIcon,
} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import React from 'react';
import * as GuildMemberActionCreators from '~/actions/GuildMemberActionCreators';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import * as SoundActionCreators from '~/actions/SoundActionCreators';
import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators';
import * as VoiceCallLayoutActionCreators from '~/actions/VoiceCallLayoutActionCreators';
import * as VoiceStateActionCreators from '~/actions/VoiceStateActionCreators';
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
import CallMediaPrefsStore from '~/stores/CallMediaPrefsStore';
import ConnectionStore from '~/stores/ConnectionStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import ParticipantVolumeStore from '~/stores/ParticipantVolumeStore';
import UserStore from '~/stores/UserStore';
import VoiceCallLayoutStore from '~/stores/VoiceCallLayoutStore';
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
import type {VoiceState} from '~/stores/voice/VoiceStateManager';
import {SoundType} from '~/utils/SoundUtils';
import {MenuItem} from '../MenuItem';
import {MenuItemCheckbox} from '../MenuItemCheckbox';
import {MenuItemSlider} from '../MenuItemSlider';
import styles from './MenuItems.module.css';
interface SelfMuteMenuItemProps {
onClose: () => void;
connectionId?: string;
isDeviceSpecific?: boolean;
label?: string;
}
export const SelfMuteMenuItem: React.FC<SelfMuteMenuItemProps> = observer(
({connectionId, isDeviceSpecific = false, label}) => {
const {t} = useLingui();
const voiceState = connectionId
? MediaEngineStore.getVoiceStateByConnectionId(connectionId)
: MediaEngineStore.getCurrentUserVoiceState();
const isSelfMuted = voiceState?.self_mute ?? false;
const handleToggle = React.useCallback(() => {
if (isDeviceSpecific && connectionId) {
VoiceStateActionCreators.toggleSelfMuteForConnection(connectionId);
} else {
VoiceStateActionCreators.toggleSelfMute(null);
}
}, [connectionId, isDeviceSpecific]);
return (
<MenuItemCheckbox
icon={<MicrophoneSlashIcon weight="fill" className={styles.icon} />}
checked={isSelfMuted}
onChange={handleToggle}
>
{label ?? t`Mute`}
</MenuItemCheckbox>
);
},
);
interface SelfDeafenMenuItemProps {
onClose: () => void;
connectionId?: string;
isDeviceSpecific?: boolean;
label?: string;
}
export const SelfDeafenMenuItem: React.FC<SelfDeafenMenuItemProps> = observer(
({connectionId, isDeviceSpecific = false, label}) => {
const {t} = useLingui();
const voiceState = connectionId
? MediaEngineStore.getVoiceStateByConnectionId(connectionId)
: MediaEngineStore.getCurrentUserVoiceState();
const isSelfDeafened = voiceState?.self_deaf ?? false;
const handleToggle = React.useCallback(() => {
if (isDeviceSpecific && connectionId) {
VoiceStateActionCreators.toggleSelfDeafenForConnection(connectionId);
} else {
VoiceStateActionCreators.toggleSelfDeaf(null);
}
}, [connectionId, isDeviceSpecific]);
return (
<MenuItemCheckbox
icon={<SpeakerSlashIcon weight="fill" className={styles.icon} />}
checked={isSelfDeafened}
onChange={handleToggle}
>
{label ?? t`Deafen`}
</MenuItemCheckbox>
);
},
);
interface VoiceVideoSettingsMenuItemProps {
onClose: () => void;
}
export const VoiceVideoSettingsMenuItem: React.FC<VoiceVideoSettingsMenuItemProps> = observer(({onClose}) => {
const {t} = useLingui();
const handleClick = React.useCallback(() => {
onClose();
ModalActionCreators.push(modal(() => <UserSettingsModal initialTab="voice_video" />));
}, [onClose]);
return (
<MenuItem icon={<GearIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Voice & Video Settings`}
</MenuItem>
);
});
interface SelfTurnOffCameraMenuItemProps {
onClose: () => void;
}
export const SelfTurnOffCameraMenuItem: React.FC<SelfTurnOffCameraMenuItemProps> = observer(({onClose}) => {
const {t} = useLingui();
const connectionId = MediaEngineStore.connectionId;
const voiceState = connectionId ? MediaEngineStore.getVoiceStateByConnectionId(connectionId) : null;
const isCameraOn = voiceState?.self_video ?? false;
const handleClick = React.useCallback(() => {
if (connectionId) VoiceStateActionCreators.turnOffCameraForConnection(connectionId);
onClose();
}, [connectionId, onClose]);
if (!isCameraOn) return null;
return (
<MenuItem icon={<VideoCameraSlashIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Turn Off Camera`}
</MenuItem>
);
});
interface SelfTurnOffStreamMenuItemProps {
onClose: () => void;
}
export const SelfTurnOffStreamMenuItem: React.FC<SelfTurnOffStreamMenuItemProps> = observer(({onClose}) => {
const {t} = useLingui();
const connectionId = MediaEngineStore.connectionId;
const voiceState = connectionId ? MediaEngineStore.getVoiceStateByConnectionId(connectionId) : null;
const isStreaming = voiceState?.self_stream ?? false;
const handleClick = React.useCallback(() => {
if (connectionId) VoiceStateActionCreators.turnOffStreamForConnection(connectionId);
onClose();
}, [connectionId, onClose]);
if (!isStreaming) return null;
return (
<MenuItem icon={<ProjectorScreenIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Turn Off Stream`}
</MenuItem>
);
});
interface ParticipantVolumeSliderProps {
userId: string;
}
export const ParticipantVolumeSlider: React.FC<ParticipantVolumeSliderProps> = observer(({userId}) => {
const {t} = useLingui();
const participantVolume = ParticipantVolumeStore.getVolume(userId);
const handleChange = React.useCallback(
(value: number) => {
ParticipantVolumeStore.setVolume(userId, value);
MediaEngineStore.applyLocalAudioPreferencesForUser(userId);
},
[userId],
);
return (
<MenuItemSlider
label={t`User Volume`}
value={participantVolume}
minValue={0}
maxValue={200}
onChange={handleChange}
onFormat={(value) => `${Math.round(value)}%`}
/>
);
});
interface LocalMuteParticipantMenuItemProps {
userId: string;
onClose: () => void;
}
export const LocalMuteParticipantMenuItem: React.FC<LocalMuteParticipantMenuItemProps> = observer(({userId}) => {
const {t} = useLingui();
const isLocalMuted = ParticipantVolumeStore.isLocalMuted(userId);
const handleToggle = React.useCallback(
(checked: boolean) => {
ParticipantVolumeStore.setLocalMute(userId, checked);
MediaEngineStore.applyLocalAudioPreferencesForUser(userId);
},
[userId],
);
return (
<MenuItemCheckbox
icon={<SpeakerSlashIcon weight="fill" className={styles.icon} />}
checked={isLocalMuted}
onChange={handleToggle}
>
{t`Mute`}
</MenuItemCheckbox>
);
});
interface LocalDisableVideoMenuItemProps {
userId: string;
connectionId: string;
onClose: () => void;
}
export const LocalDisableVideoMenuItem: React.FC<LocalDisableVideoMenuItemProps> = observer(
({userId, connectionId, onClose}) => {
const {t} = useLingui();
const callId = MediaEngineStore.connectionId ?? '';
const identity = `user_${userId}_${connectionId}`;
const disabled = callId ? CallMediaPrefsStore.isVideoDisabled(callId, identity) : false;
const handleToggle = React.useCallback(
(checked: boolean) => {
const id = MediaEngineStore.connectionId ?? '';
if (!id) return;
MediaEngineStore.setLocalVideoDisabled(identity, checked);
onClose();
},
[identity, onClose],
);
return (
<MenuItemCheckbox
icon={<VideoCameraSlashIcon weight="fill" className={styles.icon} />}
checked={disabled}
onChange={handleToggle}
>
{t`Disable Video (Local)`}
</MenuItemCheckbox>
);
},
);
interface GuildMuteMenuItemProps {
userId: string;
guildId: string;
onClose: () => void;
}
export const GuildMuteMenuItem: React.FC<GuildMuteMenuItemProps> = observer(function GuildMuteMenuItem({
userId,
guildId,
}) {
const {t} = useLingui();
const member = GuildMemberStore.getMember(guildId, userId);
const isGuildMuted = member?.mute ?? false;
const isTimedOut = member?.isTimedOut() ?? false;
const handleToggle = React.useCallback(
async (checked: boolean) => {
try {
await GuildMemberActionCreators.update(guildId, userId, {mute: checked});
if (checked) SoundActionCreators.playSound(SoundType.Mute);
else SoundActionCreators.playSound(SoundType.Unmute);
} catch {}
},
[guildId, userId],
);
return (
<MenuItemCheckbox
icon={<MicrophoneSlashIcon weight="fill" className={styles.icon} />}
checked={!!isGuildMuted}
onChange={handleToggle}
danger
disabled={isTimedOut}
description={isTimedOut ? t`Disabled while the member is timed out.` : undefined}
>
{t`Community Mute`}
</MenuItemCheckbox>
);
});
interface GuildDeafenMenuItemProps {
userId: string;
guildId: string;
onClose: () => void;
}
export const GuildDeafenMenuItem: React.FC<GuildDeafenMenuItemProps> = observer(function GuildDeafenMenuItem({
userId,
guildId,
}) {
const {t} = useLingui();
const member = GuildMemberStore.getMember(guildId, userId);
const isGuildDeafened = member?.deaf ?? false;
const handleToggle = React.useCallback(
async (checked: boolean) => {
try {
await GuildMemberActionCreators.update(guildId, userId, {deaf: checked});
if (checked) SoundActionCreators.playSound(SoundType.Deaf);
else SoundActionCreators.playSound(SoundType.Undeaf);
} catch {}
},
[guildId, userId],
);
return (
<MenuItemCheckbox
icon={<SpeakerSlashIcon weight="fill" className={styles.icon} />}
checked={!!isGuildDeafened}
onChange={handleToggle}
danger
>
{t`Community Deafen`}
</MenuItemCheckbox>
);
});
interface DisconnectParticipantMenuItemProps {
userId: string;
guildId: string;
participantName: string;
connectionId?: string;
onClose: () => void;
label?: string;
}
export const DisconnectParticipantMenuItem: React.FC<DisconnectParticipantMenuItemProps> = observer(
function DisconnectParticipantMenuItem({userId, guildId, connectionId, onClose, label}) {
const {t} = useLingui();
const currentUser = UserStore.currentUser;
const isSelf = currentUser?.id === userId;
const handleClick = React.useCallback(async () => {
onClose();
if (isSelf) {
const socket = ConnectionStore.socket;
const cid = connectionId ?? MediaEngineStore.connectionId ?? null;
if (socket && cid) {
socket.updateVoiceState({
guild_id: guildId,
channel_id: null,
self_mute: true,
self_deaf: true,
self_video: false,
self_stream: false,
connection_id: cid,
});
}
} else {
try {
await GuildMemberActionCreators.update(guildId, userId, {channel_id: null, connection_id: connectionId});
} catch {}
}
}, [guildId, userId, connectionId, onClose, isSelf]);
const defaultLabel = connectionId ? t`Disconnect Device` : t`Disconnect`;
return (
<MenuItem icon={<PhoneXIcon weight="fill" className={styles.icon} />} onClick={handleClick} danger>
{label ?? defaultLabel}
</MenuItem>
);
},
);
interface TurnOffDeviceCameraMenuItemProps {
onClose: () => void;
connectionId: string;
}
export const TurnOffDeviceCameraMenuItem: React.FC<TurnOffDeviceCameraMenuItemProps> = observer(
({connectionId, onClose}) => {
const {t} = useLingui();
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
const isCameraOn = voiceState?.self_video ?? false;
const handleClick = React.useCallback(() => {
VoiceStateActionCreators.turnOffCameraForConnection(connectionId);
onClose();
}, [connectionId, onClose]);
if (!isCameraOn) return null;
return (
<MenuItem icon={<VideoCameraSlashIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Turn Off Device Camera`}
</MenuItem>
);
},
);
interface TurnOffDeviceStreamMenuItemProps {
onClose: () => void;
connectionId: string;
}
export const TurnOffDeviceStreamMenuItem: React.FC<TurnOffDeviceStreamMenuItemProps> = observer(
({connectionId, onClose}) => {
const {t} = useLingui();
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
const isStreaming = voiceState?.self_stream ?? false;
const handleClick = React.useCallback(() => {
VoiceStateActionCreators.turnOffStreamForConnection(connectionId);
onClose();
}, [connectionId, onClose]);
if (!isStreaming) return null;
return (
<MenuItem icon={<ProjectorScreenIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Turn Off Device Stream`}
</MenuItem>
);
},
);
interface CopyDeviceIdMenuItemProps {
onClose: () => void;
connectionId: string;
}
export const CopyDeviceIdMenuItem: React.FC<CopyDeviceIdMenuItemProps> = observer(({connectionId, onClose}) => {
const {t, i18n} = useLingui();
const handleClick = React.useCallback(() => {
TextCopyActionCreators.copy(i18n, connectionId, true).catch(() => {});
onClose();
}, [connectionId, onClose, i18n]);
return (
<MenuItem icon={<CopyIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Copy Device ID`}
</MenuItem>
);
});
interface BulkMuteDevicesMenuItemProps {
userVoiceStates: Array<{connectionId: string; voiceState: VoiceState}>;
onClose: () => void;
}
export const BulkMuteDevicesMenuItem: React.FC<BulkMuteDevicesMenuItemProps> = observer(
({userVoiceStates, onClose}) => {
const {t} = useLingui();
const allMuted = React.useMemo(
() => userVoiceStates.every(({voiceState}) => voiceState.self_mute),
[userVoiceStates],
);
const handleClick = React.useCallback(() => {
const connectionIds = userVoiceStates.map(({connectionId}) => connectionId);
const targetMute = !allMuted;
VoiceStateActionCreators.bulkMuteConnections(connectionIds, targetMute);
if (targetMute) SoundActionCreators.playSound(SoundType.Mute);
else SoundActionCreators.playSound(SoundType.Unmute);
onClose();
}, [userVoiceStates, allMuted, onClose]);
return (
<MenuItem icon={<MicrophoneSlashIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{allMuted ? t`Unmute All Devices` : t`Mute All Devices`}
</MenuItem>
);
},
);
interface BulkDeafenDevicesMenuItemProps {
userVoiceStates: Array<{connectionId: string; voiceState: VoiceState}>;
onClose: () => void;
}
export const BulkDeafenDevicesMenuItem: React.FC<BulkDeafenDevicesMenuItemProps> = observer(
({userVoiceStates, onClose}) => {
const {t} = useLingui();
const allDeafened = React.useMemo(
() => userVoiceStates.every(({voiceState}) => voiceState.self_deaf),
[userVoiceStates],
);
const handleClick = React.useCallback(() => {
const connectionIds = userVoiceStates.map(({connectionId}) => connectionId);
const targetDeafen = !allDeafened;
VoiceStateActionCreators.bulkDeafenConnections(connectionIds, targetDeafen);
if (targetDeafen) SoundActionCreators.playSound(SoundType.Deaf);
else SoundActionCreators.playSound(SoundType.Undeaf);
onClose();
}, [userVoiceStates, allDeafened, onClose]);
return (
<MenuItem icon={<SpeakerSlashIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{allDeafened ? t`Undeafen All Devices` : t`Deafen All Devices`}
</MenuItem>
);
},
);
interface BulkCameraDevicesMenuItemProps {
userVoiceStates: Array<{connectionId: string; voiceState: VoiceState}>;
onClose: () => void;
}
export const BulkCameraDevicesMenuItem: React.FC<BulkCameraDevicesMenuItemProps> = observer(
({userVoiceStates, onClose}) => {
const {t} = useLingui();
const handleClick = React.useCallback(() => {
const connectionIds = userVoiceStates.map(({connectionId}) => connectionId);
VoiceStateActionCreators.bulkTurnOffCameras(connectionIds);
onClose();
}, [userVoiceStates, onClose]);
return (
<MenuItem icon={<VideoIcon weight="fill" className={styles.icon} />} onClick={handleClick}>
{t`Turn Off All Device Cameras`}
</MenuItem>
);
},
);
interface BulkDisconnectDevicesMenuItemProps {
userVoiceStates: Array<{connectionId: string; voiceState: VoiceState}>;
onClose: () => void;
}
export const BulkDisconnectDevicesMenuItem: React.FC<BulkDisconnectDevicesMenuItemProps> = observer(
({userVoiceStates, onClose}) => {
const {t} = useLingui();
const handleClick = React.useCallback(async () => {
await VoiceStateActionCreators.bulkDisconnect(userVoiceStates.map(({connectionId}) => connectionId));
onClose();
}, [userVoiceStates, onClose]);
return (
<MenuItem icon={<PhoneXIcon weight="fill" className={styles.icon} />} onClick={handleClick} danger>
{t`Disconnect All Devices`}
</MenuItem>
);
},
);
interface FocusParticipantMenuItemProps {
userId: string;
connectionId: string;
onClose: () => void;
}
export const FocusParticipantMenuItem: React.FC<FocusParticipantMenuItemProps> = observer(
({userId, connectionId, onClose}) => {
const {t} = useLingui();
const identity = `user_${userId}_${connectionId}`;
const isFocused = VoiceCallLayoutStore.pinnedParticipantIdentity === identity;
const hasMultipleConnections = React.useMemo(() => {
const allStates = MediaEngineStore.getAllVoiceStates();
let count = 0;
Object.values(allStates).forEach((guildData: any) => {
Object.values(guildData).forEach((channelData: any) => {
Object.values(channelData).forEach((vs: any) => {
if ((vs as any)?.user_id === userId) count++;
});
});
});
return count > 1;
}, [userId]);
const handleClick = React.useCallback(() => {
if (isFocused) {
VoiceCallLayoutActionCreators.setPinnedParticipant(null);
VoiceCallLayoutActionCreators.setLayoutMode('grid');
VoiceCallLayoutActionCreators.markUserOverride();
} else {
VoiceCallLayoutActionCreators.setLayoutMode('focus');
VoiceCallLayoutActionCreators.setPinnedParticipant(identity);
VoiceCallLayoutActionCreators.markUserOverride();
}
onClose();
}, [identity, onClose, isFocused]);
return (
<MenuItem
icon={
isFocused ? (
<EyeSlashIcon weight="fill" className={styles.icon} />
) : (
<EyeIcon weight="fill" className={styles.icon} />
)
}
onClick={handleClick}
>
{isFocused ? t`Unfocus` : hasMultipleConnections ? t`Focus This Device` : t`Focus This Person`}
</MenuItem>
);
},
);