about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-05-06 17:34:50 +0300
committerGitHub <noreply@github.com>2025-05-06 07:34:50 -0700
commit25f8506c4152840e83ba9210452b60ea5cc0987f (patch)
treec319b601601b43fc6c0d2b464f1bd260ebcda481
parent04dc6dc9ca3cdc747004367982313fd8bc157507 (diff)
downloadvoidsky-25f8506c4152840e83ba9210452b60ea5cc0987f.tar.zst
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
-rw-r--r--src/view/com/posts/PostFeed.tsx114
-rw-r--r--src/view/com/posts/PostFeedItem.tsx41
-rw-r--r--src/view/com/posts/ShowLessFollowup.tsx46
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx16
-rw-r--r--src/view/com/util/forms/PostDropdownBtnMenuItems.tsx13
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx13
6 files changed, 183 insertions, 60 deletions
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 3a6b8f660..181b35026 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -1,15 +1,20 @@
-import React, {memo} from 'react'
+import React, {memo, useCallback} from 'react'
 import {
   ActivityIndicator,
   AppState,
   Dimensions,
+  LayoutAnimation,
   type ListRenderItemInfo,
   type StyleProp,
   StyleSheet,
   View,
   type ViewStyle,
 } from 'react-native'
-import {type AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api'
+import {
+  type AppBskyActorDefs,
+  AppBskyEmbedVideo,
+  type AppBskyFeedDefs,
+} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
@@ -51,6 +56,7 @@ import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
 import {FeedShutdownMsg} from './FeedShutdownMsg'
 import {PostFeedErrorMessage} from './PostFeedErrorMessage'
 import {PostFeedItem} from './PostFeedItem'
+import {ShowLessFollowup} from './ShowLessFollowup'
 import {ViewFullThread} from './ViewFullThread'
 
 type FeedRow =
@@ -117,6 +123,10 @@ type FeedRow =
       type: 'interstitialTrendingVideos'
       key: string
     }
+  | {
+      type: 'showLessFollowup'
+      key: string
+    }
 
 export function getItemsForFeedback(feedRow: FeedRow):
   | {
@@ -200,6 +210,20 @@ let PostFeed = ({
   const {rightNavVisible} = useLayoutBreakpoints()
   const areVideoFeedsEnabled = isNative
 
+  const [hasPressedShowLessUris, setHasPressedShowLessUris] = React.useState(
+    () => new Set<string>(),
+  )
+  const onPressShowLess = useCallback(
+    (interaction: AppBskyFeedDefs.Interaction) => {
+      if (interaction.item) {
+        const uri = interaction.item
+        setHasPressedShowLessUris(prev => new Set([...prev, uri]))
+        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+      }
+    },
+    [],
+  )
+
   const feedCacheKey = feedParams?.feedCacheKey
   const opts = React.useMemo(
     () => ({enabled, ignoreFilterFor}),
@@ -321,6 +345,19 @@ let PostFeed = ({
   const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
 
   const feedItems: FeedRow[] = React.useMemo(() => {
+    // wraps a slice item, and replaces it with a showLessFollowup item
+    // if the user has pressed show less on it
+    const sliceItem = (row: Extract<FeedRow, {type: 'sliceItem'}>) => {
+      if (hasPressedShowLessUris.has(row.slice.items[row.indexInSlice]?.uri)) {
+        return {
+          type: 'showLessFollowup',
+          key: row.key,
+        } as const
+      } else {
+        return row
+      }
+    }
+
     let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined
     if (feedType === 'following') {
       feedKind = 'following'
@@ -450,43 +487,51 @@ let PostFeed = ({
               } else if (slice.isIncompleteThread && slice.items.length >= 3) {
                 const beforeLast = slice.items.length - 2
                 const last = slice.items.length - 1
-                arr.push({
-                  type: 'sliceItem',
-                  key: slice.items[0]._reactKey,
-                  slice: slice,
-                  indexInSlice: 0,
-                  showReplyTo: false,
-                })
+                arr.push(
+                  sliceItem({
+                    type: 'sliceItem',
+                    key: slice.items[0]._reactKey,
+                    slice: slice,
+                    indexInSlice: 0,
+                    showReplyTo: false,
+                  }),
+                )
                 arr.push({
                   type: 'sliceViewFullThread',
                   key: slice._reactKey + '-viewFullThread',
                   uri: slice.items[0].uri,
                 })
-                arr.push({
-                  type: 'sliceItem',
-                  key: slice.items[beforeLast]._reactKey,
-                  slice: slice,
-                  indexInSlice: beforeLast,
-                  showReplyTo:
-                    slice.items[beforeLast].parentAuthor?.did !==
-                    slice.items[beforeLast].post.author.did,
-                })
-                arr.push({
-                  type: 'sliceItem',
-                  key: slice.items[last]._reactKey,
-                  slice: slice,
-                  indexInSlice: last,
-                  showReplyTo: false,
-                })
-              } else {
-                for (let i = 0; i < slice.items.length; i++) {
-                  arr.push({
+                arr.push(
+                  sliceItem({
+                    type: 'sliceItem',
+                    key: slice.items[beforeLast]._reactKey,
+                    slice: slice,
+                    indexInSlice: beforeLast,
+                    showReplyTo:
+                      slice.items[beforeLast].parentAuthor?.did !==
+                      slice.items[beforeLast].post.author.did,
+                  }),
+                )
+                arr.push(
+                  sliceItem({
                     type: 'sliceItem',
-                    key: slice.items[i]._reactKey,
+                    key: slice.items[last]._reactKey,
                     slice: slice,
-                    indexInSlice: i,
-                    showReplyTo: i === 0,
-                  })
+                    indexInSlice: last,
+                    showReplyTo: false,
+                  }),
+                )
+              } else {
+                for (let i = 0; i < slice.items.length; i++) {
+                  arr.push(
+                    sliceItem({
+                      type: 'sliceItem',
+                      key: slice.items[i]._reactKey,
+                      slice: slice,
+                      indexInSlice: i,
+                      showReplyTo: i === 0,
+                    }),
+                  )
                 }
               }
             }
@@ -531,6 +576,7 @@ let PostFeed = ({
     gtMobile,
     isVideoFeed,
     areVideoFeedsEnabled,
+    hasPressedShowLessUris,
   ])
 
   // events
@@ -650,6 +696,7 @@ let PostFeed = ({
             isParentNotFound={item.isParentNotFound}
             hideTopBorder={rowIndex === 0 && indexInSlice === 0}
             rootPost={slice.items[0].post}
+            onShowLess={onPressShowLess}
           />
         )
       } else if (row.type === 'sliceViewFullThread') {
@@ -684,6 +731,8 @@ let PostFeed = ({
             sourceContext={sourceContext}
           />
         )
+      } else if (row.type === 'showLessFollowup') {
+        return <ShowLessFollowup />
       } else {
         return null
       }
@@ -700,6 +749,7 @@ let PostFeed = ({
       feedUriOrActorDid,
       feedTab,
       feedCacheKey,
+      onPressShowLess,
     ],
   )
 
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 499b9ccd5..facd31e5f 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -1,23 +1,23 @@
-import React, {memo, useMemo, useState} from 'react'
+import {memo, useCallback, useMemo, useState} from 'react'
 import {StyleSheet, View} from 'react-native'
 import {
-  AppBskyActorDefs,
+  type AppBskyActorDefs,
   AppBskyFeedDefs,
   AppBskyFeedPost,
   AppBskyFeedThreadgate,
   AtUri,
-  ModerationDecision,
+  type ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {
   FontAwesomeIcon,
-  FontAwesomeIconStyle,
+  type FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
 
-import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types'
+import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
 import {MAX_POST_LINES} from '#/lib/constants'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {makeProfileLink} from '#/lib/routes/links'
@@ -25,7 +25,11 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {countLines} from '#/lib/strings/helpers'
 import {s} from '#/lib/styles'
-import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
+import {
+  POST_TOMBSTONE,
+  type Shadow,
+  usePostShadow,
+} from '#/state/cache/post-shadow'
 import {useFeedFeedbackContext} from '#/state/feed-feedback'
 import {precacheProfile} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
@@ -43,7 +47,7 @@ import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/R
 import {ContentHider} from '#/components/moderation/ContentHider'
 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
-import {AppModerationCause} from '#/components/Pills'
+import {type AppModerationCause} from '#/components/Pills'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
@@ -86,9 +90,11 @@ export function PostFeedItem({
   isParentBlocked,
   isParentNotFound,
   rootPost,
+  onShowLess,
 }: FeedItemProps & {
   post: AppBskyFeedDefs.PostView
   rootPost: AppBskyFeedDefs.PostView
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
 }): React.ReactNode {
   const postShadowed = usePostShadow(post)
   const richText = useMemo(
@@ -122,6 +128,7 @@ export function PostFeedItem({
         isParentBlocked={isParentBlocked}
         isParentNotFound={isParentNotFound}
         rootPost={rootPost}
+        onShowLess={onShowLess}
       />
     )
   }
@@ -144,23 +151,27 @@ let FeedItemInner = ({
   isParentBlocked,
   isParentNotFound,
   rootPost,
+  onShowLess,
 }: FeedItemProps & {
   richText: RichTextAPI
   post: Shadow<AppBskyFeedDefs.PostView>
   rootPost: AppBskyFeedDefs.PostView
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
 }): React.ReactNode => {
   const queryClient = useQueryClient()
   const {openComposer} = useComposerControls()
   const pal = usePalette('default')
   const {_} = useLingui()
 
+  const [hover, setHover] = useState(false)
+
   const href = useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey)
   }, [post.uri, post.author])
   const {sendInteraction} = useFeedFeedbackContext()
 
-  const onPressReply = React.useCallback(() => {
+  const onPressReply = useCallback(() => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#interactionReply',
@@ -178,7 +189,7 @@ let FeedItemInner = ({
     })
   }, [post, record, openComposer, moderation, sendInteraction, feedContext])
 
-  const onOpenAuthor = React.useCallback(() => {
+  const onOpenAuthor = useCallback(() => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughAuthor',
@@ -186,7 +197,7 @@ let FeedItemInner = ({
     })
   }, [sendInteraction, post, feedContext])
 
-  const onOpenReposter = React.useCallback(() => {
+  const onOpenReposter = useCallback(() => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughReposter',
@@ -194,7 +205,7 @@ let FeedItemInner = ({
     })
   }, [sendInteraction, post, feedContext])
 
-  const onOpenEmbed = React.useCallback(() => {
+  const onOpenEmbed = useCallback(() => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughEmbed',
@@ -202,7 +213,7 @@ let FeedItemInner = ({
     })
   }, [sendInteraction, post, feedContext])
 
-  const onBeforePress = React.useCallback(() => {
+  const onBeforePress = useCallback(() => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughItem',
@@ -240,7 +251,6 @@ let FeedItemInner = ({
     ? rootPost.threadgate.record
     : undefined
 
-  const [hover, setHover] = useState(false)
   return (
     <Link
       testID={`feedItem-by-${post.author.handle}`}
@@ -427,6 +437,7 @@ let FeedItemInner = ({
             logContext="FeedItem"
             feedContext={feedContext}
             threadgateRecord={threadgateRecord}
+            onShowLess={onShowLess}
           />
         </View>
       </View>
@@ -461,7 +472,7 @@ let PostContent = ({
   const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
     threadgateRecord,
   })
-  const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
+  const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
     const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
     const rootPostUri = bsky.dangerousIsType<AppBskyFeedPost.Record>(
       post.record,
@@ -482,7 +493,7 @@ let PostContent = ({
       : []
   }, [post, currentAccount?.did, threadgateHiddenReplies])
 
-  const onPressShowMore = React.useCallback(() => {
+  const onPressShowMore = useCallback(() => {
     setLimitLines(false)
   }, [setLimitLines])
 
diff --git a/src/view/com/posts/ShowLessFollowup.tsx b/src/view/com/posts/ShowLessFollowup.tsx
new file mode 100644
index 000000000..01412b9a1
--- /dev/null
+++ b/src/view/com/posts/ShowLessFollowup.tsx
@@ -0,0 +1,46 @@
+import {View} from 'react-native'
+import {Trans} from '@lingui/macro'
+
+import {atoms as a, useTheme} from '#/alf'
+import {CircleCheck_Stroke2_Corner0_Rounded} from '#/components/icons/CircleCheck'
+import {Text} from '#/components/Typography'
+
+export function ShowLessFollowup() {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        t.atoms.border_contrast_low,
+        a.border_t,
+        t.atoms.bg_contrast_25,
+        a.p_sm,
+      ]}>
+      <View
+        style={[
+          t.atoms.bg,
+          t.atoms.border_contrast_low,
+          a.border,
+          a.rounded_sm,
+          a.p_md,
+          a.flex_row,
+          a.gap_sm,
+        ]}>
+        <CircleCheck_Stroke2_Corner0_Rounded
+          style={[t.atoms.text_contrast_low]}
+          size="sm"
+        />
+        <Text
+          style={[
+            a.flex_1,
+            a.text_sm,
+            t.atoms.text_contrast_medium,
+            a.leading_snug,
+          ]}>
+          <Trans>
+            Thank you for your feedback! It has been sent to the feed operator.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index fd577605a..c50b36640 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useMemo, useState} from 'react'
+import {memo, useMemo, useState} from 'react'
 import {
   Pressable,
   type PressableProps,
@@ -6,16 +6,17 @@ import {
   type ViewStyle,
 } from 'react-native'
 import {
-  AppBskyFeedDefs,
-  AppBskyFeedPost,
-  AppBskyFeedThreadgate,
-  RichText as RichTextAPI,
+  type AppBskyFeedDefs,
+  type AppBskyFeedPost,
+  type AppBskyFeedThreadgate,
+  type RichText as RichTextAPI,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
 import {useTheme} from '#/lib/ThemeContext'
-import {Shadow} from '#/state/cache/post-shadow'
+import {type Shadow} from '#/state/cache/post-shadow'
 import {atoms as a, useTheme as useAlf} from '#/alf'
 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
 import {useMenuControl} from '#/components/Menu'
@@ -34,6 +35,7 @@ let PostDropdownBtn = ({
   size,
   timestamp,
   threadgateRecord,
+  onShowLess,
 }: {
   testID: string
   post: Shadow<AppBskyFeedDefs.PostView>
@@ -45,6 +47,7 @@ let PostDropdownBtn = ({
   size?: 'lg' | 'md' | 'sm'
   timestamp: string
   threadgateRecord?: AppBskyFeedThreadgate.Record
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
 }): React.ReactNode => {
   const theme = useTheme()
   const alf = useAlf()
@@ -100,6 +103,7 @@ let PostDropdownBtn = ({
             richText={richText}
             timestamp={timestamp}
             threadgateRecord={threadgateRecord}
+            onShowLess={onShowLess}
           />
         )}
       </Menu.Root>
diff --git a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
index e50a2d3e4..9c3d709d9 100644
--- a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
+++ b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
@@ -101,6 +101,7 @@ let PostDropdownMenuItems = ({
   richText,
   timestamp,
   threadgateRecord,
+  onShowLess,
 }: {
   testID: string
   post: Shadow<AppBskyFeedDefs.PostView>
@@ -112,6 +113,7 @@ let PostDropdownMenuItems = ({
   size?: 'lg' | 'md' | 'sm'
   timestamp: string
   threadgateRecord?: AppBskyFeedThreadgate.Record
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
 }): React.ReactNode => {
   const {hasSession, currentAccount} = useSession()
   const {gtMobile} = useBreakpoints()
@@ -303,8 +305,15 @@ let PostDropdownMenuItems = ({
       item: postUri,
       feedContext: postFeedContext,
     })
-    Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
-  }, [feedFeedback, postUri, postFeedContext, _])
+    if (onShowLess) {
+      onShowLess({
+        item: postUri,
+        feedContext: postFeedContext,
+      })
+    } else {
+      Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
+    }
+  }, [feedFeedback, postUri, postFeedContext, _, onShowLess])
 
   const onSelectChatToShareTo = React.useCallback(
     (conversation: string) => {
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index fe583e801..d97654a63 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -8,11 +8,11 @@ import {
 } from 'react-native'
 import * as Clipboard from 'expo-clipboard'
 import {
-  AppBskyFeedDefs,
-  AppBskyFeedPost,
-  AppBskyFeedThreadgate,
+  type AppBskyFeedDefs,
+  type AppBskyFeedPost,
+  type AppBskyFeedThreadgate,
   AtUri,
-  RichText as RichTextAPI,
+  type RichText as RichTextAPI,
 } from '@atproto/api'
 import {msg, plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -26,7 +26,7 @@ import {makeProfileLink} from '#/lib/routes/links'
 import {shareUrl} from '#/lib/sharing'
 import {useGate} from '#/lib/statsig/statsig'
 import {toShareUrl} from '#/lib/strings/url-helpers'
-import {Shadow} from '#/state/cache/types'
+import {type Shadow} from '#/state/cache/types'
 import {useFeedFeedbackContext} from '#/state/feed-feedback'
 import {
   usePostLikeMutationQueue,
@@ -60,6 +60,7 @@ let PostCtrls = ({
   onPostReply,
   logContext,
   threadgateRecord,
+  onShowLess,
 }: {
   big?: boolean
   post: Shadow<AppBskyFeedDefs.PostView>
@@ -71,6 +72,7 @@ let PostCtrls = ({
   onPostReply?: (postUri: string | undefined) => void
   logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
   threadgateRecord?: AppBskyFeedThreadgate.Record
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
 }): React.ReactNode => {
   const t = useTheme()
   const {_, i18n} = useLingui()
@@ -378,6 +380,7 @@ let PostCtrls = ({
           hitSlop={POST_CTRL_HITSLOP}
           timestamp={post.indexedAt}
           threadgateRecord={threadgateRecord}
+          onShowLess={onShowLess}
         />
       </View>
       {isDiscoverDebugUser && feedContext && (