diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index 08abe14a..171ad162 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -8,6 +8,7 @@ import { useSettingsStore } from '../stores/settingsStore'; import type { ServerEvent, ClientEvent, ActiveCallInfo, Activity } from '@backspace/shared'; import { resolveAssetUrl, normalizeUserAssets, normalizeMessageAssets } from '../utils/assetUrls'; import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../utils/voice'; +import { sortDmChannels } from '../utils/dmSorting'; import { registerSelfId } from '../utils/identity'; import { getActiveRoom } from './useLiveKit'; import { useUIStore } from '../stores/uiStore'; @@ -561,8 +562,8 @@ function handleEvent(origin: string, event: ServerEvent): void { const updatedDms = currentDmChannels.map(dm => dm.id === existingDm.id ? { ...dm, lastMessage: event.message } : dm, ); - updatedDms.sort((a, b) => (b.lastMessage?.createdAt ?? b.createdAt) - (a.lastMessage?.createdAt ?? a.createdAt)); - setDms(updatedDms); + const { unreadChannels, currentChannelId } = useChatStore.getState(); + setDms(sortDmChannels(updatedDms, unreadChannels, currentChannelId)); break; } } @@ -582,12 +583,8 @@ function handleEvent(origin: string, event: ServerEvent): void { ? { ...dm, lastMessage: event.message } : dm ); - updatedDms.sort((a, b) => { - const aTime = a.lastMessage?.createdAt ?? a.createdAt; - const bTime = b.lastMessage?.createdAt ?? b.createdAt; - return bTime - aTime; - }); - setDms(updatedDms); + const { unreadChannels: unread, currentChannelId: curCh } = useChatStore.getState(); + setDms(sortDmChannels(updatedDms, unread, curCh)); } { const { currentChannelId, markChannelUnread } = useChatStore.getState(); diff --git a/packages/web/src/stores/chatStore.ts b/packages/web/src/stores/chatStore.ts index 657568ee..d95cb43a 100644 --- a/packages/web/src/stores/chatStore.ts +++ b/packages/web/src/stores/chatStore.ts @@ -4,6 +4,7 @@ import { wsSend } from '../hooks/useWebSocket'; import { isDmChannel, getChannelOrigin, getApiForOrigin, useSpaceStore } from './spaceStore'; import { useAuthStore } from './authStore'; import { normalizeMessageAssets } from '../utils/assetUrls'; +import { sortDmChannels } from '../utils/dmSorting'; const MAX_MESSAGES_PER_CHANNEL = 200; const MAX_CACHED_CHANNELS = 20; @@ -285,12 +286,7 @@ export const useChatStore = create((set, get) => ({ ? { ...dm, lastMessage: { id: tempId, dmChannelId: channelId, userId: currentUser.id, content, createdAt: Date.now() } } : dm ); - updatedDms.sort((a, b) => { - const aTime = a.lastMessage?.createdAt ?? a.createdAt; - const bTime = b.lastMessage?.createdAt ?? b.createdAt; - return bTime - aTime; - }); - setDmChannels(updatedDms); + setDmChannels(sortDmChannels(updatedDms, get().unreadChannels, get().currentChannelId)); } } @@ -589,6 +585,13 @@ export const useChatStore = create((set, get) => ({ } set({ readStates: rsMap, unreadChannels: unread }); + + // Re-sort DM list now that unread state is known (handles initial load + // where populateFromReady runs before read states are processed) + const { dmChannels, setDmChannels } = useSpaceStore.getState(); + if (dmChannels.length > 0) { + setDmChannels(sortDmChannels(dmChannels, unread, currentChannelId)); + } }, markChannelUnread: (channelId: string) => { @@ -652,6 +655,12 @@ export const useChatStore = create((set, get) => ({ return { readStates: newReadStates, unreadChannels: newUnread }; }); + // Re-sort DM list when a DM is marked as read (moves from unread to read group) + if (isDmChannel(channelId)) { + const { dmChannels, setDmChannels } = useSpaceStore.getState(); + setDmChannels(sortDmChannels(dmChannels, get().unreadChannels, channelId)); + } + // Send to the correct instance const origin = getChannelOrigin(channelId); wsSend({ type: 'channel_ack', channelId, messageId }, origin); diff --git a/packages/web/src/stores/spaceStore.ts b/packages/web/src/stores/spaceStore.ts index c09eeba7..27284a83 100644 --- a/packages/web/src/stores/spaceStore.ts +++ b/packages/web/src/stores/spaceStore.ts @@ -3,6 +3,7 @@ import type { Space, Channel, ChannelCategory, MemberWithUser, SpaceWithChannels import { api, BackspaceApiClient } from '../api/client'; import { resolveAssetUrl, normalizeUserAssets } from '../utils/assetUrls'; import { isSelf } from '../utils/identity'; +import { sortDmChannels } from '../utils/dmSorting'; import { useAuthStore } from './authStore'; import { useChatStore } from './chatStore'; @@ -670,9 +671,15 @@ export const useSpaceStore = create((set, get) => ({ }); const mergedDms = [...existingDmsFromOtherOrigins, ...incomingDms]; + // Sort DMs using unread-first ordering. On initial load, unreadChannels may + // still be empty (read states are processed after populateFromReady); the + // safety-net re-sort in setReadStates handles that case. + const { unreadChannels, currentChannelId } = useChatStore.getState(); + const sortedDms = sortDmChannels(mergedDms, unreadChannels, currentChannelId); + const update: Partial = { spaces: mergedSpaces, - dmChannels: mergedDms, + dmChannels: sortedDms, channelToSpaceMap, channelLastMessageIds, spacePermissions,