diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts
index 0331f58bd4..8fb9f4b5ef 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 169eedac8a..3c580aca9f 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 591afe3a37..0578036d9b 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 (
-
+
)
} 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 0000000000..51c735e316
--- /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 (
+
+ )
+ }
+
+ return
+}
+
+function FeedgenErrorMessage({
+ feed,
+ knownError,
+}: {
+ feed: PostsFeedModel
+ knownError: KnownError
+}) {
+ const pal = usePalette('default')
+ const store = useStores()
+ const navigation = useNavigation()
+ 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 (
+
+ {msg}
+
+ {knownError === KnownError.FeedgenDoesNotExist && (
+
+ )}
+
+
+
+ )
+}
+
+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 292a985cd9..91df1d6bca 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)
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 859f50befb..4fc4b6c7f5 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -54,23 +54,11 @@ interface SectionRef {
type Props = NativeStackScreenProps
export const ProfileListScreen = withAuthRequired(
observer(function ProfileListScreenImpl(props: Props) {
- const pal = usePalette('default')
const store = useStores()
- const navigation = useNavigation()
-
const {name: handleOrDid} = props.route.params
-
const [listOwnerDid, setListOwnerDid] = React.useState()
const [error, setError] = React.useState()
- 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 (
-
-
- Could not load list
-
-
- {error}
-
-
-
-
-
-
+
)
}
@@ -289,7 +247,12 @@ export const ProfileListScreenInner = observer(
)
}
- return
+ return (
+
+
+ {list.error && }
+
+ )
},
)
@@ -532,7 +495,7 @@ const Header = observer(function HeaderImpl({
isOwner={list.isOwner}
creator={list.data?.creator}
avatarType="list">
- {list.isCuratelist ? (
+ {list.isCuratelist || list.isPinned ? (