Better dead feed handling (#8579)
* add space between content hider * add handling for feeds that fail to load * cleanError, in case of network funkiness * handle deleted lists * split out missingfeed
This commit is contained in:
@@ -1,18 +1,11 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function Divider({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
flatten(style),
|
||||
]}
|
||||
/>
|
||||
<View style={[a.w_full, a.border_t, t.atoms.border_contrast_low, style]} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link} from '#/components/Link'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {MissingFeed} from './MissingFeed'
|
||||
|
||||
type FeedSourceCardProps = {
|
||||
feedUri: string
|
||||
@@ -49,7 +50,7 @@ export function FeedSourceCard({
|
||||
} else {
|
||||
feed = hydrateList(feedData)
|
||||
}
|
||||
return <FeedSourceCardLoaded feed={feed} {...props} />
|
||||
return <FeedSourceCardLoaded feedUri={feedUri} feed={feed} {...props} />
|
||||
} else {
|
||||
return <FeedSourceCardWithoutData feedUri={feedUri} {...props} />
|
||||
}
|
||||
@@ -59,14 +60,22 @@ export function FeedSourceCardWithoutData({
|
||||
feedUri,
|
||||
...props
|
||||
}: Omit<FeedSourceCardProps, 'feedData'>) {
|
||||
const {data: feed} = useFeedSourceInfoQuery({
|
||||
const {data: feed, error} = useFeedSourceInfoQuery({
|
||||
uri: feedUri,
|
||||
})
|
||||
|
||||
return <FeedSourceCardLoaded feed={feed} {...props} />
|
||||
return (
|
||||
<FeedSourceCardLoaded
|
||||
feedUri={feedUri}
|
||||
feed={feed}
|
||||
error={error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeedSourceCardLoaded({
|
||||
feedUri,
|
||||
feed,
|
||||
style,
|
||||
showDescription = false,
|
||||
@@ -74,7 +83,9 @@ export function FeedSourceCardLoaded({
|
||||
showMinimalPlaceholder,
|
||||
hideTopBorder,
|
||||
link = true,
|
||||
error,
|
||||
}: {
|
||||
feedUri: string
|
||||
feed?: FeedSourceInfo
|
||||
style?: StyleProp<ViewStyle>
|
||||
showDescription?: boolean
|
||||
@@ -82,6 +93,7 @@ export function FeedSourceCardLoaded({
|
||||
showMinimalPlaceholder?: boolean
|
||||
hideTopBorder?: boolean
|
||||
link?: boolean
|
||||
error?: unknown
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -92,7 +104,18 @@ export function FeedSourceCardLoaded({
|
||||
* This state also captures the scenario where a feed can't load for whatever
|
||||
* reason.
|
||||
*/
|
||||
if (!feed)
|
||||
if (!feed) {
|
||||
if (error) {
|
||||
return (
|
||||
<MissingFeed
|
||||
uri={feedUri}
|
||||
style={style}
|
||||
hideTopBorder={hideTopBorder}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FeedLoadingPlaceholder
|
||||
style={[
|
||||
@@ -105,6 +128,7 @@ export function FeedSourceCardLoaded({
|
||||
showLowerPlaceholder={!showMinimalPlaceholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {getFeedTypeFromUri} from '#/state/queries/feed'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function MissingFeed({
|
||||
style,
|
||||
hideTopBorder,
|
||||
uri,
|
||||
error,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
hideTopBorder?: boolean
|
||||
uri: string
|
||||
error?: unknown
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
const type = getFeedTypeFromUri(uri)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={
|
||||
type === 'feed'
|
||||
? _(msg`Could not connect to custom feed`)
|
||||
: _(msg`Deleted list`)
|
||||
}
|
||||
accessibilityHint={_(msg`Tap for more information`)}
|
||||
onPress={control.open}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.p_lg,
|
||||
a.gap_md,
|
||||
!hideTopBorder && !a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.justify_start,
|
||||
style,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
{width: 36, height: 36},
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_sm,
|
||||
a.mr_md,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
]}>
|
||||
<WarningIcon size="lg" />
|
||||
</View>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_sm, a.font_bold, a.leading_snug, a.italic]}
|
||||
numberOfLines={1}>
|
||||
{type === 'feed' ? (
|
||||
<Trans>Feed unavailable</Trans>
|
||||
) : (
|
||||
<Trans>Deleted list</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_snug,
|
||||
a.italic,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{isWeb ? (
|
||||
<Trans>Click for information</Trans>
|
||||
) : (
|
||||
<Trans>Tap for information</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Button>
|
||||
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<DialogInner uri={uri} type={type} error={error} />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
uri,
|
||||
type,
|
||||
error,
|
||||
}: {
|
||||
uri: string
|
||||
type: 'feed' | 'list'
|
||||
error: unknown
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const atUri = new AtUri(uri)
|
||||
const {data: profile, isError: isProfileError} = useProfileQuery({
|
||||
did: atUri.host,
|
||||
})
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={
|
||||
type === 'feed'
|
||||
? _(msg`Unavailable feed information`)
|
||||
: _(msg`Deleted list`)
|
||||
}
|
||||
style={web({maxWidth: 500})}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||
{type === 'feed' ? (
|
||||
<Trans>Could not connect to feed service</Trans>
|
||||
) : (
|
||||
<Trans>Deleted list</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||
{type === 'feed' ? (
|
||||
<Trans>
|
||||
We could not connect to the service that provides this custom
|
||||
feed. It may be temporarily unavailable and experiencing issues,
|
||||
or permanently unavailable.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>We could not find this list. It was probably deleted.</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Divider style={[a.my_md]} />
|
||||
<Text style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||
{type === 'feed' ? (
|
||||
<Trans>Feed creator</Trans>
|
||||
) : (
|
||||
<Trans>List creator</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{profile && moderationOpts && (
|
||||
<View style={[a.w_full, a.align_start]}>
|
||||
<ProfileCard.Link profile={profile} onPress={() => control.close()}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
disabledPreview
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
</ProfileCard.Link>
|
||||
</View>
|
||||
)}
|
||||
{isProfileError && (
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_high,
|
||||
a.italic,
|
||||
a.text_center,
|
||||
a.w_full,
|
||||
]}>
|
||||
<Trans>Could not find profile</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{type === 'feed' && (
|
||||
<>
|
||||
<Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}>
|
||||
<Trans>Feed identifier</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}>
|
||||
{atUri.rkey}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{error instanceof Error && (
|
||||
<>
|
||||
<Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}>
|
||||
<Trans>Error message</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}>
|
||||
{cleanError(error.message)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
{isNative && (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
onPress={() => control.close()}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
style={[a.mt_5xl]}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export function FeedLoadingPlaceholder({
|
||||
<LoadingPlaceholder
|
||||
width={36}
|
||||
height={36}
|
||||
style={[styles.avatar, {borderRadius: 6}]}
|
||||
style={[styles.avatar, {borderRadius: 8}]}
|
||||
/>
|
||||
<View style={[s.flex1]}>
|
||||
<LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
|
||||
|
||||
Reference in New Issue
Block a user