From 206a0491ea5d678ac95ea4b2c72f70077ca7761e Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:47:41 +0100 Subject: [PATCH] feat(mobile): add full-screen voice view with controls --- .../web/src/components/layout/MobileShell.tsx | 2 +- .../layout/MobileVoiceFullScreen.tsx | 219 ++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/components/layout/MobileVoiceFullScreen.tsx diff --git a/packages/web/src/components/layout/MobileShell.tsx b/packages/web/src/components/layout/MobileShell.tsx index 26b002d4..13825df3 100644 --- a/packages/web/src/components/layout/MobileShell.tsx +++ b/packages/web/src/components/layout/MobileShell.tsx @@ -22,7 +22,7 @@ import { MobileYouScreen } from './MobileYouScreen'; import { MobileChatScreen } from './MobileChatScreen'; import { MobileSettingsScreen } from './MobileSettingsScreen'; import { MobileVoiceMiniBar } from './MobileVoiceMiniBar'; -const MobileVoiceFullScreen = () => ; +import { MobileVoiceFullScreen } from './MobileVoiceFullScreen'; const screenMap: Record) => React.ReactNode> = { 'channel-chat': (params) => , diff --git a/packages/web/src/components/layout/MobileVoiceFullScreen.tsx b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx new file mode 100644 index 00000000..24945d92 --- /dev/null +++ b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx @@ -0,0 +1,219 @@ +import React from 'react'; +import { useUIStore } from '../../stores/uiStore'; +import { useVoiceStore } from '../../stores/voiceStore'; +import { useSpaceStore } from '../../stores/spaceStore'; +import { useAuthStore } from '../../stores/authStore'; +import { Avatar } from '../ui/Avatar'; + +export function MobileVoiceFullScreen() { + const popMobileScreen = useUIStore((s) => s.popMobileScreen); + + const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); + const isMuted = useVoiceStore((s) => s.isMuted); + const isDeafened = useVoiceStore((s) => s.isDeafened); + const isCameraOn = useVoiceStore((s) => s.isCameraOn); + const isScreenSharing = useVoiceStore((s) => s.isScreenSharing); + const toggleMute = useVoiceStore((s) => s.toggleMute); + const toggleDeafen = useVoiceStore((s) => s.toggleDeafen); + const toggleCamera = useVoiceStore((s) => s.toggleCamera); + const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare); + const leaveChannel = useVoiceStore((s) => s.leaveChannel); + const voiceUsers = useVoiceStore((s) => s.voiceUsers); + const voiceUserStates = useVoiceStore((s) => s.voiceUserStates); + + const channels = useSpaceStore((s) => s.channels); + const dmChannels = useSpaceStore((s) => s.dmChannels); + const spaces = useSpaceStore((s) => s.spaces); + const channelToSpaceMap = useSpaceStore((s) => s.channelToSpaceMap); + const members = useSpaceStore((s) => s.members); + + const authUser = useAuthStore((s) => s.user); + + if (!currentVoiceChannelId) { + popMobileScreen(); + return null; + } + + const isDmCall = currentVoiceChannelId.startsWith('dm-'); + let channelName = 'Voice Call'; + let spaceName = ''; + + if (isDmCall) { + const dmId = currentVoiceChannelId.replace('dm-', ''); + const dm = dmChannels.find(d => d.id === dmId); + if (dm) { + const others = dm.members.filter(m => m.id !== authUser?.id); + channelName = others.map(m => m.displayName ?? m.username).join(', '); + } + } else { + const ch = channels.find(c => c.id === currentVoiceChannelId); + if (ch) { + channelName = ch.name; + const spaceId = channelToSpaceMap.get(ch.id); + const space = spaceId ? spaces.find(s => s.id === spaceId) : null; + if (space) spaceName = space.name; + } + } + + const participantIds = voiceUsers.get(currentVoiceChannelId) || []; + + // Resolve participant display info from members list or DM members + const getParticipantInfo = (userId: string) => { + const member = members.find(m => m.userId === userId); + if (member) { + return { + name: member.nickname ?? member.displayName ?? member.username, + avatar: member.avatar ? `/api/uploads/${member.avatar}` : null, + avatarColor: member.avatarColor, + }; + } + // Check DM members + for (const dm of dmChannels) { + const dmMember = dm.members.find(m => m.id === userId); + if (dmMember) { + return { + name: dmMember.displayName ?? dmMember.username, + avatar: dmMember.avatar ? `/api/uploads/${dmMember.avatar}` : null, + avatarColor: dmMember.avatarColor, + }; + } + } + return { name: 'User', avatar: null, avatarColor: null }; + }; + + const handleDisconnect = () => { + leaveChannel(); + popMobileScreen(); + }; + + return ( +
+ {/* Header */} +
+ +
+

{channelName}

+ {spaceName &&

{spaceName}

} +
+ {participantIds.length} connected +
+ + {/* Participant grid */} +
+
+ {participantIds.map(userId => { + const info = getParticipantInfo(userId); + const state = voiceUserStates.get(userId); + const isMe = userId === authUser?.id; + + return ( +
+
+ + {state?.isMuted && ( +
+ + + + +
+ )} +
+ + {info.name}{isMe ? ' (You)' : ''} + +
+ ); + })} +
+ + {participantIds.length === 0 && ( +
+ No one else is here yet +
+ )} +
+ + {/* Control bar */} +
+ {/* Mute */} + + + {/* Deafen */} + + + {/* Camera */} + + + {/* Screen share */} + + + {/* Disconnect */} + +
+
+ ); +}