mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useState, useMemo} from 'react'
2import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
3import {
4 AppBskyFeedDefs,
5 AppBskyFeedPost,
6 AtUri,
7 PostModeration,
8 RichText as RichTextAPI,
9} from '@atproto/api'
10import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
11import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12import {Link, TextLink} from '../util/Link'
13import {UserInfoText} from '../util/UserInfoText'
14import {PostMeta} from '../util/PostMeta'
15import {PostEmbeds} from '../util/post-embeds'
16import {PostCtrls} from '../util/post-ctrls/PostCtrls'
17import {ContentHider} from '../util/moderation/ContentHider'
18import {PostAlerts} from '../util/moderation/PostAlerts'
19import {Text} from '../util/text/Text'
20import {RichText} from '../util/text/RichText'
21import {PreviewableUserAvatar} from '../util/UserAvatar'
22import {s, colors} from 'lib/styles'
23import {usePalette} from 'lib/hooks/usePalette'
24import {makeProfileLink} from 'lib/routes/links'
25import {MAX_POST_LINES} from 'lib/constants'
26import {countLines} from 'lib/strings/helpers'
27import {useModerationOpts} from '#/state/queries/preferences'
28import {useComposerControls} from '#/state/shell/composer'
29import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
30
31export function Post({
32 post,
33 showReplyLine,
34 style,
35}: {
36 post: AppBskyFeedDefs.PostView
37 showReplyLine?: boolean
38 style?: StyleProp<ViewStyle>
39}) {
40 const moderationOpts = useModerationOpts()
41 const record = useMemo<AppBskyFeedPost.Record | undefined>(
42 () =>
43 AppBskyFeedPost.isRecord(post.record) &&
44 AppBskyFeedPost.validateRecord(post.record).success
45 ? post.record
46 : undefined,
47 [post],
48 )
49 const postShadowed = usePostShadow(post)
50 const richText = useMemo(
51 () =>
52 record
53 ? new RichTextAPI({
54 text: record.text,
55 facets: record.facets,
56 })
57 : undefined,
58 [record],
59 )
60 const moderation = useMemo(
61 () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined),
62 [moderationOpts, post],
63 )
64 if (postShadowed === POST_TOMBSTONE) {
65 return null
66 }
67 if (record && richText && moderation) {
68 return (
69 <PostInner
70 post={postShadowed}
71 record={record}
72 richText={richText}
73 moderation={moderation}
74 showReplyLine={showReplyLine}
75 style={style}
76 />
77 )
78 }
79 return null
80}
81
82function PostInner({
83 post,
84 record,
85 richText,
86 moderation,
87 showReplyLine,
88 style,
89}: {
90 post: Shadow<AppBskyFeedDefs.PostView>
91 record: AppBskyFeedPost.Record
92 richText: RichTextAPI
93 moderation: PostModeration
94 showReplyLine?: boolean
95 style?: StyleProp<ViewStyle>
96}) {
97 const pal = usePalette('default')
98 const {openComposer} = useComposerControls()
99 const [limitLines, setLimitLines] = useState(
100 () => countLines(richText?.text) >= MAX_POST_LINES,
101 )
102 const itemUrip = new AtUri(post.uri)
103 const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
104 let replyAuthorDid = ''
105 if (record.reply) {
106 const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
107 replyAuthorDid = urip.hostname
108 }
109
110 const onPressReply = React.useCallback(() => {
111 openComposer({
112 replyTo: {
113 uri: post.uri,
114 cid: post.cid,
115 text: record.text,
116 author: {
117 handle: post.author.handle,
118 displayName: post.author.displayName,
119 avatar: post.author.avatar,
120 },
121 },
122 })
123 }, [openComposer, post, record])
124
125 const onPressShowMore = React.useCallback(() => {
126 setLimitLines(false)
127 }, [setLimitLines])
128
129 return (
130 <Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}>
131 {showReplyLine && <View style={styles.replyLine} />}
132 <View style={styles.layout}>
133 <View style={styles.layoutAvi}>
134 <PreviewableUserAvatar
135 size={52}
136 did={post.author.did}
137 handle={post.author.handle}
138 avatar={post.author.avatar}
139 moderation={moderation.avatar}
140 />
141 </View>
142 <View style={styles.layoutContent}>
143 <PostMeta
144 author={post.author}
145 authorHasWarning={!!post.author.labels?.length}
146 timestamp={post.indexedAt}
147 postHref={itemHref}
148 />
149 {replyAuthorDid !== '' && (
150 <View style={[s.flexRow, s.mb2, s.alignCenter]}>
151 <FontAwesomeIcon
152 icon="reply"
153 size={9}
154 style={[pal.textLight, s.mr5]}
155 />
156 <Text
157 type="sm"
158 style={[pal.textLight, s.mr2]}
159 lineHeight={1.2}
160 numberOfLines={1}>
161 Reply to{' '}
162 <UserInfoText
163 type="sm"
164 did={replyAuthorDid}
165 attr="displayName"
166 style={[pal.textLight]}
167 />
168 </Text>
169 </View>
170 )}
171 <ContentHider
172 moderation={moderation.content}
173 style={styles.contentHider}
174 childContainerStyle={styles.contentHiderChild}>
175 <PostAlerts moderation={moderation.content} style={styles.alert} />
176 {richText.text ? (
177 <View style={styles.postTextContainer}>
178 <RichText
179 testID="postText"
180 type="post-text"
181 richText={richText}
182 lineHeight={1.3}
183 numberOfLines={limitLines ? MAX_POST_LINES : undefined}
184 style={s.flex1}
185 />
186 </View>
187 ) : undefined}
188 {limitLines ? (
189 <TextLink
190 text="Show More"
191 style={pal.link}
192 onPress={onPressShowMore}
193 href="#"
194 />
195 ) : undefined}
196 {post.embed ? (
197 <ContentHider
198 moderation={moderation.embed}
199 moderationDecisions={moderation.decisions}
200 ignoreQuoteDecisions
201 style={styles.contentHider}>
202 <PostEmbeds
203 embed={post.embed}
204 moderation={moderation.embed}
205 moderationDecisions={moderation.decisions}
206 />
207 </ContentHider>
208 ) : null}
209 </ContentHider>
210 <PostCtrls post={post} record={record} onPressReply={onPressReply} />
211 </View>
212 </View>
213 </Link>
214 )
215}
216
217const styles = StyleSheet.create({
218 outer: {
219 paddingTop: 10,
220 paddingRight: 15,
221 paddingBottom: 5,
222 paddingLeft: 10,
223 borderTopWidth: 1,
224 // @ts-ignore web only -prf
225 cursor: 'pointer',
226 },
227 layout: {
228 flexDirection: 'row',
229 },
230 layoutAvi: {
231 width: 70,
232 paddingLeft: 8,
233 },
234 layoutContent: {
235 flex: 1,
236 },
237 alert: {
238 marginBottom: 6,
239 },
240 postTextContainer: {
241 flexDirection: 'row',
242 alignItems: 'center',
243 flexWrap: 'wrap',
244 },
245 replyLine: {
246 position: 'absolute',
247 left: 36,
248 top: 70,
249 bottom: 0,
250 borderLeftWidth: 2,
251 borderLeftColor: colors.gray2,
252 },
253 contentHider: {
254 marginBottom: 2,
255 },
256 contentHiderChild: {
257 marginTop: 6,
258 },
259})