a tool for shared writing and social publishing
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};