feat: audio devices + fullscreen + banner cleanup (v0.7.0)
Audio device selection: - audioSettings: persisted inputDeviceId + outputDeviceId - CallContext: uses stored input deviceId on mic enable, new setAudioInputDevice / setAudioOutputDevice actions that hot-swap without reconnect. Output swap applies HTMLMediaElement.setSinkId to every attached remote-audio element (LiveKit's switchActiveDevice only tracks elements it attached itself) - SettingsPage: new "Mikrofon" + "Ausgabegerät" selects with enumerateDevices, devicechange listener, permission-probe button. setSinkId-unsupported fallback is messaged but non-blocking Fullscreen: - FullscreenCall was absolute inset-0 z-40 which trapped it inside the <main> pane — sidebar + chat-list stayed visible. Switched to fixed inset-0 z-[60] so the call overlays the whole window Discord-style - ScreenShareViewer fullscreen: CSS-only toggle (native Fullscreen API unreliable under Tauri WKWebView), portalled to document.body when active so no ancestor stacking context can clip it. Esc exits ActiveCallBanner: - cleanup effect returned early when presence was entirely empty, leaving the "1 im Raum" fallback stuck after both peers left. Now schedules dismissLastCall as soon as othersIn.length === 0, with a 3s grace window to absorb presence re-sync flicker Bump tauri version 0.6.0 -> 0.7.0
This commit is contained in:
@@ -210,17 +210,14 @@ function ActiveCallBanner({ conversationId }: { conversationId: string }) {
|
||||
const justLeft = state.kind === 'idle' && lastCallConversationId === conversationId;
|
||||
|
||||
// Once presence confirms the room is empty, drop the "just left" hint so the
|
||||
// banner hides cleanly instead of sticking forever.
|
||||
// banner hides cleanly instead of sticking forever. Grace window handles the
|
||||
// brief gap between hangup and presence re-sync so we don't flicker.
|
||||
useEffect(() => {
|
||||
if (!justLeft) return;
|
||||
if (active.length === 0) return;
|
||||
if (othersIn.length === 0) {
|
||||
// Active reports only us (or something stale) — wait, then dismiss.
|
||||
const id = window.setTimeout(() => dismissLastCall(), 3000);
|
||||
return () => window.clearTimeout(id);
|
||||
}
|
||||
return undefined;
|
||||
}, [justLeft, active.length, othersIn.length, dismissLastCall]);
|
||||
if (othersIn.length > 0) return; // still live — keep banner
|
||||
const id = window.setTimeout(() => dismissLastCall(), 3000);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [justLeft, othersIn.length, dismissLastCall]);
|
||||
|
||||
if (iAmIn) return null;
|
||||
if (othersIn.length === 0 && !justLeft) return null;
|
||||
|
||||
@@ -487,7 +487,7 @@ function FullscreenCall({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-40 flex flex-col overflow-hidden bg-surface">
|
||||
<div className="fixed inset-0 z-[60] flex flex-col overflow-hidden bg-surface">
|
||||
<div className="relative min-h-0 flex-1">
|
||||
{speaker && (
|
||||
<div className="absolute inset-0">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RemoteTrack } from 'livekit-client';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { RemoteScreenShare } from '../context/CallContext';
|
||||
@@ -32,30 +33,31 @@ export function ScreenShareViewer({ share, avatarUrl, displayName }: ScreenShare
|
||||
};
|
||||
}, [share.track, watching]);
|
||||
|
||||
// Esc exits CSS fullscreen.
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
setIsFullscreen(document.fullscreenElement === containerRef.current);
|
||||
if (!isFullscreen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setIsFullscreen(false);
|
||||
};
|
||||
document.addEventListener('fullscreenchange', onChange);
|
||||
return () => document.removeEventListener('fullscreenchange', onChange);
|
||||
}, []);
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [isFullscreen]);
|
||||
|
||||
// CSS-only "app fullscreen" — Discord-style: overlay the whole window
|
||||
// including the left sidebar + chat list. Native Fullscreen API is
|
||||
// unreliable in Tauri's WKWebView and doesn't add useful chrome-hiding
|
||||
// beyond what `fixed inset-0 z-[60]` already gives us.
|
||||
const toggleFullscreen = () => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
if (document.fullscreenElement === el) {
|
||||
void document.exitFullscreen();
|
||||
} else {
|
||||
void el.requestFullscreen();
|
||||
}
|
||||
setIsFullscreen((v) => !v);
|
||||
};
|
||||
|
||||
return (
|
||||
const viewerNode = (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={
|
||||
'overflow-hidden rounded-xl border border-emerald-500/30 bg-black ' +
|
||||
(isFullscreen ? 'flex h-screen w-screen flex-col rounded-none' : '')
|
||||
isFullscreen
|
||||
? 'fixed inset-0 z-[60] flex h-screen w-screen flex-col overflow-hidden border-0 bg-black'
|
||||
: 'overflow-hidden rounded-xl border border-emerald-500/30 bg-black'
|
||||
}
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-emerald-500/20 bg-emerald-500/15 px-3 py-1.5 text-xs text-emerald-700 dark:text-emerald-200">
|
||||
@@ -130,6 +132,14 @@ export function ScreenShareViewer({ share, avatarUrl, displayName }: ScreenShare
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// When in fullscreen, portal out of the call-panel subtree into <body> so
|
||||
// no ancestor can clip or stack below us. Sidebar/chat-list are siblings of
|
||||
// AppShell's root — portalled node sits above them via z-[60].
|
||||
if (isFullscreen) {
|
||||
return createPortal(viewerNode, document.body);
|
||||
}
|
||||
return viewerNode;
|
||||
}
|
||||
|
||||
function BlurredTile({ avatarUrl, letter }: { avatarUrl: string | null; letter: string }) {
|
||||
|
||||
Reference in New Issue
Block a user