Files
ChatApp/apps/desktop/src/components/UpdateToast.tsx
T
byGalax 825160ee46 feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
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>
2026-05-06 23:35:01 +02:00

116 lines
4.0 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
checkForUpdate,
IDLE_UPDATE_STATE,
installUpdate,
type UpdateState,
} from '../lib/appUpdates';
import { SparklesIcon, SpinnerIcon, XIcon } from './icons';
// Light-weight update UX: check once on mount, then every hour while the
// app is open. When an update exists show a toast with "Install & Restart"
// and a dismiss button. Dismiss is session-scoped — on next launch we check
// again.
export function UpdateToast() {
const { t } = useTranslation(['app']);
const [state, setState] = useState<UpdateState>(IDLE_UPDATE_STATE);
const [dismissed, setDismissed] = useState(false);
const [installing, setInstalling] = useState(false);
const [progressPct, setProgressPct] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
const run = async () => {
const next = await checkForUpdate();
if (!cancelled) setState(next);
};
void run();
const id = window.setInterval(run, 60 * 60 * 1000);
return () => {
cancelled = true;
window.clearInterval(id);
};
}, []);
if (!state.available || dismissed) return null;
const handleInstall = async () => {
setInstalling(true);
setProgressPct(0);
try {
await installUpdate((downloaded, total) => {
if (total && total > 0) setProgressPct((downloaded / total) * 100);
});
} catch (err: unknown) {
setState((s) => ({
...s,
error: err instanceof Error ? err.message : 'install failed',
}));
setInstalling(false);
setProgressPct(null);
}
};
return (
<div
role="status"
aria-live="polite"
className="fixed bottom-6 left-6 z-50 w-80 overflow-hidden rounded-2xl border border-line bg-surface-3 p-4 shadow-2xl"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent/20 text-accent">
<SparklesIcon className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-fg">
{t('app:update.available', { defaultValue: 'Update verfügbar' })}
{state.version ? ' · v' + state.version : ''}
</p>
{state.notes && (
<p className="mt-0.5 line-clamp-6 whitespace-pre-line text-xs text-fg-muted">
{state.notes}
</p>
)}
{state.error && (
<p className="mt-1 text-xs text-rose-500 dark:text-rose-300">{state.error}</p>
)}
</div>
{!installing && (
<button
type="button"
onClick={() => setDismissed(true)}
aria-label={t('common:close', { defaultValue: 'Schließen' })}
className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-fg-muted transition hover:bg-surface-2 hover:text-fg"
>
<XIcon className="h-3.5 w-3.5" />
</button>
)}
</div>
<button
type="button"
onClick={() => void handleInstall()}
disabled={installing}
className="mt-3 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-60"
>
{installing ? (
<>
<SpinnerIcon className="h-3.5 w-3.5" />
<span>
{progressPct != null
? t('app:update.downloading', { defaultValue: 'Lade…' }) +
' ' +
Math.round(progressPct) +
'%'
: t('app:update.installing', { defaultValue: 'Installiere…' })}
</span>
</>
) : (
<span>{t('app:update.install', { defaultValue: 'Installieren & Neustarten' })}</span>
)}
</button>
</div>
);
}