Bluesky app fork with some witchin' additions 馃挮
at main 284 lines 7.9 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 {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})