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:
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user