about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-04-19 23:37:11 +0100
committerGitHub <noreply@github.com>2024-04-19 23:37:11 +0100
commitd3c0b48da3053727dd4e02acc353f6372121d944 (patch)
treef49641f379ea119622696824430a9393f7bf55fa /src
parentc0ca891501cbc60eb945e3235800ec1e29a15ccd (diff)
downloadvoidsky-d3c0b48da3053727dd4e02acc353f6372121d944.tar.zst
Top/Latest for hashtags (#3625)
* Split HashtagScreen into two components

* Hashtag tabs

* Visual fixes
Diffstat (limited to 'src')
-rw-r--r--src/screens/Hashtag.tsx198
-rw-r--r--src/view/com/util/ViewHeader.tsx51
2 files changed, 177 insertions, 72 deletions
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
index 5388593f1..34539f510 100644
--- a/src/screens/Hashtag.tsx
+++ b/src/screens/Hashtag.tsx
@@ -1,11 +1,12 @@
 import React from 'react'
-import {ListRenderItemInfo, Pressable} from 'react-native'
+import {ListRenderItemInfo, Pressable, StyleSheet, View} from 'react-native'
 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 
+import {usePalette} from '#/lib/hooks/usePalette'
 import {HITSLOP_10} from 'lib/constants'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
 import {CommonNavigatorParams} from 'lib/routes/types'
@@ -13,18 +14,17 @@ import {shareUrl} from 'lib/sharing'
 import {cleanError} from 'lib/strings/errors'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {enforceLen} from 'lib/strings/helpers'
-import {isNative} from 'platform/detection'
+import {isNative, isWeb} from 'platform/detection'
 import {useSearchPostsQuery} from 'state/queries/search-posts'
-import {useSetMinimalShellMode} from 'state/shell'
+import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from 'state/shell'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
+import {CenteredView} from '#/view/com/util/Views'
 import {Post} from 'view/com/post/Post'
 import {List} from 'view/com/util/List'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
-import {
-  ListFooter,
-  ListHeaderDesktop,
-  ListMaybePlaceholder,
-} from '#/components/Lists'
+import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
 const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
   return <Post post={item} />
@@ -38,20 +38,13 @@ export default function HashtagScreen({
   route,
 }: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
   const {tag, author} = route.params
-  const setMinimalShellMode = useSetMinimalShellMode()
   const {_} = useLingui()
-  const initialNumToRender = useInitialNumToRender()
-  const [isPTR, setIsPTR] = React.useState(false)
+  const pal = usePalette('default')
 
   const fullTag = React.useMemo(() => {
     return `#${decodeURIComponent(tag)}`
   }, [tag])
 
-  const queryParam = React.useMemo(() => {
-    if (!author) return fullTag
-    return `${fullTag} from:${sanitizeHandle(author)}`
-  }, [fullTag, author])
-
   const headerTitle = React.useMemo(() => {
     return enforceLen(fullTag.toLowerCase(), 24, true, 'middle')
   }, [fullTag])
@@ -61,8 +54,127 @@ export default function HashtagScreen({
     return sanitizeHandle(author)
   }, [author])
 
+  const onShare = React.useCallback(() => {
+    const url = new URL('https://bsky.app')
+    url.pathname = `/hashtag/${decodeURIComponent(tag)}`
+    if (author) {
+      url.searchParams.set('author', author)
+    }
+    shareUrl(url.toString())
+  }, [tag, author])
+
+  const [activeTab, setActiveTab] = React.useState(0)
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
+
+  const onPageSelected = React.useCallback(
+    (index: number) => {
+      setMinimalShellMode(false)
+      setDrawerSwipeDisabled(index > 0)
+      setActiveTab(index)
+    },
+    [setDrawerSwipeDisabled, setMinimalShellMode],
+  )
+
+  const sections = React.useMemo(() => {
+    return [
+      {
+        title: _(msg`Top`),
+        component: (
+          <HashtagScreenTab
+            fullTag={fullTag}
+            author={author}
+            sort="top"
+            active={activeTab === 0}
+          />
+        ),
+      },
+      {
+        title: _(msg`Latest`),
+        component: (
+          <HashtagScreenTab
+            fullTag={fullTag}
+            author={author}
+            sort="latest"
+            active={activeTab === 1}
+          />
+        ),
+      },
+    ]
+  }, [_, fullTag, author, activeTab])
+
+  return (
+    <>
+      <CenteredView sideBorders style={[pal.border, pal.view]}>
+        <ViewHeader
+          showOnDesktop
+          title={headerTitle}
+          subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
+          canGoBack
+          renderButton={
+            isNative
+              ? () => (
+                  <Pressable
+                    accessibilityRole="button"
+                    onPress={onShare}
+                    hitSlop={HITSLOP_10}>
+                    <ArrowOutOfBox_Stroke2_Corner0_Rounded
+                      size="lg"
+                      onPress={onShare}
+                    />
+                  </Pressable>
+                )
+              : undefined
+          }
+        />
+      </CenteredView>
+      <Pager
+        onPageSelected={onPageSelected}
+        renderTabBar={props => (
+          <CenteredView
+            sideBorders
+            style={[pal.border, pal.view, styles.tabBarContainer]}>
+            <TabBar items={sections.map(section => section.title)} {...props} />
+          </CenteredView>
+        )}
+        initialPage={0}>
+        {sections.map((section, i) => (
+          <View key={i}>{section.component}</View>
+        ))}
+      </Pager>
+    </>
+  )
+}
+
+function HashtagScreenTab({
+  fullTag,
+  author,
+  sort,
+  active,
+}: {
+  fullTag: string
+  author: string | undefined
+  sort: 'top' | 'latest'
+  active: boolean
+}) {
+  const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
+  const [isPTR, setIsPTR] = React.useState(false)
+
+  const queryParam = React.useMemo(() => {
+    if (!author) return fullTag
+    return `${fullTag} from:${sanitizeHandle(author)}`
+  }, [fullTag, author])
+
   const {
     data,
+    isFetched,
     isFetchingNextPage,
     isLoading,
     isError,
@@ -70,27 +182,12 @@ export default function HashtagScreen({
     refetch,
     fetchNextPage,
     hasNextPage,
-  } = useSearchPostsQuery({query: queryParam})
+  } = useSearchPostsQuery({query: queryParam, sort, enabled: active})
 
   const posts = React.useMemo(() => {
     return data?.pages.flatMap(page => page.posts) || []
   }, [data])
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
-
-  const onShare = React.useCallback(() => {
-    const url = new URL('https://bsky.app')
-    url.pathname = `/hashtag/${decodeURIComponent(tag)}`
-    if (author) {
-      url.searchParams.set('author', author)
-    }
-    shareUrl(url.toString())
-  }, [tag, author])
-
   const onRefresh = React.useCallback(async () => {
     setIsPTR(true)
     await refetch()
@@ -104,29 +201,9 @@ export default function HashtagScreen({
 
   return (
     <>
-      <ViewHeader
-        title={headerTitle}
-        subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
-        canGoBack
-        renderButton={
-          isNative
-            ? () => (
-                <Pressable
-                  accessibilityRole="button"
-                  onPress={onShare}
-                  hitSlop={HITSLOP_10}>
-                  <ArrowOutOfBox_Stroke2_Corner0_Rounded
-                    size="lg"
-                    onPress={onShare}
-                  />
-                </Pressable>
-              )
-            : undefined
-        }
-      />
       {posts.length < 1 ? (
         <ListMaybePlaceholder
-          isLoading={isLoading}
+          isLoading={isLoading || !isFetched}
           isError={isError}
           onRetry={refetch}
           emptyType="results"
@@ -143,12 +220,6 @@ export default function HashtagScreen({
           onEndReachedThreshold={4}
           // @ts-ignore web only -prf
           desktopFixedHeight
-          ListHeaderComponent={
-            <ListHeaderDesktop
-              title={headerTitle}
-              subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
-            />
-          }
           ListFooterComponent={
             <ListFooter
               isFetchingNextPage={isFetchingNextPage}
@@ -163,3 +234,12 @@ export default function HashtagScreen({
     </>
   )
 }
+
+const styles = StyleSheet.create({
+  tabBarContainer: {
+    // @ts-ignore web only
+    position: isWeb ? 'sticky' : '',
+    top: 0,
+    zIndex: 1,
+  },
+})
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 872e10eef..63a2b3de3 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -1,19 +1,20 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import Animated from 'react-native-reanimated'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
-import {CenteredView} from './Views'
-import {Text} from './text/Text'
+
+import {useSetDrawerOpen} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useAnalytics} from 'lib/analytics/analytics'
 import {NavigationProp} from 'lib/routes/types'
-import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
-import Animated from 'react-native-reanimated'
-import {useSetDrawerOpen} from '#/state/shell'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 import {useTheme} from '#/alf'
+import {Text} from './text/Text'
+import {CenteredView} from './Views'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
@@ -62,6 +63,7 @@ export function ViewHeader({
       return (
         <DesktopWebHeader
           title={title}
+          subtitle={subtitle}
           renderButton={renderButton}
           showBorder={showBorder}
         />
@@ -136,14 +138,17 @@ export function ViewHeader({
 
 function DesktopWebHeader({
   title,
+  subtitle,
   renderButton,
   showBorder = true,
 }: {
   title: string
+  subtitle?: string
   renderButton?: () => JSX.Element
   showBorder?: boolean
 }) {
   const pal = usePalette('default')
+  const t = useTheme()
   return (
     <CenteredView
       style={[
@@ -153,13 +158,30 @@ function DesktopWebHeader({
         {
           borderBottomWidth: showBorder ? 1 : 0,
         },
+        {display: 'flex', flexDirection: 'column'},
       ]}>
-      <View style={styles.titleContainer} pointerEvents="none">
-        <Text type="title-lg" style={[pal.text, styles.title]}>
-          {title}
-        </Text>
+      <View>
+        <View style={styles.titleContainer} pointerEvents="none">
+          <Text type="title-lg" style={[pal.text, styles.title]}>
+            {title}
+          </Text>
+        </View>
+        {renderButton?.()}
       </View>
-      {renderButton?.()}
+      {subtitle ? (
+        <View>
+          <View style={[styles.titleContainer]} pointerEvents="none">
+            <Text
+              style={[
+                pal.text,
+                styles.subtitleDesktop,
+                t.atoms.text_contrast_medium,
+              ]}>
+              {subtitle}
+            </Text>
+          </View>
+        </View>
+      ) : null}
     </CenteredView>
   )
 }
@@ -236,6 +258,9 @@ const styles = StyleSheet.create({
   subtitle: {
     fontSize: 13,
   },
+  subtitleDesktop: {
+    fontSize: 15,
+  },
   backBtn: {
     width: 30,
     height: 30,