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

Configure Feed

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

stylize blueskypostlist better

+95 -38
+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 }