From 001d45da2bee72a8278de37b0c8be678acc99644 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 25 Mar 2026 04:59:53 +0100 Subject: [PATCH] feat: suppress URL text for image-only messages, add sourceUrl to context menu --- packages/web/src/components/chat/Message.tsx | 39 +++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/chat/Message.tsx b/packages/web/src/components/chat/Message.tsx index 5ac5cdb2..c3fa5fb1 100644 --- a/packages/web/src/components/chat/Message.tsx +++ b/packages/web/src/components/chat/Message.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; -import type { MessageWithUser } from '@backspace/shared'; +import type { MessageWithUser, Embed } from '@backspace/shared'; import { MarkdownRenderer } from './MarkdownRenderer'; import { MentionBadge } from './MentionBadge'; import { Avatar } from '../ui/Avatar'; @@ -61,6 +61,23 @@ function isGifOnlyMessage(content: string | null): boolean { return GIF_URL_REGEX.test(trimmed); } +/** + * Returns the original posted URL when a message is a single URL that resolved + * to an image embed, or null if the message should render normally. + */ +function getImageEmbedSourceUrl(content: string | null, embeds: Embed[]): string | null { + if (!content) return null; + const trimmed = content.trim(); + // Must be a single URL with no surrounding text + if (!trimmed.startsWith('http://') && !trimmed.startsWith('https://')) return null; + if (/\s/.test(trimmed)) return null; + // Must have at least one image embed whose URL matches the posted content + const hasMatchingImageEmbed = embeds.some( + (e) => e.embedType === 'image' && e.url === trimmed + ); + return hasMatchingImageEmbed ? trimmed : null; +} + export function Message({ message, isCompact, isFirstInGroup, previousMessageId }: MessageProps) { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(message.content ?? ''); @@ -130,6 +147,11 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId }, []); const isGifOnly = isGifOnlyMessage(message.content); + const imageEmbedSourceUrl = getImageEmbedSourceUrl(message.content, message.embeds || []); + // sourceUrl: the original URL for context menu Copy/Open Link actions + const sourceUrl = isGifOnly + ? (message.content?.trim() ?? null) + : imageEmbedSourceUrl; // Close reaction picker on outside click useEffect(() => { @@ -183,6 +205,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId selectedText, previousMessageId, imageUrl, + sourceUrl, isAuthor, isDm: isDmMessage, canAddReactions, @@ -354,6 +377,20 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId loading="lazy" /> + ) : imageEmbedSourceUrl ? ( + <> + {/* URL text suppressed — embeds render the image */} + {!isEditing && message.embeds && message.embeds.length > 0 && ( +
+ {message.embeds.map((embed) => ( + + ))} +
+ )} + {message.editedAt && ( + (edited) + )} + ) : ( <> {message.content && (