Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 301 lines 8.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 { 31 GalleryBleed, 32 maybeApplyGalleryOffsetStyles, 33} from '#/components/images/Gallery' 34import {ContentHider} from '#/components/moderation/ContentHider' 35import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 36import {PostAlerts} from '#/components/moderation/PostAlerts' 37import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 38import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 39import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 40import {TranslatedPost} from '#/components/Post/Translated' 41import {PostControls} from '#/components/PostControls' 42import {RichText} from '#/components/RichText' 43import {SubtleHover} from '#/components/SubtleHover' 44import * as bsky from '#/types/bsky' 45 46export function Post({ 47 post, 48 showReplyLine, 49 hideTopBorder, 50 style, 51 onBeforePress, 52}: { 53 post: AppBskyFeedDefs.PostView 54 showReplyLine?: boolean 55 hideTopBorder?: boolean 56 style?: StyleProp<ViewStyle> 57 onBeforePress?: () => void 58}) { 59 const moderationOpts = useModerationOpts() 60 const record = useMemo<AppBskyFeedPost.Record | undefined>( 61 () => 62 bsky.validate(post.record, AppBskyFeedPost.validateRecord) 63 ? post.record 64 : undefined, 65 [post], 66 ) 67 const postShadowed = usePostShadow(post) 68 const richText = useMemo( 69 () => 70 record 71 ? new RichTextAPI({ 72 text: record.text, 73 facets: record.facets, 74 }) 75 : undefined, 76 [record], 77 ) 78 const moderation = useMemo( 79 () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined), 80 [moderationOpts, post], 81 ) 82 if (postShadowed === POST_TOMBSTONE) { 83 return null 84 } 85 if (record && richText && moderation) { 86 return ( 87 <PostInner 88 post={postShadowed} 89 record={record} 90 richText={richText} 91 moderation={moderation} 92 showReplyLine={showReplyLine} 93 hideTopBorder={hideTopBorder} 94 style={style} 95 onBeforePress={onBeforePress} 96 /> 97 ) 98 } 99 return null 100} 101 102function PostInner({ 103 post, 104 record, 105 richText, 106 moderation, 107 showReplyLine, 108 hideTopBorder, 109 style, 110 onBeforePress: outerOnBeforePress, 111}: { 112 post: Shadow<AppBskyFeedDefs.PostView> 113 record: AppBskyFeedPost.Record 114 richText: RichTextAPI 115 moderation: ModerationDecision 116 showReplyLine?: boolean 117 hideTopBorder?: boolean 118 style?: StyleProp<ViewStyle> 119 onBeforePress?: () => void 120}) { 121 const queryClient = useQueryClient() 122 const pal = usePalette('default') 123 const {openComposer} = useOpenComposer() 124 const [limitLines, setLimitLines] = useState( 125 () => countLines(richText?.text) >= MAX_POST_LINES, 126 ) 127 const itemUrip = new AtUri(post.uri) 128 const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey) 129 let replyAuthorDid = '' 130 if (record.reply) { 131 const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) 132 replyAuthorDid = urip.hostname 133 } 134 135 const onPressReply = useCallback(() => { 136 openComposer({ 137 replyTo: { 138 uri: post.uri, 139 cid: post.cid, 140 text: record.text, 141 author: post.author, 142 embed: post.embed, 143 moderation, 144 langs: record.langs, 145 }, 146 logContext: 'PostReply', 147 }) 148 }, [openComposer, post, record, moderation]) 149 150 const onPressShowMore = useCallback(() => { 151 setLimitLines(false) 152 }, [setLimitLines]) 153 154 const onBeforePress = useCallback(() => { 155 unstableCacheProfileView(queryClient, post.author) 156 outerOnBeforePress?.() 157 }, [queryClient, post.author, outerOnBeforePress]) 158 159 const [hover, setHover] = useState(false) 160 161 return ( 162 <GalleryBleed> 163 <Link 164 href={itemHref} 165 style={[ 166 styles.outer, 167 pal.border, 168 !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth}, 169 style, 170 ]} 171 onBeforePress={onBeforePress} 172 onPointerEnter={() => { 173 setHover(true) 174 }} 175 onPointerLeave={() => { 176 setHover(false) 177 }}> 178 <SubtleHover hover={hover} /> 179 {showReplyLine && <View style={styles.replyLine} />} 180 <View style={styles.layout}> 181 <View style={styles.layoutAvi}> 182 <PreviewableUserAvatar 183 size={42} 184 profile={post.author} 185 moderation={moderation.ui('avatar')} 186 type={post.author.associated?.labeler ? 'labeler' : 'user'} 187 /> 188 </View> 189 <View 190 style={[ 191 styles.layoutContent, 192 maybeApplyGalleryOffsetStyles('meta', { 193 post, 194 modui: moderation.ui('contentList'), 195 additionalCauses: [], 196 }), 197 ]}> 198 <PostMeta 199 author={post.author} 200 moderation={moderation} 201 timestamp={post.indexedAt} 202 postHref={itemHref} 203 /> 204 {replyAuthorDid !== '' && ( 205 <PostRepliedTo parentAuthor={replyAuthorDid} /> 206 )} 207 <LabelsOnMyPost post={post} /> 208 <ContentHider 209 modui={moderation.ui('contentView')} 210 style={styles.contentHider} 211 childContainerStyle={styles.contentHiderChild}> 212 <PostAlerts 213 modui={moderation.ui('contentView')} 214 style={[a.pb_xs]} 215 /> 216 {richText.text ? ( 217 <View style={[a.mb_2xs]}> 218 <RichText 219 enableTags 220 testID="postText" 221 value={richText} 222 numberOfLines={limitLines ? MAX_POST_LINES : undefined} 223 style={[a.flex_1, a.text_md]} 224 authorHandle={post.author.handle} 225 shouldProxyLinks={true} 226 /> 227 {limitLines && ( 228 <ShowMoreTextButton 229 style={[a.text_md]} 230 onPress={onPressShowMore} 231 /> 232 )} 233 </View> 234 ) : undefined} 235 <TranslatedPost hideTranslateLink post={post} /> 236 {post.embed ? ( 237 <View 238 style={maybeApplyGalleryOffsetStyles('embed', { 239 post, 240 modui: moderation.ui('contentList'), 241 additionalCauses: [], 242 })}> 243 <Embed 244 embed={post.embed} 245 moderation={moderation} 246 viewContext={PostEmbedViewContext.Feed} 247 /> 248 </View> 249 ) : null} 250 </ContentHider> 251 <PostControls 252 post={post} 253 record={record} 254 richText={richText} 255 onPressReply={onPressReply} 256 logContext="Post" 257 /> 258 </View> 259 </View> 260 </Link> 261 </GalleryBleed> 262 ) 263} 264 265const styles = StyleSheet.create({ 266 outer: { 267 paddingTop: 10, 268 paddingRight: 15, 269 paddingBottom: 5, 270 paddingLeft: 10, 271 // @ts-ignore web only -prf 272 cursor: 'pointer', 273 }, 274 layout: { 275 flexDirection: 'row', 276 gap: 10, 277 }, 278 layoutAvi: { 279 paddingLeft: 8, 280 }, 281 layoutContent: { 282 flex: 1, 283 }, 284 alert: { 285 marginBottom: 6, 286 }, 287 replyLine: { 288 position: 'absolute', 289 left: 36, 290 top: 70, 291 bottom: 0, 292 borderLeftWidth: 2, 293 borderLeftColor: colors.gray2, 294 }, 295 contentHider: { 296 marginBottom: 2, 297 }, 298 contentHiderChild: { 299 marginTop: 6, 300 }, 301})