diff options
Diffstat (limited to 'src/view/com/posts/PostFeedErrorMessage.tsx')
-rw-r--r-- | src/view/com/posts/PostFeedErrorMessage.tsx | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/src/view/com/posts/PostFeedErrorMessage.tsx b/src/view/com/posts/PostFeedErrorMessage.tsx new file mode 100644 index 000000000..39347b89c --- /dev/null +++ b/src/view/com/posts/PostFeedErrorMessage.tsx @@ -0,0 +1,279 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs, AppBskyFeedGetAuthorFeed, AtUri} from '@atproto/api' +import {msg as msgLingui, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {usePalette} from '#/lib/hooks/usePalette' +import {NavigationProp} from '#/lib/routes/types' +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {FeedDescriptor} from '#/state/queries/post-feed' +import {useRemoveFeedMutation} from '#/state/queries/preferences' +import * as Prompt from '#/components/Prompt' +import {EmptyState} from '../util/EmptyState' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' + +export enum KnownError { + Block = 'Block', + FeedgenDoesNotExist = 'FeedgenDoesNotExist', + FeedgenMisconfigured = 'FeedgenMisconfigured', + FeedgenBadResponse = 'FeedgenBadResponse', + FeedgenOffline = 'FeedgenOffline', + FeedgenUnknown = 'FeedgenUnknown', + FeedSignedInOnly = 'FeedSignedInOnly', + FeedTooManyRequests = 'FeedTooManyRequests', + Unknown = 'Unknown', +} + +export function PostFeedErrorMessage({ + feedDesc, + error, + onPressTryAgain, + savedFeedConfig, +}: { + feedDesc: FeedDescriptor + error?: Error + onPressTryAgain: () => void + savedFeedConfig?: AppBskyActorDefs.SavedFeed +}) { + const {_: _l} = useLingui() + const knownError = React.useMemo( + () => detectKnownError(feedDesc, error), + [feedDesc, error], + ) + if ( + typeof knownError !== 'undefined' && + knownError !== KnownError.Unknown && + feedDesc.startsWith('feedgen') + ) { + return ( + <FeedgenErrorMessage + feedDesc={feedDesc} + knownError={knownError} + rawError={error} + savedFeedConfig={savedFeedConfig} + /> + ) + } + + if (knownError === KnownError.Block) { + return ( + <EmptyState + icon="ban" + message={_l(msgLingui`Posts hidden`)} + style={{paddingVertical: 40}} + /> + ) + } + + return ( + <ErrorMessage + message={cleanError(error)} + onPressTryAgain={onPressTryAgain} + /> + ) +} + +function FeedgenErrorMessage({ + feedDesc, + knownError, + rawError, + savedFeedConfig, +}: { + feedDesc: FeedDescriptor + knownError: KnownError + rawError?: Error + savedFeedConfig?: AppBskyActorDefs.SavedFeed +}) { + const pal = usePalette('default') + const {_: _l} = useLingui() + const navigation = useNavigation<NavigationProp>() + const msg = React.useMemo( + () => + ({ + [KnownError.Unknown]: '', + [KnownError.Block]: '', + [KnownError.FeedgenDoesNotExist]: _l( + msgLingui`Hmm, we're having trouble finding this feed. It may have been deleted.`, + ), + [KnownError.FeedgenMisconfigured]: _l( + msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`, + ), + [KnownError.FeedgenBadResponse]: _l( + msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`, + ), + [KnownError.FeedgenOffline]: _l( + msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`, + ), + [KnownError.FeedSignedInOnly]: _l( + msgLingui`This content is not viewable without a Bluesky account.`, + ), + [KnownError.FeedgenUnknown]: _l( + msgLingui`Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue.`, + ), + [KnownError.FeedTooManyRequests]: _l( + msgLingui`This feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`, + ), + }[knownError]), + [_l, knownError], + ) + const [_, uri] = feedDesc.split('|') + const [ownerDid] = safeParseFeedgenUri(uri) + const removePromptControl = Prompt.usePromptControl() + const {mutateAsync: removeFeed} = useRemoveFeedMutation() + + const onViewProfile = React.useCallback(() => { + navigation.navigate('Profile', {name: ownerDid}) + }, [navigation, ownerDid]) + + const onPressRemoveFeed = React.useCallback(() => { + removePromptControl.open() + }, [removePromptControl]) + + const onRemoveFeed = React.useCallback(async () => { + try { + if (!savedFeedConfig) return + await removeFeed(savedFeedConfig) + } catch (err) { + Toast.show( + _l( + msgLingui`There was an issue removing this feed. Please check your internet connection and try again.`, + ), + 'exclamation-circle', + ) + logger.error('Failed to remove feed', {message: err}) + } + }, [removeFeed, _l, savedFeedConfig]) + + const cta = React.useMemo(() => { + switch (knownError) { + case KnownError.FeedSignedInOnly: { + return null + } + case KnownError.FeedgenDoesNotExist: + case KnownError.FeedgenMisconfigured: + case KnownError.FeedgenBadResponse: + case KnownError.FeedgenOffline: + case KnownError.FeedgenUnknown: { + return ( + <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> + {knownError === KnownError.FeedgenDoesNotExist && + savedFeedConfig && ( + <Button + type="inverted" + label={_l(msgLingui`Remove feed`)} + onPress={onRemoveFeed} + /> + )} + <Button + type="default-light" + label={_l(msgLingui`View profile`)} + onPress={onViewProfile} + /> + </View> + ) + } + } + }, [knownError, onViewProfile, onRemoveFeed, _l, savedFeedConfig]) + + return ( + <> + <View + style={[ + pal.border, + pal.viewLight, + { + borderTopWidth: 1, + paddingHorizontal: 20, + paddingVertical: 18, + gap: 12, + }, + ]}> + <Text style={pal.text}>{msg}</Text> + + {rawError?.message && ( + <Text style={pal.textLight}> + <Trans>Message from server: {rawError.message}</Trans> + </Text> + )} + + {cta} + </View> + + <Prompt.Basic + control={removePromptControl} + title={_l(msgLingui`Remove feed?`)} + description={_l(msgLingui`Remove this feed from your saved feeds`)} + onConfirm={onPressRemoveFeed} + confirmButtonCta={_l(msgLingui`Remove`)} + confirmButtonColor="negative" + /> + </> + ) +} + +function safeParseFeedgenUri(uri: string): [string, string] { + try { + const urip = new AtUri(uri) + return [urip.hostname, urip.rkey] + } catch { + return ['', ''] + } +} + +function detectKnownError( + feedDesc: FeedDescriptor, + error: any, +): KnownError | undefined { + if (!error) { + return undefined + } + if ( + error instanceof AppBskyFeedGetAuthorFeed.BlockedActorError || + error instanceof AppBskyFeedGetAuthorFeed.BlockedByActorError + ) { + return KnownError.Block + } + + // check status codes + if (error?.status === 429) { + return KnownError.FeedTooManyRequests + } + + // convert error to string and continue + if (typeof error !== 'string') { + error = error.toString() + } + if (error.includes(KnownError.FeedSignedInOnly)) { + return KnownError.FeedSignedInOnly + } + if (!feedDesc.startsWith('feedgen')) { + return KnownError.Unknown + } + if (error.includes('could not find feed')) { + return KnownError.FeedgenDoesNotExist + } + if (error.includes('feed unavailable')) { + return KnownError.FeedgenOffline + } + if (error.includes('invalid did document')) { + return KnownError.FeedgenMisconfigured + } + if (error.includes('could not resolve did document')) { + return KnownError.FeedgenMisconfigured + } + if ( + error.includes('invalid feed generator service details in did document') + ) { + return KnownError.FeedgenMisconfigured + } + if (error.includes('invalid response')) { + return KnownError.FeedgenBadResponse + } + return KnownError.FeedgenUnknown +} |