diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/content/list.ts | 1 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 58 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 7 | ||||
-rw-r--r-- | src/view/com/posts/FeedErrorMessage.tsx | 119 | ||||
-rw-r--r-- | src/view/com/util/Views.d.ts | 9 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 99 |
6 files changed, 235 insertions, 58 deletions
diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts index 0331f58bd..8fb9f4b5e 100644 --- a/src/state/models/content/list.ts +++ b/src/state/models/content/list.ts @@ -290,6 +290,7 @@ export class ListModel { }) } + /* dont await */ this.rootStore.preferences.removeSavedFeed(this.uri) this.rootStore.emitListDeleted(this.uri) } diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 169eedac8..3c580aca9 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -25,6 +25,17 @@ import {MergeFeedAPI} from 'lib/api/feed/merge' const PAGE_SIZE = 30 +type FeedType = 'home' | 'following' | 'author' | 'custom' | 'likes' | 'list' + +export enum KnownError { + FeedgenDoesNotExist, + FeedgenMisconfigured, + FeedgenBadResponse, + FeedgenOffline, + FeedgenUnknown, + Unknown, +} + type Options = { /** * Formats the feed in a flat array with no threading of replies, just @@ -49,6 +60,7 @@ export class PostsFeedModel { isBlocking = false isBlockedBy = false error = '' + knownError: KnownError | undefined loadMoreError = '' params: QueryParams hasMore = true @@ -69,13 +81,7 @@ export class PostsFeedModel { constructor( public rootStore: RootStoreModel, - public feedType: - | 'home' - | 'following' - | 'author' - | 'custom' - | 'likes' - | 'list', + public feedType: FeedType, params: QueryParams, options?: Options, ) { @@ -305,6 +311,7 @@ export class PostsFeedModel { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' + this.knownError = undefined } _xIdle(error?: any, loadMoreError?: any) { @@ -314,6 +321,7 @@ export class PostsFeedModel { this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError this.error = cleanError(error) + this.knownError = detectKnownError(this.feedType, error) this.loadMoreError = cleanError(loadMoreError) if (error) { this.rootStore.log.error('Posts feed request failed', error) @@ -383,3 +391,39 @@ export class PostsFeedModel { }) } } + +function detectKnownError( + feedType: FeedType, + error: any, +): KnownError | undefined { + if (!error) { + return undefined + } + if (typeof error !== 'string') { + error = error.toString() + } + if (feedType !== 'custom') { + 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('feed provided an invalid response')) { + return KnownError.FeedgenBadResponse + } + return KnownError.FeedgenUnknown +} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 591afe3a3..0578036d9 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -10,7 +10,7 @@ import { } from 'react-native' import {FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' -import {ErrorMessage} from '../util/error/ErrorMessage' +import {FeedErrorMessage} from './FeedErrorMessage' import {PostsFeedModel} from 'state/models/feeds/posts' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' @@ -125,10 +125,7 @@ export const Feed = observer(function Feed({ return renderEmptyState() } else if (item === ERROR_ITEM) { return ( - <ErrorMessage - message={feed.error} - onPressTryAgain={onPressTryAgain} - /> + <FeedErrorMessage feed={feed} onPressTryAgain={onPressTryAgain} /> ) } else if (item === LOAD_MORE_ERROR_ITEM) { return ( diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx new file mode 100644 index 000000000..51c735e31 --- /dev/null +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import {View} from 'react-native' +import {AtUri, AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' +import {PostsFeedModel, KnownError} from 'state/models/feeds/posts' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import * as Toast from '../util/Toast' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {usePalette} from 'lib/hooks/usePalette' +import {useNavigation} from '@react-navigation/native' +import {NavigationProp} from 'lib/routes/types' +import {useStores} from 'state/index' + +const MESSAGES = { + [KnownError.Unknown]: '', + [KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`, + [KnownError.FeedgenMisconfigured]: + 'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.', + [KnownError.FeedgenBadResponse]: + 'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.', + [KnownError.FeedgenOffline]: + 'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.', + [KnownError.FeedgenUnknown]: + 'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.', +} + +export function FeedErrorMessage({ + feed, + onPressTryAgain, +}: { + feed: PostsFeedModel + onPressTryAgain: () => void +}) { + if ( + typeof feed.knownError === 'undefined' || + feed.knownError === KnownError.Unknown + ) { + return ( + <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} /> + ) + } + + return <FeedgenErrorMessage feed={feed} knownError={feed.knownError} /> +} + +function FeedgenErrorMessage({ + feed, + knownError, +}: { + feed: PostsFeedModel + knownError: KnownError +}) { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation<NavigationProp>() + const msg = MESSAGES[knownError] + const uri = (feed.params as GetCustomFeed.QueryParams).feed + const [ownerDid] = safeParseFeedgenUri(uri) + + const onViewProfile = React.useCallback(() => { + navigation.navigate('Profile', {name: ownerDid}) + }, [navigation, ownerDid]) + + const onRemoveFeed = React.useCallback(async () => { + store.shell.openModal({ + name: 'confirm', + title: 'Remove feed', + message: 'Remove this feed from your saved feeds?', + async onPressConfirm() { + try { + await store.preferences.removeSavedFeed(uri) + } catch (err) { + Toast.show( + 'There was an an issue removing this feed. Please check your internet connection and try again.', + ) + store.log.error('Failed to remove feed', {err}) + } + }, + onPressCancel() { + store.shell.closeModal() + }, + }) + }, [store, uri]) + + return ( + <View + style={[ + pal.border, + pal.viewLight, + { + borderTopWidth: 1, + paddingHorizontal: 20, + paddingVertical: 18, + gap: 12, + }, + ]}> + <Text style={pal.text}>{msg}</Text> + <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> + {knownError === KnownError.FeedgenDoesNotExist && ( + <Button type="inverted" label="Remove feed" onPress={onRemoveFeed} /> + )} + <Button + type="default-light" + label="View profile" + onPress={onViewProfile} + /> + </View> + </View> + ) +} + +function safeParseFeedgenUri(uri: string): [string, string] { + try { + const urip = new AtUri(uri) + return [urip.hostname, urip.rkey] + } catch { + return ['', ''] + } +} diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts index 292a985cd..91df1d6bc 100644 --- a/src/view/com/util/Views.d.ts +++ b/src/view/com/util/Views.d.ts @@ -1 +1,8 @@ -export {FlatList, ScrollView, View as CenteredView} from 'react-native' +import React from 'react' +import {ViewProps} from 'react-native' +export {FlatList, ScrollView} from 'react-native' +export function CenteredView({ + style, + sideBorders, + ...props +}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 859f50bef..4fc4b6c7f 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -54,23 +54,11 @@ interface SectionRef { type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> export const ProfileListScreen = withAuthRequired( observer(function ProfileListScreenImpl(props: Props) { - const pal = usePalette('default') const store = useStores() - const navigation = useNavigation<NavigationProp>() - const {name: handleOrDid} = props.route.params - const [listOwnerDid, setListOwnerDid] = React.useState<string | undefined>() const [error, setError] = React.useState<string | undefined>() - const onPressBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - React.useEffect(() => { /* * We must resolve the DID of the list owner before we can fetch the list. @@ -92,37 +80,7 @@ export const ProfileListScreen = withAuthRequired( if (error) { return ( <CenteredView> - <View - style={[ - pal.view, - pal.border, - { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, - ]}> - <Text type="title-lg" style={[pal.text, s.mb10]}> - Could not load list - </Text> - <Text type="md" style={[pal.text, s.mb20]}> - {error} - </Text> - - <View style={{flexDirection: 'row'}}> - <Button - type="default" - accessibilityLabel="Go Back" - accessibilityHint="Return to previous page" - onPress={onPressBack} - style={{flexShrink: 1}}> - <Text type="button" style={pal.text}> - Go Back - </Text> - </Button> - </View> - </View> + <ErrorScreen error={error} /> </CenteredView> ) } @@ -289,7 +247,12 @@ export const ProfileListScreenInner = observer( </View> ) } - return <Header rkey={rkey} list={list} /> + return ( + <CenteredView sideBorders style={s.hContentRegion}> + <Header rkey={rkey} list={list} /> + {list.error && <ErrorScreen error={list.error} />} + </CenteredView> + ) }, ) @@ -532,7 +495,7 @@ const Header = observer(function HeaderImpl({ isOwner={list.isOwner} creator={list.data?.creator} avatarType="list"> - {list.isCuratelist ? ( + {list.isCuratelist || list.isPinned ? ( <Button testID={list.isPinned ? 'unpinBtn' : 'pinBtn'} type={list.isPinned ? 'default' : 'inverted'} @@ -789,6 +752,52 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( }, ) +function ErrorScreen({error}: {error: string}) { + const pal = usePalette('default') + const navigation = useNavigation<NavigationProp>() + const onPressBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + return ( + <View + style={[ + pal.view, + pal.border, + { + marginTop: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderTopWidth: 1, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.mb10]}> + Could not load list + </Text> + <Text type="md" style={[pal.text, s.mb20]}> + {error} + </Text> + + <View style={{flexDirection: 'row'}}> + <Button + type="default" + accessibilityLabel="Go Back" + accessibilityHint="Return to previous page" + onPress={onPressBack} + style={{flexShrink: 1}}> + <Text type="button" style={pal.text}> + Go Back + </Text> + </Button> + </View> + </View> + ) +} + const styles = StyleSheet.create({ btn: { flexDirection: 'row', |