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
+5 -4
View File
@@ -45,6 +45,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
const openImagePreview = useUIStore((s) => s.openImagePreview);
const openUserProfile = useUIStore((s) => s.openUserProfile);
const channelKey = message.channelId || (message as any).dmChannelId;
const isAuthor = currentUser?.id === message.userId;
const memberRole = members.find(m => m.userId === currentUser?.id)?.role;
const isAdminUser = memberRole === 'admin' || memberRole === 'owner';
@@ -99,7 +100,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
if (canDelete) {
contextMenuItems.push({
label: 'Delete Message',
onClick: () => deleteMessage(message.id),
onClick: () => deleteMessage(message.id, channelKey),
danger: true,
});
}
@@ -108,7 +109,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (editContent.trim()) {
await editMessage(message.id, editContent.trim());
await editMessage(message.id, editContent.trim(), channelKey);
setIsEditing(false);
}
}
@@ -216,7 +217,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
escape to <button onClick={() => setIsEditing(false)} className="text-discord-text-link hover:underline">cancel</button>
{' '}&bull; enter to <button onClick={() => {
if (editContent.trim()) {
editMessage(message.id, editContent.trim());
editMessage(message.id, editContent.trim(), channelKey);
setIsEditing(false);
}
}} className="text-discord-text-link hover:underline">save</button>
@@ -365,7 +366,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
)}
{canDelete && (
<button
onClick={() => deleteMessage(message.id)}
onClick={() => deleteMessage(message.id, channelKey)}
className="px-2 h-full text-discord-text-muted hover:text-discord-red hover:bg-discord-modifier-hover transition-all flex items-center justify-center"
title="Delete"
>