From 07b028ee668afee13d878c8ff4c579276fd69f6c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 9 Jul 2025 10:14:30 +0300 Subject: Fix quote+list card padding (#8623) * fix quote padding not being pressable * fix list padding not being pressable * Fix unnecessary loading of feeds (#8578) * stop layout shifts in feed loading * don't load feed data if we already have it * adjust styles, alf stuff * remove unused button, massively simplify * fix layout shifting in notifs * use feedcard for feed post embeds * use bold text to match other style * use Link component rather than jank Pressable * prevent nested anchors in notifs * match following text size * add space between content hider * Better dead feed handling (#8579) * add space between content hider * add handling for feeds that fail to load * cleanError, in case of network funkiness * handle deleted lists * split out missingfeed --- src/components/Divider.tsx | 11 +- src/components/FeedCard.tsx | 2 +- src/components/ListCard.tsx | 12 +- src/components/Post/Embed/FeedEmbed.tsx | 43 +- src/components/Post/Embed/ListEmbed.tsx | 17 +- src/components/Post/Embed/index.tsx | 106 +++-- src/components/moderation/ContentHider.tsx | 13 +- src/view/com/feeds/FeedSourceCard.tsx | 451 ++++++++------------- src/view/com/feeds/MissingFeed.tsx | 222 ++++++++++ .../com/notifications/NotificationFeedItem.tsx | 3 +- src/view/com/util/LoadingPlaceholder.tsx | 21 +- src/view/screens/SavedFeeds.tsx | 23 +- 12 files changed, 496 insertions(+), 428 deletions(-) create mode 100644 src/view/com/feeds/MissingFeed.tsx (limited to 'src') diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index e4891aacb..ec7a7356a 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -1,18 +1,11 @@ import {View} from 'react-native' -import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf' +import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' export function Divider({style}: ViewStyleProp) { const t = useTheme() return ( - + ) } diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index f94692e5b..33905dacd 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -214,7 +214,7 @@ export function DescriptionPlaceholder() { export function Likes({count}: {count: number}) { const t = useTheme() return ( - + Liked by diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx index 30156ee0d..e34830ea9 100644 --- a/src/components/ListCard.tsx +++ b/src/components/ListCard.tsx @@ -1,10 +1,10 @@ import React from 'react' import {View} from 'react-native' import { - AppBskyGraphDefs, + type AppBskyGraphDefs, AtUri, moderateUserList, - ModerationUI, + type ModerationUI, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -22,10 +22,10 @@ import { Outer, SaveButton, } from '#/components/FeedCard' -import {Link as InternalLink, LinkProps} from '#/components/Link' +import {Link as InternalLink, type LinkProps} from '#/components/Link' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' -import * as bsky from '#/types/bsky' +import type * as bsky from '#/types/bsky' /* * This component is based on `FeedCard` and is tightly coupled with that @@ -50,7 +50,9 @@ type Props = { showPinButton?: boolean } -export function Default(props: Props) { +export function Default( + props: Props & Omit, +) { const {view, showPinButton} = props const moderationOpts = useModerationOpts() const moderation = moderationOpts diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx index fad4cd4d8..47d59e346 100644 --- a/src/components/Post/Embed/FeedEmbed.tsx +++ b/src/components/Post/Embed/FeedEmbed.tsx @@ -1,10 +1,9 @@ -import React from 'react' -import {StyleSheet} from 'react-native' +import {useMemo} from 'react' import {moderateFeedGenerator} from '@atproto/api' -import {usePalette} from '#/lib/hooks/usePalette' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {ContentHider} from '#/components/moderation/ContentHider' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' @@ -14,13 +13,22 @@ export function FeedEmbed({ }: CommonProps & { embed: EmbedType<'feed'> }) { - const pal = usePalette('default') + const t = useTheme() return ( - + + + + + + + + + ) } @@ -30,23 +38,16 @@ export function ModeratedFeedEmbed({ embed: EmbedType<'feed'> }) { const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { + const moderation = useMemo(() => { return moderationOpts ? moderateFeedGenerator(embed.view, moderationOpts) : undefined }, [embed.view, moderationOpts]) return ( - + ) } - -const styles = StyleSheet.create({ - customFeedOuter: { - borderWidth: StyleSheet.hairlineWidth, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 12, - }, -}) diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx index 82685d271..c1450bdcf 100644 --- a/src/components/Post/Embed/ListEmbed.tsx +++ b/src/components/Post/Embed/ListEmbed.tsx @@ -1,5 +1,4 @@ -import React from 'react' -import {View} from 'react-native' +import {useMemo} from 'react' import {moderateUserList} from '@atproto/api' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -16,10 +15,10 @@ export function ListEmbed({ }) { const t = useTheme() return ( - - - + ) } @@ -29,13 +28,15 @@ export function ModeratedListEmbed({ embed: EmbedType<'list'> }) { const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { + const moderation = useMemo(() => { return moderationOpts ? moderateUserList(embed.view, moderationOpts) : undefined }, [embed.view, moderationOpts]) return ( - + ) diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index ace85dc98..9c5444b27 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -268,64 +268,60 @@ export function QuoteEmbed({ const [hover, setHover] = React.useState(false) return ( { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> + style={[a.mt_sm]} + onPointerEnter={() => setHover(true)} + onPointerLeave={() => setHover(false)}> - - - - - - {moderation ? ( - - ) : null} - {richText ? ( - - ) : null} - {quote.embed && ( - - )} - + {({active}) => ( + <> + {!active && } + + + + + {moderation ? ( + + ) : null} + {richText ? ( + + ) : null} + {quote.embed && ( + + )} + + + )} ) diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index 9e94a413c..549a1b9f0 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -23,20 +23,23 @@ export function ContentHider({ modui, ignoreMute, style, + activeStyle, childContainerStyle, children, -}: React.PropsWithChildren<{ +}: { testID?: string modui: ModerationUI | undefined ignoreMute?: boolean style?: StyleProp + activeStyle?: StyleProp childContainerStyle?: StyleProp -}>) { + children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode) +}) { const blur = modui?.blurs[0] if (!blur || (ignoreMute && isJustAMute(modui))) { return ( - {children} + {typeof children === 'function' ? children({active: false}) : children} ) } @@ -44,9 +47,9 @@ export function ContentHider({ - {children} + {typeof children === 'function' ? children({active: true}) : children} ) } diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 3a658755a..18e2807a8 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -1,50 +1,33 @@ -import React from 'react' +import {type StyleProp, View, type ViewStyle} from 'react-native' import { - Linking, - Pressable, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native' -import {AtUri} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' + type $Typed, + AppBskyFeedDefs, + type AppBskyGraphDefs, + AtUri, +} from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' -import {usePalette} from '#/lib/hooks/usePalette' import {sanitizeHandle} from '#/lib/strings/handles' -import {s} from '#/lib/styles' -import {logger} from '#/logger' -import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import { - useAddSavedFeedsMutation, - usePreferencesQuery, - UsePreferencesQueryResponse, - useRemoveFeedMutation, -} from '#/state/queries/preferences' + type FeedSourceInfo, + hydrateFeedGenerator, + hydrateList, + useFeedSourceInfoQuery, +} from '#/state/queries/feed' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import * as Toast from '#/view/com/util/Toast' -import {useTheme} from '#/alf' -import {atoms as a} from '#/alf' -import {shouldClickOpenNewTab} from '#/components/Link' -import * as Prompt from '#/components/Prompt' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Link} from '#/components/Link' import {RichText} from '#/components/RichText' -import {Text} from '../util/text/Text' -import {UserAvatar} from '../util/UserAvatar' +import {Text} from '#/components/Typography' +import {MissingFeed} from './MissingFeed' -export function FeedSourceCard({ - feedUri, - style, - showSaveBtn = false, - showDescription = false, - showLikes = false, - pinOnSave = false, - showMinimalPlaceholder, - hideTopBorder, -}: { +type FeedSourceCardProps = { feedUri: string + feedData?: + | $Typed + | $Typed style?: StyleProp showSaveBtn?: boolean showDescription?: boolean @@ -52,22 +35,41 @@ export function FeedSourceCard({ pinOnSave?: boolean showMinimalPlaceholder?: boolean hideTopBorder?: boolean -}) { - const {data: preferences} = usePreferencesQuery() - const {data: feed} = useFeedSourceInfoQuery({uri: feedUri}) + link?: boolean +} + +export function FeedSourceCard({ + feedUri, + feedData, + ...props +}: FeedSourceCardProps) { + if (feedData) { + let feed: FeedSourceInfo + if (AppBskyFeedDefs.isGeneratorView(feedData)) { + feed = hydrateFeedGenerator(feedData) + } else { + feed = hydrateList(feedData) + } + return + } else { + return + } +} + +export function FeedSourceCardWithoutData({ + feedUri, + ...props +}: Omit) { + const {data: feed, error} = useFeedSourceInfoQuery({ + uri: feedUri, + }) return ( ) } @@ -75,80 +77,26 @@ export function FeedSourceCard({ export function FeedSourceCardLoaded({ feedUri, feed, - preferences, style, - showSaveBtn = false, showDescription = false, showLikes = false, - pinOnSave = false, showMinimalPlaceholder, hideTopBorder, + link = true, + error, }: { feedUri: string feed?: FeedSourceInfo - preferences?: UsePreferencesQueryResponse style?: StyleProp - showSaveBtn?: boolean showDescription?: boolean showLikes?: boolean - pinOnSave?: boolean showMinimalPlaceholder?: boolean hideTopBorder?: boolean + link?: boolean + error?: unknown }) { const t = useTheme() - const pal = usePalette('default') const {_} = useLingui() - const removePromptControl = Prompt.usePromptControl() - const navigation = useNavigationDeduped() - - const {isPending: isAddSavedFeedPending, mutateAsync: addSavedFeeds} = - useAddSavedFeedsMutation() - const {isPending: isRemovePending, mutateAsync: removeFeed} = - useRemoveFeedMutation() - - const savedFeedConfig = preferences?.savedFeeds?.find( - f => f.value === feedUri, - ) - const isSaved = Boolean(savedFeedConfig) - - const onSave = React.useCallback(async () => { - if (!feed || isSaved) return - - try { - await addSavedFeeds([ - { - type: 'feed', - value: feed.uri, - pinned: pinOnSave, - }, - ]) - Toast.show(_(msg`Added to my feeds`)) - } catch (e) { - Toast.show(_(msg`There was an issue contacting your server`), 'xmark') - logger.error('Failed to save feed', {message: e}) - } - }, [_, feed, pinOnSave, addSavedFeeds, isSaved]) - - const onUnsave = React.useCallback(async () => { - if (!savedFeedConfig) return - - try { - await removeFeed(savedFeedConfig) - // await item.unsave() - Toast.show(_(msg`Removed from my feeds`)) - } catch (e) { - Toast.show(_(msg`There was an issue contacting your server`), 'xmark') - logger.error('Failed to unsave feed', {message: e}) - } - }, [_, removeFeed, savedFeedConfig]) - - const onToggleSaved = React.useCallback(async () => { - if (isSaved) { - removePromptControl.open() - } else { - await onSave() - } - }, [isSaved, removePromptControl, onSave]) /* * LOAD STATE @@ -156,200 +104,117 @@ export function FeedSourceCardLoaded({ * This state also captures the scenario where a feed can't load for whatever * reason. */ - if (!feed || !preferences) - return ( - - {showMinimalPlaceholder ? ( - - ) : ( - - )} - - {showSaveBtn && ( - - - - )} - - ) + if (!feed) { + if (error) { + return ( + + ) + } - return ( - <> - { - const shouldOpenInNewTab = shouldClickOpenNewTab(e) - if (feed.type === 'feed') { - if (shouldOpenInNewTab) { - Linking.openURL( - `/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`, - ) - } else { - navigation.push('ProfileFeed', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) - } - } else if (feed.type === 'list') { - if (shouldOpenInNewTab) { - Linking.openURL( - `/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`, - ) - } else { - navigation.push('ProfileList', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) - } - } - }} - key={feed.uri}> - - - - - - - {feed.displayName} - - - {feed.type === 'feed' ? ( - Feed by {sanitizeHandle(feed.creatorHandle, '@')} - ) : ( - List by {sanitizeHandle(feed.creatorHandle, '@')} - )} - - + showTopBorder={false} + showLowerPlaceholder={!showMinimalPlaceholder} + /> + ) + } - {showSaveBtn && ( - - - {isSaved ? ( - - ) : ( - - )} - - - )} + const inner = ( + <> + + + - - {showDescription && feed.description ? ( - - ) : null} - - {showLikes && feed.type === 'feed' ? ( - - - Liked by{' '} - - + + + {feed.displayName} - ) : null} - - - + + {feed.type === 'feed' ? ( + Feed by {sanitizeHandle(feed.creatorHandle, '@')} + ) : ( + List by {sanitizeHandle(feed.creatorHandle, '@')} + )} + + + + {showDescription && feed.description ? ( + + ) : null} + {showLikes && feed.type === 'feed' ? ( + + + Liked by{' '} + + + + ) : null} ) -} -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 18, - paddingVertical: 20, - flexDirection: 'column', - flex: 1, - gap: 14, - }, - border: { - borderTopWidth: StyleSheet.hairlineWidth, - }, - headerContainer: { - flexDirection: 'row', - }, - headerTextContainer: { - flexDirection: 'column', - columnGap: 4, - flex: 1, - }, - description: { - flex: 1, - flexWrap: 'wrap', - }, - btn: { - paddingVertical: 6, - }, -}) + if (link) { + return ( + + {inner} + + ) + } else { + return ( + + {inner} + + ) + } +} diff --git a/src/view/com/feeds/MissingFeed.tsx b/src/view/com/feeds/MissingFeed.tsx new file mode 100644 index 000000000..3d281a731 --- /dev/null +++ b/src/view/com/feeds/MissingFeed.tsx @@ -0,0 +1,222 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {AtUri} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {isNative, isWeb} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {getFeedTypeFromUri} from '#/state/queries/feed' +import {useProfileQuery} from '#/state/queries/profile' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' + +export function MissingFeed({ + style, + hideTopBorder, + uri, + error, +}: { + style?: StyleProp + hideTopBorder?: boolean + uri: string + error?: unknown +}) { + const t = useTheme() + const {_} = useLingui() + const control = Dialog.useDialogControl() + + const type = getFeedTypeFromUri(uri) + + return ( + <> + + + + + + + + ) +} + +function DialogInner({ + uri, + type, + error, +}: { + uri: string + type: 'feed' | 'list' + error: unknown +}) { + const control = Dialog.useDialogContext() + const t = useTheme() + const {_} = useLingui() + const atUri = new AtUri(uri) + const {data: profile, isError: isProfileError} = useProfileQuery({ + did: atUri.host, + }) + const moderationOpts = useModerationOpts() + + return ( + + + + {type === 'feed' ? ( + Could not connect to feed service + ) : ( + Deleted list + )} + + + {type === 'feed' ? ( + + We could not connect to the service that provides this custom + feed. It may be temporarily unavailable and experiencing issues, + or permanently unavailable. + + ) : ( + We could not find this list. It was probably deleted. + )} + + + + {type === 'feed' ? ( + Feed creator + ) : ( + List creator + )} + + {profile && moderationOpts && ( + + control.close()}> + + + + + + + )} + {isProfileError && ( + + Could not find profile + + )} + {type === 'feed' && ( + <> + + Feed identifier + + + {atUri.rkey} + + + )} + {error instanceof Error && ( + <> + + Error message + + + {cleanError(error.message)} + + + )} + + {isNative && ( + + )} + + + ) +} diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 89e2d20e7..4de21e598 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -671,10 +671,12 @@ let NotificationFeedItem = ({ {item.type === 'feedgen-like' && item.subjectUri ? ( @@ -252,12 +251,7 @@ export function FeedLoadingPlaceholder({ {showLowerPlaceholder && ( - - + )} @@ -352,8 +346,7 @@ const styles = StyleSheet.create({ }, avatar: { borderRadius: 999, - marginRight: 10, - marginLeft: 8, + marginRight: 12, }, notification: { flexDirection: 'row', diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 9ae3dbd8d..b244c7558 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -36,6 +36,7 @@ import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/compon import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' +import {Text as NewText} from '#/components/Typography' type Props = NativeStackScreenProps export function SavedFeeds({}: Props) { @@ -296,7 +297,7 @@ function ListItem({ @@ -391,26 +392,17 @@ function ListItem({ function FollowingFeedCard() { const t = useTheme() return ( - + - - + + Following - + ) -- cgit 1.4.1