diff options
author | Hailey <me@haileyok.com> | 2024-06-21 19:59:08 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-21 19:59:08 -0700 |
commit | 35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 (patch) | |
tree | 66e2f9e98526902a5c97e3960295f06a879c328a /src | |
parent | 1715afd80ed7d9de1f2d82befa04815015d34a3a (diff) | |
download | voidsky-35f64535cb8dfa0fe46e740a6398f3b991ecfbc7.tar.zst |
Tweak feed card to prevent spinnerz when pushing to screen (#4600)
Diffstat (limited to 'src')
-rw-r--r-- | src/components/FeedCard.tsx | 102 | ||||
-rw-r--r-- | src/state/queries/feed.ts | 10 | ||||
-rw-r--r-- | src/view/com/feeds/ProfileFeedgens.tsx | 105 | ||||
-rw-r--r-- | src/view/com/lists/ProfileLists.tsx | 32 | ||||
-rw-r--r-- | src/view/screens/Feeds.tsx | 9 |
5 files changed, 153 insertions, 105 deletions
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index bd0649097..7f3cb88ff 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -8,6 +8,7 @@ import { } from '@atproto/api' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import { @@ -16,6 +17,7 @@ import { useRemoveFeedMutation, } from '#/state/queries/preferences' import {sanitizeHandle} from 'lib/strings/handles' +import {precacheFeedFromGeneratorView, precacheList} from 'state/queries/feed' import {useSession} from 'state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import * as Toast from 'view/com/util/Toast' @@ -31,10 +33,7 @@ import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' -export function Default({ - type, - view, -}: +type Props = | { type: 'feed' view: AppBskyFeedDefs.GeneratorView @@ -42,15 +41,24 @@ export function Default({ | { type: 'list' view: AppBskyGraphDefs.ListView - }) { + } + +export function Default(props: Props) { + const {type, view} = props const displayName = type === 'feed' ? view.displayName : view.name + const purpose = type === 'list' ? view.purpose : undefined return ( - <Link feed={view}> + <Link label={displayName} {...props}> <Outer> <Header> <Avatar src={view.avatar} /> - <TitleAndByline title={displayName} creator={view.creator} /> - <Action uri={view.uri} pin /> + <TitleAndByline + title={displayName} + creator={view.creator} + type={type} + purpose={purpose} + /> + <Action uri={view.uri} pin type={type} purpose={purpose} /> </Header> <Description description={view.description} /> {type === 'feed' && <Likes count={view.likeCount || 0} />} @@ -60,15 +68,31 @@ export function Default({ } export function Link({ + type, + view, + label, children, - feed, -}: { - feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView -} & Omit<LinkProps, 'to'>) { +}: Props & Omit<LinkProps, 'to'>) { + const queryClient = useQueryClient() + const href = React.useMemo(() => { - return createProfileFeedHref({feed}) - }, [feed]) - return <InternalLink to={href}>{children}</InternalLink> + return createProfileFeedHref({feed: view}) + }, [view]) + + return ( + <InternalLink + to={href} + label={label} + onPress={() => { + if (type === 'feed') { + precacheFeedFromGeneratorView(queryClient, view) + } else { + precacheList(queryClient, view) + } + }}> + {children} + </InternalLink> + ) } export function Outer({children}: {children: React.ReactNode}) { @@ -108,9 +132,13 @@ export function AvatarPlaceholder({size = 40}: Omit<AvatarProps, 'src'>) { export function TitleAndByline({ title, creator, + type, + purpose, }: { title: string creator?: AppBskyActorDefs.ProfileViewBasic + type: 'feed' | 'list' + purpose?: AppBskyGraphDefs.ListView['purpose'] }) { const t = useTheme() @@ -123,7 +151,15 @@ export function TitleAndByline({ <Text style={[a.leading_snug, t.atoms.text_contrast_medium]} numberOfLines={1}> - <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> + {type === 'list' && purpose === 'app.bsky.graph.defs#curatelist' ? ( + <Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans> + ) : type === 'list' && purpose === 'app.bsky.graph.defs#modlist' ? ( + <Trans> + Moderation list by {sanitizeHandle(creator.handle, '@')} + </Trans> + ) : ( + <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> + )} </Text> )} </View> @@ -184,13 +220,31 @@ export function Likes({count}: {count: number}) { ) } -export function Action({uri, pin}: {uri: string; pin?: boolean}) { +export function Action({ + uri, + pin, + type, + purpose, +}: { + uri: string + pin?: boolean + type: 'feed' | 'list' + purpose?: AppBskyGraphDefs.ListView['purpose'] +}) { const {hasSession} = useSession() - if (!hasSession) return null - return <ActionInner uri={uri} pin={pin} /> + if (!hasSession || purpose !== 'app.bsky.graph.defs#curatelist') return null + return <ActionInner uri={uri} pin={pin} type={type} /> } -function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { +function ActionInner({ + uri, + pin, + type, +}: { + uri: string + pin?: boolean + type: 'feed' | 'list' +}) { const {_} = useLingui() const {data: preferences} = usePreferencesQuery() const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = @@ -198,9 +252,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { const {isPending: isRemovePending, mutateAsync: removeFeed} = useRemoveFeedMutation() const savedFeedConfig = React.useMemo(() => { - return preferences?.savedFeeds?.find( - feed => feed.type === 'feed' && feed.value === uri, - ) + return preferences?.savedFeeds?.find(feed => feed.value === uri) }, [preferences?.savedFeeds, uri]) const removePromptControl = Prompt.usePromptControl() const isPending = isAddSavedFeedPending || isRemovePending @@ -216,7 +268,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { } else { await saveFeeds([ { - type: 'feed', + type, value: uri, pinned: pin || false, }, @@ -228,7 +280,7 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { Toast.show(_(msg`Failed to update feeds`)) } }, - [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig], + [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type], ) const onPrompRemoveFeed = React.useCallback( diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 972dbf995..e5d615177 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -578,7 +578,7 @@ function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) { ) } -function precacheList( +export function precacheList( queryClient: QueryClient, list: AppBskyGraphDefs.ListView, ) { @@ -588,3 +588,11 @@ function precacheList( list, ) } + +export function precacheFeedFromGeneratorView( + queryClient: QueryClient, + view: AppBskyFeedDefs.GeneratorView, +) { + const hydratedFeed = hydrateFeedGenerator(view) + precacheFeed(queryClient, hydratedFeed) +} diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 197f35e4d..ec1a55e22 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -3,7 +3,6 @@ import { findNodeHandle, ListRenderItemInfo, StyleProp, - StyleSheet, View, ViewStyle, } from 'react-native' @@ -12,18 +11,17 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' -import {hydrateFeedGenerator} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {EmptyState} from 'view/com/util/EmptyState' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {FeedSourceCardLoaded} from './FeedSourceCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -52,7 +50,7 @@ export const ProfileFeedgens = React.forwardRef< ref, ) { const {_} = useLingui() - const theme = useTheme() + const t = useTheme() const [isPTRing, setIsPTRing] = React.useState(false) const opts = React.useMemo(() => ({enabled}), [enabled]) const { @@ -79,10 +77,9 @@ export const ProfileFeedgens = React.forwardRef< items = items.concat([EMPTY]) } else if (data?.pages) { for (const page of data?.pages) { - items = items.concat(page.feeds.map(feed => hydrateFeedGenerator(feed))) + items = items.concat(page.feeds) } - } - if (isError && !isEmpty) { + } else if (isError && !isEmpty) { items = items.concat([LOAD_MORE_ERROR_ITEM]) } return items @@ -132,48 +129,46 @@ export const ProfileFeedgens = React.forwardRef< // rendering // = - const renderItemInner = React.useCallback( - ({item, index}: ListRenderItemInfo<any>) => { - if (item === EMPTY) { - return ( - <EmptyState - icon="hashtag" - message={_(msg`You have no feeds.`)} - testID="listsEmpty" - /> - ) - } else if (item === ERROR_ITEM) { - return ( - <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> - ) - } else if (item === LOAD_MORE_ERROR_ITEM) { - return ( - <LoadMoreRetryBtn - label={_( - msg`There was an issue fetching your lists. Tap here to try again.`, - )} - onPress={onPressRetryLoadMore} - /> - ) - } else if (item === LOADING) { - return <FeedLoadingPlaceholder /> - } - if (preferences) { - return ( - <FeedSourceCardLoaded - feedUri={item.uri} - feed={item} - preferences={preferences} - style={styles.item} - showLikes - hideTopBorder={index === 0 && !isWeb} - /> - ) - } - return null - }, - [error, refetch, onPressRetryLoadMore, preferences, _], - ) + const renderItem = ({item, index}: ListRenderItemInfo<any>) => { + if (item === EMPTY) { + return ( + <EmptyState + icon="hashtag" + message={_(msg`You have no feeds.`)} + testID="listsEmpty" + /> + ) + } else if (item === ERROR_ITEM) { + return ( + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + <LoadMoreRetryBtn + label={_( + msg`There was an issue fetching your lists. Tap here to try again.`, + )} + onPress={onPressRetryLoadMore} + /> + ) + } else if (item === LOADING) { + return <FeedLoadingPlaceholder /> + } + if (preferences) { + return ( + <View + style={[ + (index !== 0 || isWeb) && a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + ]}> + <FeedCard.Default type="feed" view={item} /> + </View> + ) + } + return null + } React.useEffect(() => { if (enabled && scrollElRef.current) { @@ -189,12 +184,12 @@ export const ProfileFeedgens = React.forwardRef< ref={scrollElRef} data={items} keyExtractor={(item: any) => item._reactKey || item.uri} - renderItem={renderItemInner} + renderItem={renderItem} refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}} - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} + indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf desktopFixedHeight @@ -203,9 +198,3 @@ export const ProfileFeedgens = React.forwardRef< </View> ) }) - -const styles = StyleSheet.create({ - item: { - paddingHorizontal: 18, - }, -}) diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index e7fdfe4bd..62c944efc 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -3,7 +3,6 @@ import { findNodeHandle, ListRenderItemInfo, StyleProp, - StyleSheet, View, ViewStyle, } from 'react-native' @@ -12,17 +11,17 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' import {useAnalytics} from 'lib/analytics/analytics' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {EmptyState} from 'view/com/util/EmptyState' +import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {ListCard} from './ListCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -48,7 +47,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, ref, ) { - const theme = useTheme() + const t = useTheme() const {track} = useAnalytics() const {_} = useLingui() const [isPTRing, setIsPTRing] = React.useState(false) @@ -166,15 +165,18 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( return <FeedLoadingPlaceholder /> } return ( - <ListCard - list={item} - testID={`list-${item.name}`} - style={styles.item} - noBorder={index === 0 && !isWeb} - /> + <View + style={[ + (index !== 0 || isWeb) && a.border_t, + t.atoms.border_contrast_low, + a.px_lg, + a.py_lg, + ]}> + <FeedCard.Default type="list" view={item} /> + </View> ) }, - [error, refetch, onPressRetryLoadMore, _], + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], ) React.useEffect(() => { @@ -198,7 +200,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( contentContainerStyle={ isNative && {paddingBottom: headerOffset + 100} } - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} + indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf desktopFixedHeight @@ -208,9 +210,3 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( ) }, ) - -const styles = StyleSheet.create({ - item: { - paddingHorizontal: 18, - }, -}) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 70437a9e7..2e5b48513 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -627,7 +627,7 @@ function FollowingFeed() { fill={t.palette.white} /> </View> - <FeedCard.TitleAndByline title={_(msg`Following`)} /> + <FeedCard.TitleAndByline title={_(msg`Following`)} type="feed" /> </FeedCard.Header> </View> ) @@ -644,7 +644,7 @@ function SavedFeed({ savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name return ( - <FeedCard.Link testID={`saved-feed-${feed.displayName}`} feed={feed}> + <FeedCard.Link testID={`saved-feed-${feed.displayName}`} {...savedFeed}> {({hovered, pressed}) => ( <View style={[ @@ -657,7 +657,10 @@ function SavedFeed({ ]}> <FeedCard.Header> <FeedCard.Avatar src={feed.avatar} size={28} /> - <FeedCard.TitleAndByline title={displayName} /> + <FeedCard.TitleAndByline + title={displayName} + type={savedFeed.type} + /> <ChevronRight size="sm" fill={t.atoms.text_contrast_low.color} /> </FeedCard.Header> |