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:
@@ -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) {
|
||||||
|
|||||||
@@ -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(() => {});
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
Reference in New Issue
Block a user