An ATproto social media client -- with an independent Appview.
at main 297 lines 8.6 kB view raw
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}