feat(desktop): Tier A desktop features — web side (P5-46/36/44/43/49/47/55/57)

Web half of the desktop feature wave. A shared bridge (`hooks/useTauri.ts`:
invokeTauri/isTauri/useTauriEvent) backs per-feature hooks that no-op in the
browser and drive the native Tauri commands (compiled in cinny-desktop):

- P5-46 useTauriCallPower — hold system awake while a call is active.
- P5-36 useTauriJumpList — Windows jump list of recent rooms → matrix: deep links.
- P5-44 useTauriThumbbar — taskbar Mute/Deafen/End; events toggle mic/sound/hangup.
- P5-43 useTauriSmtc — SMTC call state + button events.
- P5-49 useTauriNetwork — react to native network-change → mx.retryImmediately().
- P5-47 window chrome — opt-in `customWindowChromeAtom` + TDS `TitleBar`; DesktopChrome
  wrapper in App.tsx (zero layout impact when off) + a desktop-only settings toggle.
- P5-55 composer toolbar drag-reorder (settings order[] + pragmatic-drag-and-drop).
- P5-57 DraftIndicator — subtle "draft saved" cue in the composer.

Client-scoped hooks mount via TauriDesktopFeatures in ClientNonUIFeatures; window
chrome mounts at App level. Gates: tsc/eslint/prettier clean, build OK, 556 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:07:03 -04:00
parent a0fcdf74da
commit aab7e5ae20
18 changed files with 1180 additions and 216 deletions
+35
View File
@@ -0,0 +1,35 @@
import { useEffect, useRef } from 'react';
// Tauri v2 injects `__TAURI_INTERNALS__` into the webview at runtime; we use it
// directly so cinny doesn't need `@tauri-apps/api` as a dependency. Native Rust
// modules push data back to the web by dispatching DOM CustomEvents (see
// `emit_to_web` in cinny-desktop's `native` module), which `useTauriEvent`
// subscribes to. This module is the single source for the desktop bridge that
// every `useTauri*` feature hook builds on.
type Invoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
export const tauriInvoke = (): Invoke | undefined =>
(window as unknown as { __TAURI_INTERNALS__?: { invoke: Invoke } }).__TAURI_INTERNALS__?.invoke;
export const isTauri = (): boolean => tauriInvoke() !== undefined;
/** Fire-and-forget invoke that no-ops (and never throws) outside Tauri. */
export const invokeTauri = (cmd: string, args?: Record<string, unknown>): void => {
tauriInvoke()?.(cmd, args).catch(() => undefined);
};
/**
* Subscribe to a CustomEvent dispatched from the Rust side via `emit_to_web`.
* The handler is kept in a ref so callers don't need to memoize it to avoid
* re-subscribing. No-op outside Tauri.
*/
export function useTauriEvent<T = unknown>(name: string, handler: (detail: T) => void): void {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
if (!isTauri()) return undefined;
const listener = (e: Event): void => handlerRef.current((e as CustomEvent<T>).detail);
window.addEventListener(name, listener);
return () => window.removeEventListener(name, listener);
}, [name]);
}