diff options
author | Hailey <me@haileyok.com> | 2024-02-29 17:56:29 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-29 17:56:29 -0800 |
commit | cf8b03801fd8e98bcd2da4d099a9dfbf5876de7d (patch) | |
tree | 3eb030d12894df352609873195c642ac3634f31d /src/components | |
parent | 8900c67df25f1ab56d6f163076c780c646e9d073 (diff) | |
download | voidsky-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/components')
-rw-r--r-- | src/components/Lists.tsx | 228 | ||||
-rw-r--r-- | src/components/RichText.tsx | 11 | ||||
-rw-r--r-- | src/components/TagMenu/index.tsx | 54 | ||||
-rw-r--r-- | src/components/TagMenu/index.web.tsx | 25 |
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', |