v3: chatbox improvements (typing indicators, answer to previous messages, backlog days separators, scroll to last message position, visual tweaks)
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s

This commit is contained in:
2026-06-30 12:27:03 +02:00
parent 124866ad4f
commit d2d086831e
17 changed files with 857 additions and 86 deletions

View File

@@ -65,6 +65,9 @@ export interface WSContextValue {
* filter them out. */
deletedChatIds: Set<string>;
unreadChatCount: number;
/** Users currently composing a chat message (excludes nobody — the chatbox
* filters out the current user itself). Shaped like presence for avatars. */
typingUsers: OnlineUser[];
/** Increments on each WS reconnect so pages can backfill missed events. */
connectionEpoch: number;
castVote: (dumpId: string) => void;
@@ -73,10 +76,13 @@ export interface WSContextValue {
removeCommentLike: (commentId: string) => void;
injectDump: (dump: Dump) => void;
clearUnreadNotifications: () => void;
sendChatMessage: (body: string) => void;
sendChatMessage: (body: string, replyToId?: string) => void;
editChatMessage: (id: string, body: string) => void;
deleteChatMessage: (id: string) => void;
clearUnreadChat: () => void;
/** Inform the server whether the user is currently composing a message, so it
* can show/hide the "is typing" indicator to other clients. */
setChatTyping: (typing: boolean) => void;
/** Inform the server whether the chatbox is open (suppresses redundant
* mention notifications while the user is reading chat). */
setChatOpen: (open: boolean) => void;
@@ -104,6 +110,7 @@ export const WSContext = createContext<WSContextValue>({
chatMessages: [],
deletedChatIds: new Set(),
unreadChatCount: 0,
typingUsers: [],
connectionEpoch: 0,
castVote: () => {},
removeVote: () => {},
@@ -115,5 +122,6 @@ export const WSContext = createContext<WSContextValue>({
editChatMessage: () => {},
deleteChatMessage: () => {},
clearUnreadChat: () => {},
setChatTyping: () => {},
setChatOpen: () => {},
});

View File

@@ -119,6 +119,7 @@ export function WSProvider({ children }: WSProviderProps) {
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [deletedChatIds, setDeletedChatIds] = useState<Set<string>>(new Set());
const [unreadChatCount, setUnreadChatCount] = useState(0);
const [typingUsers, setTypingUsers] = useState<OnlineUser[]>([]);
// Live avatar overrides for site-wide Avatar refresh (own narrow context).
const [avatarOverrides, setAvatarOverrides] = useState<
Record<string, AvatarOverride>
@@ -156,6 +157,7 @@ export function WSProvider({ children }: WSProviderProps) {
setChatMessages([]);
setDeletedChatIds(new Set());
setUnreadChatCount(0);
setTypingUsers([]);
setAvatarOverrides({});
}
@@ -529,6 +531,10 @@ export function WSProvider({ children }: WSProviderProps) {
break;
}
case "chat_typing_update":
setTypingUsers(msg.users);
break;
case "force_logout":
onForceLogoutRef.current();
break;
@@ -759,13 +765,17 @@ export function WSProvider({ children }: WSProviderProps) {
setUnreadNotificationCount(0);
}, []);
const sendChatMessage = useCallback((body: string) => {
const sendChatMessage = useCallback((body: string, replyToId?: string) => {
const trimmed = body.trim();
if (!trimmed) return;
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(
JSON.stringify(
{ type: "chat_send", body: trimmed } satisfies OutgoingWSMessage,
{
type: "chat_send",
body: trimmed,
...(replyToId ? { replyToId } : {}),
} satisfies OutgoingWSMessage,
),
);
}
@@ -797,6 +807,16 @@ export function WSProvider({ children }: WSProviderProps) {
setUnreadChatCount(0);
}, []);
const setChatTyping = useCallback((typing: boolean) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(
JSON.stringify(
{ type: "chat_typing", typing } satisfies OutgoingWSMessage,
),
);
}
}, []);
const setChatOpen = useCallback((open: boolean) => {
chatOpenRef.current = open;
if (socketRef.current?.readyState === WebSocket.OPEN) {
@@ -830,6 +850,7 @@ export function WSProvider({ children }: WSProviderProps) {
chatMessages,
deletedChatIds,
unreadChatCount,
typingUsers,
connectionEpoch,
castVote,
removeVote,
@@ -841,6 +862,7 @@ export function WSProvider({ children }: WSProviderProps) {
editChatMessage,
deleteChatMessage,
clearUnreadChat,
setChatTyping,
setChatOpen,
}), [
wsStatus,
@@ -864,6 +886,7 @@ export function WSProvider({ children }: WSProviderProps) {
chatMessages,
deletedChatIds,
unreadChatCount,
typingUsers,
connectionEpoch,
castVote,
removeVote,
@@ -875,6 +898,7 @@ export function WSProvider({ children }: WSProviderProps) {
editChatMessage,
deleteChatMessage,
clearUnreadChat,
setChatTyping,
setChatOpen,
]);