825160ee46
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import type { ConversationSummary } from '@chat-app/shared/chat';
|
|
import { useCall } from '../context/CallContext';
|
|
|
|
interface Props {
|
|
conversation: ConversationSummary;
|
|
}
|
|
|
|
const STALE_AFTER_MS = 5000;
|
|
|
|
/** Discord-style live-captions overlay. Pinned to the bottom-center of the
|
|
* call surface; renders the most recent caption per participant, fading
|
|
* entries out after `STALE_AFTER_MS` of silence. Self-captions are shown
|
|
* too so the speaker can sanity-check what's being broadcast. */
|
|
export function CallCaptionsOverlay({ conversation }: Props) {
|
|
const { captions } = useCall();
|
|
// Re-render every second so stale entries fade without needing the data
|
|
// channel to fire — captions module just stores timestamps.
|
|
const [, setNow] = useState(Date.now());
|
|
useEffect(() => {
|
|
const id = window.setInterval(() => setNow(Date.now()), 1000);
|
|
return () => window.clearInterval(id);
|
|
}, []);
|
|
|
|
const now = Date.now();
|
|
const visible = Object.entries(captions)
|
|
.filter(([, v]) => now - v.timestamp < STALE_AFTER_MS)
|
|
.sort(([, a], [, b]) => a.timestamp - b.timestamp);
|
|
|
|
if (visible.length === 0) return null;
|
|
|
|
return (
|
|
<div className="pointer-events-none absolute inset-x-0 bottom-24 z-30 flex flex-col items-center gap-1.5 px-6">
|
|
{visible.map(([identity, c]) => {
|
|
const member = conversation.members.find((m) => m.userId === identity);
|
|
const name = member?.profile?.displayName ?? '?';
|
|
const age = now - c.timestamp;
|
|
const opacity = age > 3500 ? 1 - (age - 3500) / (STALE_AFTER_MS - 3500) : 1;
|
|
return (
|
|
<div
|
|
key={identity}
|
|
style={{ opacity: Math.max(0, opacity) }}
|
|
className="max-w-[760px] rounded-lg bg-black/72 px-3.5 py-1.5 text-sm text-white shadow-md backdrop-blur-md transition-opacity"
|
|
>
|
|
<span className="mr-2 text-[11px] font-semibold uppercase tracking-wide text-white/55">
|
|
{name}
|
|
</span>
|
|
<span className={c.final ? '' : 'italic text-white/85'}>{c.text}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|