diff --git a/components/common/BlobListLightbox.tsx b/components/common/BlobListLightbox.tsx
new file mode 100644
index 00000000..e8868a8a
--- /dev/null
+++ b/components/common/BlobListLightbox.tsx
@@ -0,0 +1,81 @@
+import { useServerConfig } from '@/shell/ConfigurationContext'
+import { ToolsOzoneModerationDefs } from '@atproto/api'
+import { useCallback } from 'react'
+import Lightbox from 'yet-another-react-lightbox'
+import Captions from 'yet-another-react-lightbox/plugins/captions'
+import Thumbnails from 'yet-another-react-lightbox/plugins/thumbnails'
+
+export const useBlobUrl = ({ authorDid }: { authorDid: string }) => {
+ const { appview } = useServerConfig()
+ const cdnUrl = appview?.endsWith('.bsky.app')
+ ? 'https://cdn.bsky.app'
+ : appview
+
+ return useCallback(
+ ({
+ cid,
+ isAvatar,
+ size = 'fullsize',
+ }: {
+ cid: string
+ isAvatar?: boolean
+ size?: 'thumbnail' | 'fullsize'
+ }) => {
+ let sourcePath = isAvatar ? 'avatar' : 'feed'
+
+ // avatar_fullsize doesn't exist, instead the default avatar/ serves the full size image
+ if (!isAvatar && size !== 'fullsize') {
+ sourcePath += `_${size}`
+ }
+
+ return `${cdnUrl}/img/feed_${size}/plain/${authorDid}/${cid}@jpeg`
+ },
+ [authorDid, cdnUrl],
+ )
+}
+
+export const BlobListLightbox = ({
+ authorDid,
+ blobs,
+ onClose,
+ slideIndex,
+}: {
+ authorDid: string
+ blobs: ToolsOzoneModerationDefs.BlobView[]
+ onClose: () => void
+ slideIndex: number
+}) => {
+ const getBlobUrl = useBlobUrl({ authorDid })
+ const handleKeyDown = (event) => {
+ if (event.key === 'Escape') {
+ event.stopPropagation()
+ }
+ }
+
+ return (
+
= 0}
+ plugins={[Thumbnails, Captions]}
+ carousel={{ finite: true }}
+ controller={{ closeOnBackdropClick: true }}
+ close={onClose}
+ slides={blobs.map((blob) => ({
+ src: getBlobUrl({
+ cid: blob.cid,
+ }),
+ }))}
+ index={slideIndex}
+ on={{
+ // The lightbox may open from other Dialog/modal components
+ // in that case, we want to make sure that esc button presses
+ // only close the lightbox and not the parent Dialog/modal underneath
+ entered: () => {
+ document.addEventListener('keydown', handleKeyDown)
+ },
+ exited: () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ },
+ }}
+ />
+ )
+}
diff --git a/components/common/posts/ImageList.tsx b/components/common/posts/ImageList.tsx
index ffd8d10c..1370b221 100644
--- a/components/common/posts/ImageList.tsx
+++ b/components/common/posts/ImageList.tsx
@@ -22,7 +22,7 @@ export const ImageList = ({
imageClassName?: string
images: AppBskyEmbedImages.ViewImage[]
}) => {
- const [isImageViewerOpen, setIsImageViewerOpen] = useState(false)
+ const [lightboxImageIndex, setLightboxImageIndex] = useState(-1)
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
@@ -33,15 +33,16 @@ export const ImageList = ({
return (
<>
= 0}
plugins={[Thumbnails, Captions]}
carousel={{ finite: true }}
controller={{ closeOnBackdropClick: true }}
- close={() => setIsImageViewerOpen(false)}
+ close={() => setLightboxImageIndex(-1)}
slides={images.map((img) => ({
src: img.fullsize,
description: img.alt,
}))}
+ index={lightboxImageIndex}
on={{
// The lightbox may open from other Dialog/modal components
// in that case, we want to make sure that esc button presses
@@ -56,7 +57,7 @@ export const ImageList = ({
/>
{images.map((image, i) => (
-