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
|
isRefreshing = false
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
error = ''
|
error = ''
|
||||||
|
loadMoreError = ''
|
||||||
params: ListNotifications.QueryParams
|
params: ListNotifications.QueryParams
|
||||||
hasMore = true
|
hasMore = true
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
@ -305,10 +306,9 @@ export class NotificationsFeedModel {
|
||||||
await this._appendAll(res)
|
await this._appendAll(res)
|
||||||
this._xIdle()
|
this._xIdle()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._xIdle() // don't bubble the error to the user
|
this._xIdle(undefined, e)
|
||||||
this.rootStore.log.error('NotificationsView: Failed to load more', {
|
runInAction(() => {
|
||||||
params: this.params,
|
this.hasMore = false
|
||||||
e,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} 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
|
* Load more posts at the start of the notifications
|
||||||
*/
|
*/
|
||||||
|
@ -443,13 +452,20 @@ export class NotificationsFeedModel {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
_xIdle(err?: any) {
|
_xIdle(error?: any, loadMoreError?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
this.error = cleanError(err)
|
this.error = cleanError(error)
|
||||||
if (err) {
|
this.loadMoreError = cleanError(loadMoreError)
|
||||||
this.rootStore.log.error('Failed to fetch notifications', err)
|
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
|
hasNewLatest = false
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
error = ''
|
error = ''
|
||||||
|
loadMoreError = ''
|
||||||
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
|
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
|
||||||
hasMore = true
|
hasMore = true
|
||||||
loadMoreCursor: string | undefined
|
loadMoreCursor: string | undefined
|
||||||
|
@ -382,18 +383,25 @@ export class PostsFeedModel {
|
||||||
await this._appendAll(res)
|
await this._appendAll(res)
|
||||||
this._xIdle()
|
this._xIdle()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._xIdle() // don't bubble the error to the user
|
this._xIdle(undefined, e)
|
||||||
this.rootStore.log.error('FeedView: Failed to load more', {
|
runInAction(() => {
|
||||||
params: this.params,
|
this.hasMore = false
|
||||||
e,
|
|
||||||
})
|
})
|
||||||
this.hasMore = false
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.lock.release()
|
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
|
* Update content in-place
|
||||||
*/
|
*/
|
||||||
|
@ -503,13 +511,20 @@ export class PostsFeedModel {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
_xIdle(err?: any) {
|
_xIdle(error?: any, loadMoreError?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
this.error = cleanError(err)
|
this.error = cleanError(error)
|
||||||
if (err) {
|
this.loadMoreError = cleanError(loadMoreError)
|
||||||
this.rootStore.log.error('Posts feed request failed', err)
|
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 {FeedItem} from './FeedItem'
|
||||||
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
import {EmptyState} from '../util/EmptyState'
|
import {EmptyState} from '../util/EmptyState'
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
view,
|
view,
|
||||||
|
@ -34,8 +36,11 @@ export const Feed = observer(function Feed({
|
||||||
feedItems = view.notifications
|
feedItems = view.notifications
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (view.loadMoreError) {
|
||||||
|
feedItems = (feedItems || []).concat([LOAD_MORE_ERROR_ITEM])
|
||||||
|
}
|
||||||
return feedItems
|
return feedItems
|
||||||
}, [view.hasLoaded, view.isEmpty, view.notifications])
|
}, [view.hasLoaded, view.isEmpty, view.notifications, view.loadMoreError])
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -45,6 +50,7 @@ export const Feed = observer(function Feed({
|
||||||
view.rootStore.log.error('Failed to refresh notifications feed', err)
|
view.rootStore.log.error('Failed to refresh notifications feed', err)
|
||||||
}
|
}
|
||||||
}, [view])
|
}, [view])
|
||||||
|
|
||||||
const onEndReached = React.useCallback(async () => {
|
const onEndReached = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await view.loadMore()
|
await view.loadMore()
|
||||||
|
@ -53,22 +59,36 @@ export const Feed = observer(function Feed({
|
||||||
}
|
}
|
||||||
}, [view])
|
}, [view])
|
||||||
|
|
||||||
|
const onPressRetryLoadMore = React.useCallback(() => {
|
||||||
|
view.retryLoadMore()
|
||||||
|
}, [view])
|
||||||
|
|
||||||
// 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
|
||||||
// renderItem function renders components that follow React performance best practices
|
// renderItem function renders components that follow React performance best practices
|
||||||
// like PureComponent, shouldComponentUpdate, etc
|
// like PureComponent, shouldComponentUpdate, etc
|
||||||
const renderItem = React.useCallback(({item}: {item: any}) => {
|
const renderItem = React.useCallback(
|
||||||
if (item === EMPTY_FEED_ITEM) {
|
({item}: {item: any}) => {
|
||||||
return (
|
if (item === EMPTY_FEED_ITEM) {
|
||||||
<EmptyState
|
return (
|
||||||
icon="bell"
|
<EmptyState
|
||||||
message="No notifications yet!"
|
icon="bell"
|
||||||
style={styles.emptyState}
|
message="No notifications yet!"
|
||||||
/>
|
style={styles.emptyState}
|
||||||
)
|
/>
|
||||||
}
|
)
|
||||||
return <FeedItem item={item} />
|
} 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(
|
const FeedFooter = React.useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {PostsFeedModel} from 'state/models/feeds/posts'
|
import {PostsFeedModel} from 'state/models/feeds/posts'
|
||||||
import {FeedSlice} from './FeedSlice'
|
import {FeedSlice} from './FeedSlice'
|
||||||
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
|
@ -21,6 +22,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
feed,
|
feed,
|
||||||
|
@ -58,11 +60,21 @@ export const Feed = observer(function Feed({
|
||||||
} else {
|
} else {
|
||||||
feedItems = feedItems.concat(feed.slices)
|
feedItems = feedItems.concat(feed.slices)
|
||||||
}
|
}
|
||||||
|
if (feed.loadMoreError) {
|
||||||
|
feedItems = feedItems.concat([LOAD_MORE_ERROR_ITEM])
|
||||||
|
}
|
||||||
} else if (feed.isLoading) {
|
} else if (feed.isLoading) {
|
||||||
feedItems = feedItems.concat([LOADING_ITEM])
|
feedItems = feedItems.concat([LOADING_ITEM])
|
||||||
}
|
}
|
||||||
return feedItems
|
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
|
// events
|
||||||
// =
|
// =
|
||||||
|
@ -87,6 +99,10 @@ export const Feed = observer(function Feed({
|
||||||
}
|
}
|
||||||
}, [feed, track])
|
}, [feed, track])
|
||||||
|
|
||||||
|
const onPressRetryLoadMore = React.useCallback(() => {
|
||||||
|
feed.retryLoadMore()
|
||||||
|
}, [feed])
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
@ -104,12 +120,25 @@ export const Feed = observer(function Feed({
|
||||||
onPressTryAgain={onPressTryAgain}
|
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) {
|
} else if (item === LOADING_ITEM) {
|
||||||
return <PostFeedLoadingPlaceholder />
|
return <PostFeedLoadingPlaceholder />
|
||||||
}
|
}
|
||||||
return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} />
|
return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} />
|
||||||
},
|
},
|
||||||
[feed, onPressTryAgain, showPostFollowBtn, renderEmptyState],
|
[
|
||||||
|
feed,
|
||||||
|
onPressTryAgain,
|
||||||
|
onPressRetryLoadMore,
|
||||||
|
showPostFollowBtn,
|
||||||
|
renderEmptyState,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeedFooter = React.useCallback(
|
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 {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 {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
|
||||||
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
|
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
|
||||||
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
|
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
|
||||||
|
@ -14,6 +14,7 @@ import {
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
|
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
|
||||||
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
|
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 {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
|
||||||
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
|
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
|
||||||
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
||||||
|
@ -86,6 +87,7 @@ export function setup() {
|
||||||
faArrowRightFromBracket,
|
faArrowRightFromBracket,
|
||||||
faArrowUpFromBracket,
|
faArrowUpFromBracket,
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
|
faArrowRotateLeft,
|
||||||
faArrowsRotate,
|
faArrowsRotate,
|
||||||
faAt,
|
faAt,
|
||||||
faBars,
|
faBars,
|
||||||
|
|
Loading…
Reference in New Issue