mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at sonner 304 lines 8.7 kB view raw
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})