diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-05-06 17:34:50 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-06 07:34:50 -0700 |
commit | 25f8506c4152840e83ba9210452b60ea5cc0987f (patch) | |
tree | c319b601601b43fc6c0d2b464f1bd260ebcda481 /src | |
parent | 04dc6dc9ca3cdc747004367982313fd8bc157507 (diff) | |
download | voidsky-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
Diffstat (limited to 'src')
-rw-r--r-- | src/view/com/posts/PostFeed.tsx | 114 | ||||
-rw-r--r-- | src/view/com/posts/PostFeedItem.tsx | 41 | ||||
-rw-r--r-- | src/view/com/posts/ShowLessFollowup.tsx | 46 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 16 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtnMenuItems.tsx | 13 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 13 |
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 && ( |