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>
This commit is contained in:
byGalax
2026-05-06 23:35:01 +02:00
parent 72d02e385f
commit 825160ee46
504 changed files with 14217 additions and 14366 deletions
@@ -18,6 +18,7 @@ import { playNotificationTone } from '../lib/notificationSound';
import { notify } from '../lib/osNotify';
import { supabase } from '../lib/supabase';
import { updateTrayUnread } from '../lib/trayBadge';
import { getIsWindowFocused, subscribeWindowFocus } from '../lib/windowFocus';
import { useAuth } from './AuthContext';
const LAST_READ_STORAGE_KEY = 'chatapp.conv_last_read';
@@ -69,6 +70,12 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
const lastReadRef = useRef<LastReadMap>(loadLastReadMap());
const activeConvIdRef = useRef<string | null>(null);
const presenceRef = useRef(profile?.presenceState ?? 'offline');
// Ref-mirrored window-focus state so the realtime message handler can
// read it synchronously without triggering a re-render per focus flip.
// "App focused" = our window is the foreground window in the OS,
// which is what we actually care about when deciding whether an
// arriving message was seen by the user or should ping.
const isWindowFocusedRef = useRef<boolean>(getIsWindowFocused());
const conversationsRef = useRef<ConversationSummary[]>([]);
conversationsRef.current = conversations;
@@ -76,6 +83,26 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
presenceRef.current = profile?.presenceState ?? 'offline';
}, [profile?.presenceState]);
useEffect(() => {
return subscribeWindowFocus((focused) => {
isWindowFocusedRef.current = focused;
// Regaining focus on the active conversation: the user is back
// and looking at those messages, so flip the unread counter to
// zero and persist the last-read stamp. Without this, opening
// the app after being away leaves a stale badge until the user
// re-clicks the conversation.
if (focused) {
const active = activeConvIdRef.current;
if (active) {
const now = new Date().toISOString();
lastReadRef.current[active] = now;
saveLastReadMap(lastReadRef.current);
setUnread((prev) => (prev[active] ? { ...prev, [active]: 0 } : prev));
}
}
});
}, []);
const computeUnreadForConv = useCallback(
async (convId: string): Promise<number> => {
if (!myId) return 0;
@@ -174,10 +201,16 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
const row = payload.new as unknown as { conversation_id: string; sender_id: string };
const fromSelf = row.sender_id === myId;
const active = row.conversation_id === activeConvIdRef.current;
const appFocused = isWindowFocusedRef.current;
// Only suppress sound + unread bump when the user is *actually*
// looking at the conversation — i.e. the app window is in the
// foreground AND the active conversation matches. Having the
// app parked on monitor 2 while the user is in-game still
// counts as "they didn't see it."
const seenByUser = active && appFocused;
if (!fromSelf) {
if (active) {
// Viewing this conversation — implicit read.
if (seenByUser) {
markRead(row.conversation_id);
} else {
setUnread((prev) => ({