Improve error messages

zio/stable
Paul Frazee 2022-11-15 10:46:12 -06:00
parent 6e93301542
commit fb3a43c216
15 changed files with 156 additions and 32 deletions

View File

@ -3,6 +3,7 @@ import * as GetTimeline from '../../third-party/api/src/client/types/app/bsky/fe
import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed' import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api' import * as apilib from '../lib/api'
import {cleanError} from '../../view/lib/strings'
export class FeedItemMyStateModel { export class FeedItemMyStateModel {
repost?: string repost?: string
@ -254,7 +255,7 @@ export class FeedModel {
this.isLoading = false this.isLoading = false
this.isRefreshing = false this.isRefreshing = false
this.hasLoaded = true this.hasLoaded = true
this.error = err this.error = cleanError(err)
} }
// loader functions // loader functions
@ -282,7 +283,7 @@ export class FeedModel {
this._replaceAll(res) this._replaceAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
this._xIdle(`Failed to load feed: ${e.toString()}`) this._xIdle(e.toString())
} }
} }
@ -293,7 +294,7 @@ export class FeedModel {
this._prependAll(res) this._prependAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
this._xIdle(`Failed to load feed: ${e.toString()}`) this._xIdle(e.toString())
} }
} }

View File

@ -4,6 +4,7 @@ import {RootStoreModel} from './root-store'
import {Declaration} from './_common' import {Declaration} from './_common'
import {hasProp} from '../lib/type-guards' import {hasProp} from '../lib/type-guards'
import {APP_BSKY_GRAPH} from '../../third-party/api' import {APP_BSKY_GRAPH} from '../../third-party/api'
import {cleanError} from '../../view/lib/strings'
const UNGROUPABLE_REASONS = ['trend', 'assertion'] const UNGROUPABLE_REASONS = ['trend', 'assertion']
@ -215,7 +216,7 @@ export class NotificationsViewModel {
this.isLoading = false this.isLoading = false
this.isRefreshing = false this.isRefreshing = false
this.hasLoaded = true this.hasLoaded = true
this.error = err this.error = cleanError(err)
} }
// loader functions // loader functions

View File

@ -6,11 +6,14 @@ import {
NotificationsViewItemModel, NotificationsViewItemModel,
} from '../../../state/models/notifications-view' } from '../../../state/models/notifications-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {ErrorMessage} from '../util/ErrorMessage'
export const Feed = observer(function Feed({ export const Feed = observer(function Feed({
view, view,
onPressTryAgain,
}: { }: {
view: NotificationsViewModel view: NotificationsViewModel
onPressTryAgain?: () => void
}) { }) {
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
// VirtualizedList: You have a large list that is slow to update - make sure your // VirtualizedList: You have a large list that is slow to update - make sure your
@ -30,7 +33,14 @@ export const Feed = observer(function Feed({
{view.isLoading && !view.isRefreshing && !view.hasContent && ( {view.isLoading && !view.isRefreshing && !view.hasContent && (
<Text>Loading...</Text> <Text>Loading...</Text>
)} )}
{view.hasError && <Text>{view.error}</Text>} {view.hasError && (
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}
/>
)}
{view.hasContent && ( {view.hasContent && (
<FlatList <FlatList
data={view.notifications} data={view.notifications}

View File

@ -1,18 +1,12 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {ActivityIndicator, FlatList, StyleSheet, Text, View} from 'react-native'
ActivityIndicator,
FlatList,
Image,
StyleSheet,
Text,
View,
} from 'react-native'
import { import {
RepostedByViewModel, RepostedByViewModel,
RepostedByViewItemModel, RepostedByViewItemModel,
} from '../../../state/models/reposted-by-view' } from '../../../state/models/reposted-by-view'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {ErrorMessage} from '../util/ErrorMessage'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -38,6 +32,10 @@ export const PostRepostedBy = observer(function PostRepostedBy({
.catch(err => console.error('Failed to fetch reposted by', err)) .catch(err => console.error('Failed to fetch reposted by', err))
}, [uri, view?.params.uri, store]) }, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
// loading // loading
// = // =
if ( if (
@ -57,7 +55,12 @@ export const PostRepostedBy = observer(function PostRepostedBy({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -8,6 +8,7 @@ import {
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {SharePostModel} from '../../../state/models/shell-ui' import {SharePostModel} from '../../../state/models/shell-ui'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage'
export const PostThread = observer(function PostThread({uri}: {uri: string}) { export const PostThread = observer(function PostThread({uri}: {uri: string}) {
const store = useStores() const store = useStores()
@ -50,7 +51,12 @@ export const PostThread = observer(function PostThread({uri}: {uri: string}) {
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -6,6 +6,7 @@ import {
VotesViewItemModel, VotesViewItemModel,
} from '../../../state/models/votes-view' } from '../../../state/models/votes-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {ErrorMessage} from '../util/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -31,6 +32,10 @@ export const PostVotedBy = observer(function PostVotedBy({
newView.setup().catch(err => console.error('Failed to fetch voted by', err)) newView.setup().catch(err => console.error('Failed to fetch voted by', err))
}, [uri, view?.params.uri, store]) }, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
// loading // loading
// = // =
if ( if (
@ -50,7 +55,12 @@ export const PostVotedBy = observer(function PostVotedBy({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -1,6 +1,7 @@
import React, {MutableRefObject} from 'react' import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Text, View, FlatList, StyleProp, ViewStyle} from 'react-native' import {Text, View, FlatList, StyleProp, ViewStyle} from 'react-native'
import {ErrorMessage} from '../util/ErrorMessage'
import {FeedModel, FeedItemModel} from '../../../state/models/feed-view' import {FeedModel, FeedItemModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
@ -8,10 +9,12 @@ export const Feed = observer(function Feed({
feed, feed,
style, style,
scrollElRef, scrollElRef,
onPressTryAgain,
}: { }: {
feed: FeedModel feed: FeedModel
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
}) { }) {
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
// VirtualizedList: You have a large list that is slow to update - make sure your // VirtualizedList: You have a large list that is slow to update - make sure your
@ -29,7 +32,14 @@ export const Feed = observer(function Feed({
{feed.isLoading && !feed.isRefreshing && !feed.hasContent && ( {feed.isLoading && !feed.isRefreshing && !feed.hasContent && (
<Text>Loading...</Text> <Text>Loading...</Text>
)} )}
{feed.hasError && <Text>{feed.error}</Text>} {feed.hasError && (
<ErrorMessage
dark
message={feed.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}
/>
)}
{feed.hasContent && ( {feed.hasContent && (
<FlatList <FlatList
ref={scrollElRef} ref={scrollElRef}
@ -41,7 +51,11 @@ export const Feed = observer(function Feed({
onEndReached={onEndReached} onEndReached={onEndReached}
/> />
)} )}
{feed.isEmpty && <Text>This feed is empty!</Text>} {feed.isEmpty && !feed.hasError && (
<View>
<Text>This feed is empty!</Text>
</View>
)}
</View> </View>
) )
}) })

View File

@ -6,6 +6,7 @@ import {
FollowerItem, FollowerItem,
} from '../../../state/models/user-followers-view' } from '../../../state/models/user-followers-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {ErrorMessage} from '../util/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -31,6 +32,10 @@ export const ProfileFollowers = observer(function ProfileFollowers({
.catch(err => console.error('Failed to fetch user followers', err)) .catch(err => console.error('Failed to fetch user followers', err))
}, [name, view?.params.user, store]) }, [name, view?.params.user, store])
const onRefresh = () => {
view?.refresh()
}
// loading // loading
// = // =
if ( if (
@ -50,7 +55,12 @@ export const ProfileFollowers = observer(function ProfileFollowers({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -7,6 +7,7 @@ import {
} from '../../../state/models/user-follows-view' } from '../../../state/models/user-follows-view'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {ErrorMessage} from '../util/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -31,6 +32,10 @@ export const ProfileFollows = observer(function ProfileFollows({
.catch(err => console.error('Failed to fetch user follows', err)) .catch(err => console.error('Failed to fetch user follows', err))
}, [name, view?.params.user, store]) }, [name, view?.params.user, store])
const onRefresh = () => {
view?.refresh()
}
// loading // loading
// = // =
if ( if (
@ -50,7 +55,12 @@ export const ProfileFollows = observer(function ProfileFollows({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -1,8 +1,9 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ActivityIndicator, FlatList, Text, View} from 'react-native' import {ActivityIndicator, FlatList, View} from 'react-native'
import {MembersViewModel, MemberItem} from '../../../state/models/members-view' import {MembersViewModel, MemberItem} from '../../../state/models/members-view'
import {ProfileCard} from './ProfileCard' import {ProfileCard} from './ProfileCard'
import {ErrorMessage} from '../util/ErrorMessage'
import {useStores} from '../../../state' import {useStores} from '../../../state'
export const ProfileMembers = observer(function ProfileMembers({ export const ProfileMembers = observer(function ProfileMembers({
@ -24,6 +25,10 @@ export const ProfileMembers = observer(function ProfileMembers({
newView.setup().catch(err => console.error('Failed to fetch members', err)) newView.setup().catch(err => console.error('Failed to fetch members', err))
}, [name, view?.params.actor, store]) }, [name, view?.params.actor, store])
const onRefresh = () => {
view?.refresh()
}
// loading // loading
// = // =
if ( if (
@ -43,7 +48,12 @@ export const ProfileMembers = observer(function ProfileMembers({
if (view.hasError) { if (view.hasError) {
return ( return (
<View> <View>
<Text>{view.error}</Text> <ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View> </View>
) )
} }

View File

@ -1,40 +1,66 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {
StyleSheet,
Text,
TouchableOpacity,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles' import LinearGradient from 'react-native-linear-gradient'
import {colors, gradients} from '../../lib/styles'
export function ErrorMessage({ export function ErrorMessage({
message, message,
numberOfLines, numberOfLines,
dark,
style,
onPressTryAgain, onPressTryAgain,
}: { }: {
message: string message: string
numberOfLines?: number numberOfLines?: number
dark?: boolean
style?: StyleProp<ViewStyle>
onPressTryAgain?: () => void onPressTryAgain?: () => void
}) { }) {
return ( const inner = (
<View style={styles.outer}> <>
<View style={styles.errorIcon}> <View style={[styles.errorIcon, dark ? styles.darkErrorIcon : undefined]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="exclamation" icon="exclamation"
style={{color: colors.white}} style={{color: dark ? colors.red3 : colors.white}}
size={16} size={16}
/> />
</View> </View>
<Text style={styles.message} numberOfLines={numberOfLines}> <Text
style={[styles.message, dark ? styles.darkMessage : undefined]}
numberOfLines={numberOfLines}>
{message} {message}
</Text> </Text>
{onPressTryAgain && ( {onPressTryAgain && (
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}> <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon <FontAwesomeIcon
icon="arrows-rotate" icon="arrows-rotate"
style={{color: colors.red4}} style={{color: dark ? colors.white : colors.red4}}
size={16} size={16}
/> />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </>
) )
if (dark) {
return (
<LinearGradient
colors={[gradients.error.start, gradients.error.end]}
start={{x: 0.5, y: 0}}
end={{x: 1, y: 1}}
style={[styles.outer, style]}>
{inner}
</LinearGradient>
)
}
return <View style={[styles.outer, style]}>{inner}</View>
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -57,11 +83,18 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
marginRight: 8, marginRight: 8,
}, },
darkErrorIcon: {
backgroundColor: colors.white,
},
message: { message: {
flex: 1, flex: 1,
color: colors.red4, color: colors.red4,
paddingRight: 10, paddingRight: 10,
}, },
darkMessage: {
color: colors.white,
fontWeight: '600',
},
btn: { btn: {
paddingHorizontal: 4, paddingHorizontal: 4,
paddingVertical: 4, paddingVertical: 4,

View File

@ -96,3 +96,10 @@ export function enforceLen(str: string, len: number): string {
} }
return str return str
} }
export function cleanError(str: string): string {
if (str.startsWith('Error: ')) {
return str.slice('Error: '.length)
}
return str
}

View File

@ -45,6 +45,7 @@ export const colors = {
export const gradients = { export const gradients = {
primary: {start: '#db00ff', end: '#ff007a'}, primary: {start: '#db00ff', end: '#ff007a'},
error: {start: '#ff007a', end: '#ed0d78'},
purple: {start: colors.pink3, end: colors.purple3}, purple: {start: colors.pink3, end: colors.purple3},
blue: {start: colors.purple3, end: colors.blue3}, blue: {start: colors.purple3, end: colors.blue3},
green: {start: colors.blue3, end: colors.green3}, green: {start: colors.blue3, end: colors.green3},

View File

@ -51,6 +51,9 @@ export const Home = observer(function Home({
const onCreatePost = () => { const onCreatePost = () => {
defaultFeedView.loadLatest() defaultFeedView.loadLatest()
} }
const onPressTryAgain = () => {
defaultFeedView.refresh()
}
return ( return (
<View style={s.flex1}> <View style={s.flex1}>
@ -63,6 +66,7 @@ export const Home = observer(function Home({
feed={defaultFeedView} feed={defaultFeedView}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
style={{flex: 1}} style={{flex: 1}}
onPressTryAgain={onPressTryAgain}
/> />
<FAB icon="pen-nib" onPress={onComposePress} /> <FAB icon="pen-nib" onPress={onComposePress} />
</View> </View>

View File

@ -36,10 +36,14 @@ export const Notifications = ({visible}: ScreenParams) => {
} }
}, [visible, store]) }, [visible, store])
const onPressTryAgain = () => {
notesView?.refresh()
}
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<ViewHeader title="Notifications" /> <ViewHeader title="Notifications" />
{notesView && <Feed view={notesView} />} {notesView && <Feed view={notesView} onPressTryAgain={onPressTryAgain} />}
</View> </View>
) )
} }