feat: add embeds_resolved WS handlers and optimistic embeds:[] default
- chatStore: normalize embeds to [] in addMessage, addRealtimeMessage, and updateMessage to guard against missing field from older servers/federation - useWebSocket: add embeds_resolved and dm_embeds_resolved cases that patch the in-memory message cache when the server broadcasts resolved embed data - useWebSocket: normalize embed image URLs for remote origins in message_created, message_updated, dm_message_created, dm_message_updated, and the new embeds_resolved handlers
This commit is contained in:
@@ -317,7 +317,16 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'message_created':
|
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);
|
addRealtimeMessage(event.message.channelId, event.message);
|
||||||
{
|
{
|
||||||
const { currentChannelId, markChannelUnread } = useChatStore.getState();
|
const { currentChannelId, markChannelUnread } = useChatStore.getState();
|
||||||
@@ -331,7 +340,16 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'message_updated':
|
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);
|
updateMessage(event.message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -461,7 +479,16 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
// ─── DM events (home-only) ──────────────────────────────────────────────
|
// ─── DM events (home-only) ──────────────────────────────────────────────
|
||||||
|
|
||||||
case 'dm_message_created': {
|
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);
|
addRealtimeMessage(event.message.dmChannelId, event.message as any);
|
||||||
const { dmChannels: currentDmChannels, setDmChannels: setDms, addDmChannel: addDmCh } = useSpaceStore.getState();
|
const { dmChannels: currentDmChannels, setDmChannels: setDms, addDmChannel: addDmCh } = useSpaceStore.getState();
|
||||||
const knownDm = currentDmChannels.find(dm => dm.id === event.message.dmChannelId);
|
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':
|
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);
|
updateMessage(event.message as any);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -504,6 +540,46 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
removeMessage(event.messageId, event.dmChannelId);
|
removeMessage(event.messageId, event.dmChannelId);
|
||||||
break;
|
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': {
|
case 'dm_typing': {
|
||||||
let dmTypingUsername = event.username as string;
|
let dmTypingUsername = event.username as string;
|
||||||
if (!isHome && dmTypingUsername && !dmTypingUsername.includes('@')) {
|
if (!isHome && dmTypingUsername && !dmTypingUsername.includes('@')) {
|
||||||
|
|||||||
@@ -360,20 +360,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addMessage: (channelId: string, message: MessageWithUser) => {
|
addMessage: (channelId: string, message: MessageWithUser) => {
|
||||||
|
const normalizedMessage = { ...message, embeds: message.embeds ?? [] };
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newMessages = new Map(state.messages);
|
const newMessages = new Map(state.messages);
|
||||||
const current = newMessages.get(channelId) ?? [];
|
const current = newMessages.get(channelId) ?? [];
|
||||||
// Avoid duplicates
|
// 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.
|
// Remove any optimistic temp message with same content.
|
||||||
// Don't require userId match — for federated messages the home user ID
|
// Don't require userId match — for federated messages the home user ID
|
||||||
// differs from the replicated user ID, but content match is sufficient
|
// differs from the replicated user ID, but content match is sufficient
|
||||||
// since temp messages are unique within the short optimistic window.
|
// since temp messages are unique within the short optimistic window.
|
||||||
const filtered = current.filter(m => {
|
const filtered = current.filter(m => {
|
||||||
if (!m.id.startsWith('temp_')) return true;
|
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
|
// Cap per-channel messages to prevent memory growth
|
||||||
if (updated.length > MAX_MESSAGES_PER_CHANNEL) {
|
if (updated.length > MAX_MESSAGES_PER_CHANNEL) {
|
||||||
updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL);
|
updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL);
|
||||||
@@ -384,25 +385,26 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addRealtimeMessage: (channelId: string, message: MessageWithUser) => {
|
addRealtimeMessage: (channelId: string, message: MessageWithUser) => {
|
||||||
|
const normalizedMessage = { ...message, embeds: message.embeds ?? [] };
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newMessages = new Map(state.messages);
|
const newMessages = new Map(state.messages);
|
||||||
const current = newMessages.get(channelId) ?? [];
|
const current = newMessages.get(channelId) ?? [];
|
||||||
// Avoid duplicates
|
// 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 —
|
// Remove any optimistic temp message with same content (no userId check —
|
||||||
// federated messages arrive with a different replicated user ID)
|
// federated messages arrive with a different replicated user ID)
|
||||||
const filtered = current.filter(m => {
|
const filtered = current.filter(m => {
|
||||||
if (!m.id.startsWith('temp_')) return true;
|
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
|
// Cap per-channel messages to prevent memory growth
|
||||||
if (updated.length > MAX_MESSAGES_PER_CHANNEL) {
|
if (updated.length > MAX_MESSAGES_PER_CHANNEL) {
|
||||||
updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL);
|
updated = updated.slice(updated.length - MAX_MESSAGES_PER_CHANNEL);
|
||||||
}
|
}
|
||||||
newMessages.set(channelId, updated);
|
newMessages.set(channelId, updated);
|
||||||
// Append to realtimeMessageEvents (capped at 50)
|
// 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);
|
if (newEvents.length > 50) newEvents.splice(0, newEvents.length - 50);
|
||||||
return { messages: newMessages, realtimeMessageEvents: newEvents };
|
return { messages: newMessages, realtimeMessageEvents: newEvents };
|
||||||
});
|
});
|
||||||
@@ -412,13 +414,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
// DM messages have dmChannelId instead of channelId — check both
|
// DM messages have dmChannelId instead of channelId — check both
|
||||||
const channelKey = message.channelId || (message as any).dmChannelId;
|
const channelKey = message.channelId || (message as any).dmChannelId;
|
||||||
if (!channelKey) return;
|
if (!channelKey) return;
|
||||||
|
const normalizedMessage = { ...message, embeds: message.embeds ?? [] };
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newMessages = new Map(state.messages);
|
const newMessages = new Map(state.messages);
|
||||||
const current = newMessages.get(channelKey);
|
const current = newMessages.get(channelKey);
|
||||||
if (!current) return state;
|
if (!current) return state;
|
||||||
newMessages.set(
|
newMessages.set(
|
||||||
channelKey,
|
channelKey,
|
||||||
current.map(m => m.id === message.id ? message : m),
|
current.map(m => m.id === normalizedMessage.id ? normalizedMessage : m),
|
||||||
);
|
);
|
||||||
return { messages: newMessages };
|
return { messages: newMessages };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user