fix: remove spurious reconnect sound on deploy/laptop wake, fix federated typing/asset display
This commit is contained in:
Binary file not shown.
@@ -1,27 +1,15 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
import { useChatStore } from '../../stores/chatStore';
|
import { useChatStore } from '../../stores/chatStore';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { getHomeWsConnected } from '../../hooks/useWebSocket';
|
|
||||||
import { AudioManager } from '../../audio/AudioManager';
|
import { AudioManager } from '../../audio/AudioManager';
|
||||||
|
|
||||||
export function SoundController() {
|
export function SoundController() {
|
||||||
const audioManager = AudioManager.getInstance();
|
const audioManager = AudioManager.getInstance();
|
||||||
const currentUser = useAuthStore((s) => s.user);
|
const currentUser = useAuthStore((s) => s.user);
|
||||||
const [isWsConnected, setIsWsConnected] = useState(false);
|
|
||||||
|
|
||||||
// Poll home WS connection status without managing lifecycle
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setIsWsConnected(getHomeWsConnected());
|
|
||||||
}, 500);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Refs to track previous states
|
// Refs to track previous states
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
const prevIsWsConnected = useRef<boolean>(false);
|
|
||||||
const wsDisconnectedAt = useRef<number>(0);
|
|
||||||
const prevIsMuted = useRef<boolean>(useVoiceStore.getState().isMuted);
|
const prevIsMuted = useRef<boolean>(useVoiceStore.getState().isMuted);
|
||||||
const prevIsDeafened = useRef<boolean>(useVoiceStore.getState().isDeafened);
|
const prevIsDeafened = useRef<boolean>(useVoiceStore.getState().isDeafened);
|
||||||
const prevIsCameraOn = useRef<boolean>(useVoiceStore.getState().isCameraOn);
|
const prevIsCameraOn = useRef<boolean>(useVoiceStore.getState().isCameraOn);
|
||||||
@@ -33,28 +21,10 @@ export function SoundController() {
|
|||||||
const incomingCallLoop = useRef<AudioBufferSourceNode | null>(null);
|
const incomingCallLoop = useRef<AudioBufferSourceNode | null>(null);
|
||||||
const outgoingCallLoop = useRef<AudioBufferSourceNode | null>(null);
|
const outgoingCallLoop = useRef<AudioBufferSourceNode | null>(null);
|
||||||
|
|
||||||
// WebSocket Reconnect Sound — suppress during active voice and brief blips (<3s)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialMount.current) return;
|
|
||||||
if (!isWsConnected && prevIsWsConnected.current) {
|
|
||||||
// Record when we lost connection
|
|
||||||
wsDisconnectedAt.current = Date.now();
|
|
||||||
}
|
|
||||||
if (isWsConnected && !prevIsWsConnected.current) {
|
|
||||||
const isInActiveVoice = useVoiceStore.getState().isLiveKitConnected;
|
|
||||||
const downtime = wsDisconnectedAt.current > 0 ? Date.now() - wsDisconnectedAt.current : Infinity;
|
|
||||||
if (!isInActiveVoice && downtime > 3000) {
|
|
||||||
audioManager.playSound('reconnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevIsWsConnected.current = isWsConnected;
|
|
||||||
}, [isWsConnected, audioManager]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set initial mount flag to false after first run
|
// Set initial mount flag to false after first run
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
isInitialMount.current = false;
|
isInitialMount.current = false;
|
||||||
prevIsWsConnected.current = isWsConnected;
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// 1. Listen to Voice State Changes
|
// 1. Listen to Voice State Changes
|
||||||
|
|||||||
@@ -223,9 +223,14 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
removeMessage(event.messageId, event.channelId);
|
removeMessage(event.messageId, event.channelId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'typing':
|
case 'typing': {
|
||||||
setTyping(event.channelId, event.userId, event.username);
|
let typingUsername = event.username as string;
|
||||||
|
if (!isHome && typingUsername && !typingUsername.includes('@')) {
|
||||||
|
try { typingUsername = `${typingUsername}@${new URL(origin).host}`; } catch {}
|
||||||
|
}
|
||||||
|
setTyping(event.channelId, event.userId, typingUsername);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'presence_update':
|
case 'presence_update':
|
||||||
updateMemberPresence(event.userId, event.status);
|
updateMemberPresence(event.userId, event.status);
|
||||||
@@ -300,9 +305,14 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
removeMessage(event.messageId, event.dmChannelId);
|
removeMessage(event.messageId, event.dmChannelId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'dm_typing':
|
case 'dm_typing': {
|
||||||
setTyping(event.dmChannelId, event.userId, event.username);
|
let dmTypingUsername = event.username as string;
|
||||||
|
if (!isHome && dmTypingUsername && !dmTypingUsername.includes('@')) {
|
||||||
|
try { dmTypingUsername = `${dmTypingUsername}@${new URL(origin).host}`; } catch {}
|
||||||
|
}
|
||||||
|
setTyping(event.dmChannelId, event.userId, dmTypingUsername);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Reactions (all origins) ────────────────────────────────────────────
|
// ─── Reactions (all origins) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,24 @@ export function resolveAssetUrl(filename: string | null | undefined, origin: str
|
|||||||
* Mutates in-place for efficiency (called on arrays of members/messages).
|
* Mutates in-place for efficiency (called on arrays of members/messages).
|
||||||
*/
|
*/
|
||||||
export function normalizeUserAssets<T extends { avatar?: string | null }>(user: T, origin: string): T {
|
export function normalizeUserAssets<T extends { avatar?: string | null }>(user: T, origin: string): T {
|
||||||
if (origin && user.avatar) {
|
if (!origin) return user;
|
||||||
|
if (user.avatar) {
|
||||||
user.avatar = resolveAssetUrl(user.avatar, origin) ?? user.avatar;
|
user.avatar = resolveAssetUrl(user.avatar, origin) ?? user.avatar;
|
||||||
}
|
}
|
||||||
|
// Qualify users local to the remote instance with their origin domain.
|
||||||
|
// These users have no homeInstance (they're native there), so the client
|
||||||
|
// can't distinguish them from its own local users without this step.
|
||||||
|
const u = user as Record<string, unknown>;
|
||||||
|
if (typeof u.username === 'string' && typeof u.id === 'string' && !u.homeInstance) {
|
||||||
|
try {
|
||||||
|
const host = new URL(origin).host;
|
||||||
|
u.homeInstance = host;
|
||||||
|
if (!u.homeUserId) u.homeUserId = u.id;
|
||||||
|
if (!(u.username as string).includes('@')) {
|
||||||
|
u.username = `${u.username}@${host}`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user