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:
@@ -19,6 +19,9 @@ export interface WsClient {
|
||||
pongReceived?: boolean;
|
||||
/** Whether this client currently has the chatbox open (is reading chat). */
|
||||
chatOpen?: boolean;
|
||||
/** Timestamp (ms) of the client's last "typing" signal; undefined when not
|
||||
* composing. Entries older than TYPING_TTL are treated as stale. */
|
||||
typingAt?: number;
|
||||
}
|
||||
|
||||
const clients = new Set<WsClient>();
|
||||
@@ -233,6 +236,64 @@ export function broadcastChatMessageDeleted(id: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Typing indicator: a client's "typing" signal is considered live for this long
|
||||
// after its last refresh. Must exceed the client's typing-ping cadence so a
|
||||
// steady typist never flickers out between pings.
|
||||
const TYPING_TTL = 6_000;
|
||||
|
||||
// Deduped set of users currently composing a chat message (typing signal within
|
||||
// the TTL), shaped like OnlineUser so the client renders the same avatars.
|
||||
export function getTypingUsers(): OnlineUser[] {
|
||||
const now = Date.now();
|
||||
const seen = new Map<string, OnlineUser>();
|
||||
for (const client of clients) {
|
||||
if (
|
||||
client.userId && client.typingAt && now - client.typingAt < TYPING_TTL &&
|
||||
!seen.has(client.userId)
|
||||
) {
|
||||
seen.set(client.userId, {
|
||||
userId: client.userId,
|
||||
username: client.username!,
|
||||
hasAvatar: !!client.avatarMime,
|
||||
avatarVersion: client.avatarVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
export function broadcastTyping(): void {
|
||||
const users = getTypingUsers();
|
||||
for (const client of clients) {
|
||||
send(client.socket, { type: "chat_typing_update", users });
|
||||
}
|
||||
}
|
||||
|
||||
// Record (or clear) a client's typing state and tell everyone. The client also
|
||||
// resends "true" periodically while composing; the sweep below expires it if
|
||||
// they go quiet without a clean "false" (e.g. they closed the tab).
|
||||
export function setClientTyping(client: WsClient, typing: boolean): void {
|
||||
const wasTyping = !!client.typingAt;
|
||||
client.typingAt = typing ? Date.now() : undefined;
|
||||
// Only rebroadcast on an actual edge to avoid a flood of identical updates
|
||||
// from the periodic typing pings.
|
||||
if (typing !== wasTyping) broadcastTyping();
|
||||
}
|
||||
|
||||
// Sweep stale typing entries (clients that stopped pinging without a clean
|
||||
// stop) and rebroadcast when the visible set shrinks.
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
for (const client of clients) {
|
||||
if (client.typingAt && now - client.typingAt >= TYPING_TTL) {
|
||||
client.typingAt = undefined;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) broadcastTyping();
|
||||
}, 2_000);
|
||||
|
||||
// True if the user has the chatbox open on any of their connected clients — i.e.
|
||||
// they're already reading chat, so a mention notification would be redundant.
|
||||
export function isUserChatOpen(userId: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user