a tool for shared writing and social publishing
at feature/recommend 161 lines 6.0 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 recommends = props.documents.recommends_on_documents?.[0]?.count || 0; 57 let tags = (postRecord?.tags as string[] | undefined) || []; 58 59 // For standalone posts, link directly to the document 60 let postHref = props.publication 61 ? `${props.publication.href}/${postUri.rkey}` 62 : `/p/${postUri.host}/${postUri.rkey}`; 63 64 return ( 65 <BaseThemeProvider {...theme} local> 66 <div 67 style={{ 68 backgroundImage: backgroundImage 69 ? `url(${backgroundImage})` 70 : undefined, 71 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 72 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 73 }} 74 className={`no-underline! flex flex-row gap-2 w-full relative 75 bg-bg-leaflet 76 border border-border-light rounded-lg 77 sm:p-2 p-2 selected-outline 78 hover:outline-accent-contrast hover:border-accent-contrast 79 `} 80 > 81 <Link className="h-full w-full absolute top-0 left-0" href={postHref} /> 82 <div 83 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 84 style={{ 85 backgroundColor: showPageBackground 86 ? "rgba(var(--bg-page), var(--bg-page-alpha))" 87 : "transparent", 88 }} 89 > 90 <h3 className="text-primary truncate">{postRecord.title}</h3> 91 92 <p className="text-secondary italic line-clamp-3"> 93 {postRecord.description} 94 </p> 95 <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"> 96 {props.publication && pubRecord && ( 97 <PubInfo 98 href={props.publication.href} 99 pubRecord={pubRecord} 100 uri={props.publication.uri} 101 /> 102 )} 103 <div className="flex flex-row justify-between gap-2 items-center w-full"> 104 <PostInfo publishedAt={postRecord.publishedAt} /> 105 <InteractionPreview 106 postUrl={postHref} 107 quotesCount={quotes} 108 commentsCount={comments} 109 recommendsCount={recommends} 110 documentUri={props.documents.uri} 111 tags={tags} 112 showComments={pubRecord?.preferences?.showComments !== false} 113 showMentions={pubRecord?.preferences?.showMentions !== false} 114 showRecommends={ 115 pubRecord?.preferences?.showRecommends !== false 116 } 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};