Improve "Load more" error handling in feeds (#379)

* Add explicit load-more error handling to posts feed

* Add explicit load-more error handling to notifications feed

* Properly set hasMore to false after an error
zio/stable
Paul Frazee 2023-04-03 15:57:17 -05:00 committed by GitHub
parent 2045c615a8
commit b12cd53a4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 159 additions and 33 deletions

View File

@ -191,6 +191,7 @@ export class NotificationsFeedModel {
isRefreshing = false
hasLoaded = false
error = ''
loadMoreError = ''
params: ListNotifications.QueryParams
hasMore = true
loadMoreCursor?: string
@ -305,10 +306,9 @@ export class NotificationsFeedModel {
await this._appendAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle() // don't bubble the error to the user
this.rootStore.log.error('NotificationsView: Failed to load more', {
params: this.params,
e,
this._xIdle(undefined, e)
runInAction(() => {
this.hasMore = false
})
}
} finally {
@ -316,6 +316,15 @@ export class NotificationsFeedModel {
}
})
/**
* Attempt to load more again after a failure
*/
async retryLoadMore() {
this.loadMoreError = ''
this.hasMore = true
return this.loadMore()
}
/**
* Load more posts at the start of the notifications
*/
@ -443,13 +452,20 @@ export class NotificationsFeedModel {
this.error = ''
}
_xIdle(err?: any) {
_xIdle(error?: any, loadMoreError?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = cleanError(err)
if (err) {
this.rootStore.log.error('Failed to fetch notifications', err)
this.error = cleanError(error)
this.loadMoreError = cleanError(loadMoreError)
if (error) {
this.rootStore.log.error('Failed to fetch notifications', error)
}
if (loadMoreError) {
this.rootStore.log.error(
'Failed to load more notifications',
loadMoreError,
)
}
}

View File

@ -213,6 +213,7 @@ export class PostsFeedModel {
hasNewLatest = false
hasLoaded = false
error = ''
loadMoreError = ''
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
hasMore = true
loadMoreCursor: string | undefined
@ -382,18 +383,25 @@ export class PostsFeedModel {
await this._appendAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle() // don't bubble the error to the user
this.rootStore.log.error('FeedView: Failed to load more', {
params: this.params,
e,
this._xIdle(undefined, e)
runInAction(() => {
this.hasMore = false
})
this.hasMore = false
}
} finally {
this.lock.release()
}
})
/**
* Attempt to load more again after a failure
*/
async retryLoadMore() {
this.loadMoreError = ''
this.hasMore = true
return this.loadMore()
}
/**
* Update content in-place
*/
@ -503,13 +511,20 @@ export class PostsFeedModel {
this.error = ''
}
_xIdle(err?: any) {
_xIdle(error?: any, loadMoreError?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = cleanError(err)
if (err) {
this.rootStore.log.error('Posts feed request failed', err)
this.error = cleanError(error)
this.loadMoreError = cleanError(loadMoreError)
if (error) {
this.rootStore.log.error('Posts feed request failed', error)
}
if (loadMoreError) {
this.rootStore.log.error(
'Posts feed load-more request failed',
loadMoreError,
)
}
}

View File

@ -6,12 +6,14 @@ import {NotificationsFeedModel} from 'state/models/feeds/notifications'
import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export const Feed = observer(function Feed({
view,
@ -34,8 +36,11 @@ export const Feed = observer(function Feed({
feedItems = view.notifications
}
}
if (view.loadMoreError) {
feedItems = (feedItems || []).concat([LOAD_MORE_ERROR_ITEM])
}
return feedItems
}, [view.hasLoaded, view.isEmpty, view.notifications])
}, [view.hasLoaded, view.isEmpty, view.notifications, view.loadMoreError])
const onRefresh = React.useCallback(async () => {
try {
@ -45,6 +50,7 @@ export const Feed = observer(function Feed({
view.rootStore.log.error('Failed to refresh notifications feed', err)
}
}, [view])
const onEndReached = React.useCallback(async () => {
try {
await view.loadMore()
@ -53,22 +59,36 @@ export const Feed = observer(function Feed({
}
}, [view])
const onPressRetryLoadMore = React.useCallback(() => {
view.retryLoadMore()
}, [view])
// 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
// renderItem function renders components that follow React performance best practices
// like PureComponent, shouldComponentUpdate, etc
const renderItem = React.useCallback(({item}: {item: any}) => {
if (item === EMPTY_FEED_ITEM) {
return (
<EmptyState
icon="bell"
message="No notifications yet!"
style={styles.emptyState}
/>
)
}
return <FeedItem item={item} />
}, [])
const renderItem = React.useCallback(
({item}: {item: any}) => {
if (item === EMPTY_FEED_ITEM) {
return (
<EmptyState
icon="bell"
message="No notifications yet!"
style={styles.emptyState}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching notifications. Tap here to try again."
onPress={onPressRetryLoadMore}
/>
)
}
return <FeedItem item={item} />
},
[onPressRetryLoadMore],
)
const FeedFooter = React.useCallback(
() =>

View File

@ -13,6 +13,7 @@ import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics'
@ -21,6 +22,7 @@ import {usePalette} from 'lib/hooks/usePalette'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export const Feed = observer(function Feed({
feed,
@ -58,11 +60,21 @@ export const Feed = observer(function Feed({
} else {
feedItems = feedItems.concat(feed.slices)
}
if (feed.loadMoreError) {
feedItems = feedItems.concat([LOAD_MORE_ERROR_ITEM])
}
} else if (feed.isLoading) {
feedItems = feedItems.concat([LOADING_ITEM])
}
return feedItems
}, [feed.hasError, feed.hasLoaded, feed.isLoading, feed.isEmpty, feed.slices])
}, [
feed.hasError,
feed.hasLoaded,
feed.isLoading,
feed.isEmpty,
feed.slices,
feed.loadMoreError,
])
// events
// =
@ -87,6 +99,10 @@ export const Feed = observer(function Feed({
}
}, [feed, track])
const onPressRetryLoadMore = React.useCallback(() => {
feed.retryLoadMore()
}, [feed])
// rendering
// =
@ -104,12 +120,25 @@ export const Feed = observer(function Feed({
onPressTryAgain={onPressTryAgain}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching posts. Tap here to try again."
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING_ITEM) {
return <PostFeedLoadingPlaceholder />
}
return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} />
},
[feed, onPressTryAgain, showPostFollowBtn, renderEmptyState],
[
feed,
onPressTryAgain,
onPressRetryLoadMore,
showPostFollowBtn,
renderEmptyState,
],
)
const FeedFooter = React.useCallback(

View File

@ -0,0 +1,44 @@
import React from 'react'
import {StyleSheet} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Button} from './forms/Button'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
export function LoadMoreRetryBtn({
label,
onPress,
}: {
label: string
onPress: () => void
}) {
const pal = usePalette('default')
return (
<Button type="default-light" onPress={onPress} style={styles.loadMoreRetry}>
<FontAwesomeIcon
icon="arrow-rotate-left"
style={pal.textLight as FontAwesomeIconStyle}
size={18}
/>
<Text style={[pal.textLight, styles.label]}>{label}</Text>
</Button>
)
}
const styles = StyleSheet.create({
loadMoreRetry: {
flexDirection: 'row',
gap: 14,
alignItems: 'center',
borderRadius: 0,
marginTop: 1,
paddingVertical: 12,
paddingHorizontal: 20,
},
label: {
flex: 1,
},
})

View File

@ -1,6 +1,6 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard'
import {faAddressCard} from '@fortawesome/free-regular-svg-icons'
import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
@ -14,6 +14,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft'
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
@ -86,6 +87,7 @@ export function setup() {
faArrowRightFromBracket,
faArrowUpFromBracket,
faArrowUpRightFromSquare,
faArrowRotateLeft,
faArrowsRotate,
faAt,
faBars,