From 58f548e5ea0865709ae0e583f5dbf110bedf2726 Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Mon, 24 Aug 2026 11:00:09 -0700 Subject: [PATCH 1/2] feat: navigate images across message threads Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> --- .../messages/ui/MessageThreadPanel.tsx | 4 + desktop/src/shared/ui/markdown.tsx | 8 +- .../shared/ui/markdown/ImageGalleryStatus.tsx | 31 +++ .../ui/markdown/LinkPreviewImageLightbox.tsx | 4 +- .../src/shared/ui/markdown/imageLightbox.ts | 12 +- .../e2e/image-attachment-gallery.spec.ts | 232 ++++++++++++++++++ 6 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 desktop/src/shared/ui/markdown/ImageGalleryStatus.tsx diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index d3b1394829..8797bec7a0 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -510,8 +510,12 @@ export function MessageThreadPanel({ tabIndex={-1} ref={threadBodyRef} > + {/* The gallery is intentionally DOM-scoped: only media currently rendered + in this open thread participates. Collapsed or unloaded descendants + join only after the thread UI renders them. */}
+
{menu && canActOnCurrentImage ? ( @@ -1000,7 +1002,6 @@ function ImageBlock({ alt, dim, resolvedSrc, src, thumbSrc }: ImageBlockProps) { const triggerRef = React.useRef(null); useSmoothCorners(inlineImageRef); useSmoothCorners(thumbnailImageRef); - const [spoilerMediaSize, setSpoilerMediaSize] = React.useState<{ height: number; src: string; @@ -1078,7 +1079,6 @@ function ImageBlock({ alt, dim, resolvedSrc, src, thumbSrc }: ImageBlockProps) { return () => observer.disconnect(); }, []); - const closeMenu = React.useCallback(() => setMenu(null), []); useDismissMediaContextMenu(Boolean(menu), closeMenu); @@ -1089,7 +1089,6 @@ function ImageBlock({ alt, dim, resolvedSrc, src, thumbSrc }: ImageBlockProps) { e.nativeEvent.stopImmediatePropagation(); setMenu({ x: e.clientX, y: e.clientY }); }; - const openLightbox = React.useCallback( (image: HTMLImageElement) => { if (!resolvedSrc || isInsideHiddenSpoiler(image)) { @@ -1113,6 +1112,7 @@ function ImageBlock({ alt, dim, resolvedSrc, src, thumbSrc }: ImageBlockProps) { { alt, dim, + trigger: triggerRef.current, resolvedSrc, src, thumbnailBox: sourceBox, diff --git a/desktop/src/shared/ui/markdown/ImageGalleryStatus.tsx b/desktop/src/shared/ui/markdown/ImageGalleryStatus.tsx new file mode 100644 index 0000000000..66e7b68b8b --- /dev/null +++ b/desktop/src/shared/ui/markdown/ImageGalleryStatus.tsx @@ -0,0 +1,31 @@ +type ImageGalleryStatusProps = { + currentIndex: number; + itemCount: number; +}; + +export function ImageGalleryStatus({ + currentIndex, + itemCount, +}: ImageGalleryStatusProps) { + if (itemCount <= 1) { + return null; + } + + const position = currentIndex + 1; + return ( + <> +