Bluesky app fork with some witchin' additions 💫

Remove post from feed after pressing show less (#8333)

* remove post from feed after pressing show less

* fix text overflow on android

* move state up so it won't get recycled away

* make type optional

authored by samuel.fm and committed by

GitHub 25f8506c 04dc6dc9

+183 -60
+82 -32
src/view/com/posts/PostFeed.tsx
··· 1 - import React, {memo} from 'react' 1 + import React, {memo, useCallback} from 'react' 2 2 import { 3 3 ActivityIndicator, 4 4 AppState, 5 5 Dimensions, 6 + LayoutAnimation, 6 7 type ListRenderItemInfo, 7 8 type StyleProp, 8 9 StyleSheet, 9 10 View, 10 11 type ViewStyle, 11 12 } from 'react-native' 12 - import {type AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api' 13 + import { 14 + type AppBskyActorDefs, 15 + AppBskyEmbedVideo, 16 + type AppBskyFeedDefs, 17 + } from '@atproto/api' 13 18 import {msg} from '@lingui/macro' 14 19 import {useLingui} from '@lingui/react' 15 20 import {useQueryClient} from '@tanstack/react-query' ··· 51 56 import {FeedShutdownMsg} from './FeedShutdownMsg' 52 57 import {PostFeedErrorMessage} from './PostFeedErrorMessage' 53 58 import {PostFeedItem} from './PostFeedItem' 59 + import {ShowLessFollowup} from './ShowLessFollowup' 54 60 import {ViewFullThread} from './ViewFullThread' 55 61 56 62 type FeedRow = ··· 117 123 type: 'interstitialTrendingVideos' 118 124 key: string 119 125 } 126 + | { 127 + type: 'showLessFollowup' 128 + key: string 129 + } 120 130 121 131 export function getItemsForFeedback(feedRow: FeedRow): 122 132 | { ··· 200 210 const {rightNavVisible} = useLayoutBreakpoints() 201 211 const areVideoFeedsEnabled = isNative 202 212 213 + const [hasPressedShowLessUris, setHasPressedShowLessUris] = React.useState( 214 + () => new Set<string>(), 215 + ) 216 + const onPressShowLess = useCallback( 217 + (interaction: AppBskyFeedDefs.Interaction) => { 218 + if (interaction.item) { 219 + const uri = interaction.item 220 + setHasPressedShowLessUris(prev => new Set([...prev, uri])) 221 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 222 + } 223 + }, 224 + [], 225 + ) 226 + 203 227 const feedCacheKey = feedParams?.feedCacheKey 204 228 const opts = React.useMemo( 205 229 () => ({enabled, ignoreFilterFor}), ··· 321 345 const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() 322 346 323 347 const feedItems: FeedRow[] = React.useMemo(() => { 348 + // wraps a slice item, and replaces it with a showLessFollowup item 349 + // if the user has pressed show less on it 350 + const sliceItem = (row: Extract<FeedRow, {type: 'sliceItem'}>) => { 351 + if (hasPressedShowLessUris.has(row.slice.items[row.indexInSlice]?.uri)) { 352 + return { 353 + type: 'showLessFollowup', 354 + key: row.key, 355 + } as const 356 + } else { 357 + return row 358 + } 359 + } 360 + 324 361 let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined 325 362 if (feedType === 'following') { 326 363 feedKind = 'following' ··· 450 487 } else if (slice.isIncompleteThread && slice.items.length >= 3) { 451 488 const beforeLast = slice.items.length - 2 452 489 const last = slice.items.length - 1 453 - arr.push({ 454 - type: 'sliceItem', 455 - key: slice.items[0]._reactKey, 456 - slice: slice, 457 - indexInSlice: 0, 458 - showReplyTo: false, 459 - }) 490 + arr.push( 491 + sliceItem({ 492 + type: 'sliceItem', 493 + key: slice.items[0]._reactKey, 494 + slice: slice, 495 + indexInSlice: 0, 496 + showReplyTo: false, 497 + }), 498 + ) 460 499 arr.push({ 461 500 type: 'sliceViewFullThread', 462 501 key: slice._reactKey + '-viewFullThread', 463 502 uri: slice.items[0].uri, 464 503 }) 465 - arr.push({ 466 - type: 'sliceItem', 467 - key: slice.items[beforeLast]._reactKey, 468 - slice: slice, 469 - indexInSlice: beforeLast, 470 - showReplyTo: 471 - slice.items[beforeLast].parentAuthor?.did !== 472 - slice.items[beforeLast].post.author.did, 473 - }) 474 - arr.push({ 475 - type: 'sliceItem', 476 - key: slice.items[last]._reactKey, 477 - slice: slice, 478 - indexInSlice: last, 479 - showReplyTo: false, 480 - }) 504 + arr.push( 505 + sliceItem({ 506 + type: 'sliceItem', 507 + key: slice.items[beforeLast]._reactKey, 508 + slice: slice, 509 + indexInSlice: beforeLast, 510 + showReplyTo: 511 + slice.items[beforeLast].parentAuthor?.did !== 512 + slice.items[beforeLast].post.author.did, 513 + }), 514 + ) 515 + arr.push( 516 + sliceItem({ 517 + type: 'sliceItem', 518 + key: slice.items[last]._reactKey, 519 + slice: slice, 520 + indexInSlice: last, 521 + showReplyTo: false, 522 + }), 523 + ) 481 524 } else { 482 525 for (let i = 0; i < slice.items.length; i++) { 483 - arr.push({ 484 - type: 'sliceItem', 485 - key: slice.items[i]._reactKey, 486 - slice: slice, 487 - indexInSlice: i, 488 - showReplyTo: i === 0, 489 - }) 526 + arr.push( 527 + sliceItem({ 528 + type: 'sliceItem', 529 + key: slice.items[i]._reactKey, 530 + slice: slice, 531 + indexInSlice: i, 532 + showReplyTo: i === 0, 533 + }), 534 + ) 490 535 } 491 536 } 492 537 } ··· 531 576 gtMobile, 532 577 isVideoFeed, 533 578 areVideoFeedsEnabled, 579 + hasPressedShowLessUris, 534 580 ]) 535 581 536 582 // events ··· 650 696 isParentNotFound={item.isParentNotFound} 651 697 hideTopBorder={rowIndex === 0 && indexInSlice === 0} 652 698 rootPost={slice.items[0].post} 699 + onShowLess={onPressShowLess} 653 700 /> 654 701 ) 655 702 } else if (row.type === 'sliceViewFullThread') { ··· 684 731 sourceContext={sourceContext} 685 732 /> 686 733 ) 734 + } else if (row.type === 'showLessFollowup') { 735 + return <ShowLessFollowup /> 687 736 } else { 688 737 return null 689 738 } ··· 700 749 feedUriOrActorDid, 701 750 feedTab, 702 751 feedCacheKey, 752 + onPressShowLess, 703 753 ], 704 754 ) 705 755
+26 -15
src/view/com/posts/PostFeedItem.tsx
··· 1 - import React, {memo, useMemo, useState} from 'react' 1 + import {memo, useCallback, useMemo, useState} from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 3 import { 4 - AppBskyActorDefs, 4 + type AppBskyActorDefs, 5 5 AppBskyFeedDefs, 6 6 AppBskyFeedPost, 7 7 AppBskyFeedThreadgate, 8 8 AtUri, 9 - ModerationDecision, 9 + type ModerationDecision, 10 10 RichText as RichTextAPI, 11 11 } from '@atproto/api' 12 12 import { 13 13 FontAwesomeIcon, 14 - FontAwesomeIconStyle, 14 + type FontAwesomeIconStyle, 15 15 } from '@fortawesome/react-native-fontawesome' 16 16 import {msg, Trans} from '@lingui/macro' 17 17 import {useLingui} from '@lingui/react' 18 18 import {useQueryClient} from '@tanstack/react-query' 19 19 20 - import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types' 20 + import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types' 21 21 import {MAX_POST_LINES} from '#/lib/constants' 22 22 import {usePalette} from '#/lib/hooks/usePalette' 23 23 import {makeProfileLink} from '#/lib/routes/links' ··· 25 25 import {sanitizeHandle} from '#/lib/strings/handles' 26 26 import {countLines} from '#/lib/strings/helpers' 27 27 import {s} from '#/lib/styles' 28 - import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' 28 + import { 29 + POST_TOMBSTONE, 30 + type Shadow, 31 + usePostShadow, 32 + } from '#/state/cache/post-shadow' 29 33 import {useFeedFeedbackContext} from '#/state/feed-feedback' 30 34 import {precacheProfile} from '#/state/queries/profile' 31 35 import {useSession} from '#/state/session' ··· 43 47 import {ContentHider} from '#/components/moderation/ContentHider' 44 48 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 45 49 import {PostAlerts} from '#/components/moderation/PostAlerts' 46 - import {AppModerationCause} from '#/components/Pills' 50 + import {type AppModerationCause} from '#/components/Pills' 47 51 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 48 52 import {RichText} from '#/components/RichText' 49 53 import {SubtleWebHover} from '#/components/SubtleWebHover' ··· 86 90 isParentBlocked, 87 91 isParentNotFound, 88 92 rootPost, 93 + onShowLess, 89 94 }: FeedItemProps & { 90 95 post: AppBskyFeedDefs.PostView 91 96 rootPost: AppBskyFeedDefs.PostView 97 + onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 92 98 }): React.ReactNode { 93 99 const postShadowed = usePostShadow(post) 94 100 const richText = useMemo( ··· 122 128 isParentBlocked={isParentBlocked} 123 129 isParentNotFound={isParentNotFound} 124 130 rootPost={rootPost} 131 + onShowLess={onShowLess} 125 132 /> 126 133 ) 127 134 } ··· 144 151 isParentBlocked, 145 152 isParentNotFound, 146 153 rootPost, 154 + onShowLess, 147 155 }: FeedItemProps & { 148 156 richText: RichTextAPI 149 157 post: Shadow<AppBskyFeedDefs.PostView> 150 158 rootPost: AppBskyFeedDefs.PostView 159 + onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 151 160 }): React.ReactNode => { 152 161 const queryClient = useQueryClient() 153 162 const {openComposer} = useComposerControls() 154 163 const pal = usePalette('default') 155 164 const {_} = useLingui() 165 + 166 + const [hover, setHover] = useState(false) 156 167 157 168 const href = useMemo(() => { 158 169 const urip = new AtUri(post.uri) ··· 160 171 }, [post.uri, post.author]) 161 172 const {sendInteraction} = useFeedFeedbackContext() 162 173 163 - const onPressReply = React.useCallback(() => { 174 + const onPressReply = useCallback(() => { 164 175 sendInteraction({ 165 176 item: post.uri, 166 177 event: 'app.bsky.feed.defs#interactionReply', ··· 178 189 }) 179 190 }, [post, record, openComposer, moderation, sendInteraction, feedContext]) 180 191 181 - const onOpenAuthor = React.useCallback(() => { 192 + const onOpenAuthor = useCallback(() => { 182 193 sendInteraction({ 183 194 item: post.uri, 184 195 event: 'app.bsky.feed.defs#clickthroughAuthor', ··· 186 197 }) 187 198 }, [sendInteraction, post, feedContext]) 188 199 189 - const onOpenReposter = React.useCallback(() => { 200 + const onOpenReposter = useCallback(() => { 190 201 sendInteraction({ 191 202 item: post.uri, 192 203 event: 'app.bsky.feed.defs#clickthroughReposter', ··· 194 205 }) 195 206 }, [sendInteraction, post, feedContext]) 196 207 197 - const onOpenEmbed = React.useCallback(() => { 208 + const onOpenEmbed = useCallback(() => { 198 209 sendInteraction({ 199 210 item: post.uri, 200 211 event: 'app.bsky.feed.defs#clickthroughEmbed', ··· 202 213 }) 203 214 }, [sendInteraction, post, feedContext]) 204 215 205 - const onBeforePress = React.useCallback(() => { 216 + const onBeforePress = useCallback(() => { 206 217 sendInteraction({ 207 218 item: post.uri, 208 219 event: 'app.bsky.feed.defs#clickthroughItem', ··· 240 251 ? rootPost.threadgate.record 241 252 : undefined 242 253 243 - const [hover, setHover] = useState(false) 244 254 return ( 245 255 <Link 246 256 testID={`feedItem-by-${post.author.handle}`} ··· 427 437 logContext="FeedItem" 428 438 feedContext={feedContext} 429 439 threadgateRecord={threadgateRecord} 440 + onShowLess={onShowLess} 430 441 /> 431 442 </View> 432 443 </View> ··· 461 472 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ 462 473 threadgateRecord, 463 474 }) 464 - const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { 475 + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { 465 476 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) 466 477 const rootPostUri = bsky.dangerousIsType<AppBskyFeedPost.Record>( 467 478 post.record, ··· 482 493 : [] 483 494 }, [post, currentAccount?.did, threadgateHiddenReplies]) 484 495 485 - const onPressShowMore = React.useCallback(() => { 496 + const onPressShowMore = useCallback(() => { 486 497 setLimitLines(false) 487 498 }, [setLimitLines]) 488 499
+46
src/view/com/posts/ShowLessFollowup.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import {CircleCheck_Stroke2_Corner0_Rounded} from '#/components/icons/CircleCheck' 6 + import {Text} from '#/components/Typography' 7 + 8 + export function ShowLessFollowup() { 9 + const t = useTheme() 10 + return ( 11 + <View 12 + style={[ 13 + t.atoms.border_contrast_low, 14 + a.border_t, 15 + t.atoms.bg_contrast_25, 16 + a.p_sm, 17 + ]}> 18 + <View 19 + style={[ 20 + t.atoms.bg, 21 + t.atoms.border_contrast_low, 22 + a.border, 23 + a.rounded_sm, 24 + a.p_md, 25 + a.flex_row, 26 + a.gap_sm, 27 + ]}> 28 + <CircleCheck_Stroke2_Corner0_Rounded 29 + style={[t.atoms.text_contrast_low]} 30 + size="sm" 31 + /> 32 + <Text 33 + style={[ 34 + a.flex_1, 35 + a.text_sm, 36 + t.atoms.text_contrast_medium, 37 + a.leading_snug, 38 + ]}> 39 + <Trans> 40 + Thank you for your feedback! It has been sent to the feed operator. 41 + </Trans> 42 + </Text> 43 + </View> 44 + </View> 45 + ) 46 + }
+10 -6
src/view/com/util/forms/PostDropdownBtn.tsx
··· 1 - import React, {memo, useMemo, useState} from 'react' 1 + import {memo, useMemo, useState} from 'react' 2 2 import { 3 3 Pressable, 4 4 type PressableProps, ··· 6 6 type ViewStyle, 7 7 } from 'react-native' 8 8 import { 9 - AppBskyFeedDefs, 10 - AppBskyFeedPost, 11 - AppBskyFeedThreadgate, 12 - RichText as RichTextAPI, 9 + type AppBskyFeedDefs, 10 + type AppBskyFeedPost, 11 + type AppBskyFeedThreadgate, 12 + type RichText as RichTextAPI, 13 13 } from '@atproto/api' 14 14 import {msg} from '@lingui/macro' 15 15 import {useLingui} from '@lingui/react' 16 + import type React from 'react' 16 17 17 18 import {useTheme} from '#/lib/ThemeContext' 18 - import {Shadow} from '#/state/cache/post-shadow' 19 + import {type Shadow} from '#/state/cache/post-shadow' 19 20 import {atoms as a, useTheme as useAlf} from '#/alf' 20 21 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 21 22 import {useMenuControl} from '#/components/Menu' ··· 34 35 size, 35 36 timestamp, 36 37 threadgateRecord, 38 + onShowLess, 37 39 }: { 38 40 testID: string 39 41 post: Shadow<AppBskyFeedDefs.PostView> ··· 45 47 size?: 'lg' | 'md' | 'sm' 46 48 timestamp: string 47 49 threadgateRecord?: AppBskyFeedThreadgate.Record 50 + onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 48 51 }): React.ReactNode => { 49 52 const theme = useTheme() 50 53 const alf = useAlf() ··· 100 103 richText={richText} 101 104 timestamp={timestamp} 102 105 threadgateRecord={threadgateRecord} 106 + onShowLess={onShowLess} 103 107 /> 104 108 )} 105 109 </Menu.Root>
+11 -2
src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
··· 101 101 richText, 102 102 timestamp, 103 103 threadgateRecord, 104 + onShowLess, 104 105 }: { 105 106 testID: string 106 107 post: Shadow<AppBskyFeedDefs.PostView> ··· 112 113 size?: 'lg' | 'md' | 'sm' 113 114 timestamp: string 114 115 threadgateRecord?: AppBskyFeedThreadgate.Record 116 + onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 115 117 }): React.ReactNode => { 116 118 const {hasSession, currentAccount} = useSession() 117 119 const {gtMobile} = useBreakpoints() ··· 303 305 item: postUri, 304 306 feedContext: postFeedContext, 305 307 }) 306 - Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 307 - }, [feedFeedback, postUri, postFeedContext, _]) 308 + if (onShowLess) { 309 + onShowLess({ 310 + item: postUri, 311 + feedContext: postFeedContext, 312 + }) 313 + } else { 314 + Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 315 + } 316 + }, [feedFeedback, postUri, postFeedContext, _, onShowLess]) 308 317 309 318 const onSelectChatToShareTo = React.useCallback( 310 319 (conversation: string) => {
+8 -5
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 8 8 } from 'react-native' 9 9 import * as Clipboard from 'expo-clipboard' 10 10 import { 11 - AppBskyFeedDefs, 12 - AppBskyFeedPost, 13 - AppBskyFeedThreadgate, 11 + type AppBskyFeedDefs, 12 + type AppBskyFeedPost, 13 + type AppBskyFeedThreadgate, 14 14 AtUri, 15 - RichText as RichTextAPI, 15 + type RichText as RichTextAPI, 16 16 } from '@atproto/api' 17 17 import {msg, plural} from '@lingui/macro' 18 18 import {useLingui} from '@lingui/react' ··· 26 26 import {shareUrl} from '#/lib/sharing' 27 27 import {useGate} from '#/lib/statsig/statsig' 28 28 import {toShareUrl} from '#/lib/strings/url-helpers' 29 - import {Shadow} from '#/state/cache/types' 29 + import {type Shadow} from '#/state/cache/types' 30 30 import {useFeedFeedbackContext} from '#/state/feed-feedback' 31 31 import { 32 32 usePostLikeMutationQueue, ··· 60 60 onPostReply, 61 61 logContext, 62 62 threadgateRecord, 63 + onShowLess, 63 64 }: { 64 65 big?: boolean 65 66 post: Shadow<AppBskyFeedDefs.PostView> ··· 71 72 onPostReply?: (postUri: string | undefined) => void 72 73 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 73 74 threadgateRecord?: AppBskyFeedThreadgate.Record 75 + onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 74 76 }): React.ReactNode => { 75 77 const t = useTheme() 76 78 const {_, i18n} = useLingui() ··· 378 380 hitSlop={POST_CTRL_HITSLOP} 379 381 timestamp={post.indexedAt} 380 382 threadgateRecord={threadgateRecord} 383 + onShowLess={onShowLess} 381 384 /> 382 385 </View> 383 386 {isDiscoverDebugUser && feedContext && (