diff --git a/packages/server/src/db/migrate.ts b/packages/server/src/db/migrate.ts index 94b8f863..798d1be5 100644 --- a/packages/server/src/db/migrate.ts +++ b/packages/server/src/db/migrate.ts @@ -465,9 +465,6 @@ function migrateAddIndexes(db: Database.Database): void { // Categories 'CREATE INDEX IF NOT EXISTS idx_channel_categories_space_id ON channel_categories(space_id)', - // Embeds - 'CREATE INDEX IF NOT EXISTS idx_embeds_message_id ON embeds(message_id)', - 'CREATE INDEX IF NOT EXISTS idx_embeds_dm_message_id ON embeds(dm_message_id)', ]; db.exec(indexes.join(';\n')); diff --git a/packages/server/src/routes/dm.ts b/packages/server/src/routes/dm.ts index 2b3226c0..ef6a440d 100644 --- a/packages/server/src/routes/dm.ts +++ b/packages/server/src/routes/dm.ts @@ -1007,6 +1007,9 @@ export async function dmRoutes(app: FastifyInstance): Promise { .where(eq(schema.dmMessages.id, id)) .run(); + // Delete old embeds synchronously so the broadcast reflects the edit + db.delete(schema.embeds).where(eq(schema.embeds.dmMessageId, id)).run(); + const updated = getDmMessageWithUser(id); if (!updated) { return reply.code(500).send({ error: 'Failed to update message', statusCode: 500 }); @@ -1025,9 +1028,9 @@ export async function dmRoutes(app: FastifyInstance): Promise { }); } - // Re-resolve embeds asynchronously after responding + // Resolve new embeds asynchronously (old ones already deleted above) setImmediate(() => { - reResolveEmbeds(id, content.trim(), msg.dmChannelId, true, null).catch(() => {}); + resolveEmbeds(id, content.trim(), msg.dmChannelId, true, null).catch(() => {}); }); return reply.code(200).send(updated); diff --git a/packages/server/src/routes/messages.ts b/packages/server/src/routes/messages.ts index 6d1a2485..ec620ee1 100644 --- a/packages/server/src/routes/messages.ts +++ b/packages/server/src/routes/messages.ts @@ -410,20 +410,21 @@ export async function messageRoutes(app: FastifyInstance): Promise { .where(eq(schema.attachments.messageId, id)) .all(); - // Hydrate reactions, embeds, and reply-to + // Delete old embeds synchronously so the broadcast reflects the edit + db.delete(schema.embeds).where(eq(schema.embeds.messageId, id)).run(); + + // Hydrate reactions and reply-to (embeds are empty after deletion) const reactionsMap = fetchReactionsForMessages([id]); const reactions = reactionsMap.get(id) ?? []; - const embedMap = fetchEmbedsForMessages([id]); - const embedRows = embedMap.get(id) ?? []; let replyTo: MessageWithUser | null = null; if (updatedMessage.replyToId) { const replyToMap = fetchReplyToMessages([updatedMessage]); replyTo = replyToMap.get(updatedMessage.replyToId) ?? null; } - const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo, embedRows); + const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo, []); - // Broadcast edit + // Broadcast edit (with empty embeds — new ones arrive via embeds_resolved) const spaceId = getChannelSpaceId(message.channelId); if (spaceId) { connectionManager.sendToSpace(spaceId, { @@ -431,9 +432,9 @@ export async function messageRoutes(app: FastifyInstance): Promise { message: messageWithUser, }); - // Re-resolve embeds asynchronously after responding + // Resolve new embeds asynchronously (old ones already deleted above) setImmediate(() => { - reResolveEmbeds(id, content.trim(), message.channelId, false, spaceId).catch(() => {}); + resolveEmbeds(id, content.trim(), message.channelId, false, spaceId).catch(() => {}); }); } diff --git a/packages/server/src/utils/metadataFetcher.ts b/packages/server/src/utils/metadataFetcher.ts index 0da43d05..73591abb 100644 --- a/packages/server/src/utils/metadataFetcher.ts +++ b/packages/server/src/utils/metadataFetcher.ts @@ -93,13 +93,13 @@ export async function fetchUrlMetadata(url: string): Promise const $ = cheerio.load(html); const metadata: UrlMetadata = { - title: $('meta[property="og:title"]').attr('content') ?? $('title').text() ?? null, + title: $('meta[property="og:title"]').attr('content') || $('title').text() || null, description: - $('meta[property="og:description"]').attr('content') ?? - $('meta[name="description"]').attr('content') ?? + $('meta[property="og:description"]').attr('content') || + $('meta[name="description"]').attr('content') || null, - image: $('meta[property="og:image"]').attr('content') ?? null, - siteName: $('meta[property="og:site_name"]').attr('content') ?? null, + image: $('meta[property="og:image"]').attr('content') || null, + siteName: $('meta[property="og:site_name"]').attr('content') || null, url, }; diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index b9b37348..7d667dc9 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -313,6 +313,9 @@ function handleMessageEdit(event: Record, userId: string): void const spaceId = getChannelSpaceId(message.channelId); if (!spaceId) return; + // Delete old embeds synchronously so the broadcast reflects the edit + db.delete(schema.embeds).where(eq(schema.embeds.messageId, messageId)).run(); + const updatedMessage = getMessageWithUser(messageId); if (updatedMessage) { connectionManager.sendToChannel(spaceId, message.channelId, { @@ -320,9 +323,9 @@ function handleMessageEdit(event: Record, userId: string): void message: updatedMessage, }); - // Re-resolve embeds asynchronously + // Resolve new embeds asynchronously (old ones already deleted above) setImmediate(() => { - reResolveEmbeds(messageId, content.trim(), message.channelId, false, spaceId).catch(() => {}); + resolveEmbeds(messageId, content.trim(), message.channelId, false, spaceId).catch(() => {}); }); } } @@ -862,6 +865,9 @@ function handleDmMessageEdit(event: Record, userId: string): vo .where(eq(schema.dmMessages.id, messageId)) .run(); + // Delete old embeds synchronously so the broadcast reflects the edit + db.delete(schema.embeds).where(eq(schema.embeds.dmMessageId, messageId)).run(); + const updated = getDmMessageWithUser(messageId); if (!updated) return; @@ -877,9 +883,9 @@ function handleDmMessageEdit(event: Record, userId: string): vo }); } - // Re-resolve embeds asynchronously + // Resolve new embeds asynchronously (old ones already deleted above) setImmediate(() => { - reResolveEmbeds(messageId, content.trim(), msg.dmChannelId, true, null).catch(() => {}); + resolveEmbeds(messageId, content.trim(), msg.dmChannelId, true, null).catch(() => {}); }); } diff --git a/packages/web/src/components/chat/MessageList.tsx b/packages/web/src/components/chat/MessageList.tsx index 63338ead..78743993 100644 --- a/packages/web/src/components/chat/MessageList.tsx +++ b/packages/web/src/components/chat/MessageList.tsx @@ -134,7 +134,7 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess if (el) { el.scrollIntoView({ block: 'start' }); const dist = container.scrollHeight - container.scrollTop - container.clientHeight; - const near = dist < 150; + const near = dist < 5000; setIsNearBottom(near); isNearBottomRef.current = near; return; @@ -218,7 +218,7 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess // Check if near bottom const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; - const nearBottom = distanceFromBottom < 150; + const nearBottom = distanceFromBottom < 5000; setIsNearBottom(nearBottom); isNearBottomRef.current = nearBottom;