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 typingByChat = useRef<Record<number, Set<number>>>({});
|
||||||
const recordingVoiceByChat = useRef<Record<number, Set<number>>>({});
|
const recordingVoiceByChat = useRef<Record<number, Set<number>>>({});
|
||||||
const recordingVideoByChat = 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 wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||||
const heartbeatIntervalRef = useRef<number | null>(null);
|
const heartbeatIntervalRef = useRef<number | null>(null);
|
||||||
@@ -85,6 +88,25 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
const chatStore = useChatStore.getState();
|
const chatStore = useChatStore.getState();
|
||||||
const authStore = useAuthStore.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") {
|
if (event.event === "receive_message") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
const message = event.payload.message as Message;
|
const message = event.payload.message as Message;
|
||||||
@@ -163,6 +185,11 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
typingByChat.current[chatId].add(userId);
|
typingByChat.current[chatId].add(userId);
|
||||||
chatStore.setTypingUsers(chatId, [...typingByChat.current[chatId]]);
|
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") {
|
if (event.event === "typing_stop") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -172,6 +199,8 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
typingByChat.current[chatId]?.delete(userId);
|
typingByChat.current[chatId]?.delete(userId);
|
||||||
chatStore.setTypingUsers(chatId, [...(typingByChat.current[chatId] ?? [])]);
|
chatStore.setTypingUsers(chatId, [...(typingByChat.current[chatId] ?? [])]);
|
||||||
|
const key = `${chatId}:${userId}`;
|
||||||
|
clearActivityTimer(typingTimersRef.current, key);
|
||||||
}
|
}
|
||||||
if (event.event === "recording_voice_start") {
|
if (event.event === "recording_voice_start") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -184,6 +213,11 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
recordingVoiceByChat.current[chatId].add(userId);
|
recordingVoiceByChat.current[chatId].add(userId);
|
||||||
chatStore.setRecordingUsers(chatId, "voice", [...recordingVoiceByChat.current[chatId]]);
|
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") {
|
if (event.event === "recording_voice_stop") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -193,6 +227,8 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
recordingVoiceByChat.current[chatId]?.delete(userId);
|
recordingVoiceByChat.current[chatId]?.delete(userId);
|
||||||
chatStore.setRecordingUsers(chatId, "voice", [...(recordingVoiceByChat.current[chatId] ?? [])]);
|
chatStore.setRecordingUsers(chatId, "voice", [...(recordingVoiceByChat.current[chatId] ?? [])]);
|
||||||
|
const key = `${chatId}:${userId}`;
|
||||||
|
clearActivityTimer(recordingVoiceTimersRef.current, key);
|
||||||
}
|
}
|
||||||
if (event.event === "recording_video_start") {
|
if (event.event === "recording_video_start") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -205,6 +241,11 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
recordingVideoByChat.current[chatId].add(userId);
|
recordingVideoByChat.current[chatId].add(userId);
|
||||||
chatStore.setRecordingUsers(chatId, "video", [...recordingVideoByChat.current[chatId]]);
|
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") {
|
if (event.event === "recording_video_stop") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -214,6 +255,8 @@ export function useRealtime() {
|
|||||||
}
|
}
|
||||||
recordingVideoByChat.current[chatId]?.delete(userId);
|
recordingVideoByChat.current[chatId]?.delete(userId);
|
||||||
chatStore.setRecordingUsers(chatId, "video", [...(recordingVideoByChat.current[chatId] ?? [])]);
|
chatStore.setRecordingUsers(chatId, "video", [...(recordingVideoByChat.current[chatId] ?? [])]);
|
||||||
|
const key = `${chatId}:${userId}`;
|
||||||
|
clearActivityTimer(recordingVideoTimersRef.current, key);
|
||||||
}
|
}
|
||||||
if (event.event === "message_delivered") {
|
if (event.event === "message_delivered") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
@@ -331,6 +374,12 @@ export function useRealtime() {
|
|||||||
typingByChat.current = {};
|
typingByChat.current = {};
|
||||||
recordingVoiceByChat.current = {};
|
recordingVoiceByChat.current = {};
|
||||||
recordingVideoByChat.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: {} });
|
useChatStore.setState({ typingByChat: {}, recordingVoiceByChat: {}, recordingVideoByChat: {} });
|
||||||
window.removeEventListener("focus", onFocusOrVisible);
|
window.removeEventListener("focus", onFocusOrVisible);
|
||||||
document.removeEventListener("visibilitychange", onFocusOrVisible);
|
document.removeEventListener("visibilitychange", onFocusOrVisible);
|
||||||
|
|||||||
Reference in New Issue
Block a user