about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Lists.tsx228
-rw-r--r--src/components/RichText.tsx11
-rw-r--r--src/components/TagMenu/index.tsx54
-rw-r--r--src/components/TagMenu/index.web.tsx25
4 files changed, 274 insertions, 44 deletions
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
new file mode 100644
index 000000000..cf00734f0
--- /dev/null
+++ b/src/components/Lists.tsx
@@ -0,0 +1,228 @@
+import React from 'react'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {View} from 'react-native'
+import {Loader} from '#/components/Loader'
+import {Trans} from '@lingui/macro'
+import {cleanError} from 'lib/strings/errors'
+import {Button} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {StackActions} from '@react-navigation/native'
+import {useNavigation} from '@react-navigation/core'
+import {NavigationProp} from 'lib/routes/types'
+
+export function ListFooter({
+  isFetching,
+  isError,
+  error,
+  onRetry,
+}: {
+  isFetching: boolean
+  isError: boolean
+  error?: string
+  onRetry?: () => Promise<unknown>
+}) {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.align_center,
+        a.justify_center,
+        a.border_t,
+        t.atoms.border_contrast_low,
+        {height: 100},
+      ]}>
+      {isFetching ? (
+        <Loader size="xl" />
+      ) : (
+        <ListFooterMaybeError
+          isError={isError}
+          error={error}
+          onRetry={onRetry}
+        />
+      )}
+    </View>
+  )
+}
+
+function ListFooterMaybeError({
+  isError,
+  error,
+  onRetry,
+}: {
+  isError: boolean
+  error?: string
+  onRetry?: () => Promise<unknown>
+}) {
+  const t = useTheme()
+
+  if (!isError) return null
+
+  return (
+    <View style={[a.w_full, a.px_lg]}>
+      <View
+        style={[
+          a.flex_row,
+          a.gap_md,
+          a.p_md,
+          a.rounded_sm,
+          a.align_center,
+          t.atoms.bg_contrast_25,
+        ]}>
+        <Text
+          style={[a.flex_1, a.text_sm, t.atoms.text_contrast_medium]}
+          numberOfLines={2}>
+          {error ? (
+            cleanError(error)
+          ) : (
+            <Trans>Oops, something went wrong!</Trans>
+          )}
+        </Text>
+        <Button
+          variant="gradient"
+          label="Press to retry"
+          style={[
+            a.align_center,
+            a.justify_center,
+            a.rounded_sm,
+            a.overflow_hidden,
+            a.px_md,
+            a.py_sm,
+          ]}
+          onPress={onRetry}>
+          Retry
+        </Button>
+      </View>
+    </View>
+  )
+}
+
+export function ListHeaderDesktop({
+  title,
+  subtitle,
+}: {
+  title: string
+  subtitle?: string
+}) {
+  const {gtTablet} = useBreakpoints()
+  const t = useTheme()
+
+  if (!gtTablet) return null
+
+  return (
+    <View style={[a.w_full, a.py_lg, a.px_xl, a.gap_xs]}>
+      <Text style={[a.text_3xl, a.font_bold]}>{title}</Text>
+      {subtitle ? (
+        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+          {subtitle}
+        </Text>
+      ) : undefined}
+    </View>
+  )
+}
+
+export function ListMaybePlaceholder({
+  isLoading,
+  isEmpty,
+  isError,
+  empty,
+  error,
+  onRetry,
+}: {
+  isLoading: boolean
+  isEmpty: boolean
+  isError: boolean
+  empty?: string
+  error?: string
+  onRetry?: () => Promise<unknown>
+}) {
+  const navigation = useNavigation<NavigationProp>()
+  const t = useTheme()
+
+  const canGoBack = navigation.canGoBack()
+  const onGoBack = React.useCallback(() => {
+    if (canGoBack) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('HomeTab')
+      navigation.dispatch(StackActions.popToTop())
+    }
+  }, [navigation, canGoBack])
+
+  if (!isEmpty) return null
+
+  return (
+    <View
+      style={[
+        a.flex_1,
+        a.align_center,
+        a.border_t,
+        a.justify_between,
+        t.atoms.border_contrast_low,
+        {paddingTop: 175, paddingBottom: 110},
+      ]}>
+      {isLoading ? (
+        <View style={[a.w_full, a.align_center, {top: 100}]}>
+          <Loader size="xl" />
+        </View>
+      ) : (
+        <>
+          <View style={[a.w_full, a.align_center, a.gap_lg]}>
+            <Text style={[a.font_bold, a.text_3xl]}>
+              {isError ? (
+                <Trans>Oops!</Trans>
+              ) : isEmpty ? (
+                <Trans>Page not found</Trans>
+              ) : undefined}
+            </Text>
+
+            {isError ? (
+              <Text
+                style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
+                {error ? error : <Trans>Something went wrong!</Trans>}
+              </Text>
+            ) : isEmpty ? (
+              <Text
+                style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
+                {empty ? (
+                  empty
+                ) : (
+                  <Trans>
+                    We're sorry! We can't find the page you were looking for.
+                  </Trans>
+                )}
+              </Text>
+            ) : undefined}
+          </View>
+          <View style={[a.w_full, a.px_lg, a.gap_md]}>
+            {isError && onRetry && (
+              <Button
+                variant="solid"
+                color="primary"
+                label="Click here"
+                onPress={onRetry}
+                size="large"
+                style={[
+                  a.rounded_sm,
+                  a.overflow_hidden,
+                  {paddingVertical: 10},
+                ]}>
+                Retry
+              </Button>
+            )}
+            <Button
+              variant="solid"
+              color={isError && onRetry ? 'secondary' : 'primary'}
+              label="Click here"
+              onPress={onGoBack}
+              size="large"
+              style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
+              Go Back
+            </Button>
+          </View>
+        </>
+      )}
+    </View>
+  )
+}
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 5d82d7e5e..1a14415cf 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -120,6 +120,7 @@ export function RichText({
         <RichTextTag
           key={key}
           text={segment.text}
+          tag={tag.tag}
           style={styles}
           selectable={selectable}
           authorHandle={authorHandle}
@@ -145,12 +146,14 @@ export function RichText({
 }
 
 function RichTextTag({
-  text: tag,
+  text,
+  tag,
   style,
   selectable,
   authorHandle,
 }: {
   text: string
+  tag: string
   selectable?: boolean
   authorHandle?: string
 } & TextStyleProp) {
@@ -184,8 +187,8 @@ function RichTextTag({
         <Text
           selectable={selectable}
           {...native({
-            accessibilityLabel: _(msg`Hashtag: ${tag}`),
-            accessibilityHint: _(msg`Click here to open tag menu for ${tag}`),
+            accessibilityLabel: _(msg`Hashtag: #${tag}`),
+            accessibilityHint: _(msg`Click here to open tag menu for #${tag}`),
             accessibilityRole: isNative ? 'button' : undefined,
             onPress: open,
             onPressIn: onPressIn,
@@ -213,7 +216,7 @@ function RichTextTag({
               textDecorationColor: t.palette.primary_500,
             },
           ]}>
-          {tag}
+          {text}
         </Text>
       </TagMenu>
     </React.Fragment>
diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx
index c18c0d6a2..c9ced9a54 100644
--- a/src/components/TagMenu/index.tsx
+++ b/src/components/TagMenu/index.tsx
@@ -34,6 +34,10 @@ export function TagMenu({
   authorHandle,
 }: React.PropsWithChildren<{
   control: Dialog.DialogOuterProps['control']
+  /**
+   * This should be the sanitized tag value from the facet itself, not the
+   * "display" value with a leading `#`.
+   */
   tag: string
   authorHandle?: string
 }>) {
@@ -52,16 +56,16 @@ export function TagMenu({
     variables: optimisticRemove,
     reset: resetRemove,
   } = useRemoveMutedWordMutation()
+  const displayTag = '#' + tag
 
-  const sanitizedTag = tag.replace(/^#/, '')
   const isMuted = Boolean(
     (preferences?.mutedWords?.find(
-      m => m.value === sanitizedTag && m.targets.includes('tag'),
+      m => m.value === tag && m.targets.includes('tag'),
     ) ??
       optimisticUpsert?.find(
-        m => m.value === sanitizedTag && m.targets.includes('tag'),
+        m => m.value === tag && m.targets.includes('tag'),
       )) &&
-      !(optimisticRemove?.value === sanitizedTag),
+      !(optimisticRemove?.value === tag),
   )
 
   return (
@@ -71,7 +75,7 @@ export function TagMenu({
       <Dialog.Outer control={control}>
         <Dialog.Handle />
 
-        <Dialog.Inner label={_(msg`Tag menu: ${tag}`)}>
+        <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
           {isPreferencesLoading ? (
             <View style={[a.w_full, a.align_center]}>
               <Loader size="lg" />
@@ -87,18 +91,14 @@ export function TagMenu({
                   t.atoms.bg_contrast_25,
                 ]}>
                 <Link
-                  label={_(msg`Search for all posts with tag ${tag}`)}
-                  to={makeSearchLink({query: tag})}
+                  label={_(msg`Search for all posts with tag ${displayTag}`)}
+                  to={makeSearchLink({query: displayTag})}
                   onPress={e => {
                     e.preventDefault()
 
                     control.close(() => {
-                      // @ts-ignore :ron_swanson: "I know more than you"
-                      navigation.navigate('SearchTab', {
-                        screen: 'Search',
-                        params: {
-                          q: tag,
-                        },
+                      navigation.push('Hashtag', {
+                        tag: tag.replaceAll('#', '%23'),
                       })
                     })
 
@@ -128,7 +128,7 @@ export function TagMenu({
                       <Trans>
                         See{' '}
                         <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                          {tag}
+                          {displayTag}
                         </Text>{' '}
                         posts
                       </Trans>
@@ -142,21 +142,19 @@ export function TagMenu({
 
                     <Link
                       label={_(
-                        msg`Search for all posts by @${authorHandle} with tag ${tag}`,
+                        msg`Search for all posts by @${authorHandle} with tag ${displayTag}`,
                       )}
-                      to={makeSearchLink({query: tag, from: authorHandle})}
+                      to={makeSearchLink({
+                        query: displayTag,
+                        from: authorHandle,
+                      })}
                       onPress={e => {
                         e.preventDefault()
 
                         control.close(() => {
-                          // @ts-ignore :ron_swanson: "I know more than you"
-                          navigation.navigate('SearchTab', {
-                            screen: 'Search',
-                            params: {
-                              q:
-                                tag +
-                                (authorHandle ? ` from:${authorHandle}` : ''),
-                            },
+                          navigation.push('Hashtag', {
+                            tag: tag.replaceAll('#', '%23'),
+                            author: authorHandle,
                           })
                         })
 
@@ -190,7 +188,7 @@ export function TagMenu({
                             See{' '}
                             <Text
                               style={[a.text_md, a.font_bold, t.atoms.text]}>
-                              {tag}
+                              {displayTag}
                             </Text>{' '}
                             posts by this user
                           </Trans>
@@ -207,8 +205,8 @@ export function TagMenu({
                     <Button
                       label={
                         isMuted
-                          ? _(msg`Unmute all ${tag} posts`)
-                          : _(msg`Mute all ${tag} posts`)
+                          ? _(msg`Unmute all ${displayTag} posts`)
+                          : _(msg`Mute all ${displayTag} posts`)
                       }
                       onPress={() => {
                         control.close(() => {
@@ -250,7 +248,7 @@ export function TagMenu({
                           ]}>
                           {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
                           <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                            {tag}
+                            {displayTag}
                           </Text>{' '}
                           <Trans>posts</Trans>
                         </Text>
diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx
index 4fcb4c812..a0dc2bce6 100644
--- a/src/components/TagMenu/index.web.tsx
+++ b/src/components/TagMenu/index.web.tsx
@@ -35,10 +35,13 @@ export function TagMenu({
   tag,
   authorHandle,
 }: React.PropsWithChildren<{
+  /**
+   * This should be the sanitized tag value from the facet itself, not the
+   * "display" value with a leading `#`.
+   */
   tag: string
   authorHandle?: string
 }>) {
-  const sanitizedTag = tag.replace(/^#/, '')
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const {data: preferences} = usePreferencesQuery()
@@ -48,22 +51,22 @@ export function TagMenu({
     useRemoveMutedWordMutation()
   const isMuted = Boolean(
     (preferences?.mutedWords?.find(
-      m => m.value === sanitizedTag && m.targets.includes('tag'),
+      m => m.value === tag && m.targets.includes('tag'),
     ) ??
       optimisticUpsert?.find(
-        m => m.value === sanitizedTag && m.targets.includes('tag'),
+        m => m.value === tag && m.targets.includes('tag'),
       )) &&
-      !(optimisticRemove?.value === sanitizedTag),
+      !(optimisticRemove?.value === tag),
   )
-  const truncatedTag = enforceLen(tag, 15, true, 'middle')
+  const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
 
   const dropdownItems = React.useMemo(() => {
     return [
       {
         label: _(msg`See ${truncatedTag} posts`),
         onPress() {
-          navigation.navigate('Search', {
-            q: tag,
+          navigation.push('Hashtag', {
+            tag: tag.replaceAll('#', '%23'),
           })
         },
         testID: 'tagMenuSearch',
@@ -79,11 +82,9 @@ export function TagMenu({
         !isInvalidHandle(authorHandle) && {
           label: _(msg`See ${truncatedTag} posts by user`),
           onPress() {
-            navigation.navigate({
-              name: 'Search',
-              params: {
-                q: tag + (authorHandle ? ` from:${authorHandle}` : ''),
-              },
+            navigation.push('Hashtag', {
+              tag: tag.replaceAll('#', '%23'),
+              author: authorHandle,
             })
           },
           testID: 'tagMenuSeachByUser',