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