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:
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { Tooltip } from '../ui/Tooltip';
|
||||
|
||||
@@ -12,15 +13,17 @@ interface SidebarItemProps {
|
||||
onClick: () => void;
|
||||
type?: 'server' | 'dm' | 'action';
|
||||
actionType?: 'add' | 'join';
|
||||
hasUnread?: boolean;
|
||||
}
|
||||
|
||||
function SidebarItem({ name, icon, active, onClick, type = 'server', actionType }: SidebarItemProps) {
|
||||
function SidebarItem({ name, icon, active, onClick, type = 'server', actionType, hasUnread }: SidebarItemProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const firstLetter = name.charAt(0).toUpperCase();
|
||||
|
||||
const getPillHeight = () => {
|
||||
if (active) return 'h-10';
|
||||
if (isHovered) return 'h-5';
|
||||
if (hasUnread && !active) return 'h-2';
|
||||
return 'h-2 scale-0';
|
||||
};
|
||||
|
||||
@@ -88,11 +91,32 @@ export function ServerSidebar() {
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
|
||||
const channelToServerMap = useServerStore((s) => s.channelToServerMap);
|
||||
const dmChannels = useServerStore((s) => s.dmChannels);
|
||||
const showDms = useUIStore((s) => s.showDms);
|
||||
const setShowDms = useUIStore((s) => s.setShowDms);
|
||||
const openModal = useUIStore((s) => s.openModal);
|
||||
const unreadChannels = useChatStore((s) => s.unreadChannels);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Compute which servers have unread channels
|
||||
const unreadServerIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const channelId of unreadChannels) {
|
||||
const serverId = channelToServerMap.get(channelId);
|
||||
if (serverId) ids.add(serverId);
|
||||
}
|
||||
return ids;
|
||||
}, [unreadChannels, channelToServerMap]);
|
||||
|
||||
// Check if any DM channels are unread
|
||||
const hasDmUnread = useMemo(() => {
|
||||
for (const dm of dmChannels) {
|
||||
if (unreadChannels.has(dm.id)) return true;
|
||||
}
|
||||
return false;
|
||||
}, [unreadChannels, dmChannels]);
|
||||
|
||||
const handleServerClick = (serverId: string) => {
|
||||
setCurrentServer(serverId);
|
||||
setShowDms(false);
|
||||
@@ -106,13 +130,14 @@ export function ServerSidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="w-[72px] bg-discord-bg-tertiary flex flex-col items-center py-3 overflow-y-auto flex-shrink-0 no-scrollbar select-none">
|
||||
<nav className="w-[72px] bg-discord-bg-server flex flex-col items-center py-3 overflow-y-auto flex-shrink-0 no-scrollbar select-none">
|
||||
<SidebarItem
|
||||
id="@me"
|
||||
name="Direct Messages"
|
||||
active={showDms}
|
||||
onClick={handleDmClick}
|
||||
type="dm"
|
||||
hasUnread={hasDmUnread}
|
||||
/>
|
||||
|
||||
<div className="w-8 h-[2px] bg-discord-modifier-accent rounded-full mb-2" />
|
||||
@@ -125,6 +150,7 @@ export function ServerSidebar() {
|
||||
icon={server.icon}
|
||||
active={currentServerId === server.id}
|
||||
onClick={() => handleServerClick(server.id)}
|
||||
hasUnread={unreadServerIds.has(server.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user