a tool for shared writing and social publishing
at update/thread-viewer 153 lines 5.7 kB view raw
1"use client"; 2import { AtUri } from "@atproto/api"; 3import { PubIcon } from "components/ActionBar/Publications"; 4import { CommentTiny } from "components/Icons/CommentTiny"; 5import { QuoteTiny } from "components/Icons/QuoteTiny"; 6import { Separator } from "components/Layout"; 7import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 8import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; 9import { useSmoker } from "components/Toast"; 10import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11import type { 12 NormalizedDocument, 13 NormalizedPublication, 14} from "src/utils/normalizeRecords"; 15import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 16 17import Link from "next/link"; 18import { InteractionPreview } from "./InteractionsPreview"; 19import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 20 21export const PostListing = (props: Post) => { 22 let pubRecord = props.publication?.pubRecord as 23 | NormalizedPublication 24 | undefined; 25 26 let postRecord = props.documents.data as NormalizedDocument | null; 27 28 // Don't render anything for records that can't be normalized (e.g., site.standard records without expected fields) 29 if (!postRecord) { 30 return null; 31 } 32 let postUri = new AtUri(props.documents.uri); 33 let uri = props.publication ? props.publication?.uri : props.documents.uri; 34 35 // For standalone documents (no publication), pass isStandalone to get correct defaults 36 let isStandalone = !pubRecord; 37 let theme = usePubTheme(pubRecord?.theme || postRecord?.theme, isStandalone); 38 let themeRecord = pubRecord?.theme || postRecord?.theme; 39 let backgroundImage = 40 themeRecord?.backgroundImage?.image?.ref && uri 41 ? blobRefToSrc(themeRecord.backgroundImage.image.ref, new AtUri(uri).host) 42 : null; 43 44 let backgroundImageRepeat = themeRecord?.backgroundImage?.repeat; 45 let backgroundImageSize = themeRecord?.backgroundImage?.width || 500; 46 47 let showPageBackground = pubRecord 48 ? pubRecord?.theme?.showPageBackground 49 : postRecord.theme?.showPageBackground ?? true; 50 51 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 52 let comments = 53 pubRecord?.preferences?.showComments === false 54 ? 0 55 : props.documents.comments_on_documents?.[0]?.count || 0; 56 let tags = (postRecord?.tags as string[] | undefined) || []; 57 58 // For standalone posts, link directly to the document 59 let postHref = props.publication 60 ? `${props.publication.href}/${postUri.rkey}` 61 : `/p/${postUri.host}/${postUri.rkey}`; 62 63 return ( 64 <BaseThemeProvider {...theme} local> 65 <div 66 style={{ 67 backgroundImage: backgroundImage 68 ? `url(${backgroundImage})` 69 : undefined, 70 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 71 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 72 }} 73 className={`no-underline! flex flex-row gap-2 w-full relative 74 bg-bg-leaflet 75 border border-border-light rounded-lg 76 sm:p-2 p-2 selected-outline 77 hover:outline-accent-contrast hover:border-accent-contrast 78 `} 79 > 80 <Link className="h-full w-full absolute top-0 left-0" href={postHref} /> 81 <div 82 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 83 style={{ 84 backgroundColor: showPageBackground 85 ? "rgba(var(--bg-page), var(--bg-page-alpha))" 86 : "transparent", 87 }} 88 > 89 <h3 className="text-primary truncate">{postRecord.title}</h3> 90 91 <p className="text-secondary italic">{postRecord.description}</p> 92 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 93 {props.publication && pubRecord && ( 94 <PubInfo 95 href={props.publication.href} 96 pubRecord={pubRecord} 97 uri={props.publication.uri} 98 /> 99 )} 100 <div className="flex flex-row justify-between gap-2 items-center w-full"> 101 <PostInfo publishedAt={postRecord.publishedAt} /> 102 <InteractionPreview 103 postUrl={postHref} 104 quotesCount={quotes} 105 commentsCount={comments} 106 tags={tags} 107 showComments={pubRecord?.preferences?.showComments !== false} 108 showMentions={pubRecord?.preferences?.showMentions !== false} 109 share 110 /> 111 </div> 112 </div> 113 </div> 114 </div> 115 </BaseThemeProvider> 116 ); 117}; 118 119const PubInfo = (props: { 120 href: string; 121 pubRecord: NormalizedPublication; 122 uri: string; 123}) => { 124 return ( 125 <div className="flex flex-col md:w-auto shrink-0 w-full"> 126 <hr className="md:hidden block border-border-light mb-2" /> 127 <Link 128 href={props.href} 129 className="text-accent-contrast font-bold no-underline text-sm flex gap-1 items-center md:w-fit relative shrink-0" 130 > 131 <PubIcon small record={props.pubRecord} uri={props.uri} /> 132 {props.pubRecord.name} 133 </Link> 134 </div> 135 ); 136}; 137 138const PostInfo = (props: { publishedAt: string | undefined }) => { 139 let localizedDate = useLocalizedDate(props.publishedAt || "", { 140 year: "numeric", 141 month: "short", 142 day: "numeric", 143 }); 144 return ( 145 <div className="flex gap-2 items-center shrink-0 self-start"> 146 {props.publishedAt && ( 147 <> 148 <div className="shrink-0">{localizedDate}</div> 149 </> 150 )} 151 </div> 152 ); 153};