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