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

routeless lightbox sidebars

rimar1337 55f010da 833207ed

Changed files
+534 -432
src
components
routes
profile.$did
styles
+471 -400
src/components/UniversalPostRenderer.tsx
··· 2 2 import { useAtom } from "jotai"; 3 3 import * as React from "react"; 4 4 import { type SVGProps } from "react"; 5 + import { createPortal } from "react-dom"; 5 6 7 + import { ProfilePostComponent } from "~/routes/profile.$did/post.$rkey"; 6 8 import { likedPostsAtom } from "~/utils/atoms"; 7 9 import { useHydratedEmbed } from "~/utils/useHydrated"; 8 10 import { ··· 28 30 bottomBorder?: boolean; 29 31 feedviewpost?: boolean; 30 32 repostedby?: string; 33 + style?: React.CSSProperties; 34 + ref?: React.Ref<HTMLDivElement>; 35 + dataIndexPropPass?: number; 36 + nopics?: boolean; 31 37 } 32 38 33 39 // export async function cachedGetRecord({ ··· 132 138 bottomBorder = true, 133 139 feedviewpost = false, 134 140 repostedby, 141 + style, 142 + ref, 143 + dataIndexPropPass, 144 + nopics, 135 145 }: UniversalPostRendererATURILoaderProps) { 136 146 // /*mass comment*/ console.log("atUri", atUri); 137 147 //const { get, set } = usePersistentStore(); ··· 406 416 bottomBorder={bottomBorder} 407 417 feedviewpost={feedviewpost} 408 418 repostedby={repostedby} 419 + style={style} 420 + ref={ref} 421 + dataIndexPropPass={dataIndexPropPass} 422 + nopics={nopics} 409 423 /> 410 424 ); 411 425 } ··· 430 444 bottomBorder = true, 431 445 feedviewpost = false, 432 446 repostedby, 447 + style, 448 + ref, 449 + dataIndexPropPass, 450 + nopics, 433 451 }: { 434 452 postRecord: any; 435 453 profileRecord: any; ··· 444 462 bottomBorder?: boolean; 445 463 feedviewpost?: boolean; 446 464 repostedby?: string; 465 + style?: React.CSSProperties; 466 + ref?: React.Ref<HTMLDivElement>; 467 + dataIndexPropPass?: number; 468 + nopics?: boolean; 447 469 }) { 448 470 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 449 471 const navigate = useNavigate(); ··· 638 660 //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} 639 661 feedviewpostreplyhandle={feedviewpostreplyhandle} 640 662 repostedby={feedviewpostrepostedbyhandle} 663 + style={style} 664 + ref={ref} 665 + dataIndexPropPass={dataIndexPropPass} 666 + nopics={nopics} 641 667 /> 642 668 </> 643 669 ); ··· 1079 1105 feedviewpostreplyhandle, 1080 1106 depth = 0, 1081 1107 repostedby, 1108 + style, 1109 + ref, 1110 + dataIndexPropPass, 1111 + nopics, 1082 1112 }: { 1083 1113 post: PostView; 1084 1114 // optional for now because i havent ported every use to this yet ··· 1098 1128 feedviewpostreplyhandle?: string; 1099 1129 depth?: number; 1100 1130 repostedby?: string; 1131 + style?: React.CSSProperties; 1132 + ref?: React.Ref<HTMLDivElement>; 1133 + dataIndexPropPass?: number; 1134 + nopics?: boolean; 1101 1135 }) { 1136 + const parsed = new AtUri(post.uri); 1102 1137 const navigate = useNavigate(); 1103 1138 const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom); 1104 1139 const [hasRetweeted, setHasRetweeted] = useState<boolean>( ··· 1171 1206 /* fuck you */ 1172 1207 const isMainItem = false; 1173 1208 const setMainItem = (any: any) => {}; 1209 + // eslint-disable-next-line react-hooks/refs 1210 + console.log("Received ref in UniversalPostRenderer:", ref); 1174 1211 return ( 1175 - <div 1176 - key={salt + "-" + (post.uri || emergencySalt)} 1177 - onClick={ 1178 - isMainItem 1179 - ? onPostClick 1180 - : setMainItem 1181 - ? onPostClick 1182 - ? (e) => { 1183 - setMainItem({ post: post }); 1184 - onPostClick(e); 1185 - } 1186 - : () => { 1187 - setMainItem({ post: post }); 1188 - } 1189 - : undefined 1190 - } 1191 - style={{ 1192 - //border: "1px solid #e1e8ed", 1193 - //borderRadius: 12, 1194 - opacity: "1 !important", 1195 - background: "transparent", 1196 - paddingLeft: isQuote ? 12 : 16, 1197 - paddingRight: isQuote ? 12 : 16, 1198 - //paddingTop: 16, 1199 - paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1200 - //paddingBottom: bottomReplyLine ? 0 : 16, 1201 - paddingBottom: 0, 1202 - fontFamily: "system-ui, sans-serif", 1203 - //boxShadow: "0 2px 8px rgba(0,0,0,0.04)", 1204 - position: "relative", 1205 - // dont cursor: "pointer", 1206 - borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, 1207 - }} 1208 - className="border-gray-300 dark:border-gray-600" 1209 - > 1210 - {isRepost && ( 1211 - <div 1212 - style={{ 1213 - marginLeft: 36, 1214 - display: "flex", 1215 - borderRadius: 12, 1216 - paddingBottom: "calc(22px - 1rem)", 1217 - fontSize: 14, 1218 - maxHeight: "1rem", 1219 - justifyContent: "flex-start", 1220 - //color: theme.textSecondary, 1221 - gap: 4, 1222 - alignItems: "center", 1223 - }} 1224 - className="text-gray-500 dark:text-gray-400" 1225 - > 1226 - <MdiRepost /> Reposted by @{isRepost}{" "} 1227 - </div> 1228 - )} 1229 - {!isQuote && ( 1230 - <div 1231 - style={{ 1232 - opacity: 1233 - topReplyLine || isReply /*&& (true || expanded)*/ ? 0.5 : 0, 1234 - position: "absolute", 1235 - top: 0, 1236 - left: 36, // why 36 ??? 1237 - //left: 16 + (42 / 2), 1238 - width: 2, 1239 - //height: "100%", 1240 - height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1241 - // background: theme.textSecondary, 1242 - //opacity: 0.5, 1243 - // no flex here 1244 - }} 1245 - className="bg-gray-500 dark:bg-gray-400" 1246 - /> 1247 - )} 1212 + <div ref={ref} style={style} data-index={dataIndexPropPass}> 1248 1213 <div 1214 + //ref={ref} 1215 + key={salt + "-" + (post.uri || emergencySalt)} 1216 + onClick={ 1217 + isMainItem 1218 + ? onPostClick 1219 + : setMainItem 1220 + ? onPostClick 1221 + ? (e) => { 1222 + setMainItem({ post: post }); 1223 + onPostClick(e); 1224 + } 1225 + : () => { 1226 + setMainItem({ post: post }); 1227 + } 1228 + : undefined 1229 + } 1249 1230 style={{ 1250 - position: "absolute", 1251 - //top: isRepost ? "calc(16px + 1rem)" : 16, 1252 - //left: 16, 1253 - zIndex: 1, 1254 - top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1255 - left: isQuote ? 12 : 16, 1231 + //...style, 1232 + //border: "1px solid #e1e8ed", 1233 + //borderRadius: 12, 1234 + opacity: "1 !important", 1235 + background: "transparent", 1236 + paddingLeft: isQuote ? 12 : 16, 1237 + paddingRight: isQuote ? 12 : 16, 1238 + //paddingTop: 16, 1239 + paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1240 + //paddingBottom: bottomReplyLine ? 0 : 16, 1241 + paddingBottom: 0, 1242 + fontFamily: "system-ui, sans-serif", 1243 + //boxShadow: "0 2px 8px rgba(0,0,0,0.04)", 1244 + position: "relative", 1245 + // dont cursor: "pointer", 1246 + borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, 1256 1247 }} 1257 - onClick={onProfileClick} 1248 + className="border-gray-300 dark:border-gray-600" 1258 1249 > 1259 - <img 1260 - src={post.author.avatar || defaultpfp} 1261 - alt="avatar" 1262 - // transition={{ 1263 - // type: "spring", 1264 - // stiffness: 260, 1265 - // damping: 20, 1266 - // }} 1267 - style={{ 1268 - borderRadius: "50%", 1269 - marginRight: 12, 1270 - objectFit: "cover", 1271 - //background: theme.border, 1272 - //border: `1px solid ${theme.border}`, 1273 - width: isQuote ? 16 : 42, 1274 - height: isQuote ? 16 : 42, 1275 - }} 1276 - className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600" 1277 - /> 1278 - </div> 1279 - <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1250 + {isRepost && ( 1251 + <div 1252 + style={{ 1253 + marginLeft: 36, 1254 + display: "flex", 1255 + borderRadius: 12, 1256 + paddingBottom: "calc(22px - 1rem)", 1257 + fontSize: 14, 1258 + maxHeight: "1rem", 1259 + justifyContent: "flex-start", 1260 + //color: theme.textSecondary, 1261 + gap: 4, 1262 + alignItems: "center", 1263 + }} 1264 + className="text-gray-500 dark:text-gray-400" 1265 + > 1266 + <MdiRepost /> Reposted by @{isRepost}{" "} 1267 + </div> 1268 + )} 1269 + {!isQuote && ( 1270 + <div 1271 + style={{ 1272 + opacity: 1273 + topReplyLine || isReply /*&& (true || expanded)*/ ? 0.5 : 0, 1274 + position: "absolute", 1275 + top: 0, 1276 + left: 36, // why 36 ??? 1277 + //left: 16 + (42 / 2), 1278 + width: 2, 1279 + //height: "100%", 1280 + height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1281 + // background: theme.textSecondary, 1282 + //opacity: 0.5, 1283 + // no flex here 1284 + }} 1285 + className="bg-gray-500 dark:bg-gray-400" 1286 + /> 1287 + )} 1280 1288 <div 1281 1289 style={{ 1282 - display: "flex", 1283 - flexDirection: "column", 1284 - alignSelf: "stretch", 1285 - alignItems: "center", 1286 - overflow: "hidden", 1287 - width: expanded || isQuote ? 0 : "auto", 1288 - marginRight: expanded || isQuote ? 0 : 12, 1290 + position: "absolute", 1291 + //top: isRepost ? "calc(16px + 1rem)" : 16, 1292 + //left: 16, 1293 + zIndex: 1, 1294 + top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1295 + left: isQuote ? 12 : 16, 1289 1296 }} 1297 + onClick={onProfileClick} 1290 1298 > 1291 - {/* dummy for later use */} 1292 - <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1293 - {/* reply line !!!! bottomReplyLine */} 1294 - {bottomReplyLine && ( 1295 - <div 1296 - style={{ 1297 - width: 2, 1298 - height: "100%", 1299 - //background: theme.textSecondary, 1300 - opacity: 0.5, 1301 - // no flex here 1302 - //color: "Red", 1303 - //zIndex: 99 1304 - }} 1305 - className="bg-gray-500 dark:bg-gray-400" 1306 - /> 1307 - )} 1308 - {/* <div 1299 + <img 1300 + src={post.author.avatar || defaultpfp} 1301 + alt="avatar" 1302 + // transition={{ 1303 + // type: "spring", 1304 + // stiffness: 260, 1305 + // damping: 20, 1306 + // }} 1307 + style={{ 1308 + borderRadius: "50%", 1309 + marginRight: 12, 1310 + objectFit: "cover", 1311 + //background: theme.border, 1312 + //border: `1px solid ${theme.border}`, 1313 + width: isQuote ? 16 : 42, 1314 + height: isQuote ? 16 : 42, 1315 + }} 1316 + className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600" 1317 + /> 1318 + </div> 1319 + <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1320 + <div 1321 + style={{ 1322 + display: "flex", 1323 + flexDirection: "column", 1324 + alignSelf: "stretch", 1325 + alignItems: "center", 1326 + overflow: "hidden", 1327 + width: expanded || isQuote ? 0 : "auto", 1328 + marginRight: expanded || isQuote ? 0 : 12, 1329 + }} 1330 + > 1331 + {/* dummy for later use */} 1332 + <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1333 + {/* reply line !!!! bottomReplyLine */} 1334 + {bottomReplyLine && ( 1335 + <div 1336 + style={{ 1337 + width: 2, 1338 + height: "100%", 1339 + //background: theme.textSecondary, 1340 + opacity: 0.5, 1341 + // no flex here 1342 + //color: "Red", 1343 + //zIndex: 99 1344 + }} 1345 + className="bg-gray-500 dark:bg-gray-400" 1346 + /> 1347 + )} 1348 + {/* <div 1309 1349 layout 1310 1350 transition={{ duration: 0.2 }} 1311 1351 animate={{ height: expanded ? 0 : '100%' }} ··· 1315 1355 // no flex here 1316 1356 }} 1317 1357 /> */} 1318 - </div> 1319 - <div style={{ flex: 1, maxWidth: "100%" }}> 1320 - <div 1321 - style={{ 1322 - display: "flex", 1323 - flexDirection: "row", 1324 - alignItems: "center", 1325 - flexWrap: "nowrap", 1326 - maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1327 - width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1328 - marginLeft: !expanded ? (isQuote ? 26 : 0) : 54, 1329 - marginBottom: !expanded ? 4 : 6, 1330 - }} 1331 - > 1358 + </div> 1359 + <div style={{ flex: 1, maxWidth: "100%" }}> 1332 1360 <div 1333 1361 style={{ 1334 1362 display: "flex", 1335 - //overflow: "hidden", // hey why is overflow hidden unapplied 1336 - overflow: "hidden", 1337 - textOverflow: "ellipsis", 1338 - flexShrink: 1, 1339 - flexGrow: 1, 1340 - flexBasis: 0, 1341 - width: 0, 1342 - gap: expanded ? 0 : 6, 1343 - alignItems: expanded ? "flex-start" : "center", 1344 - flexDirection: expanded ? "column" : "row", 1345 - height: expanded ? 42 : "1rem", 1363 + flexDirection: "row", 1364 + alignItems: "center", 1365 + flexWrap: "nowrap", 1366 + maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1367 + width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`, 1368 + marginLeft: !expanded ? (isQuote ? 26 : 0) : 54, 1369 + marginBottom: !expanded ? 4 : 6, 1346 1370 }} 1347 1371 > 1348 - <span 1372 + <div 1349 1373 style={{ 1350 1374 display: "flex", 1351 - fontWeight: 700, 1352 - fontSize: 16, 1375 + //overflow: "hidden", // hey why is overflow hidden unapplied 1353 1376 overflow: "hidden", 1354 1377 textOverflow: "ellipsis", 1355 - whiteSpace: "nowrap", 1356 1378 flexShrink: 1, 1357 - minWidth: 0, 1358 - gap: 4, 1359 - alignItems: "center", 1360 - //color: theme.text, 1379 + flexGrow: 1, 1380 + flexBasis: 0, 1381 + width: 0, 1382 + gap: expanded ? 0 : 6, 1383 + alignItems: expanded ? "flex-start" : "center", 1384 + flexDirection: expanded ? "column" : "row", 1385 + height: expanded ? 42 : "1rem", 1361 1386 }} 1362 - className="text-gray-900 dark:text-gray-100" 1363 1387 > 1364 - {/* verified checkmark */} 1365 - {post.author.displayName || post.author.handle}{" "} 1366 - {post.author.verification?.verifiedStatus == "valid" && ( 1367 - <MdiVerified /> 1368 - )} 1369 - </span> 1388 + <span 1389 + style={{ 1390 + display: "flex", 1391 + fontWeight: 700, 1392 + fontSize: 16, 1393 + overflow: "hidden", 1394 + textOverflow: "ellipsis", 1395 + whiteSpace: "nowrap", 1396 + flexShrink: 1, 1397 + minWidth: 0, 1398 + gap: 4, 1399 + alignItems: "center", 1400 + //color: theme.text, 1401 + }} 1402 + className="text-gray-900 dark:text-gray-100" 1403 + > 1404 + {/* verified checkmark */} 1405 + {post.author.displayName || post.author.handle}{" "} 1406 + {post.author.verification?.verifiedStatus == "valid" && ( 1407 + <MdiVerified /> 1408 + )} 1409 + </span> 1370 1410 1371 - <span 1411 + <span 1412 + style={{ 1413 + //color: theme.textSecondary, 1414 + fontSize: 16, 1415 + overflowX: "hidden", 1416 + textOverflow: "ellipsis", 1417 + whiteSpace: "nowrap", 1418 + flexShrink: 1, 1419 + flexGrow: 0, 1420 + minWidth: 0, 1421 + }} 1422 + className="text-gray-500 dark:text-gray-400" 1423 + > 1424 + @{post.author.handle} 1425 + </span> 1426 + </div> 1427 + <div 1372 1428 style={{ 1373 - //color: theme.textSecondary, 1374 - fontSize: 16, 1375 - overflowX: "hidden", 1376 - textOverflow: "ellipsis", 1377 - whiteSpace: "nowrap", 1378 - flexShrink: 1, 1379 - flexGrow: 0, 1380 - minWidth: 0, 1429 + display: "flex", 1430 + alignItems: "center", 1431 + height: "1rem", 1381 1432 }} 1382 - className="text-gray-500 dark:text-gray-400" 1383 1433 > 1384 - @{post.author.handle} 1385 - </span> 1434 + <span 1435 + style={{ 1436 + //color: theme.textSecondary, 1437 + fontSize: 16, 1438 + marginLeft: 8, 1439 + whiteSpace: "nowrap", 1440 + flexShrink: 0, 1441 + maxWidth: "100%", 1442 + }} 1443 + className="text-gray-500 dark:text-gray-400" 1444 + > 1445 + · {/* time placeholder */} 1446 + {shortTimeAgo(post.indexedAt)} 1447 + </span> 1448 + </div> 1386 1449 </div> 1387 - <div 1388 - style={{ 1389 - display: "flex", 1390 - alignItems: "center", 1391 - height: "1rem", 1392 - }} 1393 - > 1394 - <span 1450 + {/* reply indicator */} 1451 + {!!feedviewpostreplyhandle && ( 1452 + <div 1395 1453 style={{ 1454 + display: "flex", 1455 + borderRadius: 12, 1456 + paddingBottom: 2, 1457 + fontSize: 14, 1458 + justifyContent: "flex-start", 1396 1459 //color: theme.textSecondary, 1397 - fontSize: 16, 1398 - marginLeft: 8, 1399 - whiteSpace: "nowrap", 1400 - flexShrink: 0, 1401 - maxWidth: "100%", 1460 + gap: 4, 1461 + alignItems: "center", 1462 + //marginLeft: 36, 1463 + height: 1464 + !(expanded || isQuote) && !!feedviewpostreplyhandle 1465 + ? "1rem" 1466 + : 0, 1467 + opacity: 1468 + !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0, 1402 1469 }} 1403 1470 className="text-gray-500 dark:text-gray-400" 1404 1471 > 1405 - · {/* time placeholder */} 1406 - {shortTimeAgo(post.indexedAt)} 1407 - </span> 1408 - </div> 1409 - </div> 1410 - {/* reply indicator */} 1411 - {!!feedviewpostreplyhandle && ( 1472 + <MdiReply /> Reply to @{feedviewpostreplyhandle} 1473 + </div> 1474 + )} 1412 1475 <div 1413 1476 style={{ 1414 - display: "flex", 1415 - borderRadius: 12, 1416 - paddingBottom: 2, 1417 - fontSize: 14, 1418 - justifyContent: "flex-start", 1419 - //color: theme.textSecondary, 1420 - gap: 4, 1421 - alignItems: "center", 1422 - //marginLeft: 36, 1423 - height: 1424 - !(expanded || isQuote) && !!feedviewpostreplyhandle 1425 - ? "1rem" 1426 - : 0, 1427 - opacity: 1428 - !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0, 1477 + fontSize: 16, 1478 + marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8, 1479 + whiteSpace: "pre-wrap", 1480 + textAlign: "left", 1481 + overflowWrap: "anywhere", 1482 + wordBreak: "break-word", 1483 + //color: theme.text, 1429 1484 }} 1430 - className="text-gray-500 dark:text-gray-400" 1485 + className="text-gray-900 dark:text-gray-100" 1431 1486 > 1432 - <MdiReply /> Reply to @{feedviewpostreplyhandle} 1487 + {renderTextWithFacets({ 1488 + text: (post.record as { text?: string }).text ?? "", 1489 + facets: (post.record.facets as Facet[]) ?? [], 1490 + navigate: navigate, 1491 + })} 1492 + {} 1433 1493 </div> 1434 - )} 1435 - <div 1436 - style={{ 1437 - fontSize: 16, 1438 - marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8, 1439 - whiteSpace: "pre-wrap", 1440 - textAlign: "left", 1441 - overflowWrap: "anywhere", 1442 - wordBreak: "break-word", 1443 - //color: theme.text, 1444 - }} 1445 - className="text-gray-900 dark:text-gray-100" 1446 - > 1447 - {renderTextWithFacets({ 1448 - text: (post.record as { text?: string }).text ?? "", 1449 - facets: (post.record.facets as Facet[]) ?? [], 1450 - navigate: navigate, 1451 - })} 1452 - {} 1453 - </div> 1454 - {post.embed && depth < 1 ? ( 1455 - <PostEmbeds 1456 - embed={post.embed} 1457 - //moderation={moderation} 1458 - viewContext={PostEmbedViewContext.Feed} 1459 - salt={salt} 1460 - navigate={navigate} 1461 - /> 1462 - ) : null} 1463 - {post.embed && depth > 0 && ( 1464 - /* pretty bad hack imo. its trying to sync up with how the embed shim doesnt 1494 + {post.embed && depth < 1 ? ( 1495 + <PostEmbeds 1496 + embed={post.embed} 1497 + //moderation={moderation} 1498 + viewContext={PostEmbedViewContext.Feed} 1499 + salt={salt} 1500 + navigate={navigate} 1501 + postid={{ did: post.author.did, rkey: parsed.rkey }} 1502 + nopics={nopics} 1503 + /> 1504 + ) : null} 1505 + {post.embed && depth > 0 && ( 1506 + /* pretty bad hack imo. its trying to sync up with how the embed shim doesnt 1465 1507 hydrate embeds this deep but the connection here is implicit 1466 1508 todo: idk make this a real part of the embed shim so its not implicit */ 1467 - <> 1468 - <div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1469 - (there is an embed here thats too deep to render) 1470 - </div> 1471 - </> 1472 - )} 1473 - <div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}> 1474 - <> 1475 - {expanded && ( 1509 + <> 1510 + <div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1511 + (there is an embed here thats too deep to render) 1512 + </div> 1513 + </> 1514 + )} 1515 + <div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}> 1516 + <> 1517 + {expanded && ( 1518 + <div 1519 + style={{ 1520 + overflow: "hidden", 1521 + //color: theme.textSecondary, 1522 + fontSize: 14, 1523 + display: "flex", 1524 + borderBottomStyle: "solid", 1525 + //borderBottomColor: theme.border, 1526 + //background: "#f00", 1527 + // height: "1rem", 1528 + paddingTop: 4, 1529 + paddingBottom: 8, 1530 + borderBottomWidth: 1, 1531 + marginBottom: 8, 1532 + }} // important for height animation 1533 + className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700" 1534 + > 1535 + {fullDateTimeFormat(post.indexedAt)} 1536 + </div> 1537 + )} 1538 + </> 1539 + {!isQuote && ( 1476 1540 <div 1477 1541 style={{ 1478 - overflow: "hidden", 1479 - //color: theme.textSecondary, 1480 - fontSize: 14, 1481 1542 display: "flex", 1482 - borderBottomStyle: "solid", 1483 - //borderBottomColor: theme.border, 1484 - //background: "#f00", 1485 - // height: "1rem", 1486 - paddingTop: 4, 1487 - paddingBottom: 8, 1488 - borderBottomWidth: 1, 1489 - marginBottom: 8, 1490 - }} // important for height animation 1491 - className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700" 1492 - > 1493 - {fullDateTimeFormat(post.indexedAt)} 1494 - </div> 1495 - )} 1496 - </> 1497 - {!isQuote && ( 1498 - <div 1499 - style={{ 1500 - display: "flex", 1501 - gap: 32, 1502 - paddingTop: 8, 1503 - //color: theme.textSecondary, 1504 - fontSize: 15, 1505 - justifyContent: "space-between", 1506 - //background: "#0f0", 1507 - }} 1508 - className="text-gray-500 dark:text-gray-400" 1509 - > 1510 - <span style={btnstyle}> 1511 - <MdiCommentOutline /> 1512 - {post.replyCount} 1513 - </span> 1514 - <HitSlopButton 1515 - onClick={() => { 1516 - repostOrUnrepostPost(); 1543 + gap: 32, 1544 + paddingTop: 8, 1545 + //color: theme.textSecondary, 1546 + fontSize: 15, 1547 + justifyContent: "space-between", 1548 + //background: "#0f0", 1517 1549 }} 1518 - style={{ 1519 - ...btnstyle, 1520 - ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1521 - }} 1522 - > 1523 - {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1524 - {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1525 - </HitSlopButton> 1526 - <HitSlopButton 1527 - onClick={() => { 1528 - likeOrUnlikePost(); 1529 - }} 1530 - style={{ 1531 - ...btnstyle, 1532 - ...(hasLiked ? { color: "#EC4899" } : {}), 1533 - }} 1550 + className="text-gray-500 dark:text-gray-400" 1534 1551 > 1535 - {hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />} 1536 - {(post.likeCount || 0) + (hasLiked ? 1 : 0)} 1537 - </HitSlopButton> 1538 - <div style={{ display: "flex", gap: 8 }}> 1552 + <span style={btnstyle}> 1553 + <MdiCommentOutline /> 1554 + {post.replyCount} 1555 + </span> 1556 + <HitSlopButton 1557 + onClick={() => { 1558 + repostOrUnrepostPost(); 1559 + }} 1560 + style={{ 1561 + ...btnstyle, 1562 + ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1563 + }} 1564 + > 1565 + {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1566 + {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1567 + </HitSlopButton> 1539 1568 <HitSlopButton 1540 - onClick={async (e) => { 1541 - e.stopPropagation(); 1542 - try { 1543 - await navigator.clipboard.writeText( 1544 - "https://bsky.app" + 1545 - "/profile/" + 1546 - post.author.handle + 1547 - "/post/" + 1548 - post.uri.split("/").pop() 1549 - ); 1550 - } catch (_e) { 1551 - // idk 1552 - } 1569 + onClick={() => { 1570 + likeOrUnlikePost(); 1553 1571 }} 1554 1572 style={{ 1555 1573 ...btnstyle, 1574 + ...(hasLiked ? { color: "#EC4899" } : {}), 1556 1575 }} 1557 1576 > 1558 - <MdiShareVariant /> 1577 + {hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />} 1578 + {(post.likeCount || 0) + (hasLiked ? 1 : 0)} 1559 1579 </HitSlopButton> 1560 - <span style={btnstyle}> 1561 - <MdiMoreHoriz /> 1562 - </span> 1580 + <div style={{ display: "flex", gap: 8 }}> 1581 + <HitSlopButton 1582 + onClick={async (e) => { 1583 + e.stopPropagation(); 1584 + try { 1585 + await navigator.clipboard.writeText( 1586 + "https://bsky.app" + 1587 + "/profile/" + 1588 + post.author.handle + 1589 + "/post/" + 1590 + post.uri.split("/").pop() 1591 + ); 1592 + } catch (_e) { 1593 + // idk 1594 + } 1595 + }} 1596 + style={{ 1597 + ...btnstyle, 1598 + }} 1599 + > 1600 + <MdiShareVariant /> 1601 + </HitSlopButton> 1602 + <span style={btnstyle}> 1603 + <MdiMoreHoriz /> 1604 + </span> 1605 + </div> 1563 1606 </div> 1564 - </div> 1565 - )} 1607 + )} 1608 + </div> 1609 + <div 1610 + style={{ 1611 + //height: bottomReplyLine ? 16 : 0 1612 + height: isQuote ? 12 : 16, 1613 + }} 1614 + /> 1566 1615 </div> 1567 - <div 1568 - style={{ 1569 - //height: bottomReplyLine ? 16 : 0 1570 - height: isQuote ? 12 : 16, 1571 - }} 1572 - /> 1573 1616 </div> 1574 1617 </div> 1575 1618 </div> ··· 1661 1704 viewContext, 1662 1705 salt, 1663 1706 navigate, 1707 + postid, 1708 + nopics, 1664 1709 }: { 1665 1710 embed?: Embed; 1666 1711 moderation?: ModerationDecision; ··· 1669 1714 viewContext?: PostEmbedViewContext; 1670 1715 salt: string; 1671 1716 navigate: (_: any) => void; 1717 + postid?: { did: string; rkey: string }; 1718 + nopics?: boolean; 1672 1719 }) { 1673 1720 const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1674 1721 if ( ··· 1704 1751 viewContext={viewContext} 1705 1752 salt={salt} 1706 1753 navigate={navigate} 1754 + postid={postid} 1755 + nopics={nopics} 1707 1756 /> 1708 1757 {/* padding empty div of 8px height */} 1709 1758 <div style={{ height: 12 }} /> ··· 1871 1920 1872 1921 // image embed 1873 1922 // = 1874 - if (AppBskyEmbedImages.isView(embed)) { 1923 + if (AppBskyEmbedImages.isView(embed) && !nopics) { 1875 1924 const { images } = embed; 1876 1925 1877 1926 const lightboxImages = images.map((img) => ({ ··· 1915 1964 index={lightboxIndex} 1916 1965 onClose={() => setLightboxIndex(null)} 1917 1966 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1967 + post={postid} 1918 1968 /> 1919 1969 )} 1920 1970 <img ··· 1955 2005 index={lightboxIndex} 1956 2006 onClose={() => setLightboxIndex(null)} 1957 2007 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2008 + post={postid} 1958 2009 /> 1959 2010 )} 1960 2011 {images.map((img, i) => ( ··· 2004 2055 index={lightboxIndex} 2005 2056 onClose={() => setLightboxIndex(null)} 2006 2057 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2058 + post={postid} 2007 2059 /> 2008 2060 )} 2009 2061 {/* Left: 1:1 */} ··· 2088 2140 index={lightboxIndex} 2089 2141 onClose={() => setLightboxIndex(null)} 2090 2142 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2143 + post={postid} 2091 2144 /> 2092 2145 )} 2093 2146 {images.map((img, i) => ( ··· 2180 2233 return <div />; 2181 2234 } 2182 2235 2183 - import { createPortal } from "react-dom"; 2184 2236 type LightboxProps = { 2185 2237 images: { src: string; alt?: string }[]; 2186 2238 index: number; 2187 2239 onClose: () => void; 2188 2240 onNavigate?: (newIndex: number) => void; 2241 + post?: { did: string; rkey: string }; 2189 2242 }; 2190 2243 export function Lightbox({ 2191 2244 images, 2192 2245 index, 2193 2246 onClose, 2194 2247 onNavigate, 2248 + post, 2195 2249 }: LightboxProps) { 2196 2250 const image = images[index]; 2197 2251 ··· 2208 2262 }, [index, images.length, onClose, onNavigate]); 2209 2263 2210 2264 return createPortal( 2211 - <div 2212 - className="fixed inset-0 z-50 flex items-center justify-center bg-black/80" 2213 - onClick={(e) => { 2214 - e.stopPropagation(); 2215 - onClose(); 2216 - }} 2217 - > 2218 - <img 2219 - src={image.src} 2220 - alt={image.alt} 2221 - className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg" 2222 - onClick={(e) => e.stopPropagation()} 2223 - /> 2265 + <> 2266 + {post && ( 2267 + <div 2268 + onClick={(e) => { 2269 + e.stopPropagation(); 2270 + e.nativeEvent.stopImmediatePropagation(); 2271 + }} 2272 + className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-700 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" 2273 + > 2274 + <ProfilePostComponent 2275 + did={post.did} 2276 + rkey={post.rkey} 2277 + nopics={onClose} 2278 + /> 2279 + </div> 2280 + )} 2281 + <div 2282 + className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px-var(--scrollbar-width)*0)] lg:max-w-[calc(100vw-350px-var(--scrollbar-width)*0)]" 2283 + onClick={(e) => { 2284 + e.stopPropagation(); 2285 + onClose(); 2286 + }} 2287 + > 2288 + <img 2289 + src={image.src} 2290 + alt={image.alt} 2291 + className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg" 2292 + onClick={(e) => e.stopPropagation()} 2293 + /> 2224 2294 2225 - {images.length > 1 && ( 2226 - <> 2227 - <button 2228 - onClick={(e) => { 2229 - e.stopPropagation(); 2230 - onNavigate?.((index - 1 + images.length) % images.length); 2231 - }} 2232 - 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" 2233 - > 2234 - <svg 2235 - xmlns="http://www.w3.org/2000/svg" 2236 - width={28} 2237 - height={28} 2238 - viewBox="0 0 24 24" 2295 + {images.length > 1 && ( 2296 + <> 2297 + <button 2298 + onClick={(e) => { 2299 + e.stopPropagation(); 2300 + onNavigate?.((index - 1 + images.length) % images.length); 2301 + }} 2302 + 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" 2239 2303 > 2240 - <g fill="none" fillRule="evenodd"> 2241 - <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> 2242 - <path 2243 - fill="currentColor" 2244 - 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" 2245 - ></path> 2246 - </g> 2247 - </svg> 2248 - </button> 2249 - <button 2250 - onClick={(e) => { 2251 - e.stopPropagation(); 2252 - onNavigate?.((index + 1) % images.length); 2253 - }} 2254 - 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" 2255 - > 2256 - <svg 2257 - xmlns="http://www.w3.org/2000/svg" 2258 - width={28} 2259 - height={28} 2260 - viewBox="0 0 24 24" 2304 + <svg 2305 + xmlns="http://www.w3.org/2000/svg" 2306 + width={28} 2307 + height={28} 2308 + viewBox="0 0 24 24" 2309 + > 2310 + <g fill="none" fillRule="evenodd"> 2311 + <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> 2312 + <path 2313 + fill="currentColor" 2314 + 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" 2315 + ></path> 2316 + </g> 2317 + </svg> 2318 + </button> 2319 + <button 2320 + onClick={(e) => { 2321 + e.stopPropagation(); 2322 + onNavigate?.((index + 1) % images.length); 2323 + }} 2324 + 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" 2261 2325 > 2262 - <g fill="none" fillRule="evenodd"> 2263 - <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> 2264 - <path 2265 - fill="currentColor" 2266 - 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" 2267 - ></path> 2268 - </g> 2269 - </svg> 2270 - </button> 2271 - </> 2272 - )} 2273 - </div>, 2326 + <svg 2327 + xmlns="http://www.w3.org/2000/svg" 2328 + width={28} 2329 + height={28} 2330 + viewBox="0 0 24 24" 2331 + > 2332 + <g fill="none" fillRule="evenodd"> 2333 + <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> 2334 + <path 2335 + fill="currentColor" 2336 + 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" 2337 + ></path> 2338 + </g> 2339 + </svg> 2340 + </button> 2341 + </> 2342 + )} 2343 + </div> 2344 + </>, 2274 2345 document.body 2275 2346 ); 2276 2347 }
+9 -9
src/main.tsx
··· 1 - import { StrictMode } from "react"; 2 - import ReactDOM from "react-dom/client"; 3 - import { RouterProvider, createRouter } from "@tanstack/react-router"; 1 + import "~/styles/app.css"; 4 2 5 - // Import the generated route tree 6 - import { routeTree } from "./routeTree.gen"; 7 - 8 - import "~/styles/app.css"; 9 - import reportWebVitals from "./reportWebVitals.ts"; 3 + import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 10 4 import { QueryClient, QueryClientProvider, } from "@tanstack/react-query"; 11 5 import { 12 6 persistQueryClient, 13 7 } from "@tanstack/react-query-persist-client"; 14 - import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 8 + import { createRouter,RouterProvider } from "@tanstack/react-router"; 9 + //import { StrictMode } from "react"; 10 + import ReactDOM from "react-dom/client"; 11 + 12 + import reportWebVitals from "./reportWebVitals.ts"; 13 + // Import the generated route tree 14 + import { routeTree } from "./routeTree.gen"; 15 15 16 16 17 17 const queryClient = new QueryClient({
+42 -21
src/routes/profile.$did/post.$rkey.tsx
··· 1 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute, Link } from "@tanstack/react-router"; 2 + import { createFileRoute } from "@tanstack/react-router"; 3 3 import React, { useLayoutEffect } from "react"; 4 4 5 5 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; ··· 32 32 ); 33 33 } 34 34 35 - function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { 35 + export function ProfilePostComponent({ 36 + did, 37 + rkey, 38 + nopics, 39 + }: { 40 + did: string; 41 + rkey: string; 42 + nopics?: () => void; 43 + }) { 36 44 //const { get, set } = usePersistentStore(); 37 45 const queryClient = useQueryClient(); 38 46 // const [resolvedDid, setResolvedDid] = React.useState<string | null>(null); ··· 201 209 202 210 const scrollAnchor = React.useRef<{ top: number } | null>(null); 203 211 204 - 205 212 React.useEffect(() => { 206 213 const onScroll = () => { 207 - 208 214 if (window.scrollY > 50) { 209 215 userHasScrolled.current = true; 210 216 ··· 291 297 return ( 292 298 <> 293 299 <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 294 - <Link 295 - to=".." 296 - className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 297 - onClick={(e) => { 298 - e.preventDefault(); 299 - if (window.history.length > 1) { 300 - window.history.back(); 301 - } else { 302 - window.location.assign("/"); 303 - } 304 - }} 305 - aria-label="Go back" 306 - > 307 - 308 - </Link> 300 + {!nopics ? ( 301 + <button 302 + //to=".." 303 + className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 304 + onClick={(e) => { 305 + e.preventDefault(); 306 + if (window.history.length > 1) { 307 + window.history.back(); 308 + } else { 309 + window.location.assign("/"); 310 + } 311 + }} 312 + aria-label="Go back" 313 + > 314 + 315 + </button> 316 + ) : ( 317 + <button 318 + //to=".." 319 + className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 320 + onClick={(e) => { 321 + e.preventDefault(); 322 + nopics(); 323 + }} 324 + aria-label="Go back" 325 + > 326 + 327 + </button> 328 + )} 309 329 <span className="text-xl font-bold ml-2">Post</span> 310 330 </div> 311 331 ··· 322 342 )} 323 343 324 344 {/* we should use the reply lines here thats provided by UPR*/} 325 - <div style={{ maxWidth: 600, margin: "0px auto 0", padding: 0 }}> 345 + <div style={{ maxWidth: 600, padding: 0 }}> 326 346 {parents.map((parent, index) => ( 327 347 <UniversalPostRendererATURILoader 328 348 key={parent.uri} ··· 338 358 atUri={atUri} 339 359 detailed={true} 340 360 topReplyLine={parentsLoading || parents.length > 0} 361 + nopics={!!nopics} 341 362 /> 342 363 </div> 343 364 <div 344 365 style={{ 345 366 maxWidth: 600, 346 - margin: "0px auto 0", 367 + //margin: "0px auto 0", 347 368 padding: 0, 348 369 minHeight: "100dvh", 349 370 }}
+12 -2
src/styles/app.css
··· 48 48 } 49 49 50 50 @media (width >= 64rem /* 1024px */) { 51 - html, 52 - body { 51 + html:not(:has(.disablegutter)), 52 + body:not(:has(.disablegutter)) { 53 53 scrollbar-gutter: stable both-edges !important; 54 54 } 55 + html:has(.disablegutter), 56 + body:has(.disablegutter) { 57 + scrollbar-width: none; 58 + overflow-y: hidden; 59 + } 55 60 } 61 + 62 + .lightbox:has(+.lightbox-sidebar){ 63 + opacity: 0; 64 + } 65 + 56 66 .scroll-thin { 57 67 scrollbar-width: thin; 58 68 /*scrollbar-gutter: stable both-edges !important;*/