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
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
This commit is contained in:
@@ -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: () => {},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user