an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

lightbox is now a route

rimar1337 efb287ec 07f352c4

Changed files
+256 -143
src
+40 -125
src/components/UniversalPostRenderer.tsx
··· 3 3 import { useAtom } from "jotai"; 4 4 import * as React from "react"; 5 5 import { type SVGProps } from "react"; 6 - import { createPortal } from "react-dom"; 7 6 8 - import { ProfilePostComponent } from "~/routes/profile.$did/post.$rkey"; 9 7 import { likedPostsAtom } from "~/utils/atoms"; 10 8 import { useHydratedEmbed } from "~/utils/useHydrated"; 11 9 import { ··· 35 33 ref?: React.Ref<HTMLDivElement>; 36 34 dataIndexPropPass?: number; 37 35 nopics?: boolean; 36 + lightboxCallback?: (d:LightboxProps) => void; 38 37 } 39 38 40 39 // export async function cachedGetRecord({ ··· 143 142 ref, 144 143 dataIndexPropPass, 145 144 nopics, 145 + lightboxCallback, 146 146 }: UniversalPostRendererATURILoaderProps) { 147 147 // /*mass comment*/ console.log("atUri", atUri); 148 148 //const { get, set } = usePersistentStore(); ··· 421 421 ref={ref} 422 422 dataIndexPropPass={dataIndexPropPass} 423 423 nopics={nopics} 424 + lightboxCallback={lightboxCallback} 424 425 /> 425 426 ); 426 427 } ··· 449 450 ref, 450 451 dataIndexPropPass, 451 452 nopics, 453 + lightboxCallback, 452 454 }: { 453 455 postRecord: any; 454 456 profileRecord: any; ··· 467 469 ref?: React.Ref<HTMLDivElement>; 468 470 dataIndexPropPass?: number; 469 471 nopics?: boolean; 472 + lightboxCallback?: (d:LightboxProps) => void; 470 473 }) { 471 474 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 472 475 const navigate = useNavigate(); ··· 665 668 ref={ref} 666 669 dataIndexPropPass={dataIndexPropPass} 667 670 nopics={nopics} 671 + lightboxCallback={lightboxCallback} 668 672 /> 669 673 </> 670 674 ); ··· 983 987 984 988 import defaultpfp from "~/../public/favicon.png"; 985 989 import { useAuth } from "~/providers/UnifiedAuthProvider"; 990 + import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 986 991 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 987 992 // import type { 988 993 // ViewRecord, ··· 1110 1115 ref, 1111 1116 dataIndexPropPass, 1112 1117 nopics, 1118 + lightboxCallback 1113 1119 }: { 1114 1120 post: PostView; 1115 1121 // optional for now because i havent ported every use to this yet ··· 1133 1139 ref?: React.Ref<HTMLDivElement>; 1134 1140 dataIndexPropPass?: number; 1135 1141 nopics?: boolean; 1142 + lightboxCallback?: (d:LightboxProps) => void; 1136 1143 }) { 1137 1144 const parsed = new AtUri(post.uri); 1138 1145 const navigate = useNavigate(); ··· 1514 1521 navigate={navigate} 1515 1522 postid={{ did: post.author.did, rkey: parsed.rkey }} 1516 1523 nopics={nopics} 1524 + lightboxCallback={lightboxCallback} 1517 1525 /> 1518 1526 ) : null} 1519 1527 {post.embed && depth > 0 && ( ··· 1720 1728 navigate, 1721 1729 postid, 1722 1730 nopics, 1731 + lightboxCallback 1723 1732 }: { 1724 1733 embed?: Embed; 1725 1734 moderation?: ModerationDecision; ··· 1730 1739 navigate: (_: any) => void; 1731 1740 postid?: { did: string; rkey: string }; 1732 1741 nopics?: boolean; 1742 + lightboxCallback?: (d:LightboxProps) => void; 1733 1743 }) { 1734 - const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1744 + //const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1745 + function setLightboxIndex(number:number) { 1746 + navigate({ 1747 + to: "/profile/$did/post/$rkey/image/$i", 1748 + params: { 1749 + did: postid?.did, 1750 + rkey: postid?.rkey, 1751 + i: number.toString(), 1752 + }, 1753 + }); 1754 + } 1735 1755 if ( 1736 1756 AppBskyEmbedRecordWithMedia.isView(embed) && 1737 1757 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 1767 1787 navigate={navigate} 1768 1788 postid={postid} 1769 1789 nopics={nopics} 1790 + lightboxCallback={lightboxCallback} 1770 1791 /> 1771 1792 {/* padding empty div of 8px height */} 1772 1793 <div style={{ height: 12 }} /> ··· 1934 1955 1935 1956 // image embed 1936 1957 // = 1937 - if (AppBskyEmbedImages.isView(embed) && !nopics) { 1958 + if (AppBskyEmbedImages.isView(embed)) { 1938 1959 const { images } = embed; 1939 1960 1940 1961 const lightboxImages = images.map((img) => ({ 1941 1962 src: img.fullsize, 1942 1963 alt: img.alt, 1943 1964 })); 1965 + console.log("rendering images") 1966 + if (lightboxCallback) { 1967 + lightboxCallback({images: lightboxImages}) 1968 + console.log("rendering images") 1969 + }; 1970 + 1971 + if (nopics) return; 1944 1972 1945 1973 if (images.length > 0) { 1946 1974 // const items = embed.images.map(img => ({ ··· 1972 2000 }} 1973 2001 className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" 1974 2002 > 1975 - {lightboxIndex !== null && ( 2003 + {/* {lightboxIndex !== null && ( 1976 2004 <Lightbox 1977 2005 images={lightboxImages} 1978 2006 index={lightboxIndex} ··· 1980 2008 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1981 2009 post={postid} 1982 2010 /> 1983 - )} 2011 + )} */} 1984 2012 <img 1985 2013 src={image.fullsize} 1986 2014 alt={image.alt} ··· 2013 2041 }} 2014 2042 className="border border-gray-200 dark:border-gray-800 was7" 2015 2043 > 2016 - {lightboxIndex !== null && ( 2044 + {/* {lightboxIndex !== null && ( 2017 2045 <Lightbox 2018 2046 images={lightboxImages} 2019 2047 index={lightboxIndex} ··· 2021 2049 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2022 2050 post={postid} 2023 2051 /> 2024 - )} 2052 + )} */} 2025 2053 {images.map((img, i) => ( 2026 2054 <div 2027 2055 key={i} ··· 2063 2091 }} 2064 2092 className="border border-gray-200 dark:border-gray-800 was7" 2065 2093 > 2066 - {lightboxIndex !== null && ( 2094 + {/* {lightboxIndex !== null && ( 2067 2095 <Lightbox 2068 2096 images={lightboxImages} 2069 2097 index={lightboxIndex} ··· 2071 2099 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2072 2100 post={postid} 2073 2101 /> 2074 - )} 2102 + )} */} 2075 2103 {/* Left: 1:1 */} 2076 2104 <div 2077 2105 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2148 2176 }} 2149 2177 className="border border-gray-200 dark:border-gray-800 was7" 2150 2178 > 2151 - {lightboxIndex !== null && ( 2179 + {/* {lightboxIndex !== null && ( 2152 2180 <Lightbox 2153 2181 images={lightboxImages} 2154 2182 index={lightboxIndex} ··· 2156 2184 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2157 2185 post={postid} 2158 2186 /> 2159 - )} 2187 + )} */} 2160 2188 {images.map((img, i) => ( 2161 2189 <div 2162 2190 key={i} ··· 2245 2273 } 2246 2274 2247 2275 return <div />; 2248 - } 2249 - 2250 - type LightboxProps = { 2251 - images: { src: string; alt?: string }[]; 2252 - index: number; 2253 - onClose: () => void; 2254 - onNavigate?: (newIndex: number) => void; 2255 - post?: { did: string; rkey: string }; 2256 - }; 2257 - export function Lightbox({ 2258 - images, 2259 - index, 2260 - onClose, 2261 - onNavigate, 2262 - post, 2263 - }: LightboxProps) { 2264 - const image = images[index]; 2265 - 2266 - useEffect(() => { 2267 - function handleKey(e: KeyboardEvent) { 2268 - if (e.key === "Escape") onClose(); 2269 - if (e.key === "ArrowRight" && onNavigate) 2270 - onNavigate((index + 1) % images.length); 2271 - if (e.key === "ArrowLeft" && onNavigate) 2272 - onNavigate((index - 1 + images.length) % images.length); 2273 - } 2274 - window.addEventListener("keydown", handleKey); 2275 - return () => window.removeEventListener("keydown", handleKey); 2276 - }, [index, images.length, onClose, onNavigate]); 2277 - 2278 - return createPortal( 2279 - <> 2280 - {post && ( 2281 - <div 2282 - onClick={(e) => { 2283 - e.stopPropagation(); 2284 - e.nativeEvent.stopImmediatePropagation(); 2285 - }} 2286 - className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white" 2287 - > 2288 - <ProfilePostComponent 2289 - did={post.did} 2290 - rkey={post.rkey} 2291 - nopics={onClose} 2292 - /> 2293 - </div> 2294 - )} 2295 - <div 2296 - className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]" 2297 - onClick={(e) => { 2298 - e.stopPropagation(); 2299 - onClose(); 2300 - }} 2301 - > 2302 - <img 2303 - src={image.src} 2304 - alt={image.alt} 2305 - className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg" 2306 - onClick={(e) => e.stopPropagation()} 2307 - /> 2308 - 2309 - {images.length > 1 && ( 2310 - <> 2311 - <button 2312 - onClick={(e) => { 2313 - e.stopPropagation(); 2314 - onNavigate?.((index - 1 + images.length) % images.length); 2315 - }} 2316 - className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 2317 - > 2318 - <svg 2319 - xmlns="http://www.w3.org/2000/svg" 2320 - width={28} 2321 - height={28} 2322 - viewBox="0 0 24 24" 2323 - > 2324 - <g fill="none" fillRule="evenodd"> 2325 - <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 2326 - <path 2327 - fill="currentColor" 2328 - d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z" 2329 - ></path> 2330 - </g> 2331 - </svg> 2332 - </button> 2333 - <button 2334 - onClick={(e) => { 2335 - e.stopPropagation(); 2336 - onNavigate?.((index + 1) % images.length); 2337 - }} 2338 - className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 2339 - > 2340 - <svg 2341 - xmlns="http://www.w3.org/2000/svg" 2342 - width={28} 2343 - height={28} 2344 - viewBox="0 0 24 24" 2345 - > 2346 - <g fill="none" fillRule="evenodd"> 2347 - <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 2348 - <path 2349 - fill="currentColor" 2350 - d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z" 2351 - ></path> 2352 - </g> 2353 - </svg> 2354 - </button> 2355 - </> 2356 - )} 2357 - </div> 2358 - </>, 2359 - document.body 2360 - ); 2361 2276 } 2362 2277 2363 2278 function getDomain(url: string) {
+36 -5
src/routeTree.gen.ts
··· 21 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 + import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i' 24 25 25 26 const SettingsRoute = SettingsRouteImport.update({ 26 27 id: '/settings', ··· 83 84 path: '/profile/$did/post/$rkey', 84 85 getParentRoute: () => rootRouteImport, 85 86 } as any) 87 + const ProfileDidPostRkeyImageIRoute = 88 + ProfileDidPostRkeyImageIRouteImport.update({ 89 + id: '/image/$i', 90 + path: '/image/$i', 91 + getParentRoute: () => ProfileDidPostRkeyRoute, 92 + } as any) 86 93 87 94 export interface FileRoutesByFullPath { 88 95 '/': typeof IndexRoute ··· 94 101 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 95 102 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 96 103 '/profile/$did': typeof ProfileDidIndexRoute 97 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 104 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 105 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 98 106 } 99 107 export interface FileRoutesByTo { 100 108 '/': typeof IndexRoute ··· 106 114 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 107 115 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 108 116 '/profile/$did': typeof ProfileDidIndexRoute 109 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 117 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 118 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 110 119 } 111 120 export interface FileRoutesById { 112 121 __root__: typeof rootRouteImport ··· 121 130 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 122 131 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 123 132 '/profile/$did/': typeof ProfileDidIndexRoute 124 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 133 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 134 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 125 135 } 126 136 export interface FileRouteTypes { 127 137 fileRoutesByFullPath: FileRoutesByFullPath ··· 136 146 | '/route-b' 137 147 | '/profile/$did' 138 148 | '/profile/$did/post/$rkey' 149 + | '/profile/$did/post/$rkey/image/$i' 139 150 fileRoutesByTo: FileRoutesByTo 140 151 to: 141 152 | '/' ··· 148 159 | '/route-b' 149 160 | '/profile/$did' 150 161 | '/profile/$did/post/$rkey' 162 + | '/profile/$did/post/$rkey/image/$i' 151 163 id: 152 164 | '__root__' 153 165 | '/' ··· 162 174 | '/_pathlessLayout/_nested-layout/route-b' 163 175 | '/profile/$did/' 164 176 | '/profile/$did/post/$rkey' 177 + | '/profile/$did/post/$rkey/image/$i' 165 178 fileRoutesById: FileRoutesById 166 179 } 167 180 export interface RootRouteChildren { ··· 173 186 SettingsRoute: typeof SettingsRoute 174 187 CallbackIndexRoute: typeof CallbackIndexRoute 175 188 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 176 - ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute 189 + ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren 177 190 } 178 191 179 192 declare module '@tanstack/react-router' { ··· 262 275 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 263 276 parentRoute: typeof rootRouteImport 264 277 } 278 + '/profile/$did/post/$rkey/image/$i': { 279 + id: '/profile/$did/post/$rkey/image/$i' 280 + path: '/image/$i' 281 + fullPath: '/profile/$did/post/$rkey/image/$i' 282 + preLoaderRoute: typeof ProfileDidPostRkeyImageIRouteImport 283 + parentRoute: typeof ProfileDidPostRkeyRoute 284 + } 265 285 } 266 286 } 267 287 ··· 295 315 PathlessLayoutRouteChildren, 296 316 ) 297 317 318 + interface ProfileDidPostRkeyRouteChildren { 319 + ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute 320 + } 321 + 322 + const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = { 323 + ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute, 324 + } 325 + 326 + const ProfileDidPostRkeyRouteWithChildren = 327 + ProfileDidPostRkeyRoute._addFileChildren(ProfileDidPostRkeyRouteChildren) 328 + 298 329 const rootRouteChildren: RootRouteChildren = { 299 330 IndexRoute: IndexRoute, 300 331 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, ··· 304 335 SettingsRoute: SettingsRoute, 305 336 CallbackIndexRoute: CallbackIndexRoute, 306 337 ProfileDidIndexRoute: ProfileDidIndexRoute, 307 - ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute, 338 + ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren, 308 339 } 309 340 export const routeTree = rootRouteImport 310 341 ._addFileChildren(rootRouteChildren)
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
··· 1 + import { 2 + createFileRoute, 3 + useNavigate, 4 + type UseNavigateResult, 5 + } from "@tanstack/react-router"; 6 + import { useEffect, useState } from "react"; 7 + import { createPortal } from "react-dom"; 8 + 9 + import { ProfilePostComponent } from "./post.$rkey"; 10 + 11 + export const Route = createFileRoute("/profile/$did/post/$rkey/image/$i")({ 12 + component: Lightbox, 13 + }); 14 + 15 + export type LightboxProps = { 16 + images: { src: string; alt?: string }[]; 17 + }; 18 + 19 + function nextprev({ 20 + index, 21 + images, 22 + navigate, 23 + did, 24 + rkey, 25 + prev, 26 + }: { 27 + index?: number; 28 + images?: LightboxProps["images"]; 29 + navigate: UseNavigateResult<string>; 30 + did: string; 31 + rkey: string; 32 + prev?: boolean; 33 + }) { 34 + const len = images?.length ?? 0; 35 + if (len === 0) return; 36 + 37 + const nextIndex = ((index ?? 0) + (prev ? -1 : 1) + len) % len; 38 + 39 + navigate({ 40 + to: "/profile/$did/post/$rkey/image/$i", 41 + params: { 42 + did, 43 + rkey, 44 + i: nextIndex.toString(), 45 + }, 46 + replace: true, 47 + }); 48 + } 49 + 50 + export function Lightbox() { 51 + console.log("hey the $i route is loaded w!!!"); 52 + const { did, rkey, i } = Route.useParams(); 53 + const [images, setImages] = useState<LightboxProps["images"] | undefined>( 54 + undefined 55 + ); 56 + const index = Number(i); 57 + const navigate = useNavigate(); 58 + const post = true; 59 + const image = images?.[index] ?? undefined; 60 + 61 + function lightboxCallback(d: LightboxProps) { 62 + console.log("callback actually called!"); 63 + setImages(d.images); 64 + } 65 + 66 + useEffect(() => { 67 + function handleKey(e: KeyboardEvent) { 68 + if (e.key === "Escape") window.history.back(); 69 + if (e.key === "ArrowRight") 70 + nextprev({ index, images, navigate, did, rkey }); 71 + //onNavigate((index + 1) % images.length); 72 + if (e.key === "ArrowLeft") 73 + nextprev({ index, images, navigate, did, rkey, prev: true }); 74 + //onNavigate((index - 1 + images.length) % images.length); 75 + } 76 + window.addEventListener("keydown", handleKey); 77 + return () => window.removeEventListener("keydown", handleKey); 78 + }, [index, navigate, did, rkey, images]); 79 + 80 + return createPortal( 81 + <> 82 + {post && ( 83 + <div 84 + onClick={(e) => { 85 + e.stopPropagation(); 86 + e.nativeEvent.stopImmediatePropagation(); 87 + }} 88 + className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white" 89 + > 90 + <ProfilePostComponent 91 + key={`/profile/${did}/post/${rkey}`} 92 + did={did} 93 + rkey={rkey} 94 + nopics 95 + lightboxCallback={lightboxCallback} 96 + /> 97 + </div> 98 + )} 99 + <div 100 + className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]" 101 + onClick={(e) => { 102 + e.stopPropagation(); 103 + window.history.back(); 104 + }} 105 + > 106 + <img 107 + src={image?.src} 108 + alt={image?.alt} 109 + className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg" 110 + onClick={(e) => e.stopPropagation()} 111 + /> 112 + 113 + {(images?.length ?? 0) > 1 && ( 114 + <> 115 + <button 116 + onClick={(e) => { 117 + e.stopPropagation(); 118 + nextprev({ index, images, navigate, did, rkey, prev: true }); 119 + }} 120 + className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 121 + > 122 + <svg 123 + xmlns="http://www.w3.org/2000/svg" 124 + width={28} 125 + height={28} 126 + viewBox="0 0 24 24" 127 + > 128 + <g fill="none" fillRule="evenodd"> 129 + <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 130 + <path 131 + fill="currentColor" 132 + d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z" 133 + ></path> 134 + </g> 135 + </svg> 136 + </button> 137 + <button 138 + onClick={(e) => { 139 + e.stopPropagation(); 140 + nextprev({ index, images, navigate, did, rkey }); 141 + }} 142 + className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center" 143 + > 144 + <svg 145 + xmlns="http://www.w3.org/2000/svg" 146 + width={28} 147 + height={28} 148 + viewBox="0 0 24 24" 149 + > 150 + <g fill="none" fillRule="evenodd"> 151 + <path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path> 152 + <path 153 + fill="currentColor" 154 + d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z" 155 + ></path> 156 + </g> 157 + </svg> 158 + </button> 159 + </> 160 + )} 161 + </div> 162 + </>, 163 + document.body 164 + ); 165 + }
+15 -13
src/routes/profile.$did/post.$rkey.tsx
··· 1 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute } from "@tanstack/react-router"; 2 + import { createFileRoute, Outlet } from "@tanstack/react-router"; 3 3 import React, { useLayoutEffect } from "react"; 4 4 5 5 import { Header } from "~/components/Header"; ··· 11 11 useQueryIdentity, 12 12 useQueryPost, 13 13 } from "~/utils/useQuery"; 14 + 15 + import type { LightboxProps } from "./post.$rkey.image.$i"; 14 16 15 17 //const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 16 18 ··· 37 39 did, 38 40 rkey, 39 41 nopics, 42 + lightboxCallback 40 43 }: { 41 44 did: string; 42 45 rkey: string; 43 - nopics?: () => void; 46 + nopics?: boolean; 47 + lightboxCallback?: (d:LightboxProps) => void; 44 48 }) { 45 49 //const { get, set } = usePersistentStore(); 46 50 const queryClient = useQueryClient(); ··· 297 301 298 302 return ( 299 303 <> 304 + <Outlet /> 300 305 <Header 301 306 title={`Post`} 302 - backButtonCallback={ 303 - nopics 304 - ? nopics 305 - : () => { 306 - if (window.history.length > 1) { 307 - window.history.back(); 308 - } else { 309 - window.location.assign("/"); 310 - } 311 - } 312 - } 307 + backButtonCallback={() => { 308 + if (window.history.length > 1) { 309 + window.history.back(); 310 + } else { 311 + window.location.assign("/"); 312 + } 313 + }} 313 314 /> 314 315 315 316 {parentsLoading && ( ··· 342 343 detailed={true} 343 344 topReplyLine={parentsLoading || parents.length > 0} 344 345 nopics={!!nopics} 346 + lightboxCallback={lightboxCallback} 345 347 /> 346 348 </div> 347 349 <div