about summary refs log tree commit diff
path: root/src/screens/Hashtag.tsx
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-02-29 17:56:29 -0800
committerGitHub <noreply@github.com>2024-02-29 17:56:29 -0800
commitcf8b03801fd8e98bcd2da4d099a9dfbf5876de7d (patch)
tree3eb030d12894df352609873195c642ac3634f31d /src/screens/Hashtag.tsx
parent8900c67df25f1ab56d6f163076c780c646e9d073 (diff)
downloadvoidsky-cf8b03801fd8e98bcd2da4d099a9dfbf5876de7d.tar.zst
Dedicated screen for hashtags, POC ALF list (#3047)
* create dedicated hashtag "search" screen

clarify loading component name

more adjustments

rework `ViewHeader` to keep chevron centered w/ first line

adjustments

adjustments

use `author` instead of `handle` in route

add web route for url

add web route for url

Add desktop list header

support web

keep header lowercase

add optional subtitle to view header

correct isFetching logic

oops

use `isFetching` for clarity in footer

combine logic

update bskyweb

finish screen

style, add footer, add spinner, etc

add list

add header, params

create a screen

* add variable to server path

* localize `By`

* add empty state

* more adjustments

* sanitize author

* fix web

* add custom message for hashtag not found error

* ellipsis in middle

* fix

* fix trans

* account for multiple #

* encode #

* replaceall

* Use sanitized tag

* don't call function in lingui

* add share button

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/screens/Hashtag.tsx')
-rw-r--r--src/screens/Hashtag.tsx157
1 files changed, 157 insertions, 0 deletions
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
new file mode 100644
index 000000000..794753ea3
--- /dev/null
+++ b/src/screens/Hashtag.tsx
@@ -0,0 +1,157 @@
+import React from 'react'
+import {ListRenderItemInfo, Pressable} from 'react-native'
+import {atoms as a} from '#/alf'
+import {useFocusEffect} from '@react-navigation/native'
+import {useSetMinimalShellMode} from 'state/shell'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {useSearchPostsQuery} from 'state/queries/search-posts'
+import {Post} from 'view/com/post/Post'
+import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
+import {enforceLen} from 'lib/strings/helpers'
+import {
+  ListFooter,
+  ListHeaderDesktop,
+  ListMaybePlaceholder,
+} from '#/components/Lists'
+import {List} from 'view/com/util/List'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {CenteredView} from 'view/com/util/Views'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
+import {shareUrl} from 'lib/sharing'
+import {HITSLOP_10} from 'lib/constants'
+
+const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
+  return <Post post={item} />
+}
+
+const keyExtractor = (item: PostView, index: number) => {
+  return `${item.uri}-${index}`
+}
+
+export default function HashtagScreen({
+  route,
+}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
+  const {tag, author} = route.params
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
+  const [isPTR, setIsPTR] = React.useState(false)
+
+  const fullTag = React.useMemo(() => {
+    return `#${tag.replaceAll('%23', '#')}`
+  }, [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])
+
+  const sanitizedAuthor = React.useMemo(() => {
+    if (!author) return
+    return sanitizeHandle(author)
+  }, [author])
+
+  const {
+    data,
+    isFetching,
+    isLoading,
+    isRefetching,
+    isError,
+    error,
+    refetch,
+    fetchNextPage,
+    hasNextPage,
+  } = useSearchPostsQuery({query: queryParam})
+
+  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/${tag}`
+    if (author) {
+      url.searchParams.set('author', author)
+    }
+    shareUrl(url.toString())
+  }, [tag, author])
+
+  const onRefresh = React.useCallback(async () => {
+    setIsPTR(true)
+    await refetch()
+    setIsPTR(false)
+  }, [refetch])
+
+  const onEndReached = React.useCallback(() => {
+    if (isFetching || !hasNextPage || error) return
+    fetchNextPage()
+  }, [isFetching, hasNextPage, error, fetchNextPage])
+
+  return (
+    <CenteredView style={a.flex_1}>
+      <ViewHeader
+        title={headerTitle}
+        subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
+        canGoBack={true}
+        renderButton={() => (
+          <Pressable
+            accessibilityRole="button"
+            onPress={onShare}
+            hitSlop={HITSLOP_10}>
+            <ArrowOutOfBox_Stroke2_Corner0_Rounded
+              size="lg"
+              onPress={onShare}
+            />
+          </Pressable>
+        )}
+      />
+      <ListMaybePlaceholder
+        isLoading={isLoading || isRefetching}
+        isError={isError}
+        isEmpty={posts.length < 1}
+        onRetry={refetch}
+        empty={_(msg`We couldn't find any results for that hashtag.`)}
+      />
+      {!isLoading && posts.length > 0 && (
+        <List<PostView>
+          data={posts}
+          renderItem={renderItem}
+          keyExtractor={keyExtractor}
+          refreshing={isPTR}
+          onRefresh={onRefresh}
+          onEndReached={onEndReached}
+          onEndReachedThreshold={4}
+          // @ts-ignore web only -prf
+          desktopFixedHeight
+          ListHeaderComponent={
+            <ListHeaderDesktop
+              title={headerTitle}
+              subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
+            />
+          }
+          ListFooterComponent={
+            <ListFooter
+              isFetching={isFetching && !isRefetching}
+              isError={isError}
+              error={error?.name}
+              onRetry={fetchNextPage}
+            />
+          }
+        />
+      )}
+    </CenteredView>
+  )
+}