diff --git a/packages/web/src/components/layout/MobileShell.tsx b/packages/web/src/components/layout/MobileShell.tsx index 3a8b0f28..ea7c945b 100644 --- a/packages/web/src/components/layout/MobileShell.tsx +++ b/packages/web/src/components/layout/MobileShell.tsx @@ -20,7 +20,7 @@ import { MobileDmsScreen } from './MobileDmsScreen'; import { MobileYouScreen } from './MobileYouScreen'; import { MobileChatScreen } from './MobileChatScreen'; import { MobileSettingsScreen } from './MobileSettingsScreen'; -const MobileVoiceMiniBar = () => null; +import { MobileVoiceMiniBar } from './MobileVoiceMiniBar'; const MobileVoiceFullScreen = () => ; const screenMap: Record) => React.ReactNode> = { diff --git a/packages/web/src/components/layout/MobileVoiceMiniBar.tsx b/packages/web/src/components/layout/MobileVoiceMiniBar.tsx new file mode 100644 index 00000000..776557d1 --- /dev/null +++ b/packages/web/src/components/layout/MobileVoiceMiniBar.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { useUIStore } from '../../stores/uiStore'; +import { useVoiceStore } from '../../stores/voiceStore'; +import { useSpaceStore } from '../../stores/spaceStore'; + +export function MobileVoiceMiniBar() { + const pushMobileScreen = useUIStore((s) => s.pushMobileScreen); + const mobileStack = useUIStore((s) => s.mobileStack); + + const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); + const isMuted = useVoiceStore((s) => s.isMuted); + const isDeafened = useVoiceStore((s) => s.isDeafened); + const toggleMute = useVoiceStore((s) => s.toggleMute); + const toggleDeafen = useVoiceStore((s) => s.toggleDeafen); + const voiceUsers = useVoiceStore((s) => s.voiceUsers); + const leaveChannel = useVoiceStore((s) => s.leaveChannel); + + const channels = useSpaceStore((s) => s.channels); + const dmChannels = useSpaceStore((s) => s.dmChannels); + + if (!currentVoiceChannelId) return null; + + // Don't show mini-bar if voice full-screen is on top of the stack + const topScreen = mobileStack.length > 0 ? mobileStack[mobileStack.length - 1].screen : null; + if (topScreen === 'voice-full') return null; + + // Resolve channel name + const isDmCall = currentVoiceChannelId.startsWith('dm-'); + let channelName = 'Voice Call'; + if (isDmCall) { + const dmId = currentVoiceChannelId.replace('dm-', ''); + const dm = dmChannels.find(d => d.id === dmId); + if (dm) channelName = 'DM Call'; + } else { + const ch = channels.find(c => c.id === currentVoiceChannelId); + if (ch) channelName = ch.name; + } + + const participantCount = voiceUsers.get(currentVoiceChannelId)?.length ?? 0; + + return ( + + {/* Tap to expand */} + pushMobileScreen('voice-full')} + className="flex-1 flex items-center gap-2 min-w-0" + > + + + + + + + {channelName} + {participantCount > 0 && ( + {participantCount} connected + )} + + + + {/* Quick controls */} + + { e.stopPropagation(); toggleMute(); }} + className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${ + isMuted ? 'bg-accent-rose/20 text-accent-rose' : 'text-txt-secondary hover:text-txt-primary hover:bg-interactive-hover' + }`} + > + {isMuted ? ( + + + + + ) : ( + + + + )} + + + { e.stopPropagation(); toggleDeafen(); }} + className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${ + isDeafened ? 'bg-accent-rose/20 text-accent-rose' : 'text-txt-secondary hover:text-txt-primary hover:bg-interactive-hover' + }`} + > + + + {isDeafened && } + + + + { e.stopPropagation(); leaveChannel(); }} + className="w-8 h-8 rounded-full flex items-center justify-center bg-accent-rose/20 text-accent-rose hover:bg-accent-rose/30 transition-colors" + > + + + + + + + ); +}
{channelName}
{participantCount} connected