diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index 3aace391..4522e181 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -317,7 +317,16 @@ function handleEvent(origin: string, event: ServerEvent): void { break; case 'message_created': - if (!isHome) normalizeMessageAssets(event.message, origin); + if (!isHome) { + normalizeMessageAssets(event.message, origin); + if (event.message.embeds) { + for (const embed of event.message.embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + } addRealtimeMessage(event.message.channelId, event.message); { const { currentChannelId, markChannelUnread } = useChatStore.getState(); @@ -331,7 +340,16 @@ function handleEvent(origin: string, event: ServerEvent): void { break; case 'message_updated': - if (!isHome) normalizeMessageAssets(event.message, origin); + if (!isHome) { + normalizeMessageAssets(event.message, origin); + if (event.message.embeds) { + for (const embed of event.message.embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + } updateMessage(event.message); break; @@ -461,7 +479,16 @@ function handleEvent(origin: string, event: ServerEvent): void { // ─── DM events (home-only) ────────────────────────────────────────────── case 'dm_message_created': { - if (!isHome) normalizeMessageAssets(event.message as any, origin); + if (!isHome) { + normalizeMessageAssets(event.message as any, origin); + if ((event.message as any).embeds) { + for (const embed of (event.message as any).embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + } addRealtimeMessage(event.message.dmChannelId, event.message as any); const { dmChannels: currentDmChannels, setDmChannels: setDms, addDmChannel: addDmCh } = useSpaceStore.getState(); const knownDm = currentDmChannels.find(dm => dm.id === event.message.dmChannelId); @@ -496,7 +523,16 @@ function handleEvent(origin: string, event: ServerEvent): void { } case 'dm_message_updated': - if (!isHome) normalizeMessageAssets(event.message as any, origin); + if (!isHome) { + normalizeMessageAssets(event.message as any, origin); + if ((event.message as any).embeds) { + for (const embed of (event.message as any).embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + } updateMessage(event.message as any); break; @@ -504,6 +540,46 @@ function handleEvent(origin: string, event: ServerEvent): void { removeMessage(event.messageId, event.dmChannelId); break; + case 'embeds_resolved': { + if (!isHome) { + for (const embed of event.embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + const resolvedMsgs = useChatStore.getState().messages.get(event.channelId); + if (resolvedMsgs) { + const newResolvedMsgs = resolvedMsgs.map(m => + m.id === event.messageId ? { ...m, embeds: event.embeds } : m + ); + const newResolvedMessages = new Map(useChatStore.getState().messages); + newResolvedMessages.set(event.channelId, newResolvedMsgs); + useChatStore.setState({ messages: newResolvedMessages }); + } + break; + } + + case 'dm_embeds_resolved': { + if (!isHome) { + for (const embed of event.embeds) { + if (embed.image && !embed.image.startsWith('http')) { + embed.image = resolveAssetUrl(embed.image, origin) ?? embed.image; + } + } + } + const dmResolvedMsgs = useChatStore.getState().messages.get(event.dmChannelId); + if (dmResolvedMsgs) { + const newDmResolvedMsgs = dmResolvedMsgs.map(m => + m.id === event.messageId ? { ...m, embeds: event.embeds } : m + ); + const newDmResolvedMessages = new Map(useChatStore.getState().messages); + newDmResolvedMessages.set(event.dmChannelId, newDmResolvedMsgs); + useChatStore.setState({ messages: newDmResolvedMessages }); + } + break; + } + case 'dm_typing': { let dmTypingUsername = event.username as string; if (!isHome && dmTypingUsername && !dmTypingUsername.includes('@')) { diff --git a/packages/web/src/stores/chatStore.ts b/packages/web/src/stores/chatStore.ts index 15697a3e..fc16003a 100644 --- a/packages/web/src/stores/chatStore.ts +++ b/packages/web/src/stores/chatStore.ts @@ -360,20 +360,21 @@ export const useChatStore = create((set, get) => ({ }, addMessage: (channelId: string, message: MessageWithUser) => { + const normalizedMessage = { ...message, embeds: message.embeds ?? [] }; set((state) => { const newMessages = new Map(state.messages); const current = newMessages.get(channelId) ?? []; // Avoid duplicates - if (current.find(m => m.id === message.id)) return state; + if (current.find(m => m.id === normalizedMessage.id)) return state; // Remove any optimistic temp message with same content. // Don't require userId match — for federated messages the home user ID // differs from the replicated user ID, but content match is sufficient // since temp messages are unique within the short optimistic window. const filtered = current.filter(m => { if (!m.id.startsWith('temp_')) return true; - return m.content !== message.content; + return m.content !== normalizedMessage.content; }); - let updated = [...filtered, message]; + let updated = [...filtered, normalizedMessage]; // Cap per-channel messages to prevent memory growth if (updated.length > MAX_MESSAGES_PER_CHANNEL) { updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL); @@ -384,25 +385,26 @@ export const useChatStore = create((set, get) => ({ }, addRealtimeMessage: (channelId: string, message: MessageWithUser) => { + const normalizedMessage = { ...message, embeds: message.embeds ?? [] }; set((state) => { const newMessages = new Map(state.messages); const current = newMessages.get(channelId) ?? []; // Avoid duplicates - if (current.find(m => m.id === message.id)) return state; + if (current.find(m => m.id === normalizedMessage.id)) return state; // Remove any optimistic temp message with same content (no userId check — // federated messages arrive with a different replicated user ID) const filtered = current.filter(m => { if (!m.id.startsWith('temp_')) return true; - return m.content !== message.content; + return m.content !== normalizedMessage.content; }); - let updated = [...filtered, message]; + let updated = [...filtered, normalizedMessage]; // Cap per-channel messages to prevent memory growth if (updated.length > MAX_MESSAGES_PER_CHANNEL) { updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL); } newMessages.set(channelId, updated); // Append to realtimeMessageEvents (capped at 50) - const newEvents = [...state.realtimeMessageEvents, { channelId, message }]; + const newEvents = [...state.realtimeMessageEvents, { channelId, message: normalizedMessage }]; if (newEvents.length > 50) newEvents.splice(0, newEvents.length - 50); return { messages: newMessages, realtimeMessageEvents: newEvents }; }); @@ -412,13 +414,14 @@ export const useChatStore = create((set, get) => ({ // DM messages have dmChannelId instead of channelId — check both const channelKey = message.channelId || (message as any).dmChannelId; if (!channelKey) return; + const normalizedMessage = { ...message, embeds: message.embeds ?? [] }; set((state) => { const newMessages = new Map(state.messages); const current = newMessages.get(channelKey); if (!current) return state; newMessages.set( channelKey, - current.map(m => m.id === message.id ? message : m), + current.map(m => m.id === normalizedMessage.id ? normalizedMessage : m), ); return { messages: newMessages }; });