An ATproto social media client -- with an independent Appview.
1import React from 'react'
2import {View} from 'react-native'
3import {
4 type AppBskyActorDefs,
5 AppBskyFeedGetAuthorFeed,
6 AtUri,
7} from '@atproto/api'
8import {msg as msgLingui, Trans} from '@lingui/macro'
9import {useLingui} from '@lingui/react'
10import {useNavigation} from '@react-navigation/native'
11
12import {type NavigationProp} from '#/lib/routes/types'
13import {cleanError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15import {type FeedDescriptor} from '#/state/queries/post-feed'
16import {useRemoveFeedMutation} from '#/state/queries/preferences'
17import {useTheme} from '#/alf'
18import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
19import * as Prompt from '#/components/Prompt'
20import {EmptyState} from '../util/EmptyState'
21import {ErrorMessage} from '../util/error/ErrorMessage'
22import {Button} from '../util/forms/Button'
23import {Text} from '../util/text/Text'
24import * as Toast from '../util/Toast'
25
26export enum KnownError {
27 Block = 'Block',
28 FeedgenDoesNotExist = 'FeedgenDoesNotExist',
29 FeedgenMisconfigured = 'FeedgenMisconfigured',
30 FeedgenBadResponse = 'FeedgenBadResponse',
31 FeedgenOffline = 'FeedgenOffline',
32 FeedgenUnknown = 'FeedgenUnknown',
33 FeedSignedInOnly = 'FeedSignedInOnly',
34 FeedTooManyRequests = 'FeedTooManyRequests',
35 Unknown = 'Unknown',
36}
37
38export function PostFeedErrorMessage({
39 feedDesc,
40 error,
41 onPressTryAgain,
42 savedFeedConfig,
43}: {
44 feedDesc: FeedDescriptor
45 error?: Error
46 onPressTryAgain: () => void
47 savedFeedConfig?: AppBskyActorDefs.SavedFeed
48}) {
49 const {_: _l} = useLingui()
50 const knownError = React.useMemo(
51 () => detectKnownError(feedDesc, error),
52 [feedDesc, error],
53 )
54 if (
55 typeof knownError !== 'undefined' &&
56 knownError !== KnownError.Unknown &&
57 feedDesc.startsWith('feedgen')
58 ) {
59 return (
60 <FeedgenErrorMessage
61 feedDesc={feedDesc}
62 knownError={knownError}
63 rawError={error}
64 savedFeedConfig={savedFeedConfig}
65 />
66 )
67 }
68
69 if (knownError === KnownError.Block) {
70 return (
71 <EmptyState
72 icon="ban"
73 message={_l(msgLingui`Posts hidden`)}
74 style={{paddingVertical: 40}}
75 />
76 )
77 }
78
79 return (
80 <ErrorMessage
81 message={cleanError(error)}
82 onPressTryAgain={onPressTryAgain}
83 />
84 )
85}
86
87function FeedgenErrorMessage({
88 feedDesc,
89 knownError,
90 rawError,
91 savedFeedConfig,
92}: {
93 feedDesc: FeedDescriptor
94 knownError: KnownError
95 rawError?: Error
96 savedFeedConfig?: AppBskyActorDefs.SavedFeed
97}) {
98 const theme = useTheme()
99 const colorMode = useColorModeTheme()
100 const {_: _l} = useLingui()
101 const navigation = useNavigation<NavigationProp>()
102 const msg = React.useMemo(
103 () =>
104 ({
105 [KnownError.Unknown]: '',
106 [KnownError.Block]: '',
107 [KnownError.FeedgenDoesNotExist]: _l(
108 msgLingui`Hmm, we're having trouble finding this feed. It may have been deleted.`,
109 ),
110 [KnownError.FeedgenMisconfigured]: _l(
111 msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`,
112 ),
113 [KnownError.FeedgenBadResponse]: _l(
114 msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`,
115 ),
116 [KnownError.FeedgenOffline]: _l(
117 msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`,
118 ),
119 [KnownError.FeedSignedInOnly]: _l(
120 msgLingui`This content is not viewable without a Bluesky account.`,
121 ),
122 [KnownError.FeedgenUnknown]: _l(
123 msgLingui`Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue.`,
124 ),
125 [KnownError.FeedTooManyRequests]: _l(
126 msgLingui`This feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`,
127 ),
128 })[knownError],
129 [_l, knownError],
130 )
131 const [__, uri] = feedDesc.split('|')
132 const [ownerDid] = safeParseFeedgenUri(uri)
133 const removePromptControl = Prompt.usePromptControl()
134 const {mutateAsync: removeFeed} = useRemoveFeedMutation()
135
136 const onViewProfile = React.useCallback(() => {
137 navigation.navigate('Profile', {name: ownerDid})
138 }, [navigation, ownerDid])
139
140 const onPressRemoveFeed = React.useCallback(() => {
141 removePromptControl.open()
142 }, [removePromptControl])
143
144 const onRemoveFeed = React.useCallback(async () => {
145 try {
146 if (!savedFeedConfig) return
147 await removeFeed(savedFeedConfig)
148 } catch (err) {
149 Toast.show(
150 _l(
151 msgLingui`There was an issue removing this feed. Please check your internet connection and try again.`,
152 ),
153 'exclamation-circle',
154 )
155 logger.error('Failed to remove feed', {message: err})
156 }
157 }, [removeFeed, _l, savedFeedConfig])
158
159 const cta = React.useMemo(() => {
160 switch (knownError) {
161 case KnownError.FeedSignedInOnly: {
162 return null
163 }
164 case KnownError.FeedgenDoesNotExist:
165 case KnownError.FeedgenMisconfigured:
166 case KnownError.FeedgenBadResponse:
167 case KnownError.FeedgenOffline:
168 case KnownError.FeedgenUnknown: {
169 return (
170 <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
171 {knownError === KnownError.FeedgenDoesNotExist &&
172 savedFeedConfig && (
173 <Button
174 type="inverted"
175 label={_l(msgLingui`Remove feed`)}
176 onPress={onRemoveFeed}
177 />
178 )}
179 <Button
180 type="default-light"
181 label={_l(msgLingui`View profile`)}
182 onPress={onViewProfile}
183 />
184 </View>
185 )
186 }
187 }
188 }, [knownError, onViewProfile, onRemoveFeed, _l, savedFeedConfig])
189
190 return (
191 <>
192 <View
193 style={[
194 {borderColor: theme.palette.contrast_100},
195 {backgroundColor: theme.palette.contrast_25},
196 {
197 borderTopWidth: 1,
198 paddingHorizontal: 20,
199 paddingVertical: 18,
200 gap: 12,
201 },
202 ]}>
203 <Text
204 style={{
205 color:
206 colorMode === 'light' ? theme.palette.black : theme.palette.white,
207 }}>
208 {msg}
209 </Text>
210
211 {rawError?.message && (
212 <Text
213 style={{
214 color:
215 colorMode === 'dark'
216 ? theme.palette.contrast_600
217 : theme.palette.contrast_700,
218 }}>
219 <Trans>Message from server: {rawError.message} </Trans>
220 </Text>
221 )}
222
223 {cta}
224 </View>
225
226 <Prompt.Basic
227 control={removePromptControl}
228 title={_l(msgLingui`Remove feed?`)}
229 description={_l(msgLingui`Remove this feed from your saved feeds`)}
230 onConfirm={onPressRemoveFeed}
231 confirmButtonCta={_l(msgLingui`Remove`)}
232 confirmButtonColor="negative"
233 />
234 </>
235 )
236}
237
238function safeParseFeedgenUri(uri: string): [string, string] {
239 try {
240 const urip = new AtUri(uri)
241 return [urip.hostname, urip.rkey]
242 } catch {
243 return ['', '']
244 }
245}
246
247function detectKnownError(
248 feedDesc: FeedDescriptor,
249 error: any,
250): KnownError | undefined {
251 if (!error) {
252 return undefined
253 }
254 if (
255 error instanceof AppBskyFeedGetAuthorFeed.BlockedActorError ||
256 error instanceof AppBskyFeedGetAuthorFeed.BlockedByActorError
257 ) {
258 return KnownError.Block
259 }
260
261 // check status codes
262 if (error?.status === 429) {
263 return KnownError.FeedTooManyRequests
264 }
265
266 // convert error to string and continue
267 if (typeof error !== 'string') {
268 error = error.toString()
269 }
270 if (error.includes(KnownError.FeedSignedInOnly)) {
271 return KnownError.FeedSignedInOnly
272 }
273 if (!feedDesc.startsWith('feedgen')) {
274 return KnownError.Unknown
275 }
276 if (error.includes('could not find feed')) {
277 return KnownError.FeedgenDoesNotExist
278 }
279 if (error.includes('feed unavailable')) {
280 return KnownError.FeedgenOffline
281 }
282 if (error.includes('invalid did document')) {
283 return KnownError.FeedgenMisconfigured
284 }
285 if (error.includes('could not resolve did document')) {
286 return KnownError.FeedgenMisconfigured
287 }
288 if (
289 error.includes('invalid feed generator service details in did document')
290 ) {
291 return KnownError.FeedgenMisconfigured
292 }
293 if (error.includes('invalid response')) {
294 return KnownError.FeedgenBadResponse
295 }
296 return KnownError.FeedgenUnknown
297}