forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 AppBskyFeedDefs,
3 AppBskyFeedPost,
4 AppBskyRichtextFacet,
5 RichText,
6} from '@atproto/api'
7import {h} from 'preact'
8
9import logo from '../../assets/logo_full_name.svg'
10import {Like as LikeIcon} from '../icons/Like'
11import {Reply as ReplyIcon} from '../icons/Reply'
12import {Repost as RepostIcon} from '../icons/Repost'
13import {Robot as RobotIcon} from '../icons/Robot'
14import {CONTENT_LABELS} from '../labels'
15import * as bsky from '../types/bsky'
16import {niceDate} from '../util/nice-date'
17import {prettyNumber} from '../util/pretty-number'
18import {getRkey} from '../util/rkey'
19import {getVerificationState} from '../util/verification-state'
20import {Container} from './container'
21import {Embed} from './embed'
22import {Link} from './link'
23import {VerificationCheck} from './verification-check'
24
25interface Props {
26 thread: AppBskyFeedDefs.ThreadViewPost
27}
28
29export function Post({thread}: Props) {
30 const post = thread.post
31
32 const isAuthorLabeled = post.author.labels?.some(label =>
33 CONTENT_LABELS.includes(label.val),
34 )
35
36 let record: AppBskyFeedPost.Record | null = null
37 if (
38 bsky.dangerousIsType<AppBskyFeedPost.Record>(
39 post.record,
40 AppBskyFeedPost.isRecord,
41 )
42 ) {
43 record = post.record
44 }
45
46 const verification = getVerificationState({profile: post.author})
47 const isBot = post.author.labels?.some(
48 l => l.val === 'bot' && l.src === post.author.did,
49 )
50
51 const href = `/profile/${post.author.did}/post/${getRkey(post)}`
52
53 return (
54 <Container href={href}>
55 <div
56 className="flex-1 flex-col flex gap-2 bg-neutral-50 dark:bg-black dark:hover:bg-slate-900 hover:bg-blue-50 rounded-[14px] p-4"
57 lang={record?.langs?.[0]}>
58 <div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full ">
59 <Link
60 href={`/profile/${post.author.did}`}
61 className="rounded-full shrink-0">
62 <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
63 <img
64 src={post.author.avatar}
65 style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
66 />
67 </div>
68 </Link>
69 <div className="flex flex-1 flex-col min-w-0">
70 <div className="flex flex-1 items-center">
71 <Link
72 href={`/profile/${post.author.did}`}
73 className="block font-bold text-[17px] leading-5 line-clamp-1 hover:underline underline-offset-2 text-ellipsis decoration-2">
74 {post.author.displayName?.trim() || post.author.handle}
75 </Link>
76 {verification.isVerified && (
77 <VerificationCheck
78 className="pl-[3px] mt-px shrink-0"
79 verifier={verification.role === 'verifier'}
80 size={15}
81 />
82 )}
83 {isBot && (
84 <RobotIcon
85 className="pl-[3px] mt-px shrink-0 text-slate-500 dark:text-slate-400"
86 size={15}
87 />
88 )}
89 </div>
90 <Link
91 href={`/profile/${post.author.did}`}
92 className="block text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
93 @{post.author.handle}
94 </Link>
95 </div>
96 </div>
97 <PostContent record={record} />
98 <Embed content={post.embed} labels={post.labels} />
99
100 <div className="flex items-center justify-between w-full pt-2.5 text-sm">
101 <div className="flex items-center gap-3 text-sm cursor-pointer">
102 {!!post.likeCount && (
103 <div className="flex items-center gap-1 cursor-pointer group">
104 <LikeIcon
105 width={20}
106 height={20}
107 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
108 />
109 <p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
110 {prettyNumber(post.likeCount)}
111 </p>
112 </div>
113 )}
114 {!!post.replyCount && (
115 <div className="flex items-center gap-1 cursor-pointer group">
116 <ReplyIcon
117 width={20}
118 height={20}
119 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
120 />
121 <p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
122 {prettyNumber(post.replyCount)}
123 </p>
124 </div>
125 )}
126
127 {!!post.repostCount && (
128 <div className="flex items-center gap-1 cursor-pointer group">
129 <RepostIcon
130 width={20}
131 height={20}
132 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
133 />
134 <p className="font-medium text-slate-600 dark:text-slate-400 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors">
135 {prettyNumber(post.repostCount)}
136 </p>
137 </div>
138 )}
139 </div>
140 <Link href={href}>
141 <time
142 datetime={new Date(post.indexedAt).toISOString()}
143 className="text-slate-500 dark:text-textDimmed text-sm hover:underline dark:text-slate-500">
144 {niceDate(post.indexedAt)}
145 </time>
146 </Link>
147 </div>
148 </div>
149 <div className="flex items-center justify-end pt-2">
150 <Link
151 href={href}
152 className="transition-transform hover:scale-110 shrink-0">
153 <img src={logo} className="h-8" />
154 </Link>
155 </div>
156 </Container>
157 )
158}
159
160function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
161 if (!record) return null
162
163 const rt = new RichText({
164 text: record.text,
165 facets: record.facets,
166 })
167
168 const richText = []
169
170 let counter = 0
171 for (const segment of rt.segments()) {
172 if (
173 segment.link &&
174 AppBskyRichtextFacet.validateLink(segment.link).success
175 ) {
176 richText.push(
177 <Link
178 key={counter}
179 href={segment.link.uri}
180 className="text-blue-500 hover:underline"
181 disableTracking={
182 !segment.link.uri.startsWith('https://bsky.app') &&
183 !segment.link.uri.startsWith('https://go.bsky.app')
184 }>
185 {segment.text}
186 </Link>,
187 )
188 } else if (
189 segment.mention &&
190 AppBskyRichtextFacet.validateMention(segment.mention).success
191 ) {
192 richText.push(
193 <Link
194 key={counter}
195 href={`/profile/${segment.mention.did}`}
196 className="text-blue-500 hover:underline">
197 {segment.text}
198 </Link>,
199 )
200 } else if (
201 segment.tag &&
202 AppBskyRichtextFacet.validateTag(segment.tag).success
203 ) {
204 richText.push(
205 <Link
206 key={counter}
207 href={`/hashtag/${segment.tag.tag}`}
208 className="text-blue-500 hover:underline">
209 {segment.text}
210 </Link>,
211 )
212 } else {
213 richText.push(segment.text)
214 }
215
216 counter++
217 }
218
219 return (
220 <p className="min-[300px]:text-lg leading-6 min-[300px]:leading-6 break-word break-words whitespace-pre-wrap">
221 {richText}
222 </p>
223 )
224}