import React from "react"; import type { FeedPostRecord } from "../types/bluesky"; import { parseAtUri, toBlueskyPostUrl, formatDidForLabel, type ParsedAtUri, } from "../utils/at-uri"; import { useDidResolution } from "../hooks/useDidResolution"; import { useBlob } from "../hooks/useBlob"; import { BlueskyIcon } from "../components/BlueskyIcon"; import { isBlobWithCdn, extractCidFromBlob } from "../utils/blob"; import { RichText } from "../components/RichText"; export interface BlueskyPostRendererProps { record: FeedPostRecord; loading: boolean; error?: Error; authorHandle?: string; authorDisplayName?: string; avatarUrl?: string; authorDid?: string; embed?: React.ReactNode; iconPlacement?: "cardBottomRight" | "timestamp" | "linkInline"; showIcon?: boolean; atUri?: string; isInThread?: boolean; threadDepth?: number; isQuotePost?: boolean; showThreadBorder?: boolean; } export const BlueskyPostRenderer: React.FC = ({ record, loading, error, authorDisplayName, authorHandle, avatarUrl, authorDid, embed, iconPlacement = "timestamp", showIcon = true, atUri, isInThread = false, threadDepth = 0, isQuotePost = false, showThreadBorder = false }) => { void threadDepth; const replyParentUri = record.reply?.parent?.uri; const replyTarget = replyParentUri ? parseAtUri(replyParentUri) : undefined; const { handle: parentHandle, loading: parentHandleLoading } = useDidResolution(replyTarget?.did); if (error) { return (
Failed to load post.
); } if (loading && !record) return
Loading…
; const text = record.text; const createdDate = new Date(record.createdAt); const created = createdDate.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); const primaryName = authorDisplayName || authorHandle || "…"; const replyHref = replyTarget ? toBlueskyPostUrl(replyTarget) : undefined; const replyLabel = replyTarget ? formatReplyLabel(replyTarget, parentHandle, parentHandleLoading) : undefined; const makeIcon = () => (showIcon ? : null); const resolvedEmbed = embed ?? createAutoEmbed(record, authorDid); const parsedSelf = atUri ? parseAtUri(atUri) : undefined; const postUrl = parsedSelf ? toBlueskyPostUrl(parsedSelf) : undefined; const cardPadding = typeof baseStyles.card.padding === "number" ? baseStyles.card.padding : 12; const cardStyle: React.CSSProperties = { ...baseStyles.card, border: (isInThread && !isQuotePost && !showThreadBorder) ? "none" : `1px solid var(--atproto-color-border)`, background: `var(--atproto-color-bg)`, color: `var(--atproto-color-text)`, borderRadius: (isInThread && !isQuotePost && !showThreadBorder) ? "0" : "12px", ...(iconPlacement === "cardBottomRight" && showIcon && !isInThread ? { paddingBottom: cardPadding + 16 } : {}), }; return (
{isInThread ? ( ) : ( )}
); }; interface LayoutProps { avatarUrl?: string; primaryName: string; authorDisplayName?: string; authorHandle?: string; iconPlacement: "cardBottomRight" | "timestamp" | "linkInline"; showIcon: boolean; makeIcon: () => React.ReactNode; replyHref?: string; replyLabel?: string; text: string; record: FeedPostRecord; created: string; postUrl?: string; resolvedEmbed: React.ReactNode; } const AuthorInfo: React.FC<{ primaryName: string; authorDisplayName?: string; authorHandle?: string; inline?: boolean; }> = ({ primaryName, authorDisplayName, authorHandle, inline = false }) => (
{authorDisplayName || primaryName} {authorHandle && ( @{authorHandle} )}
); const Avatar: React.FC<{ avatarUrl?: string; name?: string }> = ({ avatarUrl, name }) => avatarUrl ? ( {`${name ) : (