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