an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
92
fork

Configure Feed

Select the types of activity you want to include in your feed.

image lightbox

rimar1337 268124d6 c64e32b7

+148 -7
+1 -1
src/components/PostError.tsx
··· 1 - import { ErrorComponent, ErrorComponentProps } from "@tanstack/react-router"; 1 + import { ErrorComponent, type ErrorComponentProps } from "@tanstack/react-router"; 2 2 3 3 export function PostErrorComponent({ error }: ErrorComponentProps) { 4 4 return <ErrorComponent error={error} />;
+144 -3
src/components/UniversalPostRenderer.tsx
··· 1167 1167 // const agent = new AtpAgent({ 1168 1168 // service: 'https://public.api.bsky.app' 1169 1169 // }) 1170 + type HitSlopButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { 1171 + hitSlop?: number; 1172 + }; 1173 + 1174 + const HitSlopButtonCustom: React.FC<HitSlopButtonProps> = ({ 1175 + children, 1176 + hitSlop = 8, 1177 + style, 1178 + ...rest 1179 + }) => ( 1180 + <button 1181 + {...rest} 1182 + style={{ 1183 + position: "relative", 1184 + background: "none", 1185 + border: "none", 1186 + padding: 0, 1187 + cursor: "pointer", 1188 + ...style, 1189 + }} 1190 + > 1191 + {/* Invisible hit slop area */} 1192 + <span 1193 + style={{ 1194 + position: "absolute", 1195 + top: -hitSlop, 1196 + left: -hitSlop, 1197 + right: -hitSlop, 1198 + bottom: -hitSlop, 1199 + }} 1200 + /> 1201 + {/* Actual button content stays positioned normally */} 1202 + <span style={{ position: "relative", zIndex: 1 }}>{children}</span> 1203 + </button> 1204 + ); 1170 1205 1171 1206 const HitSlopButton = ({ 1172 1207 onClick, 1173 1208 children, 1174 1209 style = {}, 1175 - }: { 1210 + ...rest 1211 + }: React.HTMLAttributes<HTMLSpanElement> & { 1176 1212 onClick?: (e: React.MouseEvent) => void; 1177 1213 children: React.ReactNode; 1178 1214 style?: React.CSSProperties; ··· 1201 1237 zIndex: 1, 1202 1238 pointerEvents: "none", 1203 1239 }} 1240 + {...rest} 1204 1241 > 1205 1242 {children} 1206 1243 </span> ··· 1350 1387 //boxShadow: "0 2px 8px rgba(0,0,0,0.04)", 1351 1388 position: "relative", 1352 1389 // dont cursor: "pointer", 1353 - borderBottomWidth: bottomBorder ? 1 : 0, 1390 + borderBottomWidth: bottomBorder ? isQuote ? 0 : 1 : 0, 1354 1391 }} 1355 1392 className="border-gray-300 dark:border-gray-600" 1356 1393 > ··· 1577 1614 <div 1578 1615 style={{ 1579 1616 fontSize: 16, 1580 - marginBottom: 8, 1617 + marginBottom: (!post.embed && !expanded) ? 0 : 8, 1581 1618 whiteSpace: "pre-wrap", 1582 1619 textAlign: "left", 1583 1620 overflowWrap: "anywhere", ··· 1799 1836 salt: string; 1800 1837 navigate: ({}: any) => void; 1801 1838 }) { 1839 + const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1802 1840 if ( 1803 1841 AppBskyEmbedRecordWithMedia.isView(embed) && 1804 1842 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 2000 2038 if (AppBskyEmbedImages.isView(embed)) { 2001 2039 const { images } = embed; 2002 2040 2041 + const lightboxImages = images.map((img) => ({ 2042 + src: img.fullsize, 2043 + alt: img.alt, 2044 + })); 2045 + 2046 + 2003 2047 if (images.length > 0) { 2004 2048 // const items = embed.images.map(img => ({ 2005 2049 // uri: img.fullsize, ··· 2030 2074 }} 2031 2075 className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900" 2032 2076 > 2077 + {lightboxIndex !== null && ( 2078 + <Lightbox 2079 + images={lightboxImages} 2080 + index={lightboxIndex} 2081 + onClose={() => setLightboxIndex(null)} 2082 + onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2083 + /> 2084 + )} 2033 2085 <img 2034 2086 src={image.fullsize} 2035 2087 alt={image.alt} ··· 2038 2090 height: "100%", 2039 2091 objectFit: "contain", // letterbox or scale to fit 2040 2092 }} 2093 + onClick={(e) => {e.stopPropagation();setLightboxIndex(0)}} 2041 2094 /> 2042 2095 </div> 2043 2096 </div> ··· 2058 2111 }} 2059 2112 className="border border-gray-200 dark:border-gray-700" 2060 2113 > 2114 + {lightboxIndex !== null && ( 2115 + <Lightbox 2116 + images={lightboxImages} 2117 + index={lightboxIndex} 2118 + onClose={() => setLightboxIndex(null)} 2119 + onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2120 + /> 2121 + )} 2061 2122 {images.map((img, i) => ( 2062 2123 <div 2063 2124 key={i} ··· 2072 2133 objectFit: "cover", 2073 2134 borderRadius: i === 0 ? "12px 0 0 12px" : "0 12px 12px 0", 2074 2135 }} 2136 + onClick={(e) => {e.stopPropagation();setLightboxIndex(i)}} 2075 2137 /> 2076 2138 </div> 2077 2139 ))} ··· 2095 2157 }} 2096 2158 className="border border-gray-200 dark:border-gray-700" 2097 2159 > 2160 + {lightboxIndex !== null && ( 2161 + <Lightbox 2162 + images={lightboxImages} 2163 + index={lightboxIndex} 2164 + onClose={() => setLightboxIndex(null)} 2165 + onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2166 + /> 2167 + )} 2098 2168 {/* Left: 1:1 */} 2099 2169 <div 2100 2170 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2108 2178 objectFit: "cover", 2109 2179 borderRadius: "12px 0 0 12px", 2110 2180 }} 2181 + onClick={(e) => {e.stopPropagation();setLightboxIndex(0)}} 2111 2182 /> 2112 2183 </div> 2113 2184 {/* Right: two stacked 2:1 */} ··· 2137 2208 objectFit: "cover", 2138 2209 borderRadius: i === 1 ? "0 12px 0 0" : "0 0 12px 0", 2139 2210 }} 2211 + onClick={(e) => {e.stopPropagation();setLightboxIndex(i+1)}} 2140 2212 /> 2141 2213 </div> 2142 2214 ))} ··· 2163 2235 }} 2164 2236 className="border border-gray-200 dark:border-gray-700" 2165 2237 > 2238 + {lightboxIndex !== null && ( 2239 + <Lightbox 2240 + images={lightboxImages} 2241 + index={lightboxIndex} 2242 + onClose={() => setLightboxIndex(null)} 2243 + onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2244 + /> 2245 + )} 2166 2246 {images.map((img, i) => ( 2167 2247 <div 2168 2248 key={i} ··· 2189 2269 ? "0 0 0 12px" 2190 2270 : "0 0 12px 0", 2191 2271 }} 2272 + onClick={(e) => {e.stopPropagation();setLightboxIndex(i)}} 2192 2273 /> 2193 2274 </div> 2194 2275 ))} ··· 2248 2329 2249 2330 return <div />; 2250 2331 } 2332 + 2333 + import { createPortal } from "react-dom"; 2334 + type LightboxProps = { 2335 + images: { src: string; alt?: string }[]; 2336 + index: number; 2337 + onClose: () => void; 2338 + onNavigate?: (newIndex: number) => void; 2339 + }; 2340 + export function Lightbox({ images, index, onClose, onNavigate }: LightboxProps) { 2341 + const image = images[index]; 2342 + 2343 + useEffect(() => { 2344 + function handleKey(e: KeyboardEvent) { 2345 + if (e.key === "Escape") onClose(); 2346 + if (e.key === "ArrowRight" && onNavigate) onNavigate((index + 1) % images.length); 2347 + if (e.key === "ArrowLeft" && onNavigate) onNavigate((index - 1 + images.length) % images.length); 2348 + } 2349 + window.addEventListener("keydown", handleKey); 2350 + return () => window.removeEventListener("keydown", handleKey); 2351 + }, [index, images.length, onClose, onNavigate]); 2352 + 2353 + return createPortal( 2354 + <div 2355 + className="fixed inset-0 z-50 flex items-center justify-center bg-black/80" 2356 + onClick={(e)=>{e.stopPropagation();onClose()}} 2357 + > 2358 + <img 2359 + src={image.src} 2360 + alt={image.alt} 2361 + className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg" 2362 + onClick={(e) => e.stopPropagation()} 2363 + /> 2364 + 2365 + {images.length > 1 && ( 2366 + <> 2367 + <button 2368 + onClick={(e) => { 2369 + e.stopPropagation(); 2370 + onNavigate?.((index - 1 + images.length) % images.length); 2371 + }} 2372 + 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" 2373 + > 2374 + <svg xmlns="http://www.w3.org/2000/svg" width={28} height={28} viewBox="0 0 24 24"><g fill="none" fillRule="evenodd"><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><path fill="currentColor" 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"></path></g></svg> 2375 + </button> 2376 + <button 2377 + onClick={(e) => { 2378 + e.stopPropagation(); 2379 + onNavigate?.((index + 1) % images.length); 2380 + }} 2381 + 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" 2382 + > 2383 + <svg xmlns="http://www.w3.org/2000/svg" width={28} height={28} viewBox="0 0 24 24"><g fill="none" fillRule="evenodd"><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><path fill="currentColor" 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"></path></g></svg> 2384 + </button> 2385 + </> 2386 + )} 2387 + </div>, 2388 + document.body 2389 + ); 2390 + } 2391 + 2251 2392 function getDomain(url: string) { 2252 2393 try { 2253 2394 const { hostname } = new URL(url);
+1 -1
src/components/UserError.tsx
··· 1 - import { ErrorComponent, ErrorComponentProps } from "@tanstack/react-router"; 1 + import { ErrorComponent, type ErrorComponentProps } from "@tanstack/react-router"; 2 2 3 3 export function UserErrorComponent({ error }: ErrorComponentProps) { 4 4 return <ErrorComponent error={error} />;
+2 -2
src/routes/__root.tsx
··· 329 329 /> 330 330 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 331 331 Red Dwarf{" "} 332 - <span className="text-gray-500 dark:text-gray-400 text-sm"> 332 + {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 333 333 lite 334 - </span> 334 + </span> */} 335 335 </span> 336 336 </div> 337 337 <div className="flex items-center gap-2">