fix: prevent duplicate DMs at creation time, clean up corrupted read states
Replace the unreliable client-side DM dedup loop in populateFromReady with a creation-time guard (findExistingDmForUser) that checks all instances before opening a new DM. Guards added to FriendsPage, NewDmModal, and UserProfilePopout. Also fixes: corrupted read_states from temp_ optimistic message IDs (server migration + client-side validation), federation-aware closeDm/addDmMember API routing, isSelf-based DM member filtering in sidebar/header, and WS event error isolation.
This commit is contained in:
@@ -478,7 +478,16 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
const unread = new Set<string>();
|
||||
for (const [channelId, lastMsgId] of channelLastMessageIds) {
|
||||
const lastRead = rsMap.get(channelId);
|
||||
if (!lastRead || BigInt(lastMsgId) > BigInt(lastRead)) {
|
||||
if (!lastRead) {
|
||||
unread.add(channelId);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (BigInt(lastMsgId) > BigInt(lastRead)) {
|
||||
unread.add(channelId);
|
||||
}
|
||||
} catch {
|
||||
// Corrupted read state (e.g. temp_ ID) — treat as unread
|
||||
unread.add(channelId);
|
||||
}
|
||||
}
|
||||
@@ -500,6 +509,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
const lastMsg = msgs[msgs.length - 1];
|
||||
if (!lastMsg) return;
|
||||
const messageId = lastMsg.id;
|
||||
// Don't ack optimistic/temp messages — wait for the real server ID
|
||||
if (messageId.startsWith('temp_')) return;
|
||||
|
||||
// Update local state immediately
|
||||
set((state) => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { create } from 'zustand';
|
||||
import type { Server, Channel, MemberWithUser, ServerWithChannelsAndMembers, Role, ServerFolder, DmChannel, User, UpdateServerRequest } from '@backspace/shared';
|
||||
import { api, BackspaceApiClient } from '../api/client';
|
||||
import { resolveAssetUrl, normalizeUserAssets } from '../utils/assetUrls';
|
||||
import { isSelf } from '../utils/identity';
|
||||
import { useAuthStore } from './authStore';
|
||||
|
||||
// ─── Instance-aware types ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -63,6 +65,7 @@ interface ServerState {
|
||||
populateFromReady: (origin: string, servers: ServerWithChannelsAndMembers[], folders?: ServerFolder[], dmChannels?: DmChannel[]) => void;
|
||||
addServerFromReady: (origin: string, server: ServerWithChannelsAndMembers) => void;
|
||||
removeInstanceServers: (origin: string) => void;
|
||||
findExistingDmForUser: (targetUser: { id: string; homeUserId?: string | null }) => { dm: DmChannel; origin: string } | null;
|
||||
}
|
||||
|
||||
export const useServerStore = create<ServerState>((set, get) => ({
|
||||
@@ -118,7 +121,9 @@ export const useServerStore = create<ServerState>((set, get) => ({
|
||||
})),
|
||||
|
||||
closeDm: async (id) => {
|
||||
await api.dm.close(id);
|
||||
const origin = get().channelOriginMap.get(id) || '';
|
||||
const targetApi = getApiForOrigin(origin);
|
||||
await targetApi.dm.close(id);
|
||||
set((state) => ({
|
||||
dmChannels: state.dmChannels.filter(c => c.id !== id)
|
||||
}));
|
||||
@@ -442,6 +447,25 @@ export const useServerStore = create<ServerState>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
|
||||
findExistingDmForUser: (targetUser) => {
|
||||
const { dmChannels, channelOriginMap } = get();
|
||||
const me = useAuthStore.getState().user;
|
||||
if (!me) return null;
|
||||
|
||||
const targetHomeId = targetUser.homeUserId || targetUser.id;
|
||||
|
||||
for (const dm of dmChannels) {
|
||||
if (dm.members.length !== 2) continue;
|
||||
const other = dm.members.find(m => !isSelf(m, me));
|
||||
if (!other) continue;
|
||||
const otherHomeId = other.homeUserId || other.id;
|
||||
if (otherHomeId === targetHomeId) {
|
||||
return { dm, origin: channelOriginMap.get(dm.id) || '' };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
removeInstanceServers: (origin: string) => {
|
||||
set((state) => {
|
||||
const remainingServers = state.servers.filter(s => s._instanceOrigin !== origin);
|
||||
|
||||
Reference in New Issue
Block a user