Files
ChatApp/design_handoff_chatapp/components/call.jsx
T
byGalax 0ca29952ba
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled
feat: profile avatar upload + share_conv_keys rpc + favicon + smtp tweaks
2026-04-19 23:04:03 +02:00

195 lines
8.2 KiB
React

// Call UI components — Discord-style, matched to Clean-Rail aesthetic
const CALL_PARTICIPANTS = [
{ id: 'dennis', name: 'dennis', initial: 'D', color: 'teal', speaking: false, muted: false, video: false, me: true, e2ee: true },
{ id: 'max', name: 'Max', initial: 'M', color: 'teal', speaking: true, muted: false, video: true, sharing: true, e2ee: true },
{ id: 'lena', name: 'Lena', initial: 'L', color: 'amber', speaking: false, muted: true, video: false, e2ee: true },
{ id: 'tom', name: 'Tom', initial: 'T', color: 'violet', speaking: false, muted: false, video: false, e2ee: true },
{ id: 'nico', name: 'nico', initial: 'N', color: 'teal', speaking: true, muted: false, video: false, e2ee: true },
];
const ParticipantTile = ({ p, focused, onClick, small }) => (
<div className={`participant-tile ${p.speaking ? 'speaking' : ''} ${focused ? 'focused' : ''} ${small ? 'small' : ''}`} onClick={onClick}>
{p.video || p.sharing ? (
<div className="participant-video">
{p.sharing ? (
<div className="screenshare-stub">
<div className="ss-window">
<div className="ss-chrome"><span></span><span></span><span></span></div>
<div className="ss-content">
<div className="ss-line" style={{width: '60%'}}></div>
<div className="ss-line" style={{width: '80%'}}></div>
<div className="ss-line" style={{width: '40%'}}></div>
<div className="ss-block"></div>
<div className="ss-line" style={{width: '70%'}}></div>
</div>
</div>
<div className="ss-badge">
<Icon.Monitor /> {p.name} teilt Bildschirm
</div>
</div>
) : (
<div className="video-stub">
<Avatar chat={{initial: p.initial, color: p.color}} size="lg" />
</div>
)}
</div>
) : (
<div className="participant-audio">
<div className={`audio-ring ${p.speaking ? 'pulse' : ''}`}>
<Avatar chat={{initial: p.initial, color: p.color}} size="lg" />
</div>
</div>
)}
<div className="participant-meta">
<div className="participant-name">
{p.me && <Icon.Crown />}
<span>{p.name}{p.me ? ' (du)' : ''}</span>
{p.e2ee && <span className="e2ee-dot" title="End-to-End verschlüsselt"><Icon.Lock /></span>}
</div>
<div className="participant-icons">
{p.muted && <span className="pi muted"><Icon.MicOff /></span>}
{p.sharing && <span className="pi sharing"><Icon.Monitor /></span>}
</div>
</div>
</div>
);
const CallControls = ({ muted, onToggleMute, sharing, onToggleShare, video, onToggleVideo, onHangup, compact }) => (
<div className={`call-controls ${compact ? 'compact' : ''}`}>
<button className={`cc-btn ${muted ? 'active-danger' : ''}`} onClick={onToggleMute} title={muted ? 'Unmute' : 'Mute'}>
{muted ? <Icon.MicOff /> : <Icon.Mic />}
</button>
<button className={`cc-btn ${video ? 'active-brand' : ''}`} onClick={onToggleVideo} title="Video">
<Icon.Video />
</button>
<button className={`cc-btn ${sharing ? 'active-brand' : ''}`} onClick={onToggleShare} title="Bildschirm teilen">
<Icon.Monitor />
</button>
<button className="cc-btn" title="Teilnehmer"><Icon.Friends /></button>
<button className="cc-btn hangup" onClick={onHangup} title="Auflegen"><Icon.PhoneOff /></button>
</div>
);
const IncomingCallScreen = ({ chat, onAccept, onDecline }) => (
<div className="incoming-call">
<div className="incoming-card">
<div className="incoming-ringing">Eingehender Anruf</div>
<div className="incoming-avatar">
<div className="pulse-ring"></div>
<div className="pulse-ring d2"></div>
<Avatar chat={{initial: chat.initial, color: chat.color}} size="lg" />
</div>
<div className="incoming-name">{chat.name}</div>
<div className="incoming-sub">
<Icon.Lock /> E2E verschlüsselt · Voice Call
</div>
<div className="incoming-actions">
<button className="incoming-btn decline" onClick={onDecline}>
<Icon.PhoneOff /> Ablehnen
</button>
<button className="incoming-btn accept" onClick={onAccept}>
<Icon.Phone /> Annehmen
</button>
</div>
</div>
</div>
);
const ActiveCallScreen = ({ participants, mode, setMode, focused, setFocused, onHangup, callState, setCallState }) => {
const speaker = participants.find(p => p.id === focused) || participants.find(p => p.sharing) || participants[0];
const others = participants.filter(p => p.id !== speaker.id);
const sharingParticipant = participants.find(p => p.sharing);
return (
<div className={`active-call mode-${mode}`}>
<div className="call-topbar">
<div className="call-topbar-left">
<div className="call-title">
<Icon.Friends />
<span>Die Squad</span>
<span className="call-dot">·</span>
<span className="call-duration">{callState.duration}</span>
</div>
<div className="call-e2ee"><Icon.Lock /> E2E verschlüsselt</div>
</div>
<div className="call-topbar-right">
<button className={`cc-btn sm ${mode === 'grid' ? 'active-brand' : ''}`} onClick={() => setMode('grid')} title="Grid"><Icon.Grid /></button>
<button className={`cc-btn sm ${mode === 'focus' ? 'active-brand' : ''}`} onClick={() => setMode('focus')} title="Fokus"><Icon.Focus /></button>
<button className={`cc-btn sm ${mode === 'fullscreen' ? 'active-brand' : ''}`} onClick={() => setMode('fullscreen')} title="Vollbild"><Icon.Maximize /></button>
</div>
</div>
<div className="call-stage">
{mode === 'grid' && (
<div className={`grid grid-${participants.length}`}>
{participants.map(p => (
<ParticipantTile key={p.id} p={p} onClick={() => { setFocused(p.id); setMode('focus'); }} />
))}
</div>
)}
{mode === 'focus' && (
<div className="focus-layout">
<div className="focus-main">
<ParticipantTile p={speaker} focused />
</div>
<div className="focus-strip">
{others.map(p => (
<ParticipantTile key={p.id} p={p} small onClick={() => setFocused(p.id)} />
))}
</div>
</div>
)}
{mode === 'fullscreen' && (
<div className="fullscreen-layout">
<ParticipantTile p={speaker} focused />
<div className="fs-strip">
{others.slice(0,4).map(p => (
<ParticipantTile key={p.id} p={p} small onClick={() => setFocused(p.id)} />
))}
</div>
<div className="fs-hint">Esc zum Verlassen</div>
</div>
)}
</div>
<CallControls
muted={callState.muted}
onToggleMute={() => setCallState(s => ({...s, muted: !s.muted}))}
sharing={callState.sharing}
onToggleShare={() => setCallState(s => ({...s, sharing: !s.sharing}))}
video={callState.video}
onToggleVideo={() => setCallState(s => ({...s, video: !s.video}))}
onHangup={onHangup}
/>
</div>
);
};
const PipCall = ({ onExpand, onHangup, participants }) => {
const speaker = participants.find(p => p.sharing) || participants.find(p => p.speaking) || participants[0];
return (
<div className="pip-call" onClick={onExpand}>
<div className="pip-preview">
{speaker.sharing ? (
<div className="screenshare-stub small"><div className="ss-window mini"></div></div>
) : (
<Avatar chat={{initial: speaker.initial, color: speaker.color}} size="sm" />
)}
</div>
<div className="pip-info">
<div className="pip-title">Die Squad · {participants.length}</div>
<div className="pip-sub">
<span className="live-dot"></span>
Live · tippe zum Öffnen
</div>
</div>
<button className="pip-hangup" onClick={(e) => { e.stopPropagation(); onHangup(); }}>
<Icon.PhoneOff />
</button>
</div>
);
};
Object.assign(window, { CALL_PARTICIPANTS, ParticipantTile, CallControls, IncomingCallScreen, ActiveCallScreen, PipCall });