From d0438b6b28ff87628f737f8dd26bace8916ef59c Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Sat, 2 Mar 2024 13:26:21 +0900 Subject: Update Lists.tsx --- src/components/Lists.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'src/components') diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 12a935807..9778545d2 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -2,7 +2,8 @@ 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 {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {cleanError} from 'lib/strings/errors' import {Button} from '#/components/Button' import {Text} from '#/components/Typography' @@ -58,6 +59,7 @@ function ListFooterMaybeError({ onRetry?: () => Promise }) { const t = useTheme() + const {_} = useLingui() if (!isError) return null @@ -83,7 +85,7 @@ function ListFooterMaybeError({ @@ -144,6 +146,7 @@ export function ListMaybePlaceholder({ const navigation = useNavigation() const t = useTheme() const {gtMobile} = useBreakpoints() + const {_} = useLingui() const canGoBack = navigation.canGoBack() const onGoBack = React.useCallback(() => { @@ -218,7 +221,7 @@ export function ListMaybePlaceholder({ )} -- cgit 1.4.1 From 2938a1397e763f44e088cde11d8b975b036a6405 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Wed, 13 Mar 2024 10:41:34 +0900 Subject: Update index.tsx --- src/components/Menu/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 9be9dd86b..051e95b95 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -17,7 +17,7 @@ import { ItemIconProps, } from '#/components/Menu/types' import {Button, ButtonText} from '#/components/Button' -import {msg} from '@lingui/macro' +import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isNative} from 'platform/detection' @@ -209,7 +209,9 @@ function Cancel() { variant="ghost" color="secondary" onPress={() => control.close()}> - Cancel + + Cancel + ) } -- cgit 1.4.1 From 1760043f79f6e50de3bb2df97c3d6fe9c700b035 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Wed, 13 Mar 2024 10:41:38 +0900 Subject: Update index.tsx --- src/components/TagMenu/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index 849a3f42d..ae4b50bf4 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -264,7 +264,9 @@ export function TagMenu({ variant="ghost" color="secondary" onPress={() => control.close()}> - Cancel + + Cancel + )} -- cgit 1.4.1 From 7a0bf7266a5a871fafb08194d794318fb02ac999 Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Sat, 16 Mar 2024 16:48:58 +0900 Subject: Update Lists.tsx --- src/components/Lists.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src/components') diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 8a6dfdc7c..f506f52ce 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -6,7 +6,7 @@ import {Loader} from '#/components/Loader' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {cleanError} from 'lib/strings/errors' -import {Button} from '#/components/Button' +import {Button, ButtonText} from '#/components/Button' import {Text} from '#/components/Typography' import {StackActions} from '@react-navigation/native' import {router} from '#/routes' @@ -223,7 +223,7 @@ export function ListMaybePlaceholder({ )} -- cgit 1.4.1 From 8ac5144a584450f037771de4da775d545b70ffa3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 19 Mar 2024 09:57:14 -0500 Subject: Bday modal tweaks (#3252) * Smol tweaks to bday dialog * Juse use existing DateInput for now * Remove unused code * Remove passed-in prefs * Adjust load state * Revert "Adjust load state" This reverts commit 802459fd044b380ccc4f96432af416996219a0de. * Fix type error --- src/components/dialogs/BirthDateSettings.tsx | 74 +++++++++++++++------------- src/screens/Moderation/index.tsx | 5 +- src/view/screens/Settings/index.tsx | 11 +---- 3 files changed, 44 insertions(+), 46 deletions(-) (limited to 'src/components') diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx index 62c95c78d..4a3e96e56 100644 --- a/src/components/dialogs/BirthDateSettings.tsx +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -1,48 +1,64 @@ import React from 'react' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' +import {View} from 'react-native' import * as Dialog from '#/components/Dialog' import {Text} from '../Typography' import {DateInput} from '#/view/com/util/forms/DateInput' import {logger} from '#/logger' import { + usePreferencesQuery, usePreferencesSetBirthDateMutation, UsePreferencesQueryResponse, } from '#/state/queries/preferences' -import {Button, ButtonText} from '../Button' +import {Button, ButtonIcon, ButtonText} from '../Button' import {atoms as a, useTheme} from '#/alf' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {cleanError} from '#/lib/strings/errors' -import {ActivityIndicator, View} from 'react-native' import {isIOS, isWeb} from '#/platform/detection' +import {Loader} from '#/components/Loader' export function BirthDateSettingsDialog({ control, - preferences, }: { control: Dialog.DialogControlProps - preferences: UsePreferencesQueryResponse | undefined }) { + const t = useTheme() const {_} = useLingui() - const {isPending, isError, error, mutateAsync} = - usePreferencesSetBirthDateMutation() + const {isLoading, error, data: preferences} = usePreferencesQuery() return ( + - {preferences && !isPending ? ( - + + My Birthday + + + This information is not shared with other users. + + + + {isLoading ? ( + + ) : error || !preferences ? ( + ) : ( - + )} + + ) @@ -51,20 +67,18 @@ export function BirthDateSettingsDialog({ function BirthdayInner({ control, preferences, - isError, - error, - setBirthDate, }: { control: Dialog.DialogControlProps preferences: UsePreferencesQueryResponse - isError: boolean - error: unknown - setBirthDate: (args: {birthDate: Date}) => Promise }) { const {_} = useLingui() const [date, setDate] = React.useState(preferences.birthDate || new Date()) - const t = useTheme() - + const { + isPending, + isError, + error, + mutateAsync: setBirthDate, + } = usePreferencesSetBirthDateMutation() const hasChanged = date !== preferences.birthDate const onSave = React.useCallback(async () => { @@ -74,21 +88,13 @@ function BirthdayInner({ await setBirthDate({birthDate: date}) } control.close() - } catch (e) { - logger.error(`setBirthDate failed`, {message: e}) + } catch (e: any) { + logger.error(`setBirthDate failed`, {message: e.message}) } }, [date, setBirthDate, control, hasChanged]) return ( - - - My Birthday - - - This information is not shared with other users. - - + {isError ? ( ) : undefined} @@ -110,13 +117,14 @@ function BirthdayInner({ diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 26fa9ec77..d0bfaeade 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -308,10 +308,7 @@ export function ModerationScreenInner({ - + )} - + Date: Tue, 19 Mar 2024 08:14:29 -0700 Subject: add `Partial` to `.open()` (#3227) --- src/components/Dialog/types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index 9e7ad3c04..b1a46f853 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -1,5 +1,5 @@ import React from 'react' -import type {AccessibilityProps} from 'react-native' +import type {AccessibilityProps, GestureResponderEvent} from 'react-native' import {BottomSheetProps} from '@gorhom/bottom-sheet' import {ViewStyleProp} from '#/alf' @@ -10,9 +10,15 @@ type A11yProps = Required * Mutated by useImperativeHandle to provide a public API for controlling the * dialog. The methods here will actually become the handlers defined within * the `Dialog.Outer` component. + * + * `Partial` here allows us to add this directly to the + * `onPress` prop of a button, for example. If this type was not added, we + * would need to create a function to wrap `.open()` with. */ export type DialogControlRefProps = { - open: (options?: DialogControlOpenOptions) => void + open: ( + options?: DialogControlOpenOptions & Partial, + ) => void close: (callback?: () => void) => void } -- cgit 1.4.1 From b622f6391805f9499034b2ba497dbb9b5c19bd93 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 19 Mar 2024 11:56:11 -0500 Subject: Mobile mod label setting component (#3267) * Mobile mod label setting component * Bump label title size * Dont show disabled label config on mobile --------- Co-authored-by: Paul Frazee --- src/alf/index.tsx | 4 + src/components/moderation/ModerationLabelPref.tsx | 97 ++++++++++++++--------- 2 files changed, 64 insertions(+), 37 deletions(-) (limited to 'src/components') diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 27738e91d..f0a0ede7a 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -16,6 +16,7 @@ type BreakpointName = keyof typeof breakpoints const breakpoints: { [key: string]: number } = { + gtPhone: 500, gtMobile: 800, gtTablet: 1300, } @@ -26,6 +27,7 @@ function getActiveBreakpoints({width}: {width: number}) { return { active: active[active.length - 1], + gtPhone: active.includes('gtPhone'), gtMobile: active.includes('gtMobile'), gtTablet: active.includes('gtTablet'), } @@ -39,6 +41,7 @@ export const Context = React.createContext<{ theme: themes.Theme breakpoints: { active: BreakpointName | undefined + gtPhone: boolean gtMobile: boolean gtTablet: boolean } @@ -47,6 +50,7 @@ export const Context = React.createContext<{ theme: themes.light, breakpoints: { active: undefined, + gtPhone: false, gtMobile: false, gtTablet: false, }, diff --git a/src/components/moderation/ModerationLabelPref.tsx b/src/components/moderation/ModerationLabelPref.tsx index f14550488..b16c24859 100644 --- a/src/components/moderation/ModerationLabelPref.tsx +++ b/src/components/moderation/ModerationLabelPref.tsx @@ -12,7 +12,7 @@ import { } from '#/state/queries/preferences' import {getLabelStrings} from '#/lib/moderation/useLabelInfo' -import {useTheme, atoms as a} from '#/alf' +import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' import {InlineLink} from '#/components/Link' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo' @@ -29,6 +29,7 @@ export function ModerationLabelPref({ }) { const {_, i18n} = useLingui() const t = useTheme() + const {gtPhone} = useBreakpoints() const isGlobalLabel = !labelValueDefinition.definedBy const {identifier} = labelValueDefinition @@ -57,6 +58,7 @@ export function ModerationLabelPref({ adultOnly && !preferences?.moderationPrefs.adultContentEnabled // are there any reasons we cant configure this label here? const cantConfigure = isGlobalLabel || adultDisabled + const showConfig = !disabled && (gtPhone || !cantConfigure) // adjust the pref based on whether warn is available let prefAdjusted = pref @@ -85,9 +87,19 @@ export function ModerationLabelPref({ ) return ( - + - {labelStrings.name} + + {labelStrings.name} + {labelStrings.description} @@ -113,40 +125,51 @@ export function ModerationLabelPref({ )} - {disabled ? ( - <> - ) : cantConfigure ? ( - - - {currentPrefLabel} - - - ) : ( - - - mutate({ - label: identifier, - visibility: newPref[0] as LabelPreference, - labelerDid, - }) - }> - - {ignoreLabel} - - {canWarn && ( - - {warnLabel} - - )} - - {hideLabel} - - + + {showConfig && ( + + {cantConfigure ? ( + + + {currentPrefLabel} + + + ) : ( + + + mutate({ + label: identifier, + visibility: newPref[0] as LabelPreference, + labelerDid, + }) + }> + + {ignoreLabel} + + {canWarn && ( + + {warnLabel} + + )} + + {hideLabel} + + + + )} )} -- cgit 1.4.1 From c9c3bd98b7418845c4e7ee64060a4362eb2e3128 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 19 Mar 2024 11:38:17 -0700 Subject: Rework the labeler selection step of the report flow (#3269) * Rework the labeler selection step of the report flow * Fix: use gtMobile * Use primitives, fix avatar * Spacing tweaks * Show handle instead of description --------- Co-authored-by: Eric Bailey --- src/components/LabelingServiceCard/index.tsx | 2 +- src/components/ReportDialog/SelectLabelerView.tsx | 75 ++++++++-------------- .../ReportDialog/SelectReportOptionView.tsx | 5 +- src/screens/Moderation/index.tsx | 2 +- 4 files changed, 31 insertions(+), 53 deletions(-) (limited to 'src/components') diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx index 6d0613511..f924f0f59 100644 --- a/src/components/LabelingServiceCard/index.tsx +++ b/src/components/LabelingServiceCard/index.tsx @@ -104,7 +104,7 @@ export function Default({ }: LabelingServiceProps & ViewStyleProp) { return ( - + - <View style={[a.justify_center, a.gap_sm]}> + <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}> <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Select moderation service</Trans> + <Trans>Select moderator</Trans> </Text> <Text style={[a.text_md, t.atoms.text_contrast_medium]}> - <Trans>Who do you want to send this report to?</Trans> + <Trans>To whom would you like to send this report?</Trans> </Text> </View> <Divider /> - <View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}> + <View style={[a.gap_xs, {marginHorizontal: a.p_md.padding * -1}]}> {props.labelers.map(labeler => { return ( <Button key={labeler.creator.did} label={_(msg`Send report to ${labeler.creator.displayName}`)} onPress={() => props.onSelectLabeler(labeler.creator.did)}> - <LabelerButton - title={labeler.creator.displayName || labeler.creator.handle} - description={labeler.creator.description || ''} - /> + <LabelerButton labeler={labeler} /> </Button> ) })} @@ -56,11 +55,9 @@ export function SelectLabelerView({ } function LabelerButton({ - title, - description, + labeler, }: { - title: string - description: string + labeler: AppBskyLabelerDefs.LabelerViewDetailed }) { const t = useTheme() const {hovered, pressed} = useButtonContext() @@ -75,41 +72,21 @@ function LabelerButton({ }, [t]) return ( - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_between, - a.p_md, - a.rounded_md, - {paddingRight: 70}, - interacted && styles.interacted, - ]}> - <View style={[a.flex_1, a.gap_xs]}> - <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> - {title} - </Text> - <Text style={[a.leading_tight, {maxWidth: 400}]} numberOfLines={3}> - {description} - </Text> - </View> - - <View - style={[ - a.absolute, - a.inset_0, - a.justify_center, - a.pr_md, - {left: 'auto'}, - ]}> - <ChevronRight - size="md" - fill={ - hovered ? t.palette.primary_500 : t.atoms.text_contrast_low.color - } + <LabelingServiceCard.Outer + style={[a.p_md, a.rounded_sm, interacted && styles.interacted]}> + <LabelingServiceCard.Avatar avatar={labeler.creator.avatar} /> + <LabelingServiceCard.Content> + <LabelingServiceCard.Title + value={getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + })} /> - </View> - </View> + <Text + style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}> + @{labeler.creator.handle} + </Text> + </LabelingServiceCard.Content> + </LabelingServiceCard.Outer> ) } diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx index 037a62fce..bacf5a867 100644 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -9,7 +9,7 @@ import {DMCA_LINK} from '#/components/ReportDialog/const' import {Link} from '#/components/Link' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, useTheme, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' import { Button, @@ -35,6 +35,7 @@ export function SelectReportOptionView({ }) { const t = useTheme() const {_} = useLingui() + const {gtMobile} = useBreakpoints() const allReportOptions = useReportOptions() const reportOptions = allReportOptions[props.params.type] @@ -76,7 +77,7 @@ export function SelectReportOptionView({ </Button> ) : null} - <View style={[a.justify_center, a.gap_sm]}> + <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}> <Text style={[a.text_2xl, a.font_bold]}>{i18n.title}</Text> <Text style={[a.text_md, t.atoms.text_contrast_medium]}> {i18n.description} diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 50e5ee3b9..d73823fad 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -411,7 +411,7 @@ export function ModerationScreenInner({ t.atoms.bg_contrast_50, ], ]}> - <LabelingService.Avatar /> + <LabelingService.Avatar avatar={labeler.creator.avatar} /> <LabelingService.Content> <LabelingService.Title value={getLabelingServiceTitle({ -- cgit 1.4.1 From addd66b37f922fda12a99298a37a0166cf509f89 Mon Sep 17 00:00:00 2001 From: Hailey <me@haileyok.com> Date: Tue, 19 Mar 2024 12:10:10 -0700 Subject: `PostThread` cleanup (#3183) * cleanup PostThread rm some more unnecessary code cleanup some more pieces fix `isLoading` logic few fixes organize refactor `PostThread` allow chaining of `postThreadQuery` Update `Hashtag` screen with the component changes Make some changes to the List components adjust height and padding of bottom loader to account for bottom bar * rm unnecessary chaining logic * maxReplies logic * adjust error logic * use `<` instead of `<=` * add back warning comment * remove unused prop * adjust order * update prop name * don't show error if `isLoading` --- src/components/Error.tsx | 90 +++++++ src/components/Lists.tsx | 177 +++++-------- src/screens/Hashtag.tsx | 4 +- src/view/com/post-thread/PostThread.tsx | 439 +++++++++++--------------------- 4 files changed, 307 insertions(+), 403 deletions(-) create mode 100644 src/components/Error.tsx (limited to 'src/components') diff --git a/src/components/Error.tsx b/src/components/Error.tsx new file mode 100644 index 000000000..1dbf68284 --- /dev/null +++ b/src/components/Error.tsx @@ -0,0 +1,90 @@ +import React from 'react' + +import {CenteredView} from 'view/com/util/Views' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {View} from 'react-native' +import {Button} from '#/components/Button' +import {useNavigation} from '@react-navigation/core' +import {NavigationProp} from 'lib/routes/types' +import {StackActions} from '@react-navigation/native' +import {router} from '#/routes' + +export function Error({ + title, + message, + onRetry, +}: { + title?: string + message?: string + onRetry?: () => unknown +}) { + const navigation = useNavigation<NavigationProp>() + const t = useTheme() + const {gtMobile} = useBreakpoints() + + const canGoBack = navigation.canGoBack() + const onGoBack = React.useCallback(() => { + if (canGoBack) { + navigation.goBack() + } else { + navigation.navigate('HomeTab') + + // Checking the state for routes ensures that web doesn't encounter errors while going back + if (navigation.getState()?.routes) { + navigation.dispatch(StackActions.push(...router.matchPath('/'))) + } else { + navigation.navigate('HomeTab') + navigation.dispatch(StackActions.popToTop()) + } + } + }, [navigation, canGoBack]) + + return ( + <CenteredView + style={[ + a.flex_1, + a.align_center, + !gtMobile ? a.justify_between : a.gap_5xl, + t.atoms.border_contrast_low, + {paddingTop: 175, paddingBottom: 110}, + ]} + sideBorders> + <View style={[a.w_full, a.align_center, a.gap_lg]}> + <Text style={[a.font_bold, a.text_3xl]}>{title}</Text> + <Text + style={[ + a.text_md, + a.text_center, + t.atoms.text_contrast_high, + {lineHeight: 1.4}, + gtMobile && {width: 450}, + ]}> + {message} + </Text> + </View> + <View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}> + {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={onRetry ? 'secondary' : 'primary'} + label="Click here" + onPress={onGoBack} + size="large" + style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}> + Go Back + </Button> + </View> + </CenteredView> + ) +} diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 8a889c15e..a74484b71 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -1,26 +1,28 @@ import React from 'react' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {View} from 'react-native' +import {useLingui} from '@lingui/react' + import {CenteredView} from 'view/com/util/Views' import {Loader} from '#/components/Loader' -import {Trans} from '@lingui/macro' +import {msg, 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 {router} from '#/routes' -import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' +import {Error} from '#/components/Error' export function ListFooter({ isFetching, isError, error, onRetry, + height, }: { - isFetching: boolean - isError: boolean + isFetching?: boolean + isError?: boolean error?: string onRetry?: () => Promise<unknown> + height?: number }) { const t = useTheme() @@ -29,11 +31,10 @@ export function ListFooter({ style={[ a.w_full, a.align_center, - a.justify_center, a.border_t, a.pb_lg, t.atoms.border_contrast_low, - {height: 180}, + {height: height ?? 180, paddingTop: 30}, ]}> {isFetching ? ( <Loader size="xl" /> @@ -53,7 +54,7 @@ function ListFooterMaybeError({ error, onRetry, }: { - isError: boolean + isError?: boolean error?: string onRetry?: () => Promise<unknown> }) { @@ -128,121 +129,71 @@ export function ListMaybePlaceholder({ isLoading, isEmpty, isError, - empty, - error, - notFoundType = 'page', + emptyTitle, + emptyMessage, + errorTitle, + errorMessage, + emptyType = 'page', onRetry, }: { isLoading: boolean - isEmpty: boolean - isError: boolean - empty?: string - error?: string - notFoundType?: 'page' | 'results' + isEmpty?: boolean + isError?: boolean + emptyTitle?: string + emptyMessage?: string + errorTitle?: string + errorMessage?: string + emptyType?: 'page' | 'results' onRetry?: () => Promise<unknown> }) { - const navigation = useNavigationDeduped() const t = useTheme() + const {_} = useLingui() const {gtMobile, gtTablet} = useBreakpoints() - const canGoBack = navigation.canGoBack() - const onGoBack = React.useCallback(() => { - if (canGoBack) { - navigation.goBack() - } else { - navigation.navigate('HomeTab') - - // Checking the state for routes ensures that web doesn't encounter errors while going back - if (navigation.getState()?.routes) { - navigation.dispatch(StackActions.push(...router.matchPath('/'))) - } else { - navigation.navigate('HomeTab') - navigation.dispatch(StackActions.popToTop()) - } - } - }, [navigation, canGoBack]) + if (!isLoading && isError) { + return ( + <Error + title={errorTitle ?? _(msg`Oops!`)} + message={errorMessage ?? _(`Something went wrong!`)} + onRetry={onRetry} + /> + ) + } - if (!isEmpty) return null - - return ( - <CenteredView - style={[ - a.flex_1, - a.align_center, - !gtMobile ? a.justify_between : a.gap_5xl, - t.atoms.border_contrast_low, - {paddingTop: 175, paddingBottom: 110}, - ]} - sideBorders={gtMobile} - topBorder={!gtTablet}> - {isLoading ? ( + if (isLoading) { + return ( + <CenteredView + style={[ + a.flex_1, + a.align_center, + !gtMobile ? a.justify_between : a.gap_5xl, + t.atoms.border_contrast_low, + {paddingTop: 175, paddingBottom: 110}, + ]} + sideBorders={gtMobile} + topBorder={!gtTablet}> <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 ? ( - <> - {notFoundType === 'results' ? ( - <Trans>No results found</Trans> - ) : ( - <Trans>Page not found</Trans> - )} - </> - ) : undefined} - </Text> + </CenteredView> + ) + } - {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.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}> - {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> - </> - )} - </CenteredView> - ) + if (isEmpty) { + return ( + <Error + title={ + emptyTitle ?? + (emptyType === 'results' + ? _(msg`No results found`) + : _(msg`Page not found`)) + } + message={ + emptyMessage ?? + _(msg`We're sorry! We can't find the page you were looking for.`) + } + onRetry={onRetry} + /> + ) + } } diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 776cc585e..46452f087 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -128,8 +128,8 @@ export default function HashtagScreen({ isError={isError} isEmpty={posts.length < 1} onRetry={refetch} - notFoundType="results" - empty={_(msg`We couldn't find any results for that hashtag.`)} + emptyTitle="results" + emptyMessage={_(msg`We couldn't find any results for that hashtag.`)} /> {!isLoading && posts.length > 0 && ( <List<PostView> diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index bac7018c3..8042e7bd5 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,25 +1,14 @@ import React, {useEffect, useRef} from 'react' -import { - ActivityIndicator, - Pressable, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {StyleSheet, useWindowDimensions, View} from 'react-native' import {AppBskyFeedDefs} from '@atproto/api' -import {CenteredView} from '../util/Views' -import {LoadingScreen} from '../util/LoadingScreen' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + import {List, ListMethods} from '../util/List' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' import {PostThreadItem} from './PostThreadItem' import {ComposePrompt} from '../composer/Prompt' import {ViewHeader} from '../util/ViewHeader' -import {ErrorMessage} from '../util/error/ErrorMessage' import {Text} from '../util/text/Text' -import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import { @@ -30,21 +19,18 @@ import { usePostThreadQuery, sortThread, } from '#/state/queries/post-thread' -import {useNavigation} from '@react-navigation/native' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' import {sanitizeDisplayName} from 'lib/strings/display-names' -import {cleanError} from '#/lib/strings/errors' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import { - UsePreferencesQueryResponse, useModerationOpts, usePreferencesQuery, } from '#/state/queries/preferences' import {useSession} from '#/state/session' import {isAndroid, isNative, isWeb} from '#/platform/detection' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' +import {cleanError} from 'lib/strings/errors' // FlatList maintainVisibleContentPosition breaks if too many items // are prepended. This seems to be an optimal number based on *shrug*. @@ -58,9 +44,7 @@ const MAINTAIN_VISIBLE_CONTENT_POSITION = { const TOP_COMPONENT = {_reactKey: '__top_component__'} const REPLY_PROMPT = {_reactKey: '__reply__'} -const CHILD_SPINNER = {_reactKey: '__child_spinner__'} const LOAD_MORE = {_reactKey: '__load_more__'} -const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'} type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound type RowItem = @@ -68,9 +52,7 @@ type RowItem = // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape. | typeof TOP_COMPONENT | typeof REPLY_PROMPT - | typeof CHILD_SPINNER | typeof LOAD_MORE - | typeof BOTTOM_COMPONENT type ThreadSkeletonParts = { parents: YieldedItem[] @@ -78,6 +60,10 @@ type ThreadSkeletonParts = { replies: YieldedItem[] } +const keyExtractor = (item: RowItem) => { + return item._reactKey +} + export function PostThread({ uri, onCanReply, @@ -85,17 +71,30 @@ export function PostThread({ }: { uri: string | undefined onCanReply: (canReply: boolean) => void - onPressReply: () => void + onPressReply: () => unknown }) { + const {hasSession} = useSession() + const {_} = useLingui() + const pal = usePalette('default') + const {isMobile, isTabletOrMobile} = useWebMediaQueries() + const initialNumToRender = useInitialNumToRender() + const {height: windowHeight} = useWindowDimensions() + + const {data: preferences} = usePreferencesQuery() const { - isLoading, - isError, - error, + isFetching, + isError: isThreadError, + error: threadError, refetch, data: thread, } = usePostThreadQuery(uri) - const {data: preferences} = usePreferencesQuery() + const treeView = React.useMemo( + () => + !!preferences?.threadViewPrefs?.lab_treeViewEnabled && + hasBranchingReplies(thread), + [preferences?.threadViewPrefs, thread], + ) const rootPost = thread?.type === 'post' ? thread.post : undefined const rootPostRecord = thread?.type === 'post' ? thread.record : undefined @@ -105,7 +104,6 @@ export function PostThread({ rootPost && moderationOpts ? moderatePost(rootPost, moderationOpts) : undefined - return !!mod ?.ui('contentList') .blurs.find( @@ -114,6 +112,14 @@ export function PostThread({ ) }, [rootPost, moderationOpts]) + // Values used for proper rendering of parents + const ref = useRef<ListMethods>(null) + const highlightedPostRef = useRef<View | null>(null) + const [maxParents, setMaxParents] = React.useState( + isWeb ? Infinity : PARENTS_CHUNK_SIZE, + ) + const [maxReplies, setMaxReplies] = React.useState(50) + useSetTitle( rootPost && !isNoPwi ? `${sanitizeDisplayName( @@ -121,62 +127,6 @@ export function PostThread({ )}: "${rootPostRecord!.text}"` : '', ) - useEffect(() => { - if (rootPost) { - onCanReply(!rootPost.viewer?.replyDisabled) - } - }, [rootPost, onCanReply]) - - if (isError || AppBskyFeedDefs.isNotFoundPost(thread)) { - return ( - <PostThreadError - error={error} - notFound={AppBskyFeedDefs.isNotFoundPost(thread)} - onRefresh={refetch} - /> - ) - } - if (AppBskyFeedDefs.isBlockedPost(thread)) { - return <PostThreadBlocked /> - } - if (!thread || isLoading || !preferences) { - return <LoadingScreen /> - } - return ( - <PostThreadLoaded - thread={thread} - threadViewPrefs={preferences.threadViewPrefs} - onRefresh={refetch} - onPressReply={onPressReply} - /> - ) -} - -function PostThreadLoaded({ - thread, - threadViewPrefs, - onRefresh, - onPressReply, -}: { - thread: ThreadNode - threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs'] - onRefresh: () => void - onPressReply: () => void -}) { - const {hasSession} = useSession() - const {_} = useLingui() - const pal = usePalette('default') - const {isMobile, isTabletOrMobile} = useWebMediaQueries() - const ref = useRef<ListMethods>(null) - const highlightedPostRef = useRef<View | null>(null) - const [maxParents, setMaxParents] = React.useState( - isWeb ? Infinity : PARENTS_CHUNK_SIZE, - ) - const [maxReplies, setMaxReplies] = React.useState(100) - const treeView = React.useMemo( - () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread), - [threadViewPrefs, thread], - ) // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed. // This ensures that the first render contains no parents--even if they are already available in the cache. @@ -184,18 +134,56 @@ function PostThreadLoaded({ // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead. const [deferParents, setDeferParents] = React.useState(isNative) - const skeleton = React.useMemo( - () => - createThreadSkeleton( - sortThread(thread, threadViewPrefs), - hasSession, - treeView, - ), - [thread, threadViewPrefs, hasSession, treeView], - ) + const skeleton = React.useMemo(() => { + const threadViewPrefs = preferences?.threadViewPrefs + if (!threadViewPrefs || !thread) return null + + return createThreadSkeleton( + sortThread(thread, threadViewPrefs), + hasSession, + treeView, + ) + }, [thread, preferences?.threadViewPrefs, hasSession, treeView]) + + const error = React.useMemo(() => { + if (AppBskyFeedDefs.isNotFoundPost(thread)) { + return { + title: _(msg`Post not found`), + message: _(msg`The post may have been deleted.`), + } + } else if (skeleton?.highlightedPost.type === 'blocked') { + return { + title: _(msg`Post hidden`), + message: _( + msg`You have blocked the author or you have been blocked by the author.`, + ), + } + } else if (threadError?.message.startsWith('Post not found')) { + return { + title: _(msg`Post not found`), + message: _(msg`The post may have been deleted.`), + } + } else if (isThreadError) { + return { + message: threadError ? cleanError(threadError) : undefined, + } + } + + return null + }, [thread, skeleton?.highlightedPost, isThreadError, _, threadError]) + + useEffect(() => { + if (error) { + onCanReply(false) + } else if (rootPost) { + onCanReply(!rootPost.viewer?.replyDisabled) + } + }, [rootPost, onCanReply, error]) // construct content const posts = React.useMemo(() => { + if (!skeleton) return [] + const {parents, highlightedPost, replies} = skeleton let arr: RowItem[] = [] if (highlightedPost.type === 'post') { @@ -231,17 +219,11 @@ function PostThreadLoaded({ if (!highlightedPost.post.viewer?.replyDisabled) { arr.push(REPLY_PROMPT) } - if (highlightedPost.ctx.isChildLoading) { - arr.push(CHILD_SPINNER) - } else { - for (let i = 0; i < replies.length; i++) { - arr.push(replies[i]) - if (i === maxReplies) { - arr.push(LOAD_MORE) - break - } + for (let i = 0; i < replies.length; i++) { + arr.push(replies[i]) + if (i === maxReplies) { + break } - arr.push(BOTTOM_COMPONENT) } } return arr @@ -256,7 +238,7 @@ function PostThreadLoaded({ return } // wait for loading to finish - if (thread.type === 'post' && !!thread.parent) { + if (thread?.type === 'post' && !!thread.parent) { function onMeasure(pageY: number) { ref.current?.scrollToOffset({ animated: false, @@ -280,10 +262,10 @@ function PostThreadLoaded({ // To work around this, we prepend rows after scroll bumps against the top and rests. const needsBumpMaxParents = React.useRef(false) const onStartReached = React.useCallback(() => { - if (maxParents < skeleton.parents.length) { + if (skeleton?.parents && maxParents < skeleton.parents.length) { needsBumpMaxParents.current = true } - }, [maxParents, skeleton.parents.length]) + }, [maxParents, skeleton?.parents]) const bumpMaxParentsIfNeeded = React.useCallback(() => { if (!isNative) { return @@ -296,6 +278,11 @@ function PostThreadLoaded({ const onMomentumScrollEnd = bumpMaxParentsIfNeeded const onScrollToTop = bumpMaxParentsIfNeeded + const onEndReached = React.useCallback(() => { + if (isFetching || posts.length < maxReplies) return + setMaxReplies(prev => prev + 50) + }, [isFetching, maxReplies, posts.length]) + const renderItem = React.useCallback( ({item, index}: {item: RowItem; index: number}) => { if (item === TOP_COMPONENT) { @@ -326,46 +313,6 @@ function PostThreadLoaded({ </Text> </View> ) - } else if (item === LOAD_MORE) { - return ( - <Pressable - onPress={() => setMaxReplies(n => n + 50)} - style={[pal.border, pal.view, styles.itemContainer]} - accessibilityLabel={_(msg`Load more posts`)} - accessibilityHint=""> - <View - style={[ - pal.viewLight, - {paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6}, - ]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Load more posts</Trans> - </Text> - </View> - </Pressable> - ) - } else if (item === BOTTOM_COMPONENT) { - // HACK - // due to some complexities with how flatlist works, this is the easiest way - // I could find to get a border positioned directly under the last item - // -prf - return ( - <View - // @ts-ignore web-only - style={{ - // Leave enough space below that the scroll doesn't jump - height: isNative ? 600 : '100vh', - borderTopWidth: 1, - borderColor: pal.colors.border, - }} - /> - ) - } else if (item === CHILD_SPINNER) { - return ( - <View style={[pal.border, styles.childSpinner]}> - <ActivityIndicator /> - </View> - ) } else if (isThreadPost(item)) { const prev = isThreadPost(posts[index - 1]) ? (posts[index - 1] as ThreadPost) @@ -374,7 +321,9 @@ function PostThreadLoaded({ ? (posts[index - 1] as ThreadPost) : undefined const hasUnrevealedParents = - index === 0 && maxParents < skeleton.parents.length + index === 0 && + skeleton?.parents && + maxParents < skeleton.parents.length return ( <View ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} @@ -391,9 +340,9 @@ function PostThreadLoaded({ showChildReplyLine={item.ctx.showChildReplyLine} showParentReplyLine={item.ctx.showParentReplyLine} hasPrecedingItem={ - !!prev?.ctx.showChildReplyLine || hasUnrevealedParents + !!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents } - onPostReply={onRefresh} + onPostReply={refetch} /> </View> ) @@ -403,142 +352,62 @@ function PostThreadLoaded({ [ hasSession, isTabletOrMobile, + _, isMobile, onPressReply, pal.border, pal.viewLight, pal.textLight, - pal.view, - pal.text, - pal.colors.border, posts, - onRefresh, + skeleton?.parents, + maxParents, deferParents, treeView, - skeleton.parents.length, - maxParents, - _, + refetch, ], ) return ( - <List - ref={ref} - data={posts} - keyExtractor={item => item._reactKey} - renderItem={renderItem} - onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} - onStartReached={onStartReached} - onMomentumScrollEnd={onMomentumScrollEnd} - onScrollToTop={onScrollToTop} - maintainVisibleContentPosition={ - isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined - } - style={s.hContentRegion} - // @ts-ignore our .web version only -prf - desktopFixedHeight - removeClippedSubviews={isAndroid ? false : undefined} - windowSize={11} - /> - ) -} - -function PostThreadBlocked() { - const {_} = useLingui() - const pal = usePalette('default') - const navigation = useNavigation<NavigationProp>() - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - return ( - <CenteredView> - <View style={[pal.view, pal.border, styles.notFoundContainer]}> - <Text type="title-lg" style={[pal.text, s.mb5]}> - <Trans>Post hidden</Trans> - </Text> - <Text type="md" style={[pal.text, s.mb10]}> - <Trans> - You have blocked the author or you have been blocked by the author. - </Trans> - </Text> - <TouchableOpacity - onPress={onPressBack} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint=""> - <Text type="2xl" style={pal.link}> - <FontAwesomeIcon - icon="angle-left" - style={[pal.link as FontAwesomeIconStyle, s.mr5]} - size={14} + <> + <ListMaybePlaceholder + isLoading={!preferences || !thread} + isError={!!error} + onRetry={refetch} + errorTitle={error?.title} + errorMessage={error?.message} + /> + {!error && thread && ( + <List + ref={ref} + data={posts} + renderItem={renderItem} + keyExtractor={keyExtractor} + onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} + onStartReached={onStartReached} + onEndReached={onEndReached} + onEndReachedThreshold={2} + onMomentumScrollEnd={onMomentumScrollEnd} + onScrollToTop={onScrollToTop} + maintainVisibleContentPosition={ + isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined + } + // @ts-ignore our .web version only -prf + desktopFixedHeight + removeClippedSubviews={isAndroid ? false : undefined} + ListFooterComponent={ + <ListFooter + isFetching={isFetching} + onRetry={refetch} + // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to + // work without causing weird jumps on web or glitches on native + height={windowHeight - 200} /> - <Trans context="action">Back</Trans> - </Text> - </TouchableOpacity> - </View> - </CenteredView> - ) -} - -function PostThreadError({ - onRefresh, - notFound, - error, -}: { - onRefresh: () => void - notFound: boolean - error: Error | null -}) { - const {_} = useLingui() - const pal = usePalette('default') - const navigation = useNavigation<NavigationProp>() - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - if (notFound) { - return ( - <CenteredView> - <View style={[pal.view, pal.border, styles.notFoundContainer]}> - <Text type="title-lg" style={[pal.text, s.mb5]}> - <Trans>Post not found</Trans> - </Text> - <Text type="md" style={[pal.text, s.mb10]}> - <Trans>The post may have been deleted.</Trans> - </Text> - <TouchableOpacity - onPress={onPressBack} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint=""> - <Text type="2xl" style={pal.link}> - <FontAwesomeIcon - icon="angle-left" - style={[pal.link as FontAwesomeIconStyle, s.mr5]} - size={14} - /> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - </View> - </CenteredView> - ) - } - return ( - <CenteredView> - <ErrorMessage message={cleanError(error)} onPressTryAgain={onRefresh} /> - </CenteredView> + } + initialNumToRender={initialNumToRender} + windowSize={11} + /> + )} + </> ) } @@ -558,7 +427,9 @@ function createThreadSkeleton( node: ThreadNode, hasSession: boolean, treeView: boolean, -): ThreadSkeletonParts { +): ThreadSkeletonParts | null { + if (!node) return null + return { parents: Array.from(flattenThreadParents(node, hasSession)), highlightedPost: node, @@ -615,7 +486,10 @@ function hasPwiOptOut(node: ThreadPost) { return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated') } -function hasBranchingReplies(node: ThreadNode) { +function hasBranchingReplies(node?: ThreadNode) { + if (!node) { + return false + } if (node.type !== 'post') { return false } @@ -629,20 +503,9 @@ function hasBranchingReplies(node: ThreadNode) { } const styles = StyleSheet.create({ - notFoundContainer: { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, itemContainer: { borderTopWidth: 1, paddingHorizontal: 18, paddingVertical: 18, }, - childSpinner: { - borderTopWidth: 1, - paddingTop: 40, - paddingBottom: 200, - }, }) -- cgit 1.4.1