about summary refs log tree commit diff
path: root/src/screens/Topic.tsx
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-12-17 21:45:39 -0600
committerGitHub <noreply@github.com>2024-12-17 19:45:39 -0800
commita2019aceec001e276272832b97ea5e2ec864c8a5 (patch)
treeeaddab8a7a009650d93bb3b49c750619d98bb44d /src/screens/Topic.tsx
parenta07949ec8e63bae178a829f65c33fcd9622b28ec (diff)
downloadvoidsky-a2019aceec001e276272832b97ea5e2ec864c8a5.tar.zst
Trending (Beta) (#7144)
* Add WIP UIs for trending topics and suggested starterpacks

* Disable SPs for now

* Improve explore treatment a bit, add some polish to cards

* Add tiny option in RightNav

* Add persisted option to hide trending from sidebar

* Add to settings, abstract state, not updating in tab

* Fix up hide/show toggle state, WITH broadcast hacK

* Clean up persisted code, add new setting

* Add new interstitial to Discover

* Exploration

* First hack at mute words

* Wire up interstitial and Explore page

* Align components

* Some skeleton UI

* Handle service config, enablement, load states, update lex contract

* Centralize mute word handling

* Stale time to 30m

* Cache enabled value for reloads, use real data for service config

* Remove broadcast hack

* Remove titleChild

* Gate settings too

* Update package, rm langs

* Add feature gate

* Only english during beta period

* Hook up real data

* Tweak config

* Straight passthrough links

* Hook up prod agent

* Fix no-show logic

* Up config query to 5 min

* Remove old file

* Remove comment

* Remove stray flex_1

* Make trending setting global

* Quick placeholder state

* Limit # in sidebar, tweak spacing

* Tweak gaps

* Handle hide/show of sidebar

* Simplify messages

* Remove interstitial

* Revert "Remove interstitial"

This reverts commit 1358ad47fdf7e633749340c410933b508af46c10.

* Only show interstitial on mobile

* Fix gap

* Add explore page recommendations

* [topics] add topic screen (#7149)

* add topic screen

* decode

* fix search query

* decode

* add server route

* Fix potential bad destructure (undefined)

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/screens/Topic.tsx')
-rw-r--r--src/screens/Topic.tsx204
1 files changed, 204 insertions, 0 deletions
diff --git a/src/screens/Topic.tsx b/src/screens/Topic.tsx
new file mode 100644
index 000000000..6cd69f05f
--- /dev/null
+++ b/src/screens/Topic.tsx
@@ -0,0 +1,204 @@
+import React from 'react'
+import {ListRenderItemInfo, 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 {HITSLOP_10} from '#/lib/constants'
+import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {shareUrl} from '#/lib/sharing'
+import {cleanError} from '#/lib/strings/errors'
+import {enforceLen} from '#/lib/strings/helpers'
+import {useSearchPostsQuery} from '#/state/queries/search-posts'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
+import {Post} from '#/view/com/post/Post'
+import {List} from '#/view/com/util/List'
+import {atoms as a, web} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import * as Layout from '#/components/Layout'
+import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
+
+const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
+  return <Post post={item} />
+}
+
+const keyExtractor = (item: PostView, index: number) => {
+  return `${item.uri}-${index}`
+}
+
+export default function TopicScreen({
+  route,
+}: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) {
+  const {topic} = route.params
+  const {_} = useLingui()
+
+  const headerTitle = React.useMemo(() => {
+    return enforceLen(decodeURIComponent(topic), 24, true, 'middle')
+  }, [topic])
+
+  const onShare = React.useCallback(() => {
+    const url = new URL('https://bsky.app')
+    url.pathname = `/topic/${topic}`
+    shareUrl(url.toString())
+  }, [topic])
+
+  const [activeTab, setActiveTab] = React.useState(0)
+  const setMinimalShellMode = useSetMinimalShellMode()
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
+
+  const onPageSelected = React.useCallback(
+    (index: number) => {
+      setMinimalShellMode(false)
+      setActiveTab(index)
+    },
+    [setMinimalShellMode],
+  )
+
+  const sections = React.useMemo(() => {
+    return [
+      {
+        title: _(msg`Top`),
+        component: (
+          <TopicScreenTab topic={topic} sort="top" active={activeTab === 0} />
+        ),
+      },
+      {
+        title: _(msg`Latest`),
+        component: (
+          <TopicScreenTab
+            topic={topic}
+            sort="latest"
+            active={activeTab === 1}
+          />
+        ),
+      },
+    ]
+  }, [_, topic, activeTab])
+
+  return (
+    <Layout.Screen>
+      <Layout.Header.Outer noBottomBorder>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot>
+          <Button
+            label={_(msg`Share`)}
+            size="small"
+            variant="ghost"
+            color="primary"
+            shape="round"
+            onPress={onShare}
+            hitSlop={HITSLOP_10}
+            style={[{right: -3}]}>
+            <ButtonIcon icon={Share} size="md" />
+          </Button>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
+      <Pager
+        onPageSelected={onPageSelected}
+        renderTabBar={props => (
+          <Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}>
+            <TabBar items={sections.map(section => section.title)} {...props} />
+          </Layout.Center>
+        )}
+        initialPage={0}>
+        {sections.map((section, i) => (
+          <View key={i}>{section.component}</View>
+        ))}
+      </Pager>
+    </Layout.Screen>
+  )
+}
+
+function TopicScreenTab({
+  topic,
+  sort,
+  active,
+}: {
+  topic: string
+  sort: 'top' | 'latest'
+  active: boolean
+}) {
+  const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
+  const [isPTR, setIsPTR] = React.useState(false)
+
+  const {
+    data,
+    isFetched,
+    isFetchingNextPage,
+    isLoading,
+    isError,
+    error,
+    refetch,
+    fetchNextPage,
+    hasNextPage,
+  } = useSearchPostsQuery({
+    query: decodeURIComponent(topic),
+    sort,
+    enabled: active,
+  })
+
+  const posts = React.useMemo(() => {
+    return data?.pages.flatMap(page => page.posts) || []
+  }, [data])
+
+  const onRefresh = React.useCallback(async () => {
+    setIsPTR(true)
+    await refetch()
+    setIsPTR(false)
+  }, [refetch])
+
+  const onEndReached = React.useCallback(() => {
+    if (isFetchingNextPage || !hasNextPage || error) return
+    fetchNextPage()
+  }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
+
+  return (
+    <>
+      {posts.length < 1 ? (
+        <ListMaybePlaceholder
+          isLoading={isLoading || !isFetched}
+          isError={isError}
+          onRetry={refetch}
+          emptyType="results"
+          emptyMessage={_(msg`We couldn't find any results for that topic.`)}
+        />
+      ) : (
+        <List
+          data={posts}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          refreshing={isPTR}
+          onRefresh={onRefresh}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={4}
+          // @ts-ignore web only -prf
+          desktopFixedHeight
+          ListFooterComponent={
+            <ListFooter
+              isFetchingNextPage={isFetchingNextPage}
+              error={cleanError(error)}
+              onRetry={fetchNextPage}
+            />
+          }
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
+        />
+      )}
+    </>
+  )
+}