about summary refs log tree commit diff
path: root/src/screens/Search/SearchResults.tsx
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-04-03 03:21:15 +0300
committerGitHub <noreply@github.com>2025-04-02 17:21:15 -0700
commit87da619aaa92e0ec762e68c13b24e58a25da10a8 (patch)
tree4da902d3ca43a226f6da8e5c090ab33c2df3297a /src/screens/Search/SearchResults.tsx
parent8d1f97b5ffac5d86762f1d4e9384ff3097acbc52 (diff)
downloadvoidsky-87da619aaa92e0ec762e68c13b24e58a25da10a8.tar.zst
[Explore] Base (#8053)
* migrate to #/screens

* rm unneeded import

* block drawer gesture on recent profiles

* rm recommendations (#8056)

* [Explore] Disable Trending videos (#8054)

* remove giant header

* disable

* [Explore] Dynamic module ordering (#8066)

* Dynamic module ordering

* [Explore] New headers, metrics (#8067)

* new sticky headers

* improve spacing between modules

* view metric on modules

* update metrics names

* [Explore] Suggested accounts module (#8072)

* use modern profile card, update load more

* add tab bar

* tabbed suggested accounts

* [Explore] Discover feeds module (#8073)

* cap number of feeds to 3

* change feed pin button

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* restore statsig to log events

* filter out followed profiles, make suer enough are loaded (#8090)

* [Explore] Trending topics (#8055)

* redesigned trending topics

* rm borders on web

* get post count / age / ranking from api

* spacing tweaks

* fetch more topics then slice

* use api data for avis/category

* rm top border

* Integrate new SDK, part out components

* Clean up

* Use status field

* Bump SDK

* Send up interests and langs

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Clean up module spacing and borders

(cherry picked from commit 63d19b6c2d67e226e0e14709b1047a1f88b3ce1c)
(cherry picked from commit 62d7d394ab1dc31b40b9c2cf59075adbf94737a1)

* Switch back border ordering

(cherry picked from commit 34e3789f8b410132c1390df3c2bb8257630ebdd9)

* [Explore] Starter Packs (#8095)

* Temp WIP

(cherry picked from commit 43b5d7b1e64b3adb1ed162262d0310e0bf026c18)

* New SP card

* Load state

* Revert change

* Cleanup

* Interests and caching

* Count total

* Format

* Caching

* [Explore] Feed previews module (#8075)

* wip new hook

* get fetching working, maybe

* get feed previews rendering!

* fix header height

* working pin button

* extract out FeedLink

* add loader

* only make preview:header sticky

* Fix headers

* Header tweaks

* Fix moderation filter

* Fix threading

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Space it out

* Fix query key

* Mock new endpoint, filter saved feeds

* Make sure we're pinning, lower cache time

* add news category

* Remove log

* Improve suggested accounts load state

* Integrate new app view endpoint

* fragment

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* lint

* maybe fix this

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/screens/Search/SearchResults.tsx')
-rw-r--r--src/screens/Search/SearchResults.tsx338
1 files changed, 338 insertions, 0 deletions
diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx
new file mode 100644
index 000000000..bb51d2deb
--- /dev/null
+++ b/src/screens/Search/SearchResults.tsx
@@ -0,0 +1,338 @@
+import {memo, useCallback, useMemo, useState} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {type AppBskyFeedDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {augmentSearchQuery} from '#/lib/strings/helpers'
+import {useActorSearch} from '#/state/queries/actor-search'
+import {usePopularFeedsSearch} from '#/state/queries/feed'
+import {useSearchPostsQuery} from '#/state/queries/search-posts'
+import {useSession} from '#/state/session'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
+import {Post} from '#/view/com/post/Post'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
+import {atoms as a, useTheme, web} from '#/alf'
+import * as FeedCard from '#/components/FeedCard'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+
+let SearchResults = ({
+  query,
+  queryWithParams,
+  activeTab,
+  onPageSelected,
+  headerHeight,
+}: {
+  query: string
+  queryWithParams: string
+  activeTab: number
+  onPageSelected: (page: number) => void
+  headerHeight: number
+}): React.ReactNode => {
+  const {_} = useLingui()
+
+  const sections = useMemo(() => {
+    if (!queryWithParams) return []
+    const noParams = queryWithParams === query
+    return [
+      {
+        title: _(msg`Top`),
+        component: (
+          <SearchScreenPostResults
+            query={queryWithParams}
+            sort="top"
+            active={activeTab === 0}
+          />
+        ),
+      },
+      {
+        title: _(msg`Latest`),
+        component: (
+          <SearchScreenPostResults
+            query={queryWithParams}
+            sort="latest"
+            active={activeTab === 1}
+          />
+        ),
+      },
+      noParams && {
+        title: _(msg`People`),
+        component: (
+          <SearchScreenUserResults query={query} active={activeTab === 2} />
+        ),
+      },
+      noParams && {
+        title: _(msg`Feeds`),
+        component: (
+          <SearchScreenFeedsResults query={query} active={activeTab === 3} />
+        ),
+      },
+    ].filter(Boolean) as {
+      title: string
+      component: React.ReactNode
+    }[]
+  }, [_, query, queryWithParams, activeTab])
+
+  return (
+    <Pager
+      onPageSelected={onPageSelected}
+      renderTabBar={props => (
+        <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}>
+          <TabBar items={sections.map(section => section.title)} {...props} />
+        </Layout.Center>
+      )}
+      initialPage={0}>
+      {sections.map((section, i) => (
+        <View key={i}>{section.component}</View>
+      ))}
+    </Pager>
+  )
+}
+SearchResults = memo(SearchResults)
+export {SearchResults}
+
+function Loader() {
+  return (
+    <Layout.Content>
+      <View style={[a.py_xl]}>
+        <ActivityIndicator />
+      </View>
+    </Layout.Content>
+  )
+}
+
+function EmptyState({message, error}: {message: string; error?: string}) {
+  const t = useTheme()
+
+  return (
+    <Layout.Content>
+      <View style={[a.p_xl]}>
+        <View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
+          <Text style={[a.text_md]}>{message}</Text>
+
+          {error && (
+            <>
+              <View
+                style={[
+                  {
+                    marginVertical: 12,
+                    height: 1,
+                    width: '100%',
+                    backgroundColor: t.atoms.text.color,
+                    opacity: 0.2,
+                  },
+                ]}
+              />
+
+              <Text style={[t.atoms.text_contrast_medium]}>
+                <Trans>Error: {error}</Trans>
+              </Text>
+            </>
+          )}
+        </View>
+      </View>
+    </Layout.Content>
+  )
+}
+
+type SearchResultSlice =
+  | {
+      type: 'post'
+      key: string
+      post: AppBskyFeedDefs.PostView
+    }
+  | {
+      type: 'loadingMore'
+      key: string
+    }
+
+let SearchScreenPostResults = ({
+  query,
+  sort,
+  active,
+}: {
+  query: string
+  sort?: 'top' | 'latest'
+  active: boolean
+}): React.ReactNode => {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const [isPTR, setIsPTR] = useState(false)
+
+  const augmentedQuery = useMemo(() => {
+    return augmentSearchQuery(query || '', {did: currentAccount?.did})
+  }, [query, currentAccount])
+
+  const {
+    isFetched,
+    data: results,
+    isFetching,
+    error,
+    refetch,
+    fetchNextPage,
+    isFetchingNextPage,
+    hasNextPage,
+  } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
+
+  const onPullToRefresh = useCallback(async () => {
+    setIsPTR(true)
+    await refetch()
+    setIsPTR(false)
+  }, [setIsPTR, refetch])
+  const onEndReached = useCallback(() => {
+    if (isFetching || !hasNextPage || error) return
+    fetchNextPage()
+  }, [isFetching, error, hasNextPage, fetchNextPage])
+
+  const posts = useMemo(() => {
+    return results?.pages.flatMap(page => page.posts) || []
+  }, [results])
+  const items = useMemo(() => {
+    let temp: SearchResultSlice[] = []
+
+    const seenUris = new Set()
+    for (const post of posts) {
+      if (seenUris.has(post.uri)) {
+        continue
+      }
+      temp.push({
+        type: 'post',
+        key: post.uri,
+        post,
+      })
+      seenUris.add(post.uri)
+    }
+
+    if (isFetchingNextPage) {
+      temp.push({
+        type: 'loadingMore',
+        key: 'loadingMore',
+      })
+    }
+
+    return temp
+  }, [posts, isFetchingNextPage])
+
+  return error ? (
+    <EmptyState
+      message={_(
+        msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
+      )}
+      error={error.toString()}
+    />
+  ) : (
+    <>
+      {isFetched ? (
+        <>
+          {posts.length ? (
+            <List
+              data={items}
+              renderItem={({item}) => {
+                if (item.type === 'post') {
+                  return <Post post={item.post} />
+                } else {
+                  return null
+                }
+              }}
+              keyExtractor={item => item.key}
+              refreshing={isPTR}
+              onRefresh={onPullToRefresh}
+              onEndReached={onEndReached}
+              desktopFixedHeight
+              contentContainerStyle={{paddingBottom: 100}}
+            />
+          ) : (
+            <EmptyState message={_(msg`No results found for ${query}`)} />
+          )}
+        </>
+      ) : (
+        <Loader />
+      )}
+    </>
+  )
+}
+SearchScreenPostResults = memo(SearchScreenPostResults)
+
+let SearchScreenUserResults = ({
+  query,
+  active,
+}: {
+  query: string
+  active: boolean
+}): React.ReactNode => {
+  const {_} = useLingui()
+
+  const {data: results, isFetched} = useActorSearch({
+    query,
+    enabled: active,
+  })
+
+  return isFetched && results ? (
+    <>
+      {results.length ? (
+        <List
+          data={results}
+          renderItem={({item}) => (
+            <ProfileCardWithFollowBtn profile={item} noBg />
+          )}
+          keyExtractor={item => item.did}
+          desktopFixedHeight
+          contentContainerStyle={{paddingBottom: 100}}
+        />
+      ) : (
+        <EmptyState message={_(msg`No results found for ${query}`)} />
+      )}
+    </>
+  ) : (
+    <Loader />
+  )
+}
+SearchScreenUserResults = memo(SearchScreenUserResults)
+
+let SearchScreenFeedsResults = ({
+  query,
+  active,
+}: {
+  query: string
+  active: boolean
+}): React.ReactNode => {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const {data: results, isFetched} = usePopularFeedsSearch({
+    query,
+    enabled: active,
+  })
+
+  return isFetched && results ? (
+    <>
+      {results.length ? (
+        <List
+          data={results}
+          renderItem={({item}) => (
+            <View
+              style={[
+                a.border_b,
+                t.atoms.border_contrast_low,
+                a.px_lg,
+                a.py_lg,
+              ]}>
+              <FeedCard.Default view={item} />
+            </View>
+          )}
+          keyExtractor={item => item.uri}
+          desktopFixedHeight
+          contentContainerStyle={{paddingBottom: 100}}
+        />
+      ) : (
+        <EmptyState message={_(msg`No results found for ${query}`)} />
+      )}
+    </>
+  ) : (
+    <Loader />
+  )
+}
+SearchScreenFeedsResults = memo(SearchScreenFeedsResults)