Compare commits

..

No commits in common. "3f39a3d05a644d6ac31823652bdc20a8c7a0159d" and "76fa04d1295831f02968764cdeeb7b4c57686df2" have entirely different histories.

3 changed files with 117 additions and 260 deletions

Binary file not shown.

View File

@ -57,11 +57,6 @@ export default function AdminChatPage() {
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const socketRef = useRef<Socket | null>(null);
const getTokenRef = useRef(getToken);
const activeThreadIdRef = useRef<string | null>(null);
const subscribedThreadIdsRef = useRef<Set<string>>(new Set());
const loadingThreadsRef = useRef(false);
const loadingMessagesRef = useRef<Record<string, boolean>>({});
const activeMessages = useMemo(
() => (activeThreadId ? (messagesByThreadId[activeThreadId] ?? []) : []),
@ -74,97 +69,71 @@ export default function AdminChatPage() {
);
useEffect(() => {
getTokenRef.current = getToken;
}, [getToken]);
useEffect(() => {
activeThreadIdRef.current = activeThreadId;
}, [activeThreadId]);
const loadThreads = async (showLoader = true) => {
if (loadingThreadsRef.current) {
return;
}
const token = await getTokenRef.current();
if (!token) {
return;
}
loadingThreadsRef.current = true;
try {
if (showLoader) {
setLoadingThreads(true);
const loadThreads = async () => {
const token = await getToken();
if (!token) {
return;
}
const response = await axios.get<{ threads: ChatThread[] }>(
"/api/chat/threads",
{
headers: { Authorization: `Bearer ${token}` },
},
);
setThreads(response.data.threads);
setActiveThreadId((prev) => prev ?? response.data.threads[0]?.id ?? null);
setReadByThreadId({});
} finally {
if (showLoader) {
try {
setLoadingThreads(true);
const response = await axios.get<{ threads: ChatThread[] }>(
"/api/chat/threads",
{
headers: { Authorization: `Bearer ${token}` },
},
);
setThreads(response.data.threads);
setActiveThreadId(
(prev) => prev ?? response.data.threads[0]?.id ?? null,
);
setReadByThreadId({});
} finally {
setLoadingThreads(false);
}
loadingThreadsRef.current = false;
}
};
};
const loadMessages = async (threadId: string, showLoader = true) => {
if (loadingMessagesRef.current[threadId]) {
return;
}
const token = await getTokenRef.current();
if (!token) {
return;
}
loadingMessagesRef.current[threadId] = true;
try {
if (showLoader) {
setLoadingMessages(true);
}
const response = await axios.get<{ messages: ChatMessage[] }>(
`/api/chat/threads/${threadId}/messages`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
setMessagesByThreadId((prev) => ({
...prev,
[threadId]: response.data.messages,
}));
} finally {
if (showLoader) {
setLoadingMessages(false);
}
loadingMessagesRef.current[threadId] = false;
}
};
useEffect(() => {
void loadThreads(true);
}, []);
void loadThreads();
}, [getToken]);
useEffect(() => {
if (!activeThreadId) {
return;
}
void loadMessages(activeThreadId, true);
}, [activeThreadId]);
const loadMessages = async () => {
const token = await getToken();
if (!token) {
return;
}
try {
setLoadingMessages(true);
const response = await axios.get<{ messages: ChatMessage[] }>(
`/api/chat/threads/${activeThreadId}/messages`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
setMessagesByThreadId((prev) => ({
...prev,
[activeThreadId]: response.data.messages,
}));
} finally {
setLoadingMessages(false);
}
};
void loadMessages();
}, [activeThreadId, getToken]);
useEffect(() => {
let mounted = true;
const setupSocket = async () => {
const token = await getTokenRef.current();
const token = await getToken();
if (!token || !mounted) {
return;
}
@ -182,14 +151,14 @@ export default function AdminChatPage() {
socket.on("connect", () => {
setSocketConnected(true);
subscribedThreadIdsRef.current.clear();
void loadThreads(false);
if (activeThreadId) {
socket.emit("chat:subscribe", { threadId: activeThreadId });
}
});
socket.on("disconnect", () => {
setSocketConnected(false);
setTypingByThreadId({});
subscribedThreadIdsRef.current.clear();
});
socket.on(
@ -216,7 +185,7 @@ export default function AdminChatPage() {
lastMessageAt: event.message.createdAt,
lastMessageBody: event.message.body,
unreadCount:
event.threadId === activeThreadIdRef.current ||
event.threadId === activeThreadId ||
event.message.senderUserId === userId
? thread.unreadCount
: thread.unreadCount + 1,
@ -294,8 +263,6 @@ export default function AdminChatPage() {
[event.userId!]: event.lastReadMessageId!,
},
}));
void loadThreads(false);
});
socket.on("chat:error", (event: { code?: string; message?: string }) => {
@ -317,57 +284,28 @@ export default function AdminChatPage() {
socketRef.current = null;
}
};
}, [userId]);
}, [activeThreadId, getToken, userId]);
useEffect(() => {
if (!socketRef.current || !socketConnected) {
return;
}
const socket = socketRef.current;
const nextIds = new Set(threads.map((thread) => thread.id));
threads.forEach((thread) => {
if (!subscribedThreadIdsRef.current.has(thread.id)) {
socket.emit("chat:subscribe", { threadId: thread.id });
subscribedThreadIdsRef.current.add(thread.id);
}
});
Array.from(subscribedThreadIdsRef.current).forEach((threadId) => {
if (!nextIds.has(threadId)) {
socket.emit("chat:unsubscribe", { threadId });
subscribedThreadIdsRef.current.delete(threadId);
}
});
}, [socketConnected, threads]);
useEffect(() => {
if (!activeThreadId) {
if (!activeThreadId || !socketRef.current || !socketConnected) {
return;
}
socketRef.current.emit("chat:subscribe", { threadId: activeThreadId });
setThreads((prev) =>
prev.map((thread) =>
thread.id === activeThreadId ? { ...thread, unreadCount: 0 } : thread,
),
);
}, [activeThreadId]);
useEffect(() => {
if (!socketConnected) {
const interval = setInterval(() => {
void loadThreads(false);
if (activeThreadIdRef.current) {
void loadMessages(activeThreadIdRef.current, false);
}
}, 5000);
return () => {
clearInterval(interval);
};
}
}, [socketConnected]);
return () => {
if (socketRef.current) {
socketRef.current.emit("chat:unsubscribe", {
threadId: activeThreadId,
});
}
};
}, [activeThreadId, socketConnected]);
useEffect(() => {
if (!activeThreadId) {
@ -381,7 +319,7 @@ export default function AdminChatPage() {
}
const markRead = async () => {
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
return;
}
@ -413,7 +351,7 @@ export default function AdminChatPage() {
};
void markRead();
}, [activeThreadId, messagesByThreadId, socketConnected, userId]);
}, [activeThreadId, getToken, messagesByThreadId, socketConnected, userId]);
const onChangeDraft = (value: string) => {
setDraft(value);
@ -481,7 +419,7 @@ export default function AdminChatPage() {
return;
}
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
return;
}

View File

@ -74,26 +74,16 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
>({});
const socketRef = useRef<Socket | null>(null);
const getTokenRef = useRef(getToken);
const lastMarkedReadByThreadRef = useRef<Record<string, string>>({});
const activeThreadIdRef = useRef<string | null>(null);
const currentUserIdRef = useRef<string | undefined>(undefined);
const subscribedThreadIdsRef = useRef<Set<string>>(new Set());
const refreshThreadsRef = useRef<(showLoader?: boolean) => Promise<void>>(
const refreshThreadsRef = useRef<() => Promise<void>>(async () => {});
const refreshMessagesRef = useRef<(threadId: string) => Promise<void>>(
async () => {},
);
const refreshMessagesRef = useRef<
(threadId: string, showLoader?: boolean) => Promise<void>
>(async () => {});
const markThreadReadRef = useRef<
(threadId: string, lastReadMessageId?: string) => Promise<void>
>(async () => {});
const refreshingThreadsRef = useRef(false);
const refreshingMessagesRef = useRef<Record<string, boolean>>({});
useEffect(() => {
getTokenRef.current = getToken;
}, [getToken]);
const clearAll = useCallback(() => {
setThreads([]);
@ -107,7 +97,6 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
setSocketConnected(false);
setTypingByThreadId({});
lastMarkedReadByThreadRef.current = {};
subscribedThreadIdsRef.current.clear();
if (socketRef.current) {
socketRef.current.disconnect();
@ -115,80 +104,56 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
}
}, []);
const refreshThreads = useCallback(
async (showLoader = true) => {
if (!isSignedIn) {
return;
}
const refreshThreads = useCallback(async () => {
if (!isSignedIn) {
return;
}
if (refreshingThreadsRef.current) {
return;
}
const token = await getToken();
if (!token) {
return;
}
refreshingThreadsRef.current = true;
const token = await getTokenRef.current();
if (!token) {
refreshingThreadsRef.current = false;
return;
}
try {
if (showLoader) {
setLoadingThreads(true);
try {
setLoadingThreads(true);
if (isClientUser) {
try {
await chatApi.getMyDmThread(token);
} catch {
// ignore bootstrap errors for now
}
if (isClientUser) {
try {
await chatApi.getMyDmThread(token);
} catch {
// ignore bootstrap errors for now
}
}
const data = await chatApi.getThreads(token);
setThreads(data);
setActiveThreadId((prev) => {
if (prev && data.some((thread) => thread.id === prev)) {
return prev;
}
const data = await chatApi.getThreads(token);
setThreads(data);
setActiveThreadId((prev) => {
if (prev && data.some((thread) => thread.id === prev)) {
return prev;
}
return data.length > 0 ? data[0].id : null;
});
} catch (error) {
log.warn("Failed to refresh chat threads", { error });
} finally {
if (showLoader) {
setLoadingThreads(false);
}
refreshingThreadsRef.current = false;
}
},
[isSignedIn, isClientUser],
);
return data.length > 0 ? data[0].id : null;
});
} catch (error) {
log.warn("Failed to refresh chat threads", { error });
} finally {
setLoadingThreads(false);
}
}, [getToken, isSignedIn, isClientUser]);
const refreshMessages = useCallback(
async (threadId: string, showLoader = true) => {
async (threadId: string) => {
if (!isSignedIn) {
return;
}
if (refreshingMessagesRef.current[threadId]) {
return;
}
refreshingMessagesRef.current[threadId] = true;
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
refreshingMessagesRef.current[threadId] = false;
return;
}
try {
if (showLoader) {
setLoadingMessages(true);
}
setLoadingMessages(true);
const response = await chatApi.getThreadMessages(threadId, token);
setMessagesByThreadId((prev) => ({
...prev,
@ -207,13 +172,10 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
} catch (error) {
log.warn("Failed to refresh chat messages", { threadId, error });
} finally {
if (showLoader) {
setLoadingMessages(false);
}
refreshingMessagesRef.current[threadId] = false;
setLoadingMessages(false);
}
},
[isSignedIn],
[getToken, isSignedIn],
);
const loadOlderMessages = useCallback(
@ -231,7 +193,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
return;
}
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
return;
}
@ -273,7 +235,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
setLoadingOlderByThreadId((prev) => ({ ...prev, [threadId]: false }));
}
},
[isSignedIn, loadingOlderByThreadId, nextCursorByThreadId],
[getToken, isSignedIn, loadingOlderByThreadId, nextCursorByThreadId],
);
const markThreadRead = useCallback(
@ -282,7 +244,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
return;
}
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
return;
}
@ -309,7 +271,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
log.warn("Failed to mark thread read", { threadId, error });
}
},
[isSignedIn, user?.id],
[getToken, isSignedIn, user?.id],
);
useEffect(() => {
@ -339,7 +301,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
return;
}
const token = await getTokenRef.current();
const token = await getToken();
if (!token) {
return;
}
@ -398,7 +360,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
throw error;
}
},
[isSignedIn, socketConnected, user?.id],
[getToken, isSignedIn, socketConnected, user?.id],
);
useEffect(() => {
@ -410,7 +372,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
let mounted = true;
const setupSocket = async () => {
const token = await getTokenRef.current();
const token = await getToken();
if (!token || !mounted) {
return;
}
@ -420,7 +382,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
socket.on("connect", () => {
setSocketConnected(true);
void refreshThreadsRef.current(false);
void refreshThreadsRef.current();
if (activeThreadIdRef.current) {
socket.emit("chat:subscribe", {
@ -432,7 +394,6 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
socket.on("disconnect", () => {
setSocketConnected(false);
setTypingByThreadId({});
subscribedThreadIdsRef.current.clear();
});
socket.on(
@ -499,8 +460,6 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
: thread,
),
);
} else if (event.threadId !== activeThreadIdRef.current) {
void refreshThreadsRef.current(false);
}
},
);
@ -564,7 +523,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
},
}));
void refreshThreadsRef.current(false);
void refreshThreadsRef.current();
},
);
@ -616,14 +575,14 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
socketRef.current = null;
}
};
}, [clearAll, isSignedIn]);
}, [clearAll, getToken, isSignedIn]);
useEffect(() => {
if (!isSignedIn) {
return;
}
void refreshThreadsRef.current(true);
void refreshThreadsRef.current();
}, [isSignedIn, userRole]);
useEffect(() => {
@ -635,7 +594,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
socketRef.current.emit("chat:subscribe", { threadId: activeThreadId });
}
void refreshMessagesRef.current(activeThreadId, true);
void refreshMessagesRef.current(activeThreadId);
setThreads((prev) =>
prev.map((thread) =>
@ -652,46 +611,6 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
};
}, [activeThreadId, socketConnected]);
useEffect(() => {
if (!socketRef.current || !socketConnected) {
return;
}
const socket = socketRef.current;
const nextIds = new Set(threads.map((thread) => thread.id));
threads.forEach((thread) => {
if (!subscribedThreadIdsRef.current.has(thread.id)) {
socket.emit("chat:subscribe", { threadId: thread.id });
subscribedThreadIdsRef.current.add(thread.id);
}
});
Array.from(subscribedThreadIdsRef.current).forEach((threadId) => {
if (!nextIds.has(threadId)) {
socket.emit("chat:unsubscribe", { threadId });
subscribedThreadIdsRef.current.delete(threadId);
}
});
}, [socketConnected, threads]);
useEffect(() => {
if (!isSignedIn || socketConnected) {
return;
}
const interval = setInterval(() => {
void refreshThreadsRef.current(false);
if (activeThreadIdRef.current) {
void refreshMessagesRef.current(activeThreadIdRef.current, false);
}
}, 5000);
return () => {
clearInterval(interval);
};
}, [isSignedIn, socketConnected]);
useEffect(() => {
if (!activeThreadId) {
return;