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