about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeeds.tsx36
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeedsItem.tsx50
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx125
-rw-r--r--src/view/com/post-thread/PostThread.tsx27
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx8
-rw-r--r--src/view/com/util/forms/Button.tsx4
-rw-r--r--src/view/com/util/post-embeds/CustomFeedEmbed.tsx38
-rw-r--r--src/view/com/util/post-embeds/index.tsx17
8 files changed, 79 insertions, 226 deletions
diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
index 400b836d0..d134dae9f 100644
--- a/src/view/com/auth/onboarding/RecommendedFeeds.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
@@ -10,10 +10,8 @@ import {Button} from 'view/com/util/forms/Button'
 import {RecommendedFeedsItem} from './RecommendedFeedsItem'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useQuery} from '@tanstack/react-query'
-import {useStores} from 'state/index'
-import {FeedSourceModel} from 'state/models/content/feed-source'
 import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds'
 
 type Props = {
   next: () => void
@@ -21,35 +19,11 @@ type Props = {
 export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
   next,
 }: Props) {
-  const store = useStores()
   const pal = usePalette('default')
   const {isTabletOrMobile} = useWebMediaQueries()
-  const {isLoading, data: recommendedFeeds} = useQuery({
-    staleTime: Infinity, // fixed list rn, never refetch
-    queryKey: ['onboarding', 'recommended_feeds'],
-    async queryFn() {
-      try {
-        const {
-          data: {feeds},
-          success,
-        } = await store.agent.app.bsky.feed.getSuggestedFeeds()
+  const {isLoading, data} = useSuggestedFeedsQuery()
 
-        if (!success) {
-          return []
-        }
-
-        return (feeds.length ? feeds : []).map(feed => {
-          const model = new FeedSourceModel(store, feed.uri)
-          model.hydrateFeedGenerator(feed)
-          return model
-        })
-      } catch (e) {
-        return []
-      }
-    },
-  })
-
-  const hasFeeds = recommendedFeeds && recommendedFeeds.length
+  const hasFeeds = data && data?.pages?.[0]?.feeds?.length
 
   const title = (
     <>
@@ -118,7 +92,7 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
           contentStyle={{paddingHorizontal: 0}}>
           {hasFeeds ? (
             <FlatList
-              data={recommendedFeeds}
+              data={data.pages[0].feeds}
               renderItem={({item}) => <RecommendedFeedsItem item={item} />}
               keyExtractor={item => item.uri}
               style={{flex: 1}}
@@ -146,7 +120,7 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
 
           {hasFeeds ? (
             <FlatList
-              data={recommendedFeeds}
+              data={data.pages[0].feeds}
               renderItem={({item}) => <RecommendedFeedsItem item={item} />}
               keyExtractor={item => item.uri}
               style={{flex: 1}}
diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
index bee23c953..2eaf3cf2d 100644
--- a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
@@ -2,6 +2,7 @@ import React from 'react'
 import {View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api'
 import {Text} from 'view/com/util/text/Text'
 import {RichText} from 'view/com/util/text/RichText'
 import {Button} from 'view/com/util/forms/Button'
@@ -11,33 +12,58 @@ import {HeartIcon} from 'lib/icons'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {FeedSourceModel} from 'state/models/content/feed-source'
+import {
+  usePreferencesQuery,
+  usePinFeedMutation,
+  useRemoveFeedMutation,
+} from '#/state/queries/preferences'
+import {logger} from '#/logger'
 
 export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
   item,
 }: {
-  item: FeedSourceModel
+  item: AppBskyFeedDefs.GeneratorView
 }) {
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
-  if (!item) return null
+  const {data: preferences} = usePreferencesQuery()
+  const {
+    mutateAsync: pinFeed,
+    variables: pinnedFeed,
+    reset: resetPinFeed,
+  } = usePinFeedMutation()
+  const {
+    mutateAsync: removeFeed,
+    variables: removedFeed,
+    reset: resetRemoveFeed,
+  } = useRemoveFeedMutation()
+
+  if (!item || !preferences) return null
+
+  const isPinned =
+    !removedFeed?.uri &&
+    (pinnedFeed?.uri || preferences.feeds.saved.includes(item.uri))
+
   const onToggle = async () => {
-    if (item.isSaved) {
+    if (isPinned) {
       try {
-        await item.unsave()
+        await removeFeed({uri: item.uri})
+        resetRemoveFeed()
       } catch (e) {
         Toast.show('There was an issue contacting your server')
-        console.error('Failed to unsave feed', {e})
+        logger.error('Failed to unsave feed', {error: e})
       }
     } else {
       try {
-        await item.pin()
+        await pinFeed({uri: item.uri})
+        resetPinFeed()
       } catch (e) {
         Toast.show('There was an issue contacting your server')
-        console.error('Failed to pin feed', {e})
+        logger.error('Failed to pin feed', {error: e})
       }
     }
   }
+
   return (
     <View testID={`feed-${item.displayName}`}>
       <View
@@ -66,10 +92,10 @@ export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
           </Text>
 
           <Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
-            by {sanitizeHandle(item.creatorHandle, '@')}
+            by {sanitizeHandle(item.creator.handle, '@')}
           </Text>
 
-          {item.descriptionRT ? (
+          {item.description ? (
             <RichText
               type="xl"
               style={[
@@ -80,7 +106,7 @@ export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
                   marginBottom: 18,
                 },
               ]}
-              richText={item.descriptionRT}
+              richText={new BskRichText({text: item.description || ''})}
               numberOfLines={6}
             />
           ) : null}
@@ -97,7 +123,7 @@ export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
                   paddingRight: 2,
                   gap: 6,
                 }}>
-                {item.isSaved ? (
+                {isPinned ? (
                   <>
                     <FontAwesomeIcon
                       icon="check"
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 6f9687be5..aaafd1959 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -7,7 +7,6 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
 import {observer} from 'mobx-react-lite'
-import {FeedSourceModel} from 'state/models/content/feed-source'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {pluralize} from 'lib/strings/helpers'
@@ -23,7 +22,7 @@ import {
 } from '#/state/queries/preferences'
 import {useFeedSourceInfoQuery} from '#/state/queries/feed'
 
-export const NewFeedSourceCard = observer(function FeedSourceCardImpl({
+export const FeedSourceCard = observer(function FeedSourceCardImpl({
   feedUri,
   style,
   showSaveBtn = false,
@@ -162,128 +161,6 @@ export const NewFeedSourceCard = observer(function FeedSourceCardImpl({
   )
 })
 
-export const FeedSourceCard = observer(function FeedSourceCardImpl({
-  item,
-  style,
-  showSaveBtn = false,
-  showDescription = false,
-  showLikes = false,
-}: {
-  item: FeedSourceModel
-  style?: StyleProp<ViewStyle>
-  showSaveBtn?: boolean
-  showDescription?: boolean
-  showLikes?: boolean
-}) {
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
-  const {openModal} = useModalControls()
-
-  const onToggleSaved = React.useCallback(async () => {
-    if (item.isSaved) {
-      openModal({
-        name: 'confirm',
-        title: 'Remove from my feeds',
-        message: `Remove ${item.displayName} from my feeds?`,
-        onPressConfirm: async () => {
-          try {
-            await item.unsave()
-            Toast.show('Removed from my feeds')
-          } catch (e) {
-            Toast.show('There was an issue contacting your server')
-            logger.error('Failed to unsave feed', {error: e})
-          }
-        },
-      })
-    } else {
-      try {
-        await item.save()
-        Toast.show('Added to my feeds')
-      } catch (e) {
-        Toast.show('There was an issue contacting your server')
-        logger.error('Failed to save feed', {error: e})
-      }
-    }
-  }, [openModal, item])
-
-  return (
-    <Pressable
-      testID={`feed-${item.displayName}`}
-      accessibilityRole="button"
-      style={[styles.container, pal.border, style]}
-      onPress={() => {
-        if (item.type === 'feed-generator') {
-          navigation.push('ProfileFeed', {
-            name: item.creatorDid,
-            rkey: new AtUri(item.uri).rkey,
-          })
-        } else if (item.type === 'list') {
-          navigation.push('ProfileList', {
-            name: item.creatorDid,
-            rkey: new AtUri(item.uri).rkey,
-          })
-        }
-      }}
-      key={item.uri}>
-      <View style={[styles.headerContainer]}>
-        <View style={[s.mr10]}>
-          <UserAvatar type="algo" size={36} avatar={item.avatar} />
-        </View>
-        <View style={[styles.headerTextContainer]}>
-          <Text style={[pal.text, s.bold]} numberOfLines={3}>
-            {item.displayName}
-          </Text>
-          <Text style={[pal.textLight]} numberOfLines={3}>
-            by {sanitizeHandle(item.creatorHandle, '@')}
-          </Text>
-        </View>
-        {showSaveBtn && (
-          <View>
-            <Pressable
-              accessibilityRole="button"
-              accessibilityLabel={
-                item.isSaved ? 'Remove from my feeds' : 'Add to my feeds'
-              }
-              accessibilityHint=""
-              onPress={onToggleSaved}
-              hitSlop={15}
-              style={styles.btn}>
-              {item.isSaved ? (
-                <FontAwesomeIcon
-                  icon={['far', 'trash-can']}
-                  size={19}
-                  color={pal.colors.icon}
-                />
-              ) : (
-                <FontAwesomeIcon
-                  icon="plus"
-                  size={18}
-                  color={pal.colors.link}
-                />
-              )}
-            </Pressable>
-          </View>
-        )}
-      </View>
-
-      {showDescription && item.descriptionRT ? (
-        <RichText
-          style={[pal.textLight, styles.description]}
-          richText={item.descriptionRT}
-          numberOfLines={3}
-        />
-      ) : null}
-
-      {showLikes ? (
-        <Text type="sm-medium" style={[pal.text, pal.textLight]}>
-          Liked by {item.likeCount || 0}{' '}
-          {pluralize(item.likeCount || 0, 'user')}
-        </Text>
-      ) : null}
-    </Pressable>
-  )
-})
-
 const styles = StyleSheet.create({
   container: {
     paddingHorizontal: 18,
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 1e85b3e31..b0e6f1a31 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -32,9 +32,12 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {NavigationProp} from 'lib/routes/types'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {cleanError} from '#/lib/strings/errors'
-import {useStores} from '#/state'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {
+  UsePreferencesQueryResponse,
+  usePreferencesQuery,
+} from '#/state/queries/preferences'
 
 // const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} TODO
 
@@ -59,11 +62,9 @@ type YieldedItem =
 export function PostThread({
   uri,
   onPressReply,
-  treeView,
 }: {
   uri: string | undefined
   onPressReply: () => void
-  treeView: boolean
 }) {
   const {
     isLoading,
@@ -74,6 +75,7 @@ export function PostThread({
     data: thread,
     dataUpdatedAt,
   } = usePostThreadQuery(uri)
+  const {data: preferences} = usePreferencesQuery()
   const rootPost = thread?.type === 'post' ? thread.post : undefined
   const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
 
@@ -96,7 +98,7 @@ export function PostThread({
   if (AppBskyFeedDefs.isBlockedPost(thread)) {
     return <PostThreadBlocked />
   }
-  if (!thread || isLoading) {
+  if (!thread || isLoading || !preferences) {
     return (
       <CenteredView>
         <View style={s.p20}>
@@ -110,7 +112,7 @@ export function PostThread({
       thread={thread}
       isRefetching={isRefetching}
       dataUpdatedAt={dataUpdatedAt}
-      treeView={treeView}
+      threadViewPrefs={preferences.threadViewPrefs}
       onRefresh={refetch}
       onPressReply={onPressReply}
     />
@@ -121,20 +123,19 @@ function PostThreadLoaded({
   thread,
   isRefetching,
   dataUpdatedAt,
-  treeView,
+  threadViewPrefs,
   onRefresh,
   onPressReply,
 }: {
   thread: ThreadNode
   isRefetching: boolean
   dataUpdatedAt: number
-  treeView: boolean
+  threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs']
   onRefresh: () => void
   onPressReply: () => void
 }) {
   const {_} = useLingui()
   const pal = usePalette('default')
-  const store = useStores()
   const {isTablet, isDesktop} = useWebMediaQueries()
   const ref = useRef<FlatList>(null)
   // const hasScrolledIntoView = useRef<boolean>(false) TODO
@@ -162,16 +163,14 @@ function PostThreadLoaded({
   // const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost)
   const posts = React.useMemo(() => {
     let arr = [TOP_COMPONENT].concat(
-      Array.from(
-        flattenThreadSkeleton(sortThread(thread, store.preferences.thread)),
-      ),
+      Array.from(flattenThreadSkeleton(sortThread(thread, threadViewPrefs))),
     )
     if (arr.length > maxVisible) {
       arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
     }
     arr.push(BOTTOM_COMPONENT)
     return arr
-  }, [thread, maxVisible, store.preferences.thread])
+  }, [thread, maxVisible, threadViewPrefs])
 
   // TODO
   /*const onContentSizeChange = React.useCallback(() => {
@@ -297,7 +296,7 @@ function PostThreadLoaded({
             post={item.post}
             record={item.record}
             dataUpdatedAt={dataUpdatedAt}
-            treeView={treeView}
+            treeView={threadViewPrefs.lab_treeViewEnabled}
             depth={item.ctx.depth}
             isHighlightedPost={item.ctx.isHighlightedPost}
             hasMore={item.ctx.hasMore}
@@ -322,7 +321,7 @@ function PostThreadLoaded({
       pal.colors.border,
       posts,
       onRefresh,
-      treeView,
+      threadViewPrefs.lab_treeViewEnabled,
       dataUpdatedAt,
       _,
     ],
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index e29b35f8a..0ace06e9a 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -8,12 +8,12 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
-import {useStores} from 'state/index'
 import {logger} from '#/logger'
 import {useModalControls} from '#/state/modals'
 import {FeedDescriptor} from '#/state/queries/post-feed'
 import {EmptyState} from '../util/EmptyState'
 import {cleanError} from '#/lib/strings/errors'
+import {useRemoveFeedMutation} from '#/state/queries/preferences'
 
 enum KnownError {
   Block,
@@ -86,12 +86,12 @@ function FeedgenErrorMessage({
   knownError: KnownError
 }) {
   const pal = usePalette('default')
-  const store = useStores()
   const navigation = useNavigation<NavigationProp>()
   const msg = MESSAGES[knownError]
   const [_, uri] = feedDesc.split('|')
   const [ownerDid] = safeParseFeedgenUri(uri)
   const {openModal, closeModal} = useModalControls()
+  const {mutateAsync: removeFeed} = useRemoveFeedMutation()
 
   const onViewProfile = React.useCallback(() => {
     navigation.navigate('Profile', {name: ownerDid})
@@ -104,7 +104,7 @@ function FeedgenErrorMessage({
       message: 'Remove this feed from your saved feeds?',
       async onPressConfirm() {
         try {
-          await store.preferences.removeSavedFeed(uri)
+          await removeFeed({uri})
         } catch (err) {
           Toast.show(
             'There was an an issue removing this feed. Please check your internet connection and try again.',
@@ -116,7 +116,7 @@ function FeedgenErrorMessage({
         closeModal()
       },
     })
-  }, [store, openModal, closeModal, uri])
+  }, [openModal, closeModal, uri, removeFeed])
 
   return (
     <View
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
index 270d98317..8f24f8288 100644
--- a/src/view/com/util/forms/Button.tsx
+++ b/src/view/com/util/forms/Button.tsx
@@ -52,6 +52,7 @@ export function Button({
   accessibilityLabelledBy,
   onAccessibilityEscape,
   withLoading = false,
+  disabled = false,
 }: React.PropsWithChildren<{
   type?: ButtonType
   label?: string
@@ -65,6 +66,7 @@ export function Button({
   accessibilityLabelledBy?: string
   onAccessibilityEscape?: () => void
   withLoading?: boolean
+  disabled?: boolean
 }>) {
   const theme = useTheme()
   const typeOuterStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(
@@ -198,7 +200,7 @@ export function Button({
     <Pressable
       style={getStyle}
       onPress={onPressWrapped}
-      disabled={isLoading}
+      disabled={disabled || isLoading}
       testID={testID}
       accessibilityRole="button"
       accessibilityLabel={accessibilityLabel}
diff --git a/src/view/com/util/post-embeds/CustomFeedEmbed.tsx b/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
deleted file mode 100644
index 624157436..000000000
--- a/src/view/com/util/post-embeds/CustomFeedEmbed.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, {useMemo} from 'react'
-import {AppBskyFeedDefs} from '@atproto/api'
-import {usePalette} from 'lib/hooks/usePalette'
-import {StyleSheet} from 'react-native'
-import {useStores} from 'state/index'
-import {FeedSourceModel} from 'state/models/content/feed-source'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-
-export function CustomFeedEmbed({
-  record,
-}: {
-  record: AppBskyFeedDefs.GeneratorView
-}) {
-  const pal = usePalette('default')
-  const store = useStores()
-  const item = useMemo(() => {
-    const model = new FeedSourceModel(store, record.uri)
-    model.hydrateFeedGenerator(record)
-    return model
-  }, [store, record])
-  return (
-    <FeedSourceCard
-      item={item}
-      style={[pal.view, pal.border, styles.customFeedOuter]}
-      showLikes
-    />
-  )
-}
-
-const styles = StyleSheet.create({
-  customFeedOuter: {
-    borderWidth: 1,
-    borderRadius: 8,
-    marginTop: 4,
-    paddingHorizontal: 12,
-    paddingVertical: 12,
-  },
-})
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 6c13bc2bb..b4c7c45ae 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -28,9 +28,9 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import {MaybeQuoteEmbed} from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import {CustomFeedEmbed} from './CustomFeedEmbed'
 import {ListEmbed} from './ListEmbed'
 import {isCauseALabelOnUri} from 'lib/moderation'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -72,7 +72,13 @@ export function PostEmbeds({
     // custom feed embed (i.e. generator view)
     // =
     if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
-      return <CustomFeedEmbed record={embed.record} />
+      return (
+        <FeedSourceCard
+          feedUri={embed.record.uri}
+          style={[pal.view, pal.border, styles.customFeedOuter]}
+          showLikes
+        />
+      )
     }
 
     // list embed
@@ -206,4 +212,11 @@ const styles = StyleSheet.create({
     fontSize: 10,
     fontWeight: 'bold',
   },
+  customFeedOuter: {
+    borderWidth: 1,
+    borderRadius: 8,
+    marginTop: 4,
+    paddingHorizontal: 12,
+    paddingVertical: 12,
+  },
 })