about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/posts/PostFeed.tsx25
-rw-r--r--src/view/screens/Search/Explore.tsx48
-rw-r--r--src/view/shell/desktop/Feeds.tsx32
-rw-r--r--src/view/shell/desktop/RightNav.tsx44
-rw-r--r--src/view/shell/desktop/SidebarTrendingTopics.tsx104
5 files changed, 228 insertions, 25 deletions
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 10eb47d0a..7860d568d 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -23,6 +23,7 @@ import {logger} from '#/logger'
 import {isIOS, isWeb} from '#/platform/detection'
 import {listenPostCreated} from '#/state/events'
 import {useFeedFeedbackContext} from '#/state/feed-feedback'
+import {useTrendingSettings} from '#/state/preferences/trending'
 import {STALE} from '#/state/queries'
 import {
   FeedDescriptor,
@@ -34,7 +35,9 @@ import {
 } from '#/state/queries/post-feed'
 import {useSession} from '#/state/session'
 import {useProgressGuide} from '#/state/shell/progress-guide'
+import {useBreakpoints} from '#/alf'
 import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
+import {TrendingInterstitial} from '#/components/interstitials/Trending'
 import {List, ListRef} from '../util/List'
 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -90,6 +93,10 @@ type FeedRow =
       type: 'interstitialProgressGuide'
       key: string
     }
+  | {
+      type: 'interstitialTrending'
+      key: string
+    }
 
 export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
   if (feedRow.type === 'sliceItem') {
@@ -156,6 +163,7 @@ let PostFeed = ({
   const checkForNewRef = React.useRef<(() => void) | null>(null)
   const lastFetchRef = React.useRef<number>(Date.now())
   const [feedType, feedUri, feedTab] = feed.split('|')
+  const {gtTablet} = useBreakpoints()
 
   const opts = React.useMemo(
     () => ({enabled, ignoreFilterFor}),
@@ -259,6 +267,8 @@ let PostFeed = ({
   const showProgressIntersitial =
     (followProgressGuide || followAndLikeProgressGuide) && !isDesktop
 
+  const {trendingDisabled} = useTrendingSettings()
+
   const feedItems: FeedRow[] = React.useMemo(() => {
     let feedKind: 'following' | 'discover' | 'profile' | undefined
     if (feedType === 'following') {
@@ -304,7 +314,16 @@ let PostFeed = ({
                     type: 'interstitialProgressGuide',
                     key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
                   })
-                } else if (sliceIndex === 20) {
+                } else if (
+                  sliceIndex === 15 &&
+                  !gtTablet &&
+                  !trendingDisabled
+                ) {
+                  arr.push({
+                    type: 'interstitialTrending',
+                    key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
+                  })
+                } else if (sliceIndex === 30) {
                   arr.push({
                     type: 'interstitialFollows',
                     key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
@@ -390,6 +409,8 @@ let PostFeed = ({
     feedTab,
     hasSession,
     showProgressIntersitial,
+    trendingDisabled,
+    gtTablet,
   ])
 
   // events
@@ -476,6 +497,8 @@ let PostFeed = ({
         return <SuggestedFollows feed={feed} />
       } else if (row.type === 'interstitialProgressGuide') {
         return <ProgressGuide />
+      } else if (row.type === 'interstitialTrending') {
+        return <TrendingInterstitial />
       } else if (row.type === 'sliceItem') {
         const slice = row.slice
         if (slice.isFallbackMarker) {
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
index bd2ebe5d5..378ea59a4 100644
--- a/src/view/screens/Search/Explore.tsx
+++ b/src/view/screens/Search/Explore.tsx
@@ -24,6 +24,8 @@ import {
   ProfileCardFeedLoadingPlaceholder,
 } from '#/view/com/util/LoadingPlaceholder'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
+import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
 import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
 import {Button} from '#/components/Button'
 import * as FeedCard from '#/components/FeedCard'
@@ -240,6 +242,14 @@ type ExploreScreenItems =
       icon: React.ComponentType<SVGIconProps>
     }
   | {
+      type: 'trendingTopics'
+      key: string
+    }
+  | {
+      type: 'recommendations'
+      key: string
+    }
+  | {
       type: 'profile'
       key: string
       profile: AppBskyActorDefs.ProfileView
@@ -325,17 +335,27 @@ export function Explore() {
   ])
 
   const items = React.useMemo<ExploreScreenItems[]>(() => {
-    const i: ExploreScreenItems[] = [
-      {
-        type: 'header',
-        key: 'suggested-follows-header',
-        title: _(msg`Suggested accounts`),
-        description: _(
-          msg`Follow more accounts to get connected to your interests and build your network.`,
-        ),
-        icon: Person,
-      },
-    ]
+    const i: ExploreScreenItems[] = []
+
+    i.push({
+      type: 'trendingTopics',
+      key: `trending-topics`,
+    })
+
+    i.push({
+      type: 'recommendations',
+      key: `recommendations`,
+    })
+
+    i.push({
+      type: 'header',
+      key: 'suggested-follows-header',
+      title: _(msg`Suggested accounts`),
+      description: _(
+        msg`Follow more accounts to get connected to your interests and build your network.`,
+      ),
+      icon: Person,
+    })
 
     if (profiles) {
       // Currently the responses contain duplicate items.
@@ -490,6 +510,12 @@ export function Explore() {
             />
           )
         }
+        case 'trendingTopics': {
+          return <ExploreTrendingTopics />
+        }
+        case 'recommendations': {
+          return <ExploreRecommendations />
+        }
         case 'profile': {
           return (
             <View style={[a.border_b, t.atoms.border_contrast_low]}>
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 83b5420ce..1d515df55 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -14,7 +14,7 @@ import {createStaticClick, InlineLinkText} from '#/components/Link'
 export function DesktopFeeds() {
   const t = useTheme()
   const {_} = useLingui()
-  const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
+  const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos()
   const selectedFeed = useSelectedFeed()
   const setSelectedFeed = useSetSelectedFeed()
   const navigation = useNavigation<NavigationProp>()
@@ -25,14 +25,40 @@ export function DesktopFeeds() {
     return getCurrentRoute(state)
   })
 
-  if (!pinnedFeedInfos) {
+  if (isLoading) {
+    return (
+      <View
+        style={[
+          {
+            gap: 12,
+          },
+        ]}>
+        {Array(5)
+          .fill(0)
+          .map((_, i) => (
+            <View
+              key={i}
+              style={[
+                a.rounded_sm,
+                t.atoms.bg_contrast_25,
+                {
+                  height: 16,
+                  width: i % 2 === 0 ? '60%' : '80%',
+                },
+              ]}
+            />
+          ))}
+      </View>
+    )
+  }
+
+  if (error || !pinnedFeedInfos) {
     return null
   }
 
   return (
     <View
       style={[
-        a.flex_1,
         web({
           gap: 10,
           /*
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 895d16021..363294aa5 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -1,6 +1,8 @@
+import React from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/core'
 
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -8,17 +10,41 @@ import {useKawaiiMode} from '#/state/preferences/kawaii'
 import {useSession} from '#/state/session'
 import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
 import {DesktopSearch} from '#/view/shell/desktop/Search'
+import {SidebarTrendingTopics} from '#/view/shell/desktop/SidebarTrendingTopics'
 import {atoms as a, useGutters, useTheme, web} from '#/alf'
+import {Divider} from '#/components/Divider'
 import {InlineLinkText} from '#/components/Link'
 import {ProgressGuideList} from '#/components/ProgressGuide/List'
 import {Text} from '#/components/Typography'
 
+function useWebQueryParams() {
+  const navigation = useNavigation()
+  const [params, setParams] = React.useState<Record<string, string>>({})
+
+  React.useEffect(() => {
+    return navigation.addListener('state', e => {
+      try {
+        const {state} = e.data
+        const lastRoute = state.routes[state.routes.length - 1]
+        const {params} = lastRoute
+        setParams(params)
+      } catch (e) {}
+    })
+  }, [navigation, setParams])
+
+  return params
+}
+
 export function DesktopRightNav({routeName}: {routeName: string}) {
   const t = useTheme()
   const {_} = useLingui()
   const {hasSession, currentAccount} = useSession()
   const kawaii = useKawaiiMode()
   const gutters = useGutters(['base', 0, 'base', 'wide'])
+  const isSearchScreen = routeName === 'Search'
+  const webqueryParams = useWebQueryParams()
+  const searchQuery = webqueryParams?.q
+  const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery)
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -29,6 +55,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
     <View
       style={[
         gutters,
+        a.gap_lg,
         web({
           position: 'fixed',
           left: '50%',
@@ -43,21 +70,18 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
           overflowY: 'auto',
         }),
       ]}>
-      {routeName !== 'Search' && (
-        <View style={[a.pb_lg]}>
-          <DesktopSearch />
-        </View>
-      )}
+      {!isSearchScreen && <DesktopSearch />}
+
       {hasSession && (
         <>
-          <ProgressGuideList style={[a.pb_xl]} />
-          <View
-            style={[a.pb_lg, a.mb_lg, a.border_b, t.atoms.border_contrast_low]}>
-            <DesktopFeeds />
-          </View>
+          <ProgressGuideList />
+          <DesktopFeeds />
+          <Divider />
         </>
       )}
 
+      {showTrending && <SidebarTrendingTopics />}
+
       <Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
         {hasSession && (
           <>
diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx
new file mode 100644
index 000000000..e22fad54d
--- /dev/null
+++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx
@@ -0,0 +1,104 @@
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {
+  useTrendingSettings,
+  useTrendingSettingsApi,
+} from '#/state/preferences/trending'
+import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
+import {useTrendingConfig} from '#/state/trending-config'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {Divider} from '#/components/Divider'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
+import * as Prompt from '#/components/Prompt'
+import {
+  TrendingTopic,
+  TrendingTopicLink,
+  TrendingTopicSkeleton,
+} from '#/components/TrendingTopics'
+import {Text} from '#/components/Typography'
+
+const TRENDING_LIMIT = 6
+
+export function SidebarTrendingTopics() {
+  const {enabled} = useTrendingConfig()
+  const {trendingDisabled} = useTrendingSettings()
+  return !enabled ? null : trendingDisabled ? null : <Inner />
+}
+
+function Inner() {
+  const t = useTheme()
+  const {_} = useLingui()
+  const trendingPrompt = Prompt.usePromptControl()
+  const {setTrendingDisabled} = useTrendingSettingsApi()
+  const {data: trending, error, isLoading} = useTrendingTopics()
+  const noTopics = !isLoading && !error && !trending?.topics?.length
+
+  return error || noTopics ? null : (
+    <>
+      <View style={[a.gap_sm, {paddingBottom: 2}]}>
+        <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+          <Graph size="sm" />
+          <Text
+            style={[
+              a.flex_1,
+              a.text_sm,
+              a.font_bold,
+              t.atoms.text_contrast_medium,
+            ]}>
+            <Trans>Trending</Trans>
+          </Text>
+          <Button
+            label={_(msg`Hide trending topics`)}
+            size="tiny"
+            variant="ghost"
+            color="secondary"
+            shape="round"
+            onPress={() => trendingPrompt.open()}>
+            <ButtonIcon icon={X} />
+          </Button>
+        </View>
+
+        <View style={[a.flex_row, a.flex_wrap, {gap: '6px 4px'}]}>
+          {isLoading ? (
+            Array(TRENDING_LIMIT)
+              .fill(0)
+              .map((_n, i) => (
+                <TrendingTopicSkeleton key={i} size="small" index={i} />
+              ))
+          ) : !trending?.topics ? null : (
+            <>
+              {trending.topics.slice(0, TRENDING_LIMIT).map(topic => (
+                <TrendingTopicLink key={topic.link} topic={topic}>
+                  {({hovered}) => (
+                    <TrendingTopic
+                      size="small"
+                      topic={topic}
+                      style={[
+                        hovered && [
+                          t.atoms.border_contrast_high,
+                          t.atoms.bg_contrast_25,
+                        ],
+                      ]}
+                    />
+                  )}
+                </TrendingTopicLink>
+              ))}
+            </>
+          )}
+        </View>
+      </View>
+      <Prompt.Basic
+        control={trendingPrompt}
+        title={_(msg`Hide trending topics?`)}
+        description={_(msg`You can update this later from your settings.`)}
+        confirmButtonCta={_(msg`Hide`)}
+        onConfirm={() => setTrendingDisabled(true)}
+      />
+      <Divider />
+    </>
+  )
+}