Bluesky app fork with some witchin' additions 馃挮
at main 277 lines 7.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 {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 logContext: 'PostReply', 142 }) 143 }, [openComposer, post, record, moderation]) 144 145 const onPressShowMore = useCallback(() => { 146 setLimitLines(false) 147 }, [setLimitLines]) 148 149 const onBeforePress = useCallback(() => { 150 unstableCacheProfileView(queryClient, post.author) 151 outerOnBeforePress?.() 152 }, [queryClient, post.author, outerOnBeforePress]) 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 <SubtleHover 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 <PostRepliedTo parentAuthor={replyAuthorDid} /> 191 )} 192 <LabelsOnMyPost post={post} /> 193 <ContentHider 194 modui={moderation.ui('contentView')} 195 style={styles.contentHider} 196 childContainerStyle={styles.contentHiderChild}> 197 <PostAlerts 198 modui={moderation.ui('contentView')} 199 style={[a.pb_xs]} 200 /> 201 {richText.text ? ( 202 <View style={[a.mb_2xs]}> 203 <RichText 204 enableTags 205 testID="postText" 206 value={richText} 207 numberOfLines={limitLines ? MAX_POST_LINES : undefined} 208 style={[a.flex_1, a.text_md]} 209 authorHandle={post.author.handle} 210 shouldProxyLinks={true} 211 /> 212 {limitLines && ( 213 <ShowMoreTextButton 214 style={[a.text_md]} 215 onPress={onPressShowMore} 216 /> 217 )} 218 </View> 219 ) : undefined} 220 {post.embed ? ( 221 <Embed 222 embed={post.embed} 223 moderation={moderation} 224 viewContext={PostEmbedViewContext.Feed} 225 /> 226 ) : null} 227 </ContentHider> 228 <PostControls 229 post={post} 230 record={record} 231 richText={richText} 232 onPressReply={onPressReply} 233 logContext="Post" 234 /> 235 </View> 236 </View> 237 </Link> 238 ) 239} 240 241const styles = StyleSheet.create({ 242 outer: { 243 paddingTop: 10, 244 paddingRight: 15, 245 paddingBottom: 5, 246 paddingLeft: 10, 247 // @ts-ignore web only -prf 248 cursor: 'pointer', 249 }, 250 layout: { 251 flexDirection: 'row', 252 gap: 10, 253 }, 254 layoutAvi: { 255 paddingLeft: 8, 256 }, 257 layoutContent: { 258 flex: 1, 259 }, 260 alert: { 261 marginBottom: 6, 262 }, 263 replyLine: { 264 position: 'absolute', 265 left: 36, 266 top: 70, 267 bottom: 0, 268 borderLeftWidth: 2, 269 borderLeftColor: colors.gray2, 270 }, 271 contentHider: { 272 marginBottom: 2, 273 }, 274 contentHiderChild: { 275 marginTop: 6, 276 }, 277})