fix(web): normalize conversation IDs to lowercase and bind DMChat messages selector
This commit is contained in:
@@ -16,9 +16,10 @@ function isSelfDM(conv: { members: { id: string }[] }, currentUserId: string) {
|
||||
|
||||
export function ConversationList() {
|
||||
const conversations = useConversationStore((s) => s.conversations);
|
||||
const activeId = useConversationStore((s) => s.activeConversationId);
|
||||
const activeId = useConversationStore((s) => s.activeConversationId?.toLowerCase() ?? null);
|
||||
const fetchConversations = useConversationStore((s) => s.fetchConversations);
|
||||
const createConversation = useConversationStore((s) => s.createConversation);
|
||||
const setActiveConversation = useConversationStore((s) => s.setActiveConversation);
|
||||
const messagesByConv = useConversationStore((s) => s.messagesByConversation);
|
||||
const currentUser = useAuthStore((s) => s.user);
|
||||
const hasConvUnread = useReadStatesStore((s) => s.hasConvUnread);
|
||||
@@ -31,8 +32,9 @@ export function ConversationList() {
|
||||
fetchConversations();
|
||||
}, [fetchConversations]);
|
||||
|
||||
const openConversation = (convId: string) => {
|
||||
// mark as read when opening
|
||||
const openConversation = (rawId: string) => {
|
||||
const convId = rawId.toLowerCase();
|
||||
setActiveConversation(convId);
|
||||
const msgs = messagesByConv[convId] || [];
|
||||
if (msgs.length > 0) {
|
||||
markConvRead(convId, msgs[msgs.length - 1].id);
|
||||
@@ -50,7 +52,7 @@ export function ConversationList() {
|
||||
try {
|
||||
const conv = await createConversation([]);
|
||||
if (conv) {
|
||||
navigate(`/dm/${conv.id}`);
|
||||
openConversation(conv.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to create notes:", err);
|
||||
@@ -59,7 +61,7 @@ export function ConversationList() {
|
||||
};
|
||||
|
||||
const getLatestMessageId = (convId: string): string | undefined => {
|
||||
const msgs = messagesByConv[convId] || [];
|
||||
const msgs = messagesByConv[convId.toLowerCase()] || [];
|
||||
return msgs.length > 0 ? msgs[msgs.length - 1].id : undefined;
|
||||
};
|
||||
|
||||
@@ -89,19 +91,20 @@ export function ConversationList() {
|
||||
<p className="text-gb-fg-f">[no conversations]</p>
|
||||
)}
|
||||
{conversations.map((conv) => {
|
||||
const convId = conv.id.toLowerCase();
|
||||
const self = isSelfDM(conv, currentUser?.id || "");
|
||||
const name = self
|
||||
? "Notes"
|
||||
: conv.type === "group_dm"
|
||||
? conv.name || conv.members.map((m) => m.username).join(", ")
|
||||
: otherMemberName(conv, currentUser?.id || "");
|
||||
const unread = hasConvUnread(conv.id, getLatestMessageId(conv.id));
|
||||
const unread = hasConvUnread(convId, getLatestMessageId(convId));
|
||||
return (
|
||||
<button
|
||||
key={conv.id}
|
||||
onClick={() => openConversation(conv.id)}
|
||||
className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${
|
||||
conv.id === activeId
|
||||
convId === activeId
|
||||
? "terminal-active"
|
||||
: "hover:bg-gb-bg-t text-gb-fg-s"
|
||||
}`}
|
||||
|
||||
@@ -152,7 +152,6 @@ export function DMChat() {
|
||||
const activeId = useConversationStore((s) => s.activeConversationId);
|
||||
const setActive = useConversationStore((s) => s.setActiveConversation);
|
||||
const conversations = useConversationStore((s) => s.conversations);
|
||||
const messagesByConv = useConversationStore((s) => s.messagesByConversation);
|
||||
const fetchMessages = useConversationStore((s) => s.fetchMessages);
|
||||
const fetchOlderMessages = useConversationStore((s) => s.fetchOlderMessages);
|
||||
const isLoadingOlder = useConversationStore((s) => s.isLoadingOlder);
|
||||
@@ -173,9 +172,13 @@ export function DMChat() {
|
||||
const markConvRead = useReadStatesStore((s) => s.markConvRead);
|
||||
const convStates = useReadStatesStore((s) => s.convStates);
|
||||
|
||||
const id = conversationId || activeId;
|
||||
const conversation = conversations.find((c) => c.id === id);
|
||||
const rawId = conversationId || activeId;
|
||||
const id = rawId ? rawId.toLowerCase() : undefined;
|
||||
const conversation = conversations.find((c) => c.id.toLowerCase() === id);
|
||||
const hasMore = useConversationStore((s) => id ? s.hasMoreByConversation[id] !== false : true);
|
||||
const messages = useConversationStore(
|
||||
useCallback((s) => (id ? s.messagesByConversation[id] || [] : []), [id])
|
||||
);
|
||||
|
||||
const handleAddReaction = useCallback(async (messageId: string, emoji: string) => {
|
||||
if (!id) return;
|
||||
@@ -200,7 +203,6 @@ export function DMChat() {
|
||||
});
|
||||
}
|
||||
}, [id, isLoadingOlder, hasMore, fetchOlderMessages]);
|
||||
const messages = id ? messagesByConv[id] || [] : [];
|
||||
|
||||
useEffect(() => {
|
||||
fetchConversations();
|
||||
|
||||
@@ -79,39 +79,42 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
|
||||
createConversation: async (userIds) => {
|
||||
const conversation = await api.post<Conversation>("/conversations", { user_ids: userIds });
|
||||
const convId = conversation.id.toLowerCase();
|
||||
const normalizedConv = { ...conversation, id: convId };
|
||||
set((state) => ({
|
||||
conversations: [conversation, ...state.conversations],
|
||||
activeConversationId: conversation.id,
|
||||
conversations: [normalizedConv, ...state.conversations],
|
||||
activeConversationId: convId,
|
||||
}));
|
||||
return conversation;
|
||||
return normalizedConv;
|
||||
},
|
||||
|
||||
setActiveConversation: (id) => set({ activeConversationId: id }),
|
||||
setActiveConversation: (id) => set({ activeConversationId: id ? id.toLowerCase() : null }),
|
||||
|
||||
fetchMessages: async (conversationId, before) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const params = before ? "?before=" + encodeURIComponent(before) : "";
|
||||
const messages = await api.get<ConversationMessage[]>(
|
||||
`/conversations/${conversationId}/messages${params}`,
|
||||
`/conversations/${convId}/messages${params}`,
|
||||
);
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
set((state) => {
|
||||
const existing = state.messagesByConversation[conversationId] || [];
|
||||
const existing = state.messagesByConversation[convId] || [];
|
||||
const map = new Map<string, ConversationMessage>();
|
||||
existing.forEach((m) => map.set(m.id, m));
|
||||
list.forEach((m) => map.set(m.id, m));
|
||||
list.forEach((m) => map.set(m.id, { ...m, conversation_id: (m.conversation_id || convId).toLowerCase() }));
|
||||
const merged = Array.from(map.values()).sort(
|
||||
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
|
||||
);
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: merged,
|
||||
[convId]: merged,
|
||||
},
|
||||
hasMoreByConversation: {
|
||||
...state.hasMoreByConversation,
|
||||
[conversationId]: list.length >= 50,
|
||||
[convId]: list.length >= 50,
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
@@ -122,32 +125,32 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
},
|
||||
|
||||
fetchOlderMessages: async (conversationId) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
const state = get();
|
||||
if (state.isLoadingOlder || state.hasMoreByConversation[conversationId] === false) return;
|
||||
const existing = state.messagesByConversation[conversationId] || [];
|
||||
if (state.isLoadingOlder || state.hasMoreByConversation[convId] === false) return;
|
||||
const existing = state.messagesByConversation[convId] || [];
|
||||
if (existing.length === 0) return;
|
||||
// ponytail: existing is now oldest-first, so existing[0] is the true oldest
|
||||
const oldestId = existing[0].id;
|
||||
set({ isLoadingOlder: true });
|
||||
try {
|
||||
const older = await api.get<ConversationMessage[]>(
|
||||
`/conversations/${conversationId}/messages?before=${encodeURIComponent(oldestId)}`,
|
||||
`/conversations/${convId}/messages?before=${encodeURIComponent(oldestId)}`,
|
||||
);
|
||||
const list = Array.isArray(older) ? older : [];
|
||||
set((state) => {
|
||||
const map = new Map<string, ConversationMessage>();
|
||||
[...list, ...existing].forEach((m) => map.set(m.id, m));
|
||||
[...list, ...existing].forEach((m) => map.set(m.id, { ...m, conversation_id: (m.conversation_id || convId).toLowerCase() }));
|
||||
const merged = Array.from(map.values()).sort(
|
||||
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
|
||||
);
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: merged,
|
||||
[convId]: merged,
|
||||
},
|
||||
hasMoreByConversation: {
|
||||
...state.hasMoreByConversation,
|
||||
[conversationId]: list.length >= 50,
|
||||
[convId]: list.length >= 50,
|
||||
},
|
||||
isLoadingOlder: false,
|
||||
};
|
||||
@@ -158,58 +161,65 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
},
|
||||
|
||||
sendMessage: async (conversationId, content) => {
|
||||
// Add locally so the message appears immediately even if WS lags.
|
||||
// addMessage dedupes, so a late WS event won't double it.
|
||||
const convId = conversationId.toLowerCase();
|
||||
const message = await api.post<ConversationMessage>(
|
||||
`/conversations/${conversationId}/messages`,
|
||||
`/conversations/${convId}/messages`,
|
||||
{ content },
|
||||
);
|
||||
// ponytail: local append as fallback for WS MESSAGE_CREATE; remove if WS reliability improves
|
||||
get().addMessage(message);
|
||||
return message;
|
||||
const normalizedMessage = { ...message, conversation_id: (message.conversation_id || convId).toLowerCase() };
|
||||
get().addMessage(normalizedMessage);
|
||||
return normalizedMessage;
|
||||
},
|
||||
|
||||
addMessage: (message) => {
|
||||
const convId = (message.conversation_id || "").toLowerCase();
|
||||
const normalizedMessage = { ...message, conversation_id: convId };
|
||||
set((state) => {
|
||||
const existing = state.messagesByConversation[message.conversation_id] || [];
|
||||
if (existing.some((m) => m.id === message.id)) {
|
||||
const existing = state.messagesByConversation[convId] || [];
|
||||
if (existing.some((m) => m.id === normalizedMessage.id)) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[message.conversation_id]: [...existing, message]
|
||||
[convId]: [...existing, normalizedMessage]
|
||||
.sort((a, b) => parseDate(a.created_at) - parseDate(b.created_at)),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage: (message) => {
|
||||
const convId = (message.conversation_id || "").toLowerCase();
|
||||
const normalizedMessage = { ...message, conversation_id: convId };
|
||||
set((state) => {
|
||||
const convMsgs = state.messagesByConversation[message.conversation_id] || [];
|
||||
const convMsgs = state.messagesByConversation[convId] || [];
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[message.conversation_id]: convMsgs.map((m) => (m.id === message.id ? message : m)),
|
||||
[convId]: convMsgs.map((m) => (m.id === normalizedMessage.id ? normalizedMessage : m)),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
deleteMessage: (conversationId, messageId) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
set((state) => {
|
||||
const convMsgs = state.messagesByConversation[conversationId] || [];
|
||||
const convMsgs = state.messagesByConversation[convId] || [];
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: convMsgs.filter((m) => m.id !== messageId),
|
||||
[convId]: convMsgs.filter((m) => m.id !== messageId),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addReaction: (conversationId, messageId, emoji, userId) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
set((state) => {
|
||||
const messages = state.messagesByConversation[conversationId];
|
||||
const messages = state.messagesByConversation[convId];
|
||||
if (!messages) return state;
|
||||
|
||||
const newMessages = messages.map((m) => {
|
||||
@@ -233,15 +243,16 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: newMessages,
|
||||
[convId]: newMessages,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeReaction: (conversationId, messageId, emoji, userId) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
set((state) => {
|
||||
const messages = state.messagesByConversation[conversationId];
|
||||
const messages = state.messagesByConversation[convId];
|
||||
if (!messages) return state;
|
||||
|
||||
const newMessages = messages.map((m) => {
|
||||
@@ -264,7 +275,7 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
return {
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: newMessages,
|
||||
[convId]: newMessages,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -38,8 +38,9 @@ export const useReadStatesStore = create<ReadStatesState>()((set, get) => ({
|
||||
},
|
||||
|
||||
markConvRead: (conversationId: string, messageId: string) => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
set((state) => ({
|
||||
convStates: { ...state.convStates, [conversationId]: messageId },
|
||||
convStates: { ...state.convStates, [convId]: messageId },
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -54,8 +55,9 @@ export const useReadStatesStore = create<ReadStatesState>()((set, get) => ({
|
||||
},
|
||||
|
||||
hasConvUnread: (conversationId: string, latestMessageId?: string): boolean => {
|
||||
const convId = conversationId.toLowerCase();
|
||||
const state = get().convStates;
|
||||
const lastRead = state[conversationId];
|
||||
const lastRead = state[convId];
|
||||
if (!lastRead) return !!latestMessageId;
|
||||
if (latestMessageId && lastRead !== latestMessageId) return true;
|
||||
return false;
|
||||
|
||||
@@ -62,7 +62,7 @@ function extractIds(payload: UnknownPayload | undefined): { channel_id?: string;
|
||||
: null;
|
||||
if (!messageId) return null;
|
||||
if (typeof payload.channel_id === 'string') return { channel_id: payload.channel_id, message_id: messageId };
|
||||
if (typeof payload.conversation_id === 'string') return { conversation_id: payload.conversation_id, message_id: messageId };
|
||||
if (typeof payload.conversation_id === 'string') return { conversation_id: payload.conversation_id.toLowerCase(), message_id: messageId };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -141,11 +141,12 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
if (isRecord(payload)) {
|
||||
if (payload.conversation_id) {
|
||||
const msg = payload as unknown as ConversationMessage;
|
||||
useConversationStore.getState().addMessage(msg);
|
||||
const normalizedMsg = { ...msg, conversation_id: (msg.conversation_id || '').toLowerCase() };
|
||||
useConversationStore.getState().addMessage(normalizedMsg);
|
||||
// auto-mark DM as read if this conversation is active
|
||||
const activeConvId = useConversationStore.getState().activeConversationId;
|
||||
if (msg.conversation_id === activeConvId && document.hasFocus()) {
|
||||
useReadStatesStore.getState().markConvRead(msg.conversation_id, msg.id);
|
||||
const activeConvId = (useConversationStore.getState().activeConversationId || '').toLowerCase();
|
||||
if (normalizedMsg.conversation_id === activeConvId && document.hasFocus()) {
|
||||
useReadStatesStore.getState().markConvRead(normalizedMsg.conversation_id, normalizedMsg.id);
|
||||
}
|
||||
} else {
|
||||
const msg = payload as unknown as Message;
|
||||
|
||||
Reference in New Issue
Block a user