fix(realtime-ui): auto-expire stale typing/recording indicators
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user