A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

stylize blueskypostlist better

Changed files
+95 -38
lib
components
+95 -38
lib/components/BlueskyPostList.tsx
··· 117 117 record={record.value} 118 118 rkey={record.rkey} 119 119 did={actorPath} 120 + uri={record.uri} 120 121 reason={record.reason} 121 122 replyParent={record.replyParent} 122 123 hasDivider={idx < records.length - 1} ··· 203 204 record: FeedPostRecord; 204 205 rkey: string; 205 206 did: string; 207 + uri?: string; 206 208 reason?: AuthorFeedReason; 207 209 replyParent?: ReplyParentInfo; 208 210 hasDivider: boolean; ··· 212 214 record, 213 215 rkey, 214 216 did, 217 + uri, 215 218 reason, 216 219 replyParent, 217 220 hasDivider, ··· 224 227 const absolute = record.createdAt 225 228 ? new Date(record.createdAt).toLocaleString() 226 229 : undefined; 227 - const href = `${blueskyAppBaseUrl}/profile/${did}/post/${rkey}`; 230 + 231 + // Parse the URI to get the actual post's DID and rkey 232 + // This handles reposts correctly by linking to the original post 233 + const parsedUri = uri ? parseAtUri(uri) : undefined; 234 + const postDid = parsedUri?.did ?? did; 235 + const postRkey = parsedUri?.rkey ?? rkey; 236 + const href = `${blueskyAppBaseUrl}/profile/${postDid}/post/${postRkey}`; 237 + 238 + // Resolve the original post author's handle for reposts 239 + const { handle: originalAuthorHandle } = useDidResolution( 240 + reason?.$type === "app.bsky.feed.defs#reasonRepost" ? postDid : undefined, 241 + ); 242 + 228 243 const repostLabel = 229 244 reason?.$type === "app.bsky.feed.defs#reasonRepost" 230 - ? `${formatActor(reason.by) ?? "Someone"} reposted` 245 + ? `${formatActor(reason.by) ?? "Someone"} reposted @${originalAuthorHandle ?? formatDid(postDid)}` 231 246 : undefined; 232 247 const parentUri = replyParent?.uri ?? record.reply?.parent?.uri; 233 248 const parentDid = ··· 236 251 const { handle: resolvedReplyHandle } = useDidResolution( 237 252 replyParent?.author?.handle ? undefined : parentDid, 238 253 ); 239 - const replyLabel = formatReplyTarget( 254 + const replyTarget = formatReplyTarget( 240 255 parentUri, 241 256 replyParent, 242 257 resolvedReplyHandle, 243 258 ); 244 259 260 + const isReply = !!replyTarget; 261 + 245 262 const postPreview = text.slice(0, 100); 246 263 const ariaLabel = text 247 264 ? `Post by ${did}: ${postPreview}${text.length > 100 ? '...' : ''}` 248 265 : `Post by ${did}`; 249 266 250 267 return ( 251 - <a 252 - href={href} 253 - target="_blank" 254 - rel="noopener noreferrer" 255 - aria-label={ariaLabel} 268 + <div 256 269 style={{ 257 - ...listStyles.row, 258 - color: `var(--atproto-color-text)`, 270 + ...listStyles.rowContainer, 259 271 borderBottom: hasDivider 260 272 ? `1px solid var(--atproto-color-border)` 261 273 : "none", 274 + borderLeft: isReply 275 + ? `3px solid #1185FE` 276 + : "3px solid transparent", 262 277 }} 263 278 > 264 279 {repostLabel && ( 265 - <span style={{ ...listStyles.rowMeta, color: `var(--atproto-color-text-secondary)` }}> 280 + <div style={{ ...listStyles.rowMeta, color: `var(--atproto-color-text-secondary)` }}> 266 281 {repostLabel} 267 - </span> 282 + </div> 268 283 )} 269 - {replyLabel && ( 270 - <span style={{ ...listStyles.rowMeta, color: `var(--atproto-color-text-secondary)` }}> 271 - {replyLabel} 272 - </span> 284 + {isReply && ( 285 + <div style={listStyles.replyHeader}> 286 + <span style={{ ...listStyles.replyArrow, color: `#1185FE` }}> 287 + 288 + </span> 289 + <span style={{ ...listStyles.replyText, color: `var(--atproto-color-text-secondary)` }}> 290 + replying to {replyTarget} 291 + </span> 292 + {relative && ( 293 + <span 294 + style={{ ...listStyles.rowTime, color: `var(--atproto-color-text-secondary)`, marginLeft: "auto" }} 295 + title={absolute} 296 + > 297 + {relative} 298 + </span> 299 + )} 300 + </div> 273 301 )} 274 - {relative && ( 302 + {!isReply && relative && ( 275 303 <span 276 304 style={{ ...listStyles.rowTime, color: `var(--atproto-color-text-secondary)` }} 277 305 title={absolute} ··· 279 307 {relative} 280 308 </span> 281 309 )} 282 - {text && ( 283 - <p style={{ ...listStyles.rowBody, color: `var(--atproto-color-text)` }}> 284 - {text} 285 - </p> 286 - )} 287 - {!text && ( 288 - <p 289 - style={{ 290 - ...listStyles.rowBody, 291 - color: `var(--atproto-color-text)`, 292 - fontStyle: "italic", 293 - }} 294 - > 295 - No text content. 296 - </p> 297 - )} 298 - </a> 310 + <a 311 + href={href} 312 + target="_blank" 313 + rel="noopener noreferrer" 314 + aria-label={ariaLabel} 315 + style={{ 316 + ...listStyles.rowLink, 317 + color: `var(--atproto-color-text)`, 318 + }} 319 + > 320 + {text && ( 321 + <p style={{ ...listStyles.rowBody, color: `var(--atproto-color-text)` }}> 322 + {text} 323 + </p> 324 + )} 325 + {!text && ( 326 + <p 327 + style={{ 328 + ...listStyles.rowBody, 329 + color: `var(--atproto-color-text)`, 330 + fontStyle: "italic", 331 + }} 332 + > 333 + No text content. 334 + </p> 335 + )} 336 + </a> 337 + </div> 299 338 ); 300 339 }; 301 340 ··· 388 427 fontSize: 13, 389 428 textAlign: "center", 390 429 } satisfies React.CSSProperties, 391 - row: { 430 + rowContainer: { 392 431 padding: "18px", 393 - textDecoration: "none", 394 432 display: "flex", 395 433 flexDirection: "column", 396 434 gap: 6, 397 435 transition: "background-color 120ms ease", 398 436 } satisfies React.CSSProperties, 437 + rowLink: { 438 + textDecoration: "none", 439 + display: "block", 440 + } satisfies React.CSSProperties, 441 + replyHeader: { 442 + display: "flex", 443 + alignItems: "center", 444 + gap: 6, 445 + fontSize: 12, 446 + fontWeight: 500, 447 + } satisfies React.CSSProperties, 448 + replyArrow: { 449 + fontSize: 14, 450 + fontWeight: 600, 451 + } satisfies React.CSSProperties, 452 + replyText: { 453 + fontSize: 12, 454 + fontWeight: 500, 455 + } satisfies React.CSSProperties, 399 456 rowHeader: { 400 457 display: "flex", 401 458 gap: 6, ··· 496 553 const directHandle = feedParent?.author?.handle; 497 554 const handle = directHandle ?? resolvedHandle; 498 555 if (handle) { 499 - return `Replying to @${handle}`; 556 + return `@${handle}`; 500 557 } 501 558 const parentDid = feedParent?.author?.did; 502 559 const targetUri = feedParent?.uri ?? parentUri; ··· 504 561 const parsed = parseAtUri(targetUri); 505 562 const did = parentDid ?? parsed?.did; 506 563 if (!did) return undefined; 507 - return `Replying to @${formatDid(did)}`; 564 + return `@${formatDid(did)}`; 508 565 }