index.tsx raw
1 import { randomString } from '@/lib/random'
2 import { cn } from '@/lib/utils'
3 import { useContentPolicy } from '@/providers/ContentPolicyProvider'
4 import blossomService from '@/services/blossom.service'
5 import modalManager from '@/services/modal-manager.service'
6 import { TImetaInfo } from '@/types'
7 import { ReactNode, useEffect, useMemo, useState } from 'react'
8 import { createPortal } from 'react-dom'
9 import Lightbox from 'yet-another-react-lightbox'
10 import Zoom from 'yet-another-react-lightbox/plugins/zoom'
11 import Image from '../Image'
12 import ImageWithLightbox from '../ImageWithLightbox'
13
14 export default function ImageGallery({
15 className,
16 images,
17 start = 0,
18 end = images.length,
19 mustLoad = false
20 }: {
21 className?: string
22 images: TImetaInfo[]
23 start?: number
24 end?: number
25 mustLoad?: boolean
26 }) {
27 const id = useMemo(() => `image-gallery-${randomString()}`, [])
28 const { autoLoadMedia } = useContentPolicy()
29 const [index, setIndex] = useState(-1)
30 const [slides, setSlides] = useState<{ src: string }[]>(images.map(({ url }) => ({ src: url })))
31 useEffect(() => {
32 if (index >= 0) {
33 modalManager.register(id, () => {
34 setIndex(-1)
35 })
36 } else {
37 modalManager.unregister(id)
38 }
39 }, [index])
40
41 useEffect(() => {
42 const loadImages = async () => {
43 const slides = await Promise.all(
44 images.map(({ url, pubkey }) => {
45 return new Promise<{ src: string }>((resolve) => {
46 const img = new window.Image()
47 let validUrl = url
48 img.onload = () => {
49 blossomService.markAsSuccess(url, validUrl)
50 resolve({ src: validUrl })
51 }
52 img.onerror = () => {
53 blossomService.tryNextUrl(url).then((nextUrl) => {
54 if (nextUrl) {
55 validUrl = nextUrl
56 resolve({ src: validUrl })
57 } else {
58 resolve({ src: url })
59 }
60 })
61 }
62 if (pubkey) {
63 blossomService
64 .getValidUrl(url, pubkey)
65 .then((u) => {
66 validUrl = u
67 img.src = validUrl
68 })
69 .catch(() => {
70 resolve({ src: url })
71 })
72 } else {
73 img.src = url
74 }
75 })
76 })
77 )
78 setSlides(slides)
79 }
80
81 loadImages()
82 }, [images])
83
84 const handlePhotoClick = (event: React.MouseEvent, current: number) => {
85 event.stopPropagation()
86 event.preventDefault()
87 setIndex(start + current)
88 }
89
90 const displayImages = images.slice(start, end)
91
92 if (!mustLoad && !autoLoadMedia) {
93 return displayImages.map((image, i) => (
94 <ImageWithLightbox
95 key={i}
96 image={image}
97 className="max-h-[80vh] sm:max-h-[50vh] object-contain"
98 classNames={{
99 wrapper: cn('w-fit max-w-full border', className)
100 }}
101 />
102 ))
103 }
104
105 let imageContent: ReactNode | null = null
106 if (displayImages.length === 1) {
107 imageContent = (
108 <Image
109 key={0}
110 className="max-h-[80vh] sm:max-h-[50vh] object-contain"
111 classNames={{
112 errorPlaceholder: 'aspect-square h-[30vh]',
113 wrapper: 'cursor-zoom-in border'
114 }}
115 image={displayImages[0]}
116 onClick={(e) => handlePhotoClick(e, 0)}
117 />
118 )
119 } else if (displayImages.length === 2 || displayImages.length === 4) {
120 imageContent = (
121 <div className="grid grid-cols-2 gap-2 w-full">
122 {displayImages.map((image, i) => (
123 <Image
124 key={i}
125 className="aspect-square w-full"
126 classNames={{ wrapper: 'cursor-zoom-in border' }}
127 image={image}
128 onClick={(e) => handlePhotoClick(e, i)}
129 />
130 ))}
131 </div>
132 )
133 } else {
134 imageContent = (
135 <div className="grid grid-cols-3 gap-2 w-full">
136 {displayImages.map((image, i) => (
137 <Image
138 key={i}
139 className="aspect-square w-full"
140 classNames={{ wrapper: 'cursor-zoom-in border' }}
141 image={image}
142 onClick={(e) => handlePhotoClick(e, i)}
143 />
144 ))}
145 </div>
146 )
147 }
148
149 return (
150 <div className={cn(displayImages.length === 1 ? 'w-fit max-w-full' : 'w-full', className)}>
151 {imageContent}
152 {index >= 0 &&
153 createPortal(
154 <div onClick={(e) => e.stopPropagation()}>
155 <Lightbox
156 index={index}
157 slides={slides}
158 plugins={[Zoom]}
159 open={index >= 0}
160 close={() => setIndex(-1)}
161 controller={{
162 closeOnBackdropClick: true,
163 closeOnPullUp: true,
164 closeOnPullDown: true
165 }}
166 styles={{
167 toolbar: { paddingTop: '2.25rem' }
168 }}
169 />
170 </div>,
171 document.body
172 )}
173 </div>
174 )
175 }
176