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