fix(realtime-ui): auto-expire stale typing/recording indicators
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
2026-03-08 20:00:59 +03:00
parent 6930e73b9f
commit f369083b6a

View File

@@ -18,6 +18,9 @@ export function useRealtime() {
const typingByChat = useRef<Record<number, Set<number>>>({});
const recordingVoiceByChat = useRef<Record<number, Set<number>>>({});
const recordingVideoByChat = useRef<Record<number, Set<number>>>({});
const typingTimersRef = useRef<Record<string, number>>({});
const recordingVoiceTimersRef = useRef<Record<string, number>>({});
const recordingVideoTimersRef = useRef<Record<string, number>>({});
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null);
const heartbeatIntervalRef = useRef<number | null>(null);
@@ -85,6 +88,25 @@ export function useRealtime() {
}
const chatStore = useChatStore.getState();
const authStore = useAuthStore.getState();
const clearActivityTimer = (timers: Record<string, number>, key: string) => {
const id = timers[key];
if (id !== undefined) {
window.clearTimeout(id);
delete timers[key];
}
};
const armActivityTimer = (
timers: Record<string, number>,
key: string,
ttlMs: number,
onExpire: () => void
) => {
clearActivityTimer(timers, key);
timers[key] = window.setTimeout(() => {
delete timers[key];
onExpire();
}, ttlMs);
};
if (event.event === "receive_message") {
const chatId = Number(event.payload.chat_id);
const message = event.payload.message as Message;
@@ -163,6 +185,11 @@ export function useRealtime() {
}
typingByChat.current[chatId].add(userId);
chatStore.setTypingUsers(chatId, [...typingByChat.current[chatId]]);
const key = `${chatId}:${userId}`;
armActivityTimer(typingTimersRef.current, key, 9000, () => {
typingByChat.current[chatId]?.delete(userId);
chatStore.setTypingUsers(chatId, [...(typingByChat.current[chatId] ?? [])]);
});
}
if (event.event === "typing_stop") {
const chatId = Number(event.payload.chat_id);
@@ -172,6 +199,8 @@ export function useRealtime() {
}
typingByChat.current[chatId]?.delete(userId);
chatStore.setTypingUsers(chatId, [...(typingByChat.current[chatId] ?? [])]);
const key = `${chatId}:${userId}`;
clearActivityTimer(typingTimersRef.current, key);
}
if (event.event === "recording_voice_start") {
const chatId = Number(event.payload.chat_id);
@@ -184,6 +213,11 @@ export function useRealtime() {
}
recordingVoiceByChat.current[chatId].add(userId);
chatStore.setRecordingUsers(chatId, "voice", [...recordingVoiceByChat.current[chatId]]);
const key = `${chatId}:${userId}`;
armActivityTimer(recordingVoiceTimersRef.current, key, 12000, () => {
recordingVoiceByChat.current[chatId]?.delete(userId);
chatStore.setRecordingUsers(chatId, "voice", [...(recordingVoiceByChat.current[chatId] ?? [])]);
});
}
if (event.event === "recording_voice_stop") {
const chatId = Number(event.payload.chat_id);
@@ -193,6 +227,8 @@ export function useRealtime() {
}
recordingVoiceByChat.current[chatId]?.delete(userId);
chatStore.setRecordingUsers(chatId, "voice", [...(recordingVoiceByChat.current[chatId] ?? [])]);
const key = `${chatId}:${userId}`;
clearActivityTimer(recordingVoiceTimersRef.current, key);
}
if (event.event === "recording_video_start") {
const chatId = Number(event.payload.chat_id);
@@ -205,6 +241,11 @@ export function useRealtime() {
}
recordingVideoByChat.current[chatId].add(userId);
chatStore.setRecordingUsers(chatId, "video", [...recordingVideoByChat.current[chatId]]);
const key = `${chatId}:${userId}`;
armActivityTimer(recordingVideoTimersRef.current, key, 12000, () => {
recordingVideoByChat.current[chatId]?.delete(userId);
chatStore.setRecordingUsers(chatId, "video", [...(recordingVideoByChat.current[chatId] ?? [])]);
});
}
if (event.event === "recording_video_stop") {
const chatId = Number(event.payload.chat_id);
@@ -214,6 +255,8 @@ export function useRealtime() {
}
recordingVideoByChat.current[chatId]?.delete(userId);
chatStore.setRecordingUsers(chatId, "video", [...(recordingVideoByChat.current[chatId] ?? [])]);
const key = `${chatId}:${userId}`;
clearActivityTimer(recordingVideoTimersRef.current, key);
}
if (event.event === "message_delivered") {
const chatId = Number(event.payload.chat_id);
@@ -331,6 +374,12 @@ export function useRealtime() {
typingByChat.current = {};
recordingVoiceByChat.current = {};
recordingVideoByChat.current = {};
Object.values(typingTimersRef.current).forEach((id) => window.clearTimeout(id));
Object.values(recordingVoiceTimersRef.current).forEach((id) => window.clearTimeout(id));
Object.values(recordingVideoTimersRef.current).forEach((id) => window.clearTimeout(id));
typingTimersRef.current = {};
recordingVoiceTimersRef.current = {};
recordingVideoTimersRef.current = {};
useChatStore.setState({ typingByChat: {}, recordingVoiceByChat: {}, recordingVideoByChat: {} });
window.removeEventListener("focus", onFocusOrVisible);
document.removeEventListener("visibilitychange", onFocusOrVisible);