Bluesky app fork with some witchin' additions 馃挮
at main 276 lines 7.6 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 }) 142 }, [openComposer, post, record, moderation]) 143 144 const onPressShowMore = useCallback(() => { 145 setLimitLines(false) 146 }, [setLimitLines]) 147 148 const onBeforePress = useCallback(() => { 149 unstableCacheProfileView(queryClient, post.author) 150 outerOnBeforePress?.() 151 }, [queryClient, post.author, outerOnBeforePress]) 152 153 const [hover, setHover] = useState(false) 154 return ( 155 <Link 156 href={itemHref} 157 style={[ 158 styles.outer, 159 pal.border, 160 !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth}, 161 style, 162 ]} 163 onBeforePress={onBeforePress} 164 onPointerEnter={() => { 165 setHover(true) 166 }} 167 onPointerLeave={() => { 168 setHover(false) 169 }}> 170 <SubtleHover hover={hover} /> 171 {showReplyLine && <View style={styles.replyLine} />} 172 <View style={styles.layout}> 173 <View style={styles.layoutAvi}> 174 <PreviewableUserAvatar 175 size={42} 176 profile={post.author} 177 moderation={moderation.ui('avatar')} 178 type={post.author.associated?.labeler ? 'labeler' : 'user'} 179 /> 180 </View> 181 <View style={styles.layoutContent}> 182 <PostMeta 183 author={post.author} 184 moderation={moderation} 185 timestamp={post.indexedAt} 186 postHref={itemHref} 187 /> 188 {replyAuthorDid !== '' && ( 189 <PostRepliedTo parentAuthor={replyAuthorDid} /> 190 )} 191 <LabelsOnMyPost post={post} /> 192 <ContentHider 193 modui={moderation.ui('contentView')} 194 style={styles.contentHider} 195 childContainerStyle={styles.contentHiderChild}> 196 <PostAlerts 197 modui={moderation.ui('contentView')} 198 style={[a.pb_xs]} 199 /> 200 {richText.text ? ( 201 <View> 202 <RichText 203 enableTags 204 testID="postText" 205 value={richText} 206 numberOfLines={limitLines ? MAX_POST_LINES : undefined} 207 style={[a.flex_1, a.text_md]} 208 authorHandle={post.author.handle} 209 shouldProxyLinks={true} 210 /> 211 {limitLines && ( 212 <ShowMoreTextButton 213 style={[a.text_md]} 214 onPress={onPressShowMore} 215 /> 216 )} 217 </View> 218 ) : undefined} 219 {post.embed ? ( 220 <Embed 221 embed={post.embed} 222 moderation={moderation} 223 viewContext={PostEmbedViewContext.Feed} 224 /> 225 ) : null} 226 </ContentHider> 227 <PostControls 228 post={post} 229 record={record} 230 richText={richText} 231 onPressReply={onPressReply} 232 logContext="Post" 233 /> 234 </View> 235 </View> 236 </Link> 237 ) 238} 239 240const styles = StyleSheet.create({ 241 outer: { 242 paddingTop: 10, 243 paddingRight: 15, 244 paddingBottom: 5, 245 paddingLeft: 10, 246 // @ts-ignore web only -prf 247 cursor: 'pointer', 248 }, 249 layout: { 250 flexDirection: 'row', 251 gap: 10, 252 }, 253 layoutAvi: { 254 paddingLeft: 8, 255 }, 256 layoutContent: { 257 flex: 1, 258 }, 259 alert: { 260 marginBottom: 6, 261 }, 262 replyLine: { 263 position: 'absolute', 264 left: 36, 265 top: 70, 266 bottom: 0, 267 borderLeftWidth: 2, 268 borderLeftColor: colors.gray2, 269 }, 270 contentHider: { 271 marginBottom: 2, 272 }, 273 contentHiderChild: { 274 marginTop: 6, 275 }, 276})