feat: unread indicators + DM bug fixes + data-driven isDmChannel

- Fix stale message cache: add force param to loadMessages, clearAllMessages action
- Fix reload race condition: URL-based isDmChannel fallback before WS ready
- Add read_states DB table for persistent unread tracking
- Add channel_ack WS event (client→server→echo) with BigInt comparison
- Wire up unread state in chatStore (readStates, unreadChannels, ackChannel)
- Auto-ack channels on MessageList view (200ms debounced)
- Unread pill indicators on server icons in ServerSidebar
- Bold text + white dot on unread channels/DMs in ChannelSidebar
- Replace all showDms reads with data-driven isDmChannel() across 8 files
- Design system, UI polish, and component fixes from previous sessions
This commit is contained in:
Jannis Braun
2026-02-18 20:48:48 +01:00
parent 7168149d98
commit 435d12e5b8
50 changed files with 711 additions and 285 deletions
+48
View File
@@ -141,6 +141,9 @@ export function handleClientEvent(
case 'reaction_remove':
handleReactionRemove(event, userId);
break;
case 'channel_ack':
handleChannelAck(event, userId);
break;
default:
connectionManager.sendToUser(userId, {
type: 'error',
@@ -677,3 +680,48 @@ function handleReactionRemove(event: Record<string, unknown>, userId: string): v
});
}
}
function handleChannelAck(event: Record<string, unknown>, userId: string): void {
const channelId = event.channelId as string;
const messageId = event.messageId as string;
if (!channelId || !messageId) return;
const db = getDb();
const existing = db.select()
.from(schema.readStates)
.where(and(
eq(schema.readStates.userId, userId),
eq(schema.readStates.channelId, channelId),
))
.get();
const now = Date.now();
if (existing) {
// Only update if the new messageId is newer (larger snowflake)
if (BigInt(messageId) > BigInt(existing.lastReadMessageId)) {
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();
}
// Echo ack back to all of this user's connections (multi-tab sync)
connectionManager.sendToUser(userId, {
type: 'channel_ack',
channelId,
messageId,
});
}