feat: add dm_typing_stop event to clear typing indicator on message send

Broadcasts dm_typing_stop to DM members before dm_message_created,
so the typing indicator clears immediately when a message arrives
instead of lingering for up to 3 seconds after delivery.
This commit is contained in:
Jannis Braun
2026-04-01 12:50:03 +02:00
parent 3464e9d991
commit 187f71e68e
4 changed files with 26 additions and 1 deletions
+11
View File
@@ -180,6 +180,17 @@ export function broadcastDmMessage(dmChannelId: string, message: DmMessageWithUs
.where(eq(schema.dmMembers.dmChannelId, dmChannelId)) .where(eq(schema.dmMembers.dmChannelId, dmChannelId))
.all(); .all();
// Clear typing indicator for the message author — the message itself proves they stopped
for (const member of dmMembers) {
if (member.userId !== message.userId) {
connectionManager.sendToUser(member.userId, {
type: 'dm_typing_stop',
dmChannelId,
userId: message.userId,
});
}
}
for (const member of dmMembers) { for (const member of dmMembers) {
// If this member had closed the DM, resurface it first // If this member had closed the DM, resurface it first
if (member.closed === 1) { if (member.closed === 1) {
+8
View File
@@ -893,6 +893,14 @@ function handleDmMessageCreate(event: Record<string, unknown>, userId: string):
// Federation: queue for relay // Federation: queue for relay
queueDmRelay(dmMessage, dmChannelId, 'create'); queueDmRelay(dmMessage, dmChannelId, 'create');
// Clear any pending typing timeout for this user+channel
const typingKey = `dm:${userId}:${dmChannelId}`;
const existingTimeout = typingTimeouts.get(typingKey);
if (existingTimeout) {
clearTimeout(existingTimeout);
typingTimeouts.delete(typingKey);
}
// Resolve embeds asynchronously // Resolve embeds asynchronously
setImmediate(() => { setImmediate(() => {
resolveEmbeds(messageId, hasContent ? content!.trim() : null, dmChannelId, true, null).catch(() => {}); resolveEmbeds(messageId, hasContent ? content!.trim() : null, dmChannelId, true, null).catch(() => {});
+1
View File
@@ -378,6 +378,7 @@ export type ServerEvent =
| { type: 'dm_message_updated'; message: DmMessageWithUser } | { type: 'dm_message_updated'; message: DmMessageWithUser }
| { type: 'dm_message_deleted'; messageId: string; dmChannelId: string } | { type: 'dm_message_deleted'; messageId: string; dmChannelId: string }
| { type: 'dm_typing'; dmChannelId: string; userId: string; username: string } | { type: 'dm_typing'; dmChannelId: string; userId: string; username: string }
| { type: 'dm_typing_stop'; dmChannelId: string; userId: string }
| { type: 'reaction_added'; messageId: string; reaction: Reaction } | { type: 'reaction_added'; messageId: string; reaction: Reaction }
| { type: 'reaction_removed'; messageId: string; userId: string; emoji: string } | { type: 'reaction_removed'; messageId: string; userId: string; emoji: string }
| { type: 'channel_ack'; channelId: string; messageId: string } | { type: 'channel_ack'; channelId: string; messageId: string }
+6 -1
View File
@@ -87,7 +87,7 @@ function handleEvent(origin: string, event: ServerEvent): void {
const isHome = origin === HOME_ORIGIN; const isHome = origin === HOME_ORIGIN;
const { setUser } = useAuthStore.getState(); const { setUser } = useAuthStore.getState();
const { populateFromReady, loadSpaceDetail, currentSpaceId, updateMemberPresence, addMember, removeMember, addDmChannel, removeDmChannel } = useSpaceStore.getState(); const { populateFromReady, loadSpaceDetail, currentSpaceId, updateMemberPresence, addMember, removeMember, addDmChannel, removeDmChannel } = useSpaceStore.getState();
const { addMessage, addRealtimeMessage, updateMessage, removeMessage, setTyping, onReactionAdded, onReactionRemoved } = useChatStore.getState(); const { addMessage, addRealtimeMessage, updateMessage, removeMessage, setTyping, clearTyping, onReactionAdded, onReactionRemoved } = useChatStore.getState();
const { addVoiceUser, removeVoiceUser, clearVoiceUsersForOrigin, setVoiceUsers, setVoiceUserStatus, clearVoiceUserStatus } = useVoiceStore.getState(); const { addVoiceUser, removeVoiceUser, clearVoiceUsersForOrigin, setVoiceUsers, setVoiceUserStatus, clearVoiceUserStatus } = useVoiceStore.getState();
switch (event.type) { switch (event.type) {
@@ -680,6 +680,11 @@ function handleEvent(origin: string, event: ServerEvent): void {
break; break;
} }
case 'dm_typing_stop': {
clearTyping(event.dmChannelId as string, event.userId as string);
break;
}
// ─── Reactions (all origins) ──────────────────────────────────────────── // ─── Reactions (all origins) ────────────────────────────────────────────
case 'reaction_added': case 'reaction_added':