about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-06-21 19:59:08 -0700
committerGitHub <noreply@github.com>2024-06-21 19:59:08 -0700
commit35f64535cb8dfa0fe46e740a6398f3b991ecfbc7 (patch)
tree66e2f9e98526902a5c97e3960295f06a879c328a
parent1715afd80ed7d9de1f2d82befa04815015d34a3a (diff)
downloadvoidsky-35f64535cb8dfa0fe46e740a6398f3b991ecfbc7.tar.zst
Tweak feed card to prevent spinnerz when pushing to screen (#4600)
-rw-r--r--src/components/FeedCard.tsx102
-rw-r--r--src/state/queries/feed.ts10
-rw-r--r--src/view/com/feeds/ProfileFeedgens.tsx105
-rw-r--r--src/view/com/lists/ProfileLists.tsx32
-rw-r--r--src/view/screens/Feeds.tsx9
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>