Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improved list and feed errors (#1798)

* Fix error-state rendering of ProfileList

* Unsave/unpin lists on delete

* Improve handling of failing feedgens

* Only show 'remove' btn on feed DNE

authored by

Paul Frazee and committed by
GitHub
445f9768 691af268

+235 -58
+1
src/state/models/content/list.ts
··· 290 290 }) 291 291 } 292 292 293 + /* dont await */ this.rootStore.preferences.removeSavedFeed(this.uri) 293 294 this.rootStore.emitListDeleted(this.uri) 294 295 } 295 296
+51 -7
src/state/models/feeds/posts.ts
··· 25 25 26 26 const PAGE_SIZE = 30 27 27 28 + type FeedType = 'home' | 'following' | 'author' | 'custom' | 'likes' | 'list' 29 + 30 + export enum KnownError { 31 + FeedgenDoesNotExist, 32 + FeedgenMisconfigured, 33 + FeedgenBadResponse, 34 + FeedgenOffline, 35 + FeedgenUnknown, 36 + Unknown, 37 + } 38 + 28 39 type Options = { 29 40 /** 30 41 * Formats the feed in a flat array with no threading of replies, just ··· 49 60 isBlocking = false 50 61 isBlockedBy = false 51 62 error = '' 63 + knownError: KnownError | undefined 52 64 loadMoreError = '' 53 65 params: QueryParams 54 66 hasMore = true ··· 69 81 70 82 constructor( 71 83 public rootStore: RootStoreModel, 72 - public feedType: 73 - | 'home' 74 - | 'following' 75 - | 'author' 76 - | 'custom' 77 - | 'likes' 78 - | 'list', 84 + public feedType: FeedType, 79 85 params: QueryParams, 80 86 options?: Options, 81 87 ) { ··· 305 311 this.isLoading = true 306 312 this.isRefreshing = isRefreshing 307 313 this.error = '' 314 + this.knownError = undefined 308 315 } 309 316 310 317 _xIdle(error?: any, loadMoreError?: any) { ··· 314 321 this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError 315 322 this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError 316 323 this.error = cleanError(error) 324 + this.knownError = detectKnownError(this.feedType, error) 317 325 this.loadMoreError = cleanError(loadMoreError) 318 326 if (error) { 319 327 this.rootStore.log.error('Posts feed request failed', error) ··· 383 391 }) 384 392 } 385 393 } 394 + 395 + function detectKnownError( 396 + feedType: FeedType, 397 + error: any, 398 + ): KnownError | undefined { 399 + if (!error) { 400 + return undefined 401 + } 402 + if (typeof error !== 'string') { 403 + error = error.toString() 404 + } 405 + if (feedType !== 'custom') { 406 + return KnownError.Unknown 407 + } 408 + if (error.includes('could not find feed')) { 409 + return KnownError.FeedgenDoesNotExist 410 + } 411 + if (error.includes('feed unavailable')) { 412 + return KnownError.FeedgenOffline 413 + } 414 + if (error.includes('invalid did document')) { 415 + return KnownError.FeedgenMisconfigured 416 + } 417 + if (error.includes('could not resolve did document')) { 418 + return KnownError.FeedgenMisconfigured 419 + } 420 + if ( 421 + error.includes('invalid feed generator service details in did document') 422 + ) { 423 + return KnownError.FeedgenMisconfigured 424 + } 425 + if (error.includes('feed provided an invalid response')) { 426 + return KnownError.FeedgenBadResponse 427 + } 428 + return KnownError.FeedgenUnknown 429 + }
+2 -5
src/view/com/posts/Feed.tsx
··· 10 10 } from 'react-native' 11 11 import {FlatList} from '../util/Views' 12 12 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 13 - import {ErrorMessage} from '../util/error/ErrorMessage' 13 + import {FeedErrorMessage} from './FeedErrorMessage' 14 14 import {PostsFeedModel} from 'state/models/feeds/posts' 15 15 import {FeedSlice} from './FeedSlice' 16 16 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' ··· 125 125 return renderEmptyState() 126 126 } else if (item === ERROR_ITEM) { 127 127 return ( 128 - <ErrorMessage 129 - message={feed.error} 130 - onPressTryAgain={onPressTryAgain} 131 - /> 128 + <FeedErrorMessage feed={feed} onPressTryAgain={onPressTryAgain} /> 132 129 ) 133 130 } else if (item === LOAD_MORE_ERROR_ITEM) { 134 131 return (
+119
src/view/com/posts/FeedErrorMessage.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {AtUri, AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' 4 + import {PostsFeedModel, KnownError} from 'state/models/feeds/posts' 5 + import {Text} from '../util/text/Text' 6 + import {Button} from '../util/forms/Button' 7 + import * as Toast from '../util/Toast' 8 + import {ErrorMessage} from '../util/error/ErrorMessage' 9 + import {usePalette} from 'lib/hooks/usePalette' 10 + import {useNavigation} from '@react-navigation/native' 11 + import {NavigationProp} from 'lib/routes/types' 12 + import {useStores} from 'state/index' 13 + 14 + const MESSAGES = { 15 + [KnownError.Unknown]: '', 16 + [KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`, 17 + [KnownError.FeedgenMisconfigured]: 18 + 'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.', 19 + [KnownError.FeedgenBadResponse]: 20 + 'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.', 21 + [KnownError.FeedgenOffline]: 22 + 'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.', 23 + [KnownError.FeedgenUnknown]: 24 + 'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.', 25 + } 26 + 27 + export function FeedErrorMessage({ 28 + feed, 29 + onPressTryAgain, 30 + }: { 31 + feed: PostsFeedModel 32 + onPressTryAgain: () => void 33 + }) { 34 + if ( 35 + typeof feed.knownError === 'undefined' || 36 + feed.knownError === KnownError.Unknown 37 + ) { 38 + return ( 39 + <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} /> 40 + ) 41 + } 42 + 43 + return <FeedgenErrorMessage feed={feed} knownError={feed.knownError} /> 44 + } 45 + 46 + function FeedgenErrorMessage({ 47 + feed, 48 + knownError, 49 + }: { 50 + feed: PostsFeedModel 51 + knownError: KnownError 52 + }) { 53 + const pal = usePalette('default') 54 + const store = useStores() 55 + const navigation = useNavigation<NavigationProp>() 56 + const msg = MESSAGES[knownError] 57 + const uri = (feed.params as GetCustomFeed.QueryParams).feed 58 + const [ownerDid] = safeParseFeedgenUri(uri) 59 + 60 + const onViewProfile = React.useCallback(() => { 61 + navigation.navigate('Profile', {name: ownerDid}) 62 + }, [navigation, ownerDid]) 63 + 64 + const onRemoveFeed = React.useCallback(async () => { 65 + store.shell.openModal({ 66 + name: 'confirm', 67 + title: 'Remove feed', 68 + message: 'Remove this feed from your saved feeds?', 69 + async onPressConfirm() { 70 + try { 71 + await store.preferences.removeSavedFeed(uri) 72 + } catch (err) { 73 + Toast.show( 74 + 'There was an an issue removing this feed. Please check your internet connection and try again.', 75 + ) 76 + store.log.error('Failed to remove feed', {err}) 77 + } 78 + }, 79 + onPressCancel() { 80 + store.shell.closeModal() 81 + }, 82 + }) 83 + }, [store, uri]) 84 + 85 + return ( 86 + <View 87 + style={[ 88 + pal.border, 89 + pal.viewLight, 90 + { 91 + borderTopWidth: 1, 92 + paddingHorizontal: 20, 93 + paddingVertical: 18, 94 + gap: 12, 95 + }, 96 + ]}> 97 + <Text style={pal.text}>{msg}</Text> 98 + <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> 99 + {knownError === KnownError.FeedgenDoesNotExist && ( 100 + <Button type="inverted" label="Remove feed" onPress={onRemoveFeed} /> 101 + )} 102 + <Button 103 + type="default-light" 104 + label="View profile" 105 + onPress={onViewProfile} 106 + /> 107 + </View> 108 + </View> 109 + ) 110 + } 111 + 112 + function safeParseFeedgenUri(uri: string): [string, string] { 113 + try { 114 + const urip = new AtUri(uri) 115 + return [urip.hostname, urip.rkey] 116 + } catch { 117 + return ['', ''] 118 + } 119 + }
+8 -1
src/view/com/util/Views.d.ts
··· 1 - export {FlatList, ScrollView, View as CenteredView} from 'react-native' 1 + import React from 'react' 2 + import {ViewProps} from 'react-native' 3 + export {FlatList, ScrollView} from 'react-native' 4 + export function CenteredView({ 5 + style, 6 + sideBorders, 7 + ...props 8 + }: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>)
+54 -45
src/view/screens/ProfileList.tsx
··· 54 54 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> 55 55 export const ProfileListScreen = withAuthRequired( 56 56 observer(function ProfileListScreenImpl(props: Props) { 57 - const pal = usePalette('default') 58 57 const store = useStores() 59 - const navigation = useNavigation<NavigationProp>() 60 - 61 58 const {name: handleOrDid} = props.route.params 62 - 63 59 const [listOwnerDid, setListOwnerDid] = React.useState<string | undefined>() 64 60 const [error, setError] = React.useState<string | undefined>() 65 61 66 - const onPressBack = useCallback(() => { 67 - if (navigation.canGoBack()) { 68 - navigation.goBack() 69 - } else { 70 - navigation.navigate('Home') 71 - } 72 - }, [navigation]) 73 - 74 62 React.useEffect(() => { 75 63 /* 76 64 * We must resolve the DID of the list owner before we can fetch the list. ··· 92 80 if (error) { 93 81 return ( 94 82 <CenteredView> 95 - <View 96 - style={[ 97 - pal.view, 98 - pal.border, 99 - { 100 - margin: 10, 101 - paddingHorizontal: 18, 102 - paddingVertical: 14, 103 - borderRadius: 6, 104 - }, 105 - ]}> 106 - <Text type="title-lg" style={[pal.text, s.mb10]}> 107 - Could not load list 108 - </Text> 109 - <Text type="md" style={[pal.text, s.mb20]}> 110 - {error} 111 - </Text> 112 - 113 - <View style={{flexDirection: 'row'}}> 114 - <Button 115 - type="default" 116 - accessibilityLabel="Go Back" 117 - accessibilityHint="Return to previous page" 118 - onPress={onPressBack} 119 - style={{flexShrink: 1}}> 120 - <Text type="button" style={pal.text}> 121 - Go Back 122 - </Text> 123 - </Button> 124 - </View> 125 - </View> 83 + <ErrorScreen error={error} /> 126 84 </CenteredView> 127 85 ) 128 86 } ··· 289 247 </View> 290 248 ) 291 249 } 292 - return <Header rkey={rkey} list={list} /> 250 + return ( 251 + <CenteredView sideBorders style={s.hContentRegion}> 252 + <Header rkey={rkey} list={list} /> 253 + {list.error && <ErrorScreen error={list.error} />} 254 + </CenteredView> 255 + ) 293 256 }, 294 257 ) 295 258 ··· 532 495 isOwner={list.isOwner} 533 496 creator={list.data?.creator} 534 497 avatarType="list"> 535 - {list.isCuratelist ? ( 498 + {list.isCuratelist || list.isPinned ? ( 536 499 <Button 537 500 testID={list.isPinned ? 'unpinBtn' : 'pinBtn'} 538 501 type={list.isPinned ? 'default' : 'inverted'} ··· 788 751 ) 789 752 }, 790 753 ) 754 + 755 + function ErrorScreen({error}: {error: string}) { 756 + const pal = usePalette('default') 757 + const navigation = useNavigation<NavigationProp>() 758 + const onPressBack = useCallback(() => { 759 + if (navigation.canGoBack()) { 760 + navigation.goBack() 761 + } else { 762 + navigation.navigate('Home') 763 + } 764 + }, [navigation]) 765 + 766 + return ( 767 + <View 768 + style={[ 769 + pal.view, 770 + pal.border, 771 + { 772 + marginTop: 10, 773 + paddingHorizontal: 18, 774 + paddingVertical: 14, 775 + borderTopWidth: 1, 776 + }, 777 + ]}> 778 + <Text type="title-lg" style={[pal.text, s.mb10]}> 779 + Could not load list 780 + </Text> 781 + <Text type="md" style={[pal.text, s.mb20]}> 782 + {error} 783 + </Text> 784 + 785 + <View style={{flexDirection: 'row'}}> 786 + <Button 787 + type="default" 788 + accessibilityLabel="Go Back" 789 + accessibilityHint="Return to previous page" 790 + onPress={onPressBack} 791 + style={{flexShrink: 1}}> 792 + <Text type="button" style={pal.text}> 793 + Go Back 794 + </Text> 795 + </Button> 796 + </View> 797 + </View> 798 + ) 799 + } 791 800 792 801 const styles = StyleSheet.create({ 793 802 btn: {