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
+8
View File
@@ -143,6 +143,14 @@ function createTables(db: Database.Database): void {
PRIMARY KEY (server_id, user_id, role_id)
);
CREATE TABLE IF NOT EXISTS read_states (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
channel_id TEXT NOT NULL,
last_read_message_id TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (user_id, channel_id)
);
CREATE TABLE IF NOT EXISTS server_folders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+9
View File
@@ -138,6 +138,15 @@ export const memberRoles = sqliteTable('member_roles', {
pk: primaryKey({ columns: [table.serverId, table.userId, table.roleId] }),
}));
export const readStates = sqliteTable('read_states', {
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
channelId: text('channel_id').notNull(),
lastReadMessageId: text('last_read_message_id').notNull(),
updatedAt: integer('updated_at').notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.channelId] }),
}));
export const serverFolders = sqliteTable('server_folders', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),