diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index 44171d64..a4cf5528 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -168,6 +168,9 @@ export function handleClientEvent( case 'channel_ack': handleChannelAck(event, userId); break; + case 'mark_unread': + handleMarkUnread(event, userId); + break; case 'dm_call_start': handleDmCallStart(event, userId, username); break; @@ -1096,6 +1099,67 @@ function handleChannelAck(event: Record, userId: string): void }); } +function handleMarkUnread(event: Record, userId: string): void { + const channelId = event.channelId as string; + const messageId = event.messageId as string; + if (!channelId || !messageId) return; + if (!/^\d+$/.test(messageId) && messageId !== '0') return; + + // Validate channel membership + const spaceId = getChannelSpaceId(channelId); + if (spaceId) { + if (!isMember(spaceId, userId)) return; + } else { + if (!isDmMember(channelId, userId)) return; + } + + const db = getDb(); + const now = Date.now(); + + if (messageId === '0') { + // '0' sentinel: delete the read state entirely → channel appears fully unread + db.delete(schema.readStates) + .where(and( + eq(schema.readStates.userId, userId), + eq(schema.readStates.channelId, channelId), + )) + .run(); + } else { + // Set read state to the specified message (allows backward writes) + const existing = db.select() + .from(schema.readStates) + .where(and( + eq(schema.readStates.userId, userId), + eq(schema.readStates.channelId, channelId), + )) + .get(); + + if (existing) { + db.update(schema.readStates) + .set({ lastReadMessageId: messageId, updatedAt: now }) + .where(and( + eq(schema.readStates.userId, userId), + eq(schema.readStates.channelId, channelId), + )) + .run(); + } else { + db.insert(schema.readStates).values({ + userId, + channelId, + lastReadMessageId: messageId, + updatedAt: now, + }).run(); + } + } + + // Broadcast to all of this user's connections (multi-tab sync) + connectionManager.sendToUser(userId, { + type: 'mark_unread', + channelId, + messageId, + }); +} + // ─── DM Call Handlers (Unified Room API) ─────────────────────────────────── function handleDmCallStart(event: Record, userId: string, username: string): void {