chore: Initial commit of Opencord base state
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
const EMPTY_VOICE_USERS = [];
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
export function VoiceChannel({ channelId, channelName, onClick }) {
|
||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS;
|
||||
const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const members = useServerStore((s) => s.members);
|
||||
const isActive = currentVoiceChannel === channelId;
|
||||
return (_jsxs("div", { children: [_jsxs("button", { onClick: onClick, className: `w-full flex items-center gap-1.5 px-2 py-1.5 rounded text-sm group ${isActive
|
||||
? 'bg-discord-bg-active text-white'
|
||||
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-bg-hover'}`, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", className: "flex-shrink-0 opacity-60", children: _jsx("path", { d: "M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" }) }), _jsx("span", { className: "truncate", children: channelName })] }), voiceUsers.length > 0 && (_jsx("div", { className: "ml-6 mt-0.5 space-y-0.5", children: voiceUsers.map((userId) => {
|
||||
const member = members.find(m => m.userId === userId);
|
||||
if (!member)
|
||||
return null;
|
||||
const displayName = member.user.displayName ?? member.user.username;
|
||||
return (_jsxs("div", { className: "flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-bg-hover", children: [_jsx(Avatar, { src: member.user.avatar, name: displayName, size: 20, status: member.user.status }), _jsx("span", { className: "text-xs text-discord-text-secondary truncate", children: displayName })] }, userId));
|
||||
}) }))] }));
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
|
||||
const EMPTY_VOICE_USERS: string[] = [];
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
|
||||
interface VoiceChannelProps {
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelProps) {
|
||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS;
|
||||
const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const members = useServerStore((s) => s.members);
|
||||
const isActive = currentVoiceChannel === channelId;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center gap-1.5 px-2 py-1.5 rounded text-sm group ${
|
||||
isActive
|
||||
? 'bg-discord-bg-active text-white'
|
||||
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-bg-hover'
|
||||
}`}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0 opacity-60">
|
||||
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" />
|
||||
</svg>
|
||||
<span className="truncate">{channelName}</span>
|
||||
</button>
|
||||
|
||||
{/* Connected users */}
|
||||
{voiceUsers.length > 0 && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5">
|
||||
{voiceUsers.map((userId) => {
|
||||
const member = members.find(m => m.userId === userId);
|
||||
if (!member) return null;
|
||||
const displayName = member.user.displayName ?? member.user.username;
|
||||
return (
|
||||
<div key={userId} className="flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-bg-hover">
|
||||
<Avatar
|
||||
src={member.user.avatar}
|
||||
name={displayName}
|
||||
size={20}
|
||||
status={member.user.status}
|
||||
/>
|
||||
<span className="text-xs text-discord-text-secondary truncate">{displayName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
export function VoiceControls({ onDisconnect, onToggleMic, onToggleCamera, onToggleScreenShare }) {
|
||||
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 channels = useServerStore((s) => s.channels);
|
||||
if (!currentVoiceChannelId)
|
||||
return null;
|
||||
const channel = channels.find(c => c.id === currentVoiceChannelId);
|
||||
const channelName = channel?.name ?? 'Voice Channel';
|
||||
const handleMic = () => {
|
||||
toggleMute();
|
||||
onToggleMic();
|
||||
};
|
||||
const handleDeafen = () => {
|
||||
toggleDeafen();
|
||||
};
|
||||
const handleCamera = () => {
|
||||
toggleCamera();
|
||||
onToggleCamera();
|
||||
};
|
||||
const handleScreenShare = () => {
|
||||
toggleScreenShare();
|
||||
onToggleScreenShare();
|
||||
};
|
||||
return (_jsxs("div", { className: "bg-discord-bg-secondary border-t border-discord-bg-tertiary p-2", children: [_jsx("div", { className: "flex items-center justify-between px-1 mb-2", children: _jsxs("div", { children: [_jsxs("div", { className: "text-xs font-medium text-discord-green flex items-center gap-1", children: [_jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" }) }), "Voice Connected"] }), _jsx("div", { className: "text-xs text-discord-text-muted truncate", children: channelName })] }) }), _jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx("button", { onClick: handleMic, className: `p-2 rounded-full transition-colors ${isMuted
|
||||
? 'bg-discord-red/20 text-discord-red hover:bg-discord-red/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'}`, title: isMuted ? 'Unmute' : 'Mute', children: isMuted ? (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("path", { d: "M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" }), _jsx("path", { d: "M2.1 2.1L1.4 2.8L7.6 9L7 12C7 14.8 9.2 17 12 17C12.9 17 13.7 16.7 14.4 16.3L16.2 18.1C15 18.9 13.6 19.4 12 19.5V22H14V24H10V22H12V19.5C8.4 19.1 5.6 16.1 5 12.5H7C7.5 14.8 9.5 16.5 12 16.5C12.5 16.5 13 16.4 13.5 16.2L14.7 17.4C13.9 17.8 13 18 12 18C8.7 18 6 15.3 6 12H4C4 15.7 7 18.8 11 19.4V22H10V24H14V22H13V19.4C14 19.3 14.9 18.9 15.7 18.4L21.9 24.6L22.6 23.9L2.1 2.1Z" })] })) : (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2ZM17 12C17 14.76 14.76 17 12 17S7 14.76 7 12H5C5 15.53 7.61 18.43 11 18.92V22H13V18.92C16.39 18.43 19 15.53 19 12H17Z" }) })) }), _jsx("button", { onClick: handleDeafen, className: `p-2 rounded-full transition-colors ${isDeafened
|
||||
? 'bg-discord-red/20 text-discord-red hover:bg-discord-red/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'}`, title: isDeafened ? 'Undeafen' : 'Deafen', children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12V20C2 21.1 2.9 22 4 22H8V12H4.04C4.28 7.57 7.77 4 12 4S19.72 7.57 19.96 12H16V22H20C21.1 22 22 21.1 22 20V12C22 6.48 17.52 2 12 2Z" }) }) }), _jsx("button", { onClick: handleCamera, className: `p-2 rounded-full transition-colors ${isCameraOn
|
||||
? 'bg-discord-green/20 text-discord-green hover:bg-discord-green/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'}`, title: isCameraOn ? 'Turn Off Camera' : 'Turn On Camera', children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M17 10.5V7C17 6.45 16.55 6 16 6H4C3.45 6 3 6.45 3 7V17C3 17.55 3.45 18 4 18H16C16.55 18 17 17.55 17 17V13.5L21 17.5V6.5L17 10.5Z" }) }) }), _jsx("button", { onClick: handleScreenShare, className: `p-2 rounded-full transition-colors ${isScreenSharing
|
||||
? 'bg-discord-green/20 text-discord-green hover:bg-discord-green/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'}`, title: isScreenSharing ? 'Stop Sharing' : 'Share Screen', children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M20 18C21.1 18 22 17.1 22 16V6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V16C2 17.1 2.9 18 4 18H0V20H24V18H20ZM4 6H20V16H4V6Z" }) }) }), _jsx("button", { onClick: onDisconnect, className: "p-2 rounded-full bg-discord-red/20 text-discord-red hover:bg-discord-red/30 transition-colors", title: "Disconnect", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 9C10.4 9 8.85 9.25 7.4 9.72V12.82C7.4 13.22 7.17 13.56 6.84 13.72C5.86 14.21 4.97 14.84 4.18 15.57C4 15.75 3.75 15.85 3.48 15.85C3.2 15.85 2.95 15.74 2.77 15.56L0.29 13.08C0.11 12.9 0 12.65 0 12.38C0 12.1 0.11 11.85 0.29 11.67C3.34 8.78 7.46 7 12 7S20.66 8.78 23.71 11.67C23.89 11.85 24 12.1 24 12.38C24 12.65 23.89 12.9 23.71 13.08L21.23 15.56C21.05 15.74 20.8 15.85 20.52 15.85C20.25 15.85 20 15.75 19.82 15.57C19.03 14.84 18.14 14.21 17.16 13.72C16.83 13.56 16.6 13.22 16.6 12.82V9.72C15.15 9.25 13.6 9 12 9Z" }) }) })] })] }));
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
|
||||
interface VoiceControlsProps {
|
||||
onDisconnect: () => void;
|
||||
onToggleMic: () => void;
|
||||
onToggleCamera: () => void;
|
||||
onToggleScreenShare: () => void;
|
||||
}
|
||||
|
||||
export function VoiceControls({ onDisconnect, onToggleMic, onToggleCamera, onToggleScreenShare }: VoiceControlsProps) {
|
||||
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 channels = useServerStore((s) => s.channels);
|
||||
|
||||
if (!currentVoiceChannelId) return null;
|
||||
|
||||
const channel = channels.find(c => c.id === currentVoiceChannelId);
|
||||
const channelName = channel?.name ?? 'Voice Channel';
|
||||
|
||||
const handleMic = () => {
|
||||
toggleMute();
|
||||
onToggleMic();
|
||||
};
|
||||
|
||||
const handleDeafen = () => {
|
||||
toggleDeafen();
|
||||
};
|
||||
|
||||
const handleCamera = () => {
|
||||
toggleCamera();
|
||||
onToggleCamera();
|
||||
};
|
||||
|
||||
const handleScreenShare = () => {
|
||||
toggleScreenShare();
|
||||
onToggleScreenShare();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-discord-bg-secondary border-t border-discord-bg-tertiary p-2">
|
||||
<div className="flex items-center justify-between px-1 mb-2">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-discord-green flex items-center gap-1">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
|
||||
</svg>
|
||||
Voice Connected
|
||||
</div>
|
||||
<div className="text-xs text-discord-text-muted truncate">
|
||||
{channelName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{/* Mic */}
|
||||
<button
|
||||
onClick={handleMic}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isMuted
|
||||
? 'bg-discord-red/20 text-discord-red hover:bg-discord-red/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'
|
||||
}`}
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" />
|
||||
<path d="M2.1 2.1L1.4 2.8L7.6 9L7 12C7 14.8 9.2 17 12 17C12.9 17 13.7 16.7 14.4 16.3L16.2 18.1C15 18.9 13.6 19.4 12 19.5V22H14V24H10V22H12V19.5C8.4 19.1 5.6 16.1 5 12.5H7C7.5 14.8 9.5 16.5 12 16.5C12.5 16.5 13 16.4 13.5 16.2L14.7 17.4C13.9 17.8 13 18 12 18C8.7 18 6 15.3 6 12H4C4 15.7 7 18.8 11 19.4V22H10V24H14V22H13V19.4C14 19.3 14.9 18.9 15.7 18.4L21.9 24.6L22.6 23.9L2.1 2.1Z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2ZM17 12C17 14.76 14.76 17 12 17S7 14.76 7 12H5C5 15.53 7.61 18.43 11 18.92V22H13V18.92C16.39 18.43 19 15.53 19 12H17Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Deafen */}
|
||||
<button
|
||||
onClick={handleDeafen}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isDeafened
|
||||
? 'bg-discord-red/20 text-discord-red hover:bg-discord-red/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'
|
||||
}`}
|
||||
title={isDeafened ? 'Undeafen' : 'Deafen'}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12V20C2 21.1 2.9 22 4 22H8V12H4.04C4.28 7.57 7.77 4 12 4S19.72 7.57 19.96 12H16V22H20C21.1 22 22 21.1 22 20V12C22 6.48 17.52 2 12 2Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Camera */}
|
||||
<button
|
||||
onClick={handleCamera}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isCameraOn
|
||||
? 'bg-discord-green/20 text-discord-green hover:bg-discord-green/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'
|
||||
}`}
|
||||
title={isCameraOn ? 'Turn Off Camera' : 'Turn On Camera'}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17 10.5V7C17 6.45 16.55 6 16 6H4C3.45 6 3 6.45 3 7V17C3 17.55 3.45 18 4 18H16C16.55 18 17 17.55 17 17V13.5L21 17.5V6.5L17 10.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Screen Share */}
|
||||
<button
|
||||
onClick={handleScreenShare}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isScreenSharing
|
||||
? 'bg-discord-green/20 text-discord-green hover:bg-discord-green/30'
|
||||
: 'bg-discord-bg-tertiary text-discord-text-secondary hover:bg-discord-bg-hover hover:text-discord-text-primary'
|
||||
}`}
|
||||
title={isScreenSharing ? 'Stop Sharing' : 'Share Screen'}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 18C21.1 18 22 17.1 22 16V6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V16C2 17.1 2.9 18 4 18H0V20H24V18H20ZM4 6H20V16H4V6Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Disconnect */}
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="p-2 rounded-full bg-discord-red/20 text-discord-red hover:bg-discord-red/30 transition-colors"
|
||||
title="Disconnect"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 9C10.4 9 8.85 9.25 7.4 9.72V12.82C7.4 13.22 7.17 13.56 6.84 13.72C5.86 14.21 4.97 14.84 4.18 15.57C4 15.75 3.75 15.85 3.48 15.85C3.2 15.85 2.95 15.74 2.77 15.56L0.29 13.08C0.11 12.9 0 12.65 0 12.38C0 12.1 0.11 11.85 0.29 11.67C3.34 8.78 7.46 7 12 7S20.66 8.78 23.71 11.67C23.89 11.85 24 12.1 24 12.38C24 12.65 23.89 12.9 23.71 13.08L21.23 15.56C21.05 15.74 20.8 15.85 20.52 15.85C20.25 15.85 20 15.75 19.82 15.57C19.03 14.84 18.14 14.21 17.16 13.72C16.83 13.56 16.6 13.22 16.6 12.82V9.72C15.15 9.25 13.6 9 12 9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { VoiceUser } from './VoiceUser';
|
||||
export function VoiceGrid({ participants }) {
|
||||
if (participants.length === 0) {
|
||||
return (_jsx("div", { className: "flex-1 flex items-center justify-center text-discord-text-muted", children: _jsx("p", { children: "No one is in this voice channel" }) }));
|
||||
}
|
||||
const gridClass = (() => {
|
||||
if (participants.length === 1)
|
||||
return 'grid-cols-1 max-w-2xl mx-auto';
|
||||
if (participants.length === 2)
|
||||
return 'grid-cols-2 max-w-4xl mx-auto';
|
||||
if (participants.length <= 4)
|
||||
return 'grid-cols-2';
|
||||
return 'grid-cols-3';
|
||||
})();
|
||||
return (_jsx("div", { className: "flex-1 p-4 overflow-auto", children: _jsx("div", { className: `grid ${gridClass} gap-2 h-full`, children: participants.map((p) => (_jsx(VoiceUser, { participant: p }, p.identity))) }) }));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { VoiceUser } from './VoiceUser';
|
||||
import type { ParticipantInfo } from '../../hooks/useLiveKit';
|
||||
|
||||
interface VoiceGridProps {
|
||||
participants: ParticipantInfo[];
|
||||
}
|
||||
|
||||
export function VoiceGrid({ participants }: VoiceGridProps) {
|
||||
if (participants.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-discord-text-muted">
|
||||
<p>No one is in this voice channel</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const gridClass = (() => {
|
||||
if (participants.length === 1) return 'grid-cols-1 max-w-2xl mx-auto';
|
||||
if (participants.length === 2) return 'grid-cols-2 max-w-4xl mx-auto';
|
||||
if (participants.length <= 4) return 'grid-cols-2';
|
||||
return 'grid-cols-3';
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className={`grid ${gridClass} gap-2 h-full`}>
|
||||
{participants.map((p) => (
|
||||
<VoiceUser key={p.identity} participant={p} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
export function VoiceUser({ participant }) {
|
||||
const videoRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current;
|
||||
if (!videoEl)
|
||||
return;
|
||||
const track = participant.videoTrack ?? participant.screenTrack;
|
||||
if (track) {
|
||||
const stream = new MediaStream([track]);
|
||||
videoEl.srcObject = stream;
|
||||
}
|
||||
else {
|
||||
videoEl.srcObject = null;
|
||||
}
|
||||
}, [participant.videoTrack, participant.screenTrack]);
|
||||
const hasVideo = participant.isCameraOn || participant.isScreenSharing;
|
||||
return (_jsxs("div", { className: `relative bg-discord-bg-secondary rounded-xl overflow-hidden flex items-center justify-center ${participant.isSpeaking ? 'ring-2 ring-discord-green' : ''}`, style: { aspectRatio: '16/9', minHeight: '200px' }, children: [hasVideo ? (_jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: participant.userId === 'local', className: "w-full h-full object-cover" })) : (_jsx(Avatar, { src: null, name: participant.username, size: 80 })), _jsx("div", { className: "absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-medium text-white", children: participant.username }), _jsx("div", { className: "flex items-center gap-1", children: participant.isMuted && (_jsx("div", { className: "w-5 h-5 bg-discord-red/80 rounded-full flex items-center justify-center", children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "white", children: [_jsx("path", { d: "M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" }), _jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "white", strokeWidth: "2" })] }) })) })] }) }), participant.isSpeaking && (_jsx("div", { className: "absolute inset-0 rounded-xl ring-2 ring-discord-green animate-pulse pointer-events-none" }))] }));
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import type { ParticipantInfo } from '../../hooks/useLiveKit';
|
||||
|
||||
interface VoiceUserProps {
|
||||
participant: ParticipantInfo;
|
||||
}
|
||||
|
||||
export function VoiceUser({ participant }: VoiceUserProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current;
|
||||
if (!videoEl) return;
|
||||
|
||||
const track = participant.videoTrack ?? participant.screenTrack;
|
||||
if (track) {
|
||||
const stream = new MediaStream([track]);
|
||||
videoEl.srcObject = stream;
|
||||
} else {
|
||||
videoEl.srcObject = null;
|
||||
}
|
||||
}, [participant.videoTrack, participant.screenTrack]);
|
||||
|
||||
const hasVideo = participant.isCameraOn || participant.isScreenSharing;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative bg-discord-bg-secondary rounded-xl overflow-hidden flex items-center justify-center ${
|
||||
participant.isSpeaking ? 'ring-2 ring-discord-green' : ''
|
||||
}`}
|
||||
style={{ aspectRatio: '16/9', minHeight: '200px' }}
|
||||
>
|
||||
{hasVideo ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={participant.userId === 'local'}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
src={null}
|
||||
name={participant.username}
|
||||
size={80}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-white">{participant.username}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{participant.isMuted && (
|
||||
<div className="w-5 h-5 bg-discord-red/80 rounded-full flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" />
|
||||
<line x1="3" y1="3" x2="21" y2="21" stroke="white" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Speaking indicator */}
|
||||
{participant.isSpeaking && (
|
||||
<div className="absolute inset-0 rounded-xl ring-2 ring-discord-green animate-pulse pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user