mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {
2 AppBskyFeedDefs,
3 AppBskyFeedPost,
4 AppBskyRichtextFacet,
5 RichText,
6} from '@atproto/api'
7import {h} from 'preact'
8
9import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
10import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
11import logo from '../../assets/logo.svg'
12import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
13import {CONTENT_LABELS} from '../labels'
14import {getRkey, niceDate, prettyNumber} from '../utils'
15import {Container} from './container'
16import {Embed} from './embed'
17import {Link} from './link'
18
19interface Props {
20 thread: AppBskyFeedDefs.ThreadViewPost
21}
22
23export function Post({thread}: Props) {
24 const post = thread.post
25
26 const isAuthorLabeled = post.author.labels?.some(label =>
27 CONTENT_LABELS.includes(label.val),
28 )
29
30 let record: AppBskyFeedPost.Record | null = null
31 if (AppBskyFeedPost.isRecord(post.record)) {
32 record = post.record
33 }
34
35 const href = `/profile/${post.author.did}/post/${getRkey(post)}`
36 return (
37 <Container href={href}>
38 <div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
39 <div className="flex gap-2.5 items-center cursor-pointer">
40 <Link href={`/profile/${post.author.did}`} className="rounded-full">
41 <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
42 <img
43 src={post.author.avatar}
44 style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
45 />
46 </div>
47 </Link>
48 <div>
49 <Link
50 href={`/profile/${post.author.did}`}
51 className="font-bold text-[17px] leading-5 line-clamp-1 hover:underline underline-offset-2 decoration-2">
52 <p>{post.author.displayName}</p>
53 </Link>
54 <Link
55 href={`/profile/${post.author.did}`}
56 className="text-[15px] text-textLight hover:underline line-clamp-1">
57 <p>@{post.author.handle}</p>
58 </Link>
59 </div>
60 <div className="flex-1" />
61 <Link
62 href={href}
63 className="transition-transform hover:scale-110 shrink-0 self-start">
64 <img src={logo} className="h-8" />
65 </Link>
66 </div>
67 <PostContent record={record} />
68 <Embed content={post.embed} labels={post.labels} />
69 <Link href={href}>
70 <time
71 datetime={new Date(post.indexedAt).toISOString()}
72 className="text-textLight mt-1 text-sm hover:underline">
73 {niceDate(post.indexedAt)}
74 </time>
75 </Link>
76 <div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
77 {!!post.likeCount && (
78 <div className="flex items-center gap-2 cursor-pointer">
79 <img src={likeIcon} className="w-5 h-5" />
80 <p className="font-bold text-neutral-500 mb-px">
81 {prettyNumber(post.likeCount)}
82 </p>
83 </div>
84 )}
85 {!!post.repostCount && (
86 <div className="flex items-center gap-2 cursor-pointer">
87 <img src={repostIcon} className="w-5 h-5" />
88 <p className="font-bold text-neutral-500 mb-px">
89 {prettyNumber(post.repostCount)}
90 </p>
91 </div>
92 )}
93 <div className="flex items-center gap-2 cursor-pointer">
94 <img src={replyIcon} className="w-5 h-5" />
95 <p className="font-bold text-neutral-500 mb-px">Reply</p>
96 </div>
97 <div className="flex-1" />
98 <p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
99 {post.replyCount
100 ? `Read ${prettyNumber(post.replyCount)} ${
101 post.replyCount > 1 ? 'replies' : 'reply'
102 } on Bluesky`
103 : `View on Bluesky`}
104 </p>
105 <p className="cursor-pointer text-brand font-bold hover:underline min-[450px]:hidden">
106 <span className="hidden min-[380px]:inline">View on </span>Bluesky
107 </p>
108 </div>
109 </div>
110 </Container>
111 )
112}
113
114function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
115 if (!record) return null
116
117 const rt = new RichText({
118 text: record.text,
119 facets: record.facets,
120 })
121
122 const richText = []
123
124 let counter = 0
125 for (const segment of rt.segments()) {
126 if (
127 segment.link &&
128 AppBskyRichtextFacet.validateLink(segment.link).success
129 ) {
130 richText.push(
131 <Link
132 key={counter}
133 href={segment.link.uri}
134 className="text-blue-400 hover:underline"
135 disableTracking={
136 !segment.link.uri.startsWith('https://bsky.app') &&
137 !segment.link.uri.startsWith('https://go.bsky.app')
138 }>
139 {segment.text}
140 </Link>,
141 )
142 } else if (
143 segment.mention &&
144 AppBskyRichtextFacet.validateMention(segment.mention).success
145 ) {
146 richText.push(
147 <Link
148 key={counter}
149 href={`/profile/${segment.mention.did}`}
150 className="text-blue-500 hover:underline">
151 {segment.text}
152 </Link>,
153 )
154 } else if (
155 segment.tag &&
156 AppBskyRichtextFacet.validateTag(segment.tag).success
157 ) {
158 richText.push(
159 <Link
160 key={counter}
161 href={`/tag/${segment.tag.tag}`}
162 className="text-blue-500 hover:underline">
163 {segment.text}
164 </Link>,
165 )
166 } else {
167 richText.push(segment.text)
168 }
169
170 counter++
171 }
172
173 return (
174 <p className="min-[300px]:text-lg leading-6 break-word break-words whitespace-pre-wrap">
175 {richText}
176 </p>
177 )
178}