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 errorzio/stable
parent
2045c615a8
commit
b12cd53a4d
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
() =>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue