mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12import {Trans} from '@lingui/macro'
13import {useQueryClient} from '@tanstack/react-query'
14
15import {MAX_POST_LINES} from '#/lib/constants'
16import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
17import {usePalette} from '#/lib/hooks/usePalette'
18import {makeProfileLink} from '#/lib/routes/links'
19import {countLines} from '#/lib/strings/helpers'
20import {colors, s} from '#/lib/styles'
21import {
22 POST_TOMBSTONE,
23 type Shadow,
24 usePostShadow,
25} from '#/state/cache/post-shadow'
26import {useModerationOpts} from '#/state/preferences/moderation-opts'
27import {precacheProfile} from '#/state/queries/profile'
28import {useSession} from '#/state/session'
29import {Link} from '#/view/com/util/Link'
30import {PostMeta} from '#/view/com/util/PostMeta'
31import {Text} from '#/view/com/util/text/Text'
32import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
33import {UserInfoText} from '#/view/com/util/UserInfoText'
34import {atoms as a} from '#/alf'
35import {ContentHider} from '#/components/moderation/ContentHider'
36import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
37import {PostAlerts} from '#/components/moderation/PostAlerts'
38import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
39import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
40import {PostControls} from '#/components/PostControls'
41import {ProfileHoverCard} from '#/components/ProfileHoverCard'
42import {RichText} from '#/components/RichText'
43import {SubtleWebHover} from '#/components/SubtleWebHover'
44import * as bsky from '#/types/bsky'
45
46export function Post({
47 post,
48 showReplyLine,
49 hideTopBorder,
50 style,
51}: {
52 post: AppBskyFeedDefs.PostView
53 showReplyLine?: boolean
54 hideTopBorder?: boolean
55 style?: StyleProp<ViewStyle>
56}) {
57 const moderationOpts = useModerationOpts()
58 const record = useMemo<AppBskyFeedPost.Record | undefined>(
59 () =>
60 bsky.validate(post.record, AppBskyFeedPost.validateRecord)
61 ? post.record
62 : undefined,
63 [post],
64 )
65 const postShadowed = usePostShadow(post)
66 const richText = useMemo(
67 () =>
68 record
69 ? new RichTextAPI({
70 text: record.text,
71 facets: record.facets,
72 })
73 : undefined,
74 [record],
75 )
76 const moderation = useMemo(
77 () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined),
78 [moderationOpts, post],
79 )
80 if (postShadowed === POST_TOMBSTONE) {
81 return null
82 }
83 if (record && richText && moderation) {
84 return (
85 <PostInner
86 post={postShadowed}
87 record={record}
88 richText={richText}
89 moderation={moderation}
90 showReplyLine={showReplyLine}
91 hideTopBorder={hideTopBorder}
92 style={style}
93 />
94 )
95 }
96 return null
97}
98
99function PostInner({
100 post,
101 record,
102 richText,
103 moderation,
104 showReplyLine,
105 hideTopBorder,
106 style,
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}) {
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 },
140 })
141 }, [openComposer, post, record, moderation])
142
143 const onPressShowMore = useCallback(() => {
144 setLimitLines(false)
145 }, [setLimitLines])
146
147 const onBeforePress = useCallback(() => {
148 precacheProfile(queryClient, post.author)
149 }, [queryClient, post.author])
150
151 const {currentAccount} = useSession()
152 const isMe = replyAuthorDid === currentAccount?.did
153
154 const [hover, setHover] = useState(false)
155 return (
156 <Link
157 href={itemHref}
158 style={[
159 styles.outer,
160 pal.border,
161 !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
162 style,
163 ]}
164 onBeforePress={onBeforePress}
165 onPointerEnter={() => {
166 setHover(true)
167 }}
168 onPointerLeave={() => {
169 setHover(false)
170 }}>
171 <SubtleWebHover hover={hover} />
172 {showReplyLine && <View style={styles.replyLine} />}
173 <View style={styles.layout}>
174 <View style={styles.layoutAvi}>
175 <PreviewableUserAvatar
176 size={42}
177 profile={post.author}
178 moderation={moderation.ui('avatar')}
179 type={post.author.associated?.labeler ? 'labeler' : 'user'}
180 />
181 </View>
182 <View style={styles.layoutContent}>
183 <PostMeta
184 author={post.author}
185 moderation={moderation}
186 timestamp={post.indexedAt}
187 postHref={itemHref}
188 />
189 {replyAuthorDid !== '' && (
190 <View style={[s.flexRow, s.mb2, s.alignCenter]}>
191 <FontAwesomeIcon
192 icon="reply"
193 size={9}
194 style={[pal.textLight, s.mr5]}
195 />
196 <Text
197 type="sm"
198 style={[pal.textLight, s.mr2]}
199 lineHeight={1.2}
200 numberOfLines={1}>
201 {isMe ? (
202 <Trans context="description">Reply to you</Trans>
203 ) : (
204 <Trans context="description">
205 Reply to{' '}
206 <ProfileHoverCard did={replyAuthorDid}>
207 <UserInfoText
208 type="sm"
209 did={replyAuthorDid}
210 attr="displayName"
211 style={[pal.textLight]}
212 />
213 </ProfileHoverCard>
214 </Trans>
215 )}
216 </Text>
217 </View>
218 )}
219 <LabelsOnMyPost post={post} />
220 <ContentHider
221 modui={moderation.ui('contentView')}
222 style={styles.contentHider}
223 childContainerStyle={styles.contentHiderChild}>
224 <PostAlerts
225 modui={moderation.ui('contentView')}
226 style={[a.py_xs]}
227 />
228 {richText.text ? (
229 <View>
230 <RichText
231 enableTags
232 testID="postText"
233 value={richText}
234 numberOfLines={limitLines ? MAX_POST_LINES : undefined}
235 style={[a.flex_1, a.text_md]}
236 authorHandle={post.author.handle}
237 shouldProxyLinks={true}
238 />
239 {limitLines && (
240 <ShowMoreTextButton
241 style={[a.text_md]}
242 onPress={onPressShowMore}
243 />
244 )}
245 </View>
246 ) : undefined}
247 {post.embed ? (
248 <Embed
249 embed={post.embed}
250 moderation={moderation}
251 viewContext={PostEmbedViewContext.Feed}
252 />
253 ) : null}
254 </ContentHider>
255 <PostControls
256 post={post}
257 record={record}
258 richText={richText}
259 onPressReply={onPressReply}
260 logContext="Post"
261 />
262 </View>
263 </View>
264 </Link>
265 )
266}
267
268const styles = StyleSheet.create({
269 outer: {
270 paddingTop: 10,
271 paddingRight: 15,
272 paddingBottom: 5,
273 paddingLeft: 10,
274 // @ts-ignore web only -prf
275 cursor: 'pointer',
276 },
277 layout: {
278 flexDirection: 'row',
279 gap: 10,
280 },
281 layoutAvi: {
282 paddingLeft: 8,
283 },
284 layoutContent: {
285 flex: 1,
286 },
287 alert: {
288 marginBottom: 6,
289 },
290 replyLine: {
291 position: 'absolute',
292 left: 36,
293 top: 70,
294 bottom: 0,
295 borderLeftWidth: 2,
296 borderLeftColor: colors.gray2,
297 },
298 contentHider: {
299 marginBottom: 2,
300 },
301 contentHiderChild: {
302 marginTop: 6,
303 },
304})