forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useMemo, useState} from 'react'
2import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
3import {
4 type AppBskyFeedDefs,
5 AppBskyFeedPost,
6 AtUri,
7 moderatePost,
8 type ModerationDecision,
9 RichText as RichTextAPI,
10} from '@atproto/api'
11import {useQueryClient} from '@tanstack/react-query'
12
13import {MAX_POST_LINES} from '#/lib/constants'
14import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
15import {usePalette} from '#/lib/hooks/usePalette'
16import {makeProfileLink} from '#/lib/routes/links'
17import {countLines} from '#/lib/strings/helpers'
18import {colors} from '#/lib/styles'
19import {
20 POST_TOMBSTONE,
21 type Shadow,
22 usePostShadow,
23} from '#/state/cache/post-shadow'
24import {useModerationOpts} from '#/state/preferences/moderation-opts'
25import {unstableCacheProfileView} from '#/state/queries/profile'
26import {Link} from '#/view/com/util/Link'
27import {PostMeta} from '#/view/com/util/PostMeta'
28import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
29import {atoms as a} from '#/alf'
30import {ContentHider} from '#/components/moderation/ContentHider'
31import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
32import {PostAlerts} from '#/components/moderation/PostAlerts'
33import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
34import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
35import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
36import {TranslatedPost} from '#/components/Post/Translated'
37import {PostControls} from '#/components/PostControls'
38import {RichText} from '#/components/RichText'
39import {SubtleHover} from '#/components/SubtleHover'
40import * as bsky from '#/types/bsky'
41
42export function Post({
43 post,
44 showReplyLine,
45 hideTopBorder,
46 style,
47 onBeforePress,
48}: {
49 post: AppBskyFeedDefs.PostView
50 showReplyLine?: boolean
51 hideTopBorder?: boolean
52 style?: StyleProp<ViewStyle>
53 onBeforePress?: () => void
54}) {
55 const moderationOpts = useModerationOpts()
56 const record = useMemo<AppBskyFeedPost.Record | undefined>(
57 () =>
58 bsky.validate(post.record, AppBskyFeedPost.validateRecord)
59 ? post.record
60 : undefined,
61 [post],
62 )
63 const postShadowed = usePostShadow(post)
64 const richText = useMemo(
65 () =>
66 record
67 ? new RichTextAPI({
68 text: record.text,
69 facets: record.facets,
70 })
71 : undefined,
72 [record],
73 )
74 const moderation = useMemo(
75 () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined),
76 [moderationOpts, post],
77 )
78 if (postShadowed === POST_TOMBSTONE) {
79 return null
80 }
81 if (record && richText && moderation) {
82 return (
83 <PostInner
84 post={postShadowed}
85 record={record}
86 richText={richText}
87 moderation={moderation}
88 showReplyLine={showReplyLine}
89 hideTopBorder={hideTopBorder}
90 style={style}
91 onBeforePress={onBeforePress}
92 />
93 )
94 }
95 return null
96}
97
98function PostInner({
99 post,
100 record,
101 richText,
102 moderation,
103 showReplyLine,
104 hideTopBorder,
105 style,
106 onBeforePress: outerOnBeforePress,
107}: {
108 post: Shadow<AppBskyFeedDefs.PostView>
109 record: AppBskyFeedPost.Record
110 richText: RichTextAPI
111 moderation: ModerationDecision
112 showReplyLine?: boolean
113 hideTopBorder?: boolean
114 style?: StyleProp<ViewStyle>
115 onBeforePress?: () => void
116}) {
117 const queryClient = useQueryClient()
118 const pal = usePalette('default')
119 const {openComposer} = useOpenComposer()
120 const [limitLines, setLimitLines] = useState(
121 () => countLines(richText?.text) >= MAX_POST_LINES,
122 )
123 const itemUrip = new AtUri(post.uri)
124 const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
125 let replyAuthorDid = ''
126 if (record.reply) {
127 const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
128 replyAuthorDid = urip.hostname
129 }
130
131 const onPressReply = useCallback(() => {
132 openComposer({
133 replyTo: {
134 uri: post.uri,
135 cid: post.cid,
136 text: record.text,
137 author: post.author,
138 embed: post.embed,
139 moderation,
140 langs: record.langs,
141 },
142 logContext: 'PostReply',
143 })
144 }, [openComposer, post, record, moderation])
145
146 const onPressShowMore = useCallback(() => {
147 setLimitLines(false)
148 }, [setLimitLines])
149
150 const onBeforePress = useCallback(() => {
151 unstableCacheProfileView(queryClient, post.author)
152 outerOnBeforePress?.()
153 }, [queryClient, post.author, outerOnBeforePress])
154
155 const [hover, setHover] = useState(false)
156
157 return (
158 <Link
159 href={itemHref}
160 style={[
161 styles.outer,
162 pal.border,
163 !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
164 style,
165 ]}
166 onBeforePress={onBeforePress}
167 onPointerEnter={() => {
168 setHover(true)
169 }}
170 onPointerLeave={() => {
171 setHover(false)
172 }}>
173 <SubtleHover hover={hover} />
174 {showReplyLine && <View style={styles.replyLine} />}
175 <View style={styles.layout}>
176 <View style={styles.layoutAvi}>
177 <PreviewableUserAvatar
178 size={42}
179 profile={post.author}
180 moderation={moderation.ui('avatar')}
181 type={post.author.associated?.labeler ? 'labeler' : 'user'}
182 />
183 </View>
184 <View style={styles.layoutContent}>
185 <PostMeta
186 author={post.author}
187 moderation={moderation}
188 timestamp={post.indexedAt}
189 postHref={itemHref}
190 />
191 {replyAuthorDid !== '' && (
192 <PostRepliedTo parentAuthor={replyAuthorDid} />
193 )}
194 <LabelsOnMyPost post={post} />
195 <ContentHider
196 modui={moderation.ui('contentView')}
197 style={styles.contentHider}
198 childContainerStyle={styles.contentHiderChild}>
199 <PostAlerts
200 modui={moderation.ui('contentView')}
201 style={[a.pb_xs]}
202 />
203 {richText.text ? (
204 <View style={[a.mb_2xs]}>
205 <RichText
206 enableTags
207 testID="postText"
208 value={richText}
209 numberOfLines={limitLines ? MAX_POST_LINES : undefined}
210 style={[a.flex_1, a.text_md]}
211 authorHandle={post.author.handle}
212 shouldProxyLinks={true}
213 />
214 {limitLines && (
215 <ShowMoreTextButton
216 style={[a.text_md]}
217 onPress={onPressShowMore}
218 />
219 )}
220 </View>
221 ) : undefined}
222 <TranslatedPost
223 hideTranslateLink={true}
224 post={post}
225 postText={record.text}
226 />
227 {post.embed ? (
228 <Embed
229 embed={post.embed}
230 moderation={moderation}
231 viewContext={PostEmbedViewContext.Feed}
232 />
233 ) : null}
234 </ContentHider>
235 <PostControls
236 post={post}
237 record={record}
238 richText={richText}
239 onPressReply={onPressReply}
240 logContext="Post"
241 />
242 </View>
243 </View>
244 </Link>
245 )
246}
247
248const styles = StyleSheet.create({
249 outer: {
250 paddingTop: 10,
251 paddingRight: 15,
252 paddingBottom: 5,
253 paddingLeft: 10,
254 // @ts-ignore web only -prf
255 cursor: 'pointer',
256 },
257 layout: {
258 flexDirection: 'row',
259 gap: 10,
260 },
261 layoutAvi: {
262 paddingLeft: 8,
263 },
264 layoutContent: {
265 flex: 1,
266 },
267 alert: {
268 marginBottom: 6,
269 },
270 replyLine: {
271 position: 'absolute',
272 left: 36,
273 top: 70,
274 bottom: 0,
275 borderLeftWidth: 2,
276 borderLeftColor: colors.gray2,
277 },
278 contentHider: {
279 marginBottom: 2,
280 },
281 contentHiderChild: {
282 marginTop: 6,
283 },
284})