feat(call): Discord-style in-call features (group D)
- PiP widget now shows a live mm:ss / hh:mm:ss duration instead of the generic "tippe zum Öffnen" while a call is in progress. - New ParticipantsPopover — portal-mounted, fixed bottom-right, lists everyone in the call with avatar, speaking ring, mute/deafen badges and a per-peer volume slider. Wired to CallControls via the users button (data-participants-trigger skips the outside-click dismiss while toggling). - Non-terminal MicErrorBanner: getUserMedia failures inside joinRoom used to be silently swallowed by a console.error; they now set a categorized message (NotAllowedError / NotFoundError / NotReadableError) on CallContext.micError, render as a rose banner in both docked and fullscreen modes, and offer a Retry button that calls the extracted setupMicPipeline without rejoining the room. - Screen-share toggle is now 1-click using the last-saved preset + displaySurface. Right-click on the share button still opens the quality dialog for users who want to adjust before starting. - Noise-suppression toggle in the control bar (SparklesIcon). Flipping it updates audioSettings and hot-swaps the mic track via setAudioInputDevice so the new constraint takes effect without a rejoin. Mirrors Discord's Krisp button placement. - Fullscreen auto-speaker now tracks "most recently started speaking" instead of "exactly one currently speaking", so two people briefly overlapping doesn't kick the focus back to grid. Tracked in a prevSpeakers ref against each activeSpeakers diff. - Fullscreen controls auto-hide after 5s of mouse idle; mousemove / touchstart bring them back. Pinned visible while any popover (soundboard / volume-menu / participants / mic-error banner) is open so users can interact without the chrome fading mid-click. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -173,6 +173,13 @@ function PipCall() {
|
||||
: conv?.peer?.displayName ?? '—';
|
||||
const participantCount = 1 + remoteParticipants.length;
|
||||
const someoneSharing = remoteScreenShares.length > 0;
|
||||
// Duration ticks while connected or reconnecting (LiveKit holds the room
|
||||
// across reconnects, so the timer shouldn't reset on a wobble). Absent
|
||||
// on outgoing/connecting where the call hasn't started yet.
|
||||
const startedAt =
|
||||
state.kind === 'connected' || state.kind === 'reconnecting'
|
||||
? state.startedAt
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -197,7 +204,11 @@ function PipCall() {
|
||||
aria-hidden="true"
|
||||
className="h-1.5 w-1.5 rounded-full bg-rose-500 animate-live-dot"
|
||||
/>
|
||||
<span>Live · {t('app:call.tap_to_open', { defaultValue: 'tippe zum Öffnen' })}</span>
|
||||
<span className="tabular-nums">
|
||||
{startedAt
|
||||
? <PipDuration startedAt={startedAt} />
|
||||
: t('app:call.tap_to_open', { defaultValue: 'tippe zum Öffnen' })}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -214,3 +225,24 @@ function PipCall() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Live-ticking `mm:ss` / `hh:mm:ss` for the PiP. Duplicated from InCallPanel
|
||||
// deliberately — the two widgets have different typography + tabular
|
||||
// contexts, and extracting a shared component would be heavier than the
|
||||
// 8-line countup it replaces.
|
||||
function PipDuration({ startedAt }: { startedAt: string }) {
|
||||
const [, tick] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => tick((v) => v + 1), 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, []);
|
||||
const total = Math.max(
|
||||
0,
|
||||
Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000),
|
||||
);
|
||||
const hh = Math.floor(total / 3600);
|
||||
const mm = Math.floor((total % 3600) / 60);
|
||||
const ss = total % 60;
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return <>{hh > 0 ? `${hh}:${pad(mm)}:${pad(ss)}` : `${pad(mm)}:${pad(ss)}`}</>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user