Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Paul Frazee and committed by
GitHub
b12cd53a 2045c615

+159 -33
+24 -8
src/state/models/feeds/notifications.ts
··· 191 191 isRefreshing = false 192 192 hasLoaded = false 193 193 error = '' 194 + loadMoreError = '' 194 195 params: ListNotifications.QueryParams 195 196 hasMore = true 196 197 loadMoreCursor?: string ··· 305 306 await this._appendAll(res) 306 307 this._xIdle() 307 308 } catch (e: any) { 308 - this._xIdle() // don't bubble the error to the user 309 - this.rootStore.log.error('NotificationsView: Failed to load more', { 310 - params: this.params, 311 - e, 309 + this._xIdle(undefined, e) 310 + runInAction(() => { 311 + this.hasMore = false 312 312 }) 313 313 } 314 314 } finally { ··· 317 317 }) 318 318 319 319 /** 320 + * Attempt to load more again after a failure 321 + */ 322 + async retryLoadMore() { 323 + this.loadMoreError = '' 324 + this.hasMore = true 325 + return this.loadMore() 326 + } 327 + 328 + /** 320 329 * Load more posts at the start of the notifications 321 330 */ 322 331 loadLatest = bundleAsync(async () => { ··· 443 452 this.error = '' 444 453 } 445 454 446 - _xIdle(err?: any) { 455 + _xIdle(error?: any, loadMoreError?: any) { 447 456 this.isLoading = false 448 457 this.isRefreshing = false 449 458 this.hasLoaded = true 450 - this.error = cleanError(err) 451 - if (err) { 452 - this.rootStore.log.error('Failed to fetch notifications', err) 459 + this.error = cleanError(error) 460 + this.loadMoreError = cleanError(loadMoreError) 461 + if (error) { 462 + this.rootStore.log.error('Failed to fetch notifications', error) 463 + } 464 + if (loadMoreError) { 465 + this.rootStore.log.error( 466 + 'Failed to load more notifications', 467 + loadMoreError, 468 + ) 453 469 } 454 470 } 455 471
+24 -9
src/state/models/feeds/posts.ts
··· 213 213 hasNewLatest = false 214 214 hasLoaded = false 215 215 error = '' 216 + loadMoreError = '' 216 217 params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams 217 218 hasMore = true 218 219 loadMoreCursor: string | undefined ··· 382 383 await this._appendAll(res) 383 384 this._xIdle() 384 385 } catch (e: any) { 385 - this._xIdle() // don't bubble the error to the user 386 - this.rootStore.log.error('FeedView: Failed to load more', { 387 - params: this.params, 388 - e, 386 + this._xIdle(undefined, e) 387 + runInAction(() => { 388 + this.hasMore = false 389 389 }) 390 - this.hasMore = false 391 390 } 392 391 } finally { 393 392 this.lock.release() 394 393 } 395 394 }) 395 + 396 + /** 397 + * Attempt to load more again after a failure 398 + */ 399 + async retryLoadMore() { 400 + this.loadMoreError = '' 401 + this.hasMore = true 402 + return this.loadMore() 403 + } 396 404 397 405 /** 398 406 * Update content in-place ··· 503 511 this.error = '' 504 512 } 505 513 506 - _xIdle(err?: any) { 514 + _xIdle(error?: any, loadMoreError?: any) { 507 515 this.isLoading = false 508 516 this.isRefreshing = false 509 517 this.hasLoaded = true 510 - this.error = cleanError(err) 511 - if (err) { 512 - this.rootStore.log.error('Posts feed request failed', err) 518 + this.error = cleanError(error) 519 + this.loadMoreError = cleanError(loadMoreError) 520 + if (error) { 521 + this.rootStore.log.error('Posts feed request failed', error) 522 + } 523 + if (loadMoreError) { 524 + this.rootStore.log.error( 525 + 'Posts feed load-more request failed', 526 + loadMoreError, 527 + ) 513 528 } 514 529 } 515 530
+33 -13
src/view/com/notifications/Feed.tsx
··· 6 6 import {FeedItem} from './FeedItem' 7 7 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 8 8 import {ErrorMessage} from '../util/error/ErrorMessage' 9 + import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 9 10 import {EmptyState} from '../util/EmptyState' 10 11 import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 11 12 import {s} from 'lib/styles' 12 13 import {usePalette} from 'lib/hooks/usePalette' 13 14 14 15 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 16 + const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} 15 17 16 18 export const Feed = observer(function Feed({ 17 19 view, ··· 34 36 feedItems = view.notifications 35 37 } 36 38 } 39 + if (view.loadMoreError) { 40 + feedItems = (feedItems || []).concat([LOAD_MORE_ERROR_ITEM]) 41 + } 37 42 return feedItems 38 - }, [view.hasLoaded, view.isEmpty, view.notifications]) 43 + }, [view.hasLoaded, view.isEmpty, view.notifications, view.loadMoreError]) 39 44 40 45 const onRefresh = React.useCallback(async () => { 41 46 try { ··· 45 50 view.rootStore.log.error('Failed to refresh notifications feed', err) 46 51 } 47 52 }, [view]) 53 + 48 54 const onEndReached = React.useCallback(async () => { 49 55 try { 50 56 await view.loadMore() 51 57 } catch (err) { 52 58 view.rootStore.log.error('Failed to load more notifications', err) 53 59 } 60 + }, [view]) 61 + 62 + const onPressRetryLoadMore = React.useCallback(() => { 63 + view.retryLoadMore() 54 64 }, [view]) 55 65 56 66 // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf 57 67 // VirtualizedList: You have a large list that is slow to update - make sure your 58 68 // renderItem function renders components that follow React performance best practices 59 69 // like PureComponent, shouldComponentUpdate, etc 60 - const renderItem = React.useCallback(({item}: {item: any}) => { 61 - if (item === EMPTY_FEED_ITEM) { 62 - return ( 63 - <EmptyState 64 - icon="bell" 65 - message="No notifications yet!" 66 - style={styles.emptyState} 67 - /> 68 - ) 69 - } 70 - return <FeedItem item={item} /> 71 - }, []) 70 + const renderItem = React.useCallback( 71 + ({item}: {item: any}) => { 72 + if (item === EMPTY_FEED_ITEM) { 73 + return ( 74 + <EmptyState 75 + icon="bell" 76 + message="No notifications yet!" 77 + style={styles.emptyState} 78 + /> 79 + ) 80 + } else if (item === LOAD_MORE_ERROR_ITEM) { 81 + return ( 82 + <LoadMoreRetryBtn 83 + label="There was an issue fetching notifications. Tap here to try again." 84 + onPress={onPressRetryLoadMore} 85 + /> 86 + ) 87 + } 88 + return <FeedItem item={item} /> 89 + }, 90 + [onPressRetryLoadMore], 91 + ) 72 92 73 93 const FeedFooter = React.useCallback( 74 94 () =>
+31 -2
src/view/com/posts/Feed.tsx
··· 13 13 import {ErrorMessage} from '../util/error/ErrorMessage' 14 14 import {PostsFeedModel} from 'state/models/feeds/posts' 15 15 import {FeedSlice} from './FeedSlice' 16 + import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 16 17 import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 17 18 import {s} from 'lib/styles' 18 19 import {useAnalytics} from 'lib/analytics' ··· 21 22 const LOADING_ITEM = {_reactKey: '__loading__'} 22 23 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 23 24 const ERROR_ITEM = {_reactKey: '__error__'} 25 + const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} 24 26 25 27 export const Feed = observer(function Feed({ 26 28 feed, ··· 58 60 } else { 59 61 feedItems = feedItems.concat(feed.slices) 60 62 } 63 + if (feed.loadMoreError) { 64 + feedItems = feedItems.concat([LOAD_MORE_ERROR_ITEM]) 65 + } 61 66 } else if (feed.isLoading) { 62 67 feedItems = feedItems.concat([LOADING_ITEM]) 63 68 } 64 69 return feedItems 65 - }, [feed.hasError, feed.hasLoaded, feed.isLoading, feed.isEmpty, feed.slices]) 70 + }, [ 71 + feed.hasError, 72 + feed.hasLoaded, 73 + feed.isLoading, 74 + feed.isEmpty, 75 + feed.slices, 76 + feed.loadMoreError, 77 + ]) 66 78 67 79 // events 68 80 // = ··· 87 99 } 88 100 }, [feed, track]) 89 101 102 + const onPressRetryLoadMore = React.useCallback(() => { 103 + feed.retryLoadMore() 104 + }, [feed]) 105 + 90 106 // rendering 91 107 // = 92 108 ··· 102 118 <ErrorMessage 103 119 message={feed.error} 104 120 onPressTryAgain={onPressTryAgain} 121 + /> 122 + ) 123 + } else if (item === LOAD_MORE_ERROR_ITEM) { 124 + return ( 125 + <LoadMoreRetryBtn 126 + label="There was an issue fetching posts. Tap here to try again." 127 + onPress={onPressRetryLoadMore} 105 128 /> 106 129 ) 107 130 } else if (item === LOADING_ITEM) { ··· 109 132 } 110 133 return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} /> 111 134 }, 112 - [feed, onPressTryAgain, showPostFollowBtn, renderEmptyState], 135 + [ 136 + feed, 137 + onPressTryAgain, 138 + onPressRetryLoadMore, 139 + showPostFollowBtn, 140 + renderEmptyState, 141 + ], 113 142 ) 114 143 115 144 const FeedFooter = React.useCallback(
+44
src/view/com/util/LoadMoreRetryBtn.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet} from 'react-native' 3 + import { 4 + FontAwesomeIcon, 5 + FontAwesomeIconStyle, 6 + } from '@fortawesome/react-native-fontawesome' 7 + import {Button} from './forms/Button' 8 + import {Text} from './text/Text' 9 + import {usePalette} from 'lib/hooks/usePalette' 10 + 11 + export function LoadMoreRetryBtn({ 12 + label, 13 + onPress, 14 + }: { 15 + label: string 16 + onPress: () => void 17 + }) { 18 + const pal = usePalette('default') 19 + return ( 20 + <Button type="default-light" onPress={onPress} style={styles.loadMoreRetry}> 21 + <FontAwesomeIcon 22 + icon="arrow-rotate-left" 23 + style={pal.textLight as FontAwesomeIconStyle} 24 + size={18} 25 + /> 26 + <Text style={[pal.textLight, styles.label]}>{label}</Text> 27 + </Button> 28 + ) 29 + } 30 + 31 + const styles = StyleSheet.create({ 32 + loadMoreRetry: { 33 + flexDirection: 'row', 34 + gap: 14, 35 + alignItems: 'center', 36 + borderRadius: 0, 37 + marginTop: 1, 38 + paddingVertical: 12, 39 + paddingHorizontal: 20, 40 + }, 41 + label: { 42 + flex: 1, 43 + }, 44 + })
+3 -1
src/view/index.ts
··· 1 1 import {library} from '@fortawesome/fontawesome-svg-core' 2 2 3 - import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard' 3 + import {faAddressCard} from '@fortawesome/free-regular-svg-icons' 4 4 import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown' 5 5 import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' 6 6 import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' ··· 14 14 } from '@fortawesome/free-solid-svg-icons' 15 15 import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' 16 16 import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' 17 + import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft' 17 18 import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate' 18 19 import {faAt} from '@fortawesome/free-solid-svg-icons/faAt' 19 20 import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' ··· 86 87 faArrowRightFromBracket, 87 88 faArrowUpFromBracket, 88 89 faArrowUpRightFromSquare, 90 + faArrowRotateLeft, 89 91 faArrowsRotate, 90 92 faAt, 91 93 faBars,