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";
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};