about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx26
-rw-r--r--src/App.web.tsx28
-rw-r--r--src/Navigation.tsx5
-rw-r--r--src/lib/ThemeContext.tsx54
-rw-r--r--src/lib/api/feed-manip.ts5
-rw-r--r--src/lib/constants.ts107
-rw-r--r--src/lib/react-query.ts3
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeeds.tsx70
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeedsItem.tsx11
-rw-r--r--src/view/shell/Drawer.tsx9
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx28
11 files changed, 160 insertions, 186 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 09782a875..d43155bf3 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -16,6 +16,8 @@ import * as notifications from 'lib/notifications/notifications'
 import * as analytics from 'lib/analytics/analytics'
 import * as Toast from './view/com/util/Toast'
 import {handleLink} from './Navigation'
+import {QueryClientProvider} from '@tanstack/react-query'
+import {queryClient} from 'lib/react-query'
 
 SplashScreen.preventAutoHideAsync()
 
@@ -51,17 +53,19 @@ const App = observer(function AppImpl() {
     return null
   }
   return (
-    <ThemeProvider theme={rootStore.shell.colorMode}>
-      <RootSiblingParent>
-        <analytics.Provider>
-          <RootStoreProvider value={rootStore}>
-            <GestureHandlerRootView style={s.h100pct}>
-              <Shell />
-            </GestureHandlerRootView>
-          </RootStoreProvider>
-        </analytics.Provider>
-      </RootSiblingParent>
-    </ThemeProvider>
+    <QueryClientProvider client={queryClient}>
+      <ThemeProvider theme={rootStore.shell.colorMode}>
+        <RootSiblingParent>
+          <analytics.Provider>
+            <RootStoreProvider value={rootStore}>
+              <GestureHandlerRootView style={s.h100pct}>
+                <Shell />
+              </GestureHandlerRootView>
+            </RootStoreProvider>
+          </analytics.Provider>
+        </RootSiblingParent>
+      </ThemeProvider>
+    </QueryClientProvider>
   )
 })
 
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 41a7189d3..a9123cc58 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -9,6 +9,8 @@ import {Shell} from './view/shell/index'
 import {ToastContainer} from './view/com/util/Toast.web'
 import {ThemeProvider} from 'lib/ThemeContext'
 import {observer} from 'mobx-react-lite'
+import {QueryClientProvider} from '@tanstack/react-query'
+import {queryClient} from 'lib/react-query'
 
 const App = observer(function AppImpl() {
   const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
@@ -30,18 +32,20 @@ const App = observer(function AppImpl() {
   }
 
   return (
-    <ThemeProvider theme={rootStore.shell.colorMode}>
-      <RootSiblingParent>
-        <analytics.Provider>
-          <RootStoreProvider value={rootStore}>
-            <SafeAreaProvider>
-              <Shell />
-            </SafeAreaProvider>
-            <ToastContainer />
-          </RootStoreProvider>
-        </analytics.Provider>
-      </RootSiblingParent>
-    </ThemeProvider>
+    <QueryClientProvider client={queryClient}>
+      <ThemeProvider theme={rootStore.shell.colorMode}>
+        <RootSiblingParent>
+          <analytics.Provider>
+            <RootStoreProvider value={rootStore}>
+              <SafeAreaProvider>
+                <Shell />
+              </SafeAreaProvider>
+              <ToastContainer />
+            </RootStoreProvider>
+          </analytics.Provider>
+        </RootSiblingParent>
+      </ThemeProvider>
+    </QueryClientProvider>
   )
 })
 
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index dac70dfc7..c16ff3a8c 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -348,7 +348,6 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
         component={ProfileScreen}
         initialParams={{
           name: store.me.did,
-          hideBackButton: true,
         }}
       />
       {commonScreens(MyProfileTab as typeof HomeTab)}
@@ -362,7 +361,9 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
  */
 const FlatNavigator = observer(function FlatNavigatorImpl() {
   const pal = usePalette('default')
-  const unreadCountLabel = useStores().me.notifications.unreadCountLabel
+  const store = useStores()
+  const unreadCountLabel = store.me.notifications.unreadCountLabel
+
   const title = (page: string) => bskyTitle(page, unreadCountLabel)
   return (
     <Flat.Navigator
diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx
index fe25dde54..483c50c42 100644
--- a/src/lib/ThemeContext.tsx
+++ b/src/lib/ThemeContext.tsx
@@ -1,8 +1,9 @@
+import {isWeb} from 'platform/detection'
 import React, {ReactNode, createContext, useContext} from 'react'
 import {
   AppState,
   TextStyle,
-  useColorScheme,
+  useColorScheme as useColorScheme_BUGGY,
   ViewStyle,
   ColorSchemeName,
 } from 'react-native'
@@ -92,33 +93,44 @@ export const ThemeContext = createContext<Theme>(defaultTheme)
 
 export const useTheme = () => useContext(ThemeContext)
 
-export const ThemeProvider: React.FC<ThemeProviderProps> = ({
-  theme,
-  children,
-}) => {
-  const colorSchemeFromRN = useColorScheme()
-  const [nativeColorScheme, setNativeColorScheme] =
-    React.useState<ColorSchemeName>(colorSchemeFromRN)
+function getTheme(theme: ColorSchemeName) {
+  return theme === 'dark' ? darkTheme : defaultTheme
+}
+
+/**
+ * With RN iOS, we can only "trust" the color scheme reported while the app is
+ * active. This is a workaround until the bug is fixed upstream.
+ *
+ * @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504
+ * @see https://github.com/facebook/react-native/pull/39439
+ */
+function useColorScheme_FIXED() {
+  const colorScheme = useColorScheme_BUGGY()
+  const [currentColorScheme, setCurrentColorScheme] =
+    React.useState<ColorSchemeName>(colorScheme)
 
   React.useEffect(() => {
+    // we don't need to be updating state on web
+    if (isWeb) return
     const subscription = AppState.addEventListener('change', state => {
       const isActive = state === 'active'
-
       if (!isActive) return
-
-      setNativeColorScheme(colorSchemeFromRN)
+      setCurrentColorScheme(colorScheme)
     })
     return () => subscription.remove()
-  }, [colorSchemeFromRN])
+  }, [colorScheme])
+
+  return isWeb ? colorScheme : currentColorScheme
+}
 
-  const value =
-    theme === 'system'
-      ? nativeColorScheme === 'dark'
-        ? darkTheme
-        : defaultTheme
-      : theme === 'dark'
-      ? darkTheme
-      : defaultTheme
+export const ThemeProvider: React.FC<ThemeProviderProps> = ({
+  theme,
+  children,
+}) => {
+  const colorScheme = useColorScheme_FIXED()
+  const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
 
-  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
+  return (
+    <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
+  )
 }
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 60b0f2641..149859ea9 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -281,7 +281,10 @@ export class FeedTuner {
 
 function getSelfReplyUri(item: FeedViewPost): string | undefined {
   if (item.reply) {
-    if (AppBskyFeedDefs.isPostView(item.reply.parent)) {
+    if (
+      AppBskyFeedDefs.isPostView(item.reply.parent) &&
+      !AppBskyFeedDefs.isReasonRepost(item.reason) // don't thread reposted self-replies
+    ) {
       return item.reply.parent.author.did === item.post.author.did
         ? item.reply.parent.uri
         : undefined
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 94551e6ef..001cdf8c3 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -148,110 +148,3 @@ export const HITSLOP_10 = createHitslop(10)
 export const HITSLOP_20 = createHitslop(20)
 export const HITSLOP_30 = createHitslop(30)
 export const BACK_HITSLOP = HITSLOP_30
-
-export const RECOMMENDED_FEEDS = [
-  {
-    did: 'did:plc:hsqwcidfez66lwm3gxhfv5in',
-    rkey: 'aaaf2pqeodmpy',
-  },
-  {
-    did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn',
-    rkey: 'aaap4tbjcfe5y',
-  },
-  {
-    did: 'did:plc:5rw2on4i56btlcajojaxwcat',
-    rkey: 'aaao6g552b33o',
-  },
-  {
-    did: 'did:plc:jfhpnnst6flqway4eaeqzj2a',
-    rkey: 'for-science',
-  },
-  {
-    did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy',
-    rkey: 'bsky-news',
-  },
-  {
-    did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25',
-    rkey: 'astro',
-  },
-  {
-    did: 'did:plc:tenurhgjptubkk5zf5qhi3og',
-    rkey: 'h-nba',
-  },
-  {
-    did: 'did:plc:vpkhqolt662uhesyj6nxm7ys',
-    rkey: 'devfeed',
-  },
-  {
-    did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5',
-    rkey: 'flipboard-tech',
-  },
-  {
-    did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw',
-    rkey: 'blacksky',
-  },
-  {
-    did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe',
-    rkey: 'aaaotfjzjplna',
-  },
-  {
-    did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z',
-    rkey: 'formula-one',
-  },
-  {
-    did: 'did:plc:q6gjnaw2blty4crticxkmujt',
-    rkey: 'positivifeed',
-  },
-  {
-    did: 'did:plc:l72uci4styb4jucsgcrrj5ap',
-    rkey: 'aaao5dzfm36u4',
-  },
-  {
-    did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n',
-    rkey: 'aaaavlyvqzst2',
-  },
-  {
-    did: 'did:plc:nkahctfdi6bxk72umytfwghw',
-    rkey: 'aaado2uvfsc6w',
-  },
-  {
-    did: 'did:plc:epihigio3d7un7u3gpqiy5gv',
-    rkey: 'aaaekwsc7zsvs',
-  },
-  {
-    did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7',
-    rkey: 'aaaejxlobe474',
-  },
-  {
-    did: 'did:plc:mlq4aycufcuolr7ax6sezpc4',
-    rkey: 'aaaoudweck6uy',
-  },
-  {
-    did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg',
-    rkey: 'aaadzjxbcddzi',
-  },
-  {
-    did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr',
-    rkey: 'aaab2vesjtszc',
-  },
-  {
-    did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if',
-    rkey: 'aaacynbxwimok',
-  },
-  {
-    did: 'did:plc:abv47bjgzjgoh3yrygwoi36x',
-    rkey: 'aaagt6amuur5e',
-  },
-  {
-    did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt',
-    rkey: 'aaacjerk7gwek',
-  },
-  {
-    did: 'did:plc:geoqe3qls5mwezckxxsewys2',
-    rkey: 'aaai43yetqshu',
-  },
-  {
-    did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34',
-    rkey: 'authors',
-  },
-]
diff --git a/src/lib/react-query.ts b/src/lib/react-query.ts
new file mode 100644
index 000000000..2a8f1d759
--- /dev/null
+++ b/src/lib/react-query.ts
@@ -0,0 +1,3 @@
+import {QueryClient} from '@tanstack/react-query'
+
+export const queryClient = new QueryClient()
diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
index 99cdcafd0..8e29a5895 100644
--- a/src/view/com/auth/onboarding/RecommendedFeeds.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {FlatList, StyleSheet, View} from 'react-native'
+import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints'
@@ -10,7 +10,10 @@ 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 {RECOMMENDED_FEEDS} from 'lib/constants'
+import {useQuery} from '@tanstack/react-query'
+import {useStores} from 'state/index'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
 
 type Props = {
   next: () => void
@@ -18,8 +21,31 @@ 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()
+
+        if (!success) return
+
+        return (feeds.length ? feeds : []).map(feed => {
+          return new CustomFeedModel(store, feed)
+        })
+      } catch (e) {
+        return
+      }
+    },
+  })
+
+  const hasFeeds = recommendedFeeds && recommendedFeeds.length
 
   const title = (
     <>
@@ -86,12 +112,20 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
           horizontal
           titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
           contentStyle={{paddingHorizontal: 0}}>
-          <FlatList
-            data={RECOMMENDED_FEEDS}
-            renderItem={({item}) => <RecommendedFeedsItem {...item} />}
-            keyExtractor={item => item.did + item.rkey}
-            style={{flex: 1}}
-          />
+          {hasFeeds ? (
+            <FlatList
+              data={recommendedFeeds}
+              renderItem={({item}) => <RecommendedFeedsItem item={item} />}
+              keyExtractor={item => item.uri}
+              style={{flex: 1}}
+            />
+          ) : isLoading ? (
+            <View>
+              <ActivityIndicator size="large" />
+            </View>
+          ) : (
+            <ErrorMessage message="Failed to load recommended feeds" />
+          )}
         </TitleColumnLayout>
       </TabletOrDesktop>
       <Mobile>
@@ -106,12 +140,20 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
             pinned feeds.
           </Text>
 
-          <FlatList
-            data={RECOMMENDED_FEEDS}
-            renderItem={({item}) => <RecommendedFeedsItem {...item} />}
-            keyExtractor={item => item.did + item.rkey}
-            style={{flex: 1}}
-          />
+          {hasFeeds ? (
+            <FlatList
+              data={recommendedFeeds}
+              renderItem={({item}) => <RecommendedFeedsItem item={item} />}
+              keyExtractor={item => item.uri}
+              style={{flex: 1}}
+            />
+          ) : isLoading ? (
+            <View>
+              <ActivityIndicator size="large" />
+            </View>
+          ) : (
+            <ErrorMessage message="Failed to load recommended feeds" />
+          )}
 
           <Button
             onPress={next}
diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
index e5d12273a..d130dc138 100644
--- a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
@@ -8,22 +8,17 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
 import * as Toast from 'view/com/util/Toast'
 import {HeartIcon} from 'lib/icons'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useCustomFeed} from 'lib/hooks/useCustomFeed'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {makeRecordUri} from 'lib/strings/url-helpers'
 import {sanitizeHandle} from 'lib/strings/handles'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
-  did,
-  rkey,
+  item,
 }: {
-  did: string
-  rkey: string
+  item: CustomFeedModel
 }) {
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
-  const uri = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
-  const item = useCustomFeed(uri)
   if (!item) return null
   const onToggle = async () => {
     if (item.isSaved) {
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 3379d0501..67092938e 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -64,8 +64,13 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       const state = navigation.getState()
       store.shell.closeDrawer()
       if (isWeb) {
-        // @ts-ignore must be Home, Search, Notifications, or MyProfile
-        navigation.navigate(tab)
+        // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
+        if (tab === 'MyProfile') {
+          navigation.navigate('Profile', {name: store.me.handle})
+        } else {
+          // @ts-ignore must be Home, Search, Notifications, or MyProfile
+          navigation.navigate(tab)
+        }
       } else {
         const tabState = getTabState(state, tab)
         if (tabState === TabState.InsideAtRoot) {
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index ee575c217..af70d3364 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -18,10 +18,12 @@ import {
   SatelliteDishIcon,
   SatelliteDishIconSolid,
   UserIcon,
+  UserIconSolid,
 } from 'lib/icons'
 import {Link} from 'view/com/util/Link'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams} from 'lib/routes/types'
 
 export const BottomBarWeb = observer(function BottomBarWebImpl() {
   const store = useStores()
@@ -89,13 +91,16 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() {
         }}
       </NavItem>
       <NavItem routeName="Profile" href={makeProfileLink(store.me)}>
-        {() => (
-          <UserIcon
-            size={28}
-            strokeWidth={1.5}
-            style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
-          />
-        )}
+        {({isActive}) => {
+          const Icon = isActive ? UserIconSolid : UserIcon
+          return (
+            <Icon
+              size={28}
+              strokeWidth={1.5}
+              style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
+            />
+          )
+        }}
       </NavItem>
     </Animated.View>
   )
@@ -107,7 +112,14 @@ const NavItem: React.FC<{
   routeName: string
 }> = ({children, href, routeName}) => {
   const currentRoute = useNavigationState(getCurrentRoute)
-  const isActive = isTab(currentRoute.name, routeName)
+  const store = useStores()
+  const isActive =
+    currentRoute.name === 'Profile'
+      ? isTab(currentRoute.name, routeName) &&
+        (currentRoute.params as CommonNavigatorParams['Profile']).name ===
+          store.me.handle
+      : isTab(currentRoute.name, routeName)
+
   return (
     <Link href={href} style={styles.ctrl}>
       {children({isActive})}