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/feeds/FeedPage.tsx21
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx74
-rw-r--r--src/view/com/home/HomeHeader.tsx19
-rw-r--r--src/view/com/lightbox/Lightbox.tsx25
-rw-r--r--src/view/com/modals/SelfLabel.tsx13
-rw-r--r--src/view/com/pager/TabBar.tsx16
-rw-r--r--src/view/com/posts/Feed.tsx15
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx53
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx13
9 files changed, 138 insertions, 111 deletions
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index bb782809d..6a9fc9346 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
-import {useWindowDimensions, View} from 'react-native'
+import {View} from 'react-native'
+import {AppBskyActorDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
@@ -17,9 +18,9 @@ import {useSession} from '#/state/session'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ComposeIcon2} from 'lib/icons'
 import {s} from 'lib/styles'
+import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
 import {Feed} from '../posts/Feed'
 import {FAB} from '../util/fab/FAB'
 import {ListMethods} from '../util/List'
@@ -35,6 +36,7 @@ export function FeedPage({
   feedParams,
   renderEmptyState,
   renderEndOfFeed,
+  savedFeedConfig,
 }: {
   testID?: string
   feed: FeedDescriptor
@@ -42,6 +44,7 @@ export function FeedPage({
   isPageFocused: boolean
   renderEmptyState: () => JSX.Element
   renderEndOfFeed?: () => JSX.Element
+  savedFeedConfig?: AppBskyActorDefs.SavedFeed
 }) {
   const {hasSession} = useSession()
   const {_} = useLingui()
@@ -129,6 +132,7 @@ export function FeedPage({
             renderEmptyState={renderEmptyState}
             renderEndOfFeed={renderEndOfFeed}
             headerOffset={headerOffset}
+            savedFeedConfig={savedFeedConfig}
           />
         </FeedFeedbackProvider>
       </MainScrollProvider>
@@ -153,16 +157,3 @@ export function FeedPage({
     </View>
   )
 }
-
-function useHeaderOffset() {
-  const {isDesktop, isTablet} = useWebMediaQueries()
-  const {fontScale} = useWindowDimensions()
-  if (isDesktop || isTablet) {
-    return 0
-  }
-  const navBarHeight = 42
-  const tabBarPad = 10 + 10 + 3 // padding + border
-  const normalLineHeight = 1.2
-  const tabBarText = 16 * normalLineHeight * fontScale
-  return navBarHeight + tabBarPad + tabBarText
-}
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 8a21d86ae..bb536bccd 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -1,29 +1,30 @@
 import React from 'react'
 import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../util/text/Text'
-import {RichText} from '#/components/RichText'
-import {usePalette} from 'lib/hooks/usePalette'
-import {s} from 'lib/styles'
-import {UserAvatar} from '../util/UserAvatar'
 import {AtUri} from '@atproto/api'
-import * as Toast from 'view/com/util/Toast'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {logger} from '#/logger'
-import {Trans, msg, Plural} from '@lingui/macro'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
 import {
-  usePinFeedMutation,
-  UsePreferencesQueryResponse,
+  useAddSavedFeedsMutation,
   usePreferencesQuery,
-  useSaveFeedMutation,
+  UsePreferencesQueryResponse,
   useRemoveFeedMutation,
 } from '#/state/queries/preferences'
-import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
+import {usePalette} from 'lib/hooks/usePalette'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {s} from 'lib/styles'
 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 * as Prompt from '#/components/Prompt'
-import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
+import {RichText} from '#/components/RichText'
+import {Text} from '../util/text/Text'
+import {UserAvatar} from '../util/UserAvatar'
 
 export function FeedSourceCard({
   feedUri,
@@ -87,53 +88,54 @@ export function FeedSourceCardLoaded({
   const removePromptControl = Prompt.usePromptControl()
   const navigation = useNavigationDeduped()
 
-  const {isPending: isSavePending, mutateAsync: saveFeed} =
-    useSaveFeedMutation()
+  const {isPending: isAddSavedFeedPending, mutateAsync: addSavedFeeds} =
+    useAddSavedFeedsMutation()
   const {isPending: isRemovePending, mutateAsync: removeFeed} =
     useRemoveFeedMutation()
-  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
 
-  const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || ''))
+  const savedFeedConfig = preferences?.savedFeeds?.find(
+    f => f.value === feed?.uri,
+  )
+  const isSaved = Boolean(savedFeedConfig)
 
   const onSave = React.useCallback(async () => {
-    if (!feed) return
+    if (!feed || isSaved) return
 
     try {
-      if (pinOnSave) {
-        await pinFeed({uri: feed.uri})
-      } else {
-        await saveFeed({uri: feed.uri})
-      }
+      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`))
       logger.error('Failed to save feed', {message: e})
     }
-  }, [_, feed, pinFeed, pinOnSave, saveFeed])
+  }, [_, feed, pinOnSave, addSavedFeeds, isSaved])
 
   const onUnsave = React.useCallback(async () => {
-    if (!feed) return
+    if (!savedFeedConfig) return
 
     try {
-      await removeFeed({uri: feed.uri})
+      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`))
       logger.error('Failed to unsave feed', {message: e})
     }
-  }, [_, feed, removeFeed])
+  }, [_, removeFeed, savedFeedConfig])
 
   const onToggleSaved = React.useCallback(async () => {
-    // Only feeds can be un/saved, lists are handled elsewhere
-    if (feed?.type !== 'feed') return
-
     if (isSaved) {
       removePromptControl.open()
     } else {
       await onSave()
     }
-  }, [feed?.type, isSaved, removePromptControl, onSave])
+  }, [isSaved, removePromptControl, onSave])
 
   /*
    * LOAD STATE
@@ -204,7 +206,7 @@ export function FeedSourceCardLoaded({
           }
         }}
         key={feed.uri}>
-        <View style={[styles.headerContainer]}>
+        <View style={[styles.headerContainer, a.align_start]}>
           <View style={[s.mr10]}>
             <UserAvatar type="algo" size={36} avatar={feed.avatar} />
           </View>
@@ -221,11 +223,11 @@ export function FeedSourceCardLoaded({
             </Text>
           </View>
 
-          {showSaveBtn && feed.type === 'feed' && (
+          {showSaveBtn && (
             <View style={[s.justifyCenter]}>
               <Pressable
                 testID={`feed-${feed.displayName}-toggleSave`}
-                disabled={isSavePending || isPinPending || isRemovePending}
+                disabled={isAddSavedFeedPending || isRemovePending}
                 accessibilityRole="button"
                 accessibilityLabel={
                   isSaved
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
index aa3ecb7fc..b068484e8 100644
--- a/src/view/com/home/HomeHeader.tsx
+++ b/src/view/com/home/HomeHeader.tsx
@@ -1,12 +1,14 @@
 import React from 'react'
-import {RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {HomeHeaderLayout} from './HomeHeaderLayout'
-import {FeedSourceInfo} from '#/state/queries/feed'
 import {useNavigation} from '@react-navigation/native'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {FeedSourceInfo} from '#/state/queries/feed'
+import {useSession} from '#/state/session'
 import {NavigationProp} from 'lib/routes/types'
 import {isWeb} from 'platform/detection'
+import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {TabBar} from '../pager/TabBar'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {HomeHeaderLayout} from './HomeHeaderLayout'
 
 export function HomeHeader(
   props: RenderTabBarFnProps & {
@@ -16,12 +18,17 @@ export function HomeHeader(
   },
 ) {
   const {feeds} = props
+  const {hasSession} = useSession()
   const navigation = useNavigation<NavigationProp>()
   const pal = usePalette('default')
 
   const hasPinnedCustom = React.useMemo<boolean>(() => {
-    return feeds.some(tab => tab.uri !== '')
-  }, [feeds])
+    if (!hasSession) return false
+    return feeds.some(tab => {
+      const isFollowing = tab.uri === 'following'
+      return !isFollowing
+    })
+  }, [feeds, hasSession])
 
   const items = React.useMemo(() => {
     const pinnedNames = feeds.map(f => f.displayName)
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index fd4c486af..a95a94835 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -1,22 +1,23 @@
 import React from 'react'
 import {LayoutAnimation, StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import ImageView from './ImageViewing'
-import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
-import * as Toast from '../util/Toast'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
-import {Button} from '../util/forms/Button'
-import {isIOS} from 'platform/detection'
 import * as MediaLibrary from 'expo-media-library'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
 import {
+  ImagesLightbox,
+  ProfileImageLightbox,
   useLightbox,
   useLightboxControls,
-  ProfileImageLightbox,
-  ImagesLightbox,
 } from '#/state/lightbox'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {saveImageToMediaLibrary, shareImageModal} from 'lib/media/manip'
+import {colors, s} from 'lib/styles'
+import {isIOS} from 'platform/detection'
+import {Button} from '../util/forms/Button'
+import {Text} from '../util/text/Text'
+import * as Toast from '../util/Toast'
+import ImageView from './ImageViewing'
 
 export function Lightbox() {
   const {activeLightbox} = useLightbox()
diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx
index 2b83c7a9a..ce3fbcef8 100644
--- a/src/view/com/modals/SelfLabel.tsx
+++ b/src/view/com/modals/SelfLabel.tsx
@@ -1,16 +1,17 @@
 import React, {useState} from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {Text} from '../util/text/Text'
-import {s, colors} from 'lib/styles'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useModalControls} from '#/state/modals'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {colors, s} from 'lib/styles'
 import {isWeb} from 'platform/detection'
+import {ScrollView} from 'view/com/modals/util'
 import {Button} from '../util/forms/Button'
 import {SelectableBtn} from '../util/forms/SelectableBtn'
-import {ScrollView} from 'view/com/modals/util'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useModalControls} from '#/state/modals'
+import {Text} from '../util/text/Text'
 
 const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
 
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index ff8acd60c..5791e26a9 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -1,11 +1,12 @@
-import React, {useRef, useMemo, useEffect, useState, useCallback} from 'react'
-import {StyleSheet, View, ScrollView, LayoutChangeEvent} from 'react-native'
-import {Text} from '../util/text/Text'
-import {PressableWithHover} from '../util/PressableWithHover'
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native'
+
+import {isNative} from '#/platform/detection'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {PressableWithHover} from '../util/PressableWithHover'
+import {Text} from '../util/text/Text'
 import {DraggableScrollView} from './DraggableScrollView'
-import {isNative} from '#/platform/detection'
 
 export interface TabBarProps {
   testID?: string
@@ -139,7 +140,10 @@ export function TabBar({
                 <Text
                   type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
                   testID={testID ? `${testID}-${item}` : undefined}
-                  style={selected ? pal.text : pal.textLight}>
+                  style={[
+                    selected ? pal.text : pal.textLight,
+                    {lineHeight: 20},
+                  ]}>
                   {item}
                 </Text>
               </View>
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 8969f7cd2..c51733d1b 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -8,6 +8,7 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import {AppBskyActorDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
@@ -64,6 +65,7 @@ let Feed = ({
   desktopFixedHeightOffset,
   ListHeaderComponent,
   extraData,
+  savedFeedConfig,
 }: {
   feed: FeedDescriptor
   feedParams?: FeedParams
@@ -82,6 +84,7 @@ let Feed = ({
   desktopFixedHeightOffset?: number
   ListHeaderComponent?: () => JSX.Element
   extraData?: any
+  savedFeedConfig?: AppBskyActorDefs.SavedFeed
 }): React.ReactNode => {
   const theme = useTheme()
   const {track} = useAnalytics()
@@ -140,7 +143,6 @@ let Feed = ({
     if (
       data?.pages.length === 1 &&
       (feed === 'following' ||
-        feed === 'home' ||
         feed === `author|${myDid}|posts_and_author_threads`)
     ) {
       queryClient.invalidateQueries({queryKey: RQKEY(feed)})
@@ -280,6 +282,7 @@ let Feed = ({
             feedDesc={feed}
             error={error ?? undefined}
             onPressTryAgain={onPressTryAgain}
+            savedFeedConfig={savedFeedConfig}
           />
         )
       } else if (item === LOAD_MORE_ERROR_ITEM) {
@@ -302,7 +305,15 @@ let Feed = ({
       }
       return <FeedSlice slice={item} />
     },
-    [feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState, _],
+    [
+      feed,
+      error,
+      onPressTryAgain,
+      onPressRetryLoadMore,
+      renderEmptyState,
+      _,
+      savedFeedConfig,
+    ],
   )
 
   const shouldRenderEndOfFeed =
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index d4ca38d07..a152bc909 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -1,21 +1,22 @@
 import React from 'react'
 import {View} from 'react-native'
-import {AppBskyFeedGetAuthorFeed, AtUri} from '@atproto/api'
-import {Text} from '../util/text/Text'
-import {Button} from '../util/forms/Button'
-import * as Toast from '../util/Toast'
-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 {logger} from '#/logger'
+import {AppBskyActorDefs, AppBskyFeedGetAuthorFeed, AtUri} from '@atproto/api'
 import {msg as msgLingui, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {FeedDescriptor} from '#/state/queries/post-feed'
-import {EmptyState} from '../util/EmptyState'
+import {useNavigation} from '@react-navigation/native'
+
 import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {FeedDescriptor} from '#/state/queries/post-feed'
 import {useRemoveFeedMutation} from '#/state/queries/preferences'
+import {usePalette} from 'lib/hooks/usePalette'
+import {NavigationProp} from 'lib/routes/types'
 import * as Prompt from '#/components/Prompt'
+import {EmptyState} from '../util/EmptyState'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {Button} from '../util/forms/Button'
+import {Text} from '../util/text/Text'
+import * as Toast from '../util/Toast'
 
 export enum KnownError {
   Block = 'Block',
@@ -33,10 +34,12 @@ export function FeedErrorMessage({
   feedDesc,
   error,
   onPressTryAgain,
+  savedFeedConfig,
 }: {
   feedDesc: FeedDescriptor
   error?: Error
   onPressTryAgain: () => void
+  savedFeedConfig?: AppBskyActorDefs.SavedFeed
 }) {
   const {_: _l} = useLingui()
   const knownError = React.useMemo(
@@ -46,13 +49,15 @@ export function FeedErrorMessage({
   if (
     typeof knownError !== 'undefined' &&
     knownError !== KnownError.Unknown &&
-    (feedDesc.startsWith('feedgen') || knownError === KnownError.FeedNSFPublic)
+    (savedFeedConfig?.type === 'feed' ||
+      knownError === KnownError.FeedNSFPublic)
   ) {
     return (
       <FeedgenErrorMessage
         feedDesc={feedDesc}
         knownError={knownError}
         rawError={error}
+        savedFeedConfig={savedFeedConfig}
       />
     )
   }
@@ -79,10 +84,12 @@ function FeedgenErrorMessage({
   feedDesc,
   knownError,
   rawError,
+  savedFeedConfig,
 }: {
   feedDesc: FeedDescriptor
   knownError: KnownError
   rawError?: Error
+  savedFeedConfig?: AppBskyActorDefs.SavedFeed
 }) {
   const pal = usePalette('default')
   const {_: _l} = useLingui()
@@ -131,7 +138,8 @@ function FeedgenErrorMessage({
 
   const onRemoveFeed = React.useCallback(async () => {
     try {
-      await removeFeed({uri})
+      if (!savedFeedConfig) return
+      await removeFeed(savedFeedConfig)
     } catch (err) {
       Toast.show(
         _l(
@@ -140,7 +148,7 @@ function FeedgenErrorMessage({
       )
       logger.error('Failed to remove feed', {message: err})
     }
-  }, [uri, removeFeed, _l])
+  }, [removeFeed, _l, savedFeedConfig])
 
   const cta = React.useMemo(() => {
     switch (knownError) {
@@ -154,13 +162,14 @@ function FeedgenErrorMessage({
       case KnownError.FeedgenUnknown: {
         return (
           <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
-            {knownError === KnownError.FeedgenDoesNotExist && (
-              <Button
-                type="inverted"
-                label={_l(msgLingui`Remove feed`)}
-                onPress={onRemoveFeed}
-              />
-            )}
+            {knownError === KnownError.FeedgenDoesNotExist &&
+              savedFeedConfig && (
+                <Button
+                  type="inverted"
+                  label={_l(msgLingui`Remove feed`)}
+                  onPress={onRemoveFeed}
+                />
+              )}
             <Button
               type="default-light"
               label={_l(msgLingui`View profile`)}
@@ -170,7 +179,7 @@ function FeedgenErrorMessage({
         )
       }
     }
-  }, [knownError, onViewProfile, onRemoveFeed, _l])
+  }, [knownError, onViewProfile, onRemoveFeed, _l, savedFeedConfig])
 
   return (
     <>
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index c1af39a5d..f58417887 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -1,14 +1,15 @@
 import React, {memo, useCallback} from 'react'
 import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
+import {msg, plural} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useModalControls} from '#/state/modals'
+import {useRequireAuth} from '#/state/session'
+import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
 import {RepostIcon} from 'lib/icons'
-import {s, colors} from 'lib/styles'
+import {colors, s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
-import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
-import {useModalControls} from '#/state/modals'
-import {useRequireAuth} from '#/state/session'
-import {msg, plural} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 
 interface Props {
   isReposted: boolean