Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Fixes to the followers list view

+74 -61
+32 -30
src/state/models/user-followers-view.ts
··· 5 5 } from '@atproto/api' 6 6 import {RootStoreModel} from './root-store' 7 7 8 - export type FollowerItem = GetFollowers.Follower & { 9 - _reactKey: string 10 - } 8 + const PAGE_SIZE = 30 9 + 10 + export type FollowerItem = GetFollowers.Follower 11 11 12 12 export class UserFollowersViewModel { 13 13 // state ··· 16 16 hasLoaded = false 17 17 error = '' 18 18 params: GetFollowers.QueryParams 19 + hasMore = true 20 + loadMoreCursor?: string 21 + private _loadMorePromise: Promise<void> | undefined 19 22 20 23 // data 21 24 subject: ActorRef.WithInfo = { ··· 55 58 // public api 56 59 // = 57 60 58 - async setup() { 59 - await this._fetch() 60 - } 61 - 62 61 async refresh() { 63 - await this._fetch(true) 62 + return this.loadMore(true) 64 63 } 65 64 66 - async loadMore() { 67 - // TODO 65 + async loadMore(isRefreshing = false) { 66 + if (this._loadMorePromise) { 67 + return this._loadMorePromise 68 + } 69 + this._loadMorePromise = this._loadMore(isRefreshing) 70 + await this._loadMorePromise 71 + this._loadMorePromise = undefined 68 72 } 69 73 70 74 // state transitions ··· 89 93 // loader functions 90 94 // = 91 95 92 - private async _fetch(isRefreshing = false) { 96 + private async _loadMore(isRefreshing = false) { 97 + if (!this.hasMore) { 98 + return 99 + } 93 100 this._xLoading(isRefreshing) 94 101 try { 95 - const res = await this.rootStore.api.app.bsky.graph.getFollowers( 96 - this.params, 97 - ) 98 - this._replaceAll(res) 102 + const params = Object.assign({}, this.params, { 103 + limit: PAGE_SIZE, 104 + before: this.loadMoreCursor, 105 + }) 106 + if (this.isRefreshing) { 107 + this.followers = [] 108 + } 109 + const res = await this.rootStore.api.app.bsky.graph.getFollowers(params) 110 + await this._appendAll(res) 99 111 this._xIdle() 100 112 } catch (e: any) { 101 - this._xIdle(`Failed to load feed: ${e.toString()}`) 113 + this._xIdle(e) 102 114 } 103 115 } 104 116 105 - private _replaceAll(res: GetFollowers.Response) { 106 - this.subject.did = res.data.subject.did 107 - this.subject.handle = res.data.subject.handle 108 - this.subject.displayName = res.data.subject.displayName 109 - this.subject.avatar = res.data.subject.avatar 110 - this.followers.length = 0 111 - let counter = 0 112 - for (const item of res.data.followers) { 113 - this._append({_reactKey: `item-${counter++}`, ...item}) 114 - } 115 - } 116 - 117 - private _append(item: FollowerItem) { 118 - this.followers.push(item) 117 + private async _appendAll(res: GetFollowers.Response) { 118 + this.loadMoreCursor = res.data.cursor 119 + this.hasMore = !!this.loadMoreCursor 120 + this.followers = this.followers.concat(res.data.followers) 119 121 } 120 122 }
+39 -27
src/view/com/profile/ProfileFollowers.tsx
··· 10 10 import {ErrorMessage} from '../util/error/ErrorMessage' 11 11 import {UserAvatar} from '../util/UserAvatar' 12 12 import {useStores} from '../../../state' 13 - import {s, colors} from '../../lib/styles' 13 + import {s} from '../../lib/styles' 14 14 import {usePalette} from '../../lib/hooks/usePalette' 15 15 16 16 export const ProfileFollowers = observer(function ProfileFollowers({ ··· 19 19 name: string 20 20 }) { 21 21 const store = useStores() 22 - const [view, setView] = React.useState<UserFollowersViewModel | undefined>() 22 + const view = React.useMemo( 23 + () => new UserFollowersViewModel(store, {user: name}), 24 + [store, name], 25 + ) 23 26 24 27 useEffect(() => { 25 - if (view?.params.user === name) { 26 - return // no change needed? or trigger refresh? 27 - } 28 - const newView = new UserFollowersViewModel(store, {user: name}) 29 - setView(newView) 30 - newView 31 - .setup() 28 + view 29 + .loadMore() 32 30 .catch(err => store.log.error('Failed to fetch user followers', err)) 33 - }, [name, view?.params.user, store]) 31 + }, [view, store.log]) 34 32 35 33 const onRefresh = () => { 36 - view?.refresh() 34 + view.refresh() 35 + } 36 + const onEndReached = () => { 37 + view 38 + .loadMore() 39 + .catch(err => 40 + view?.rootStore.log.error('Failed to load more followers', err), 41 + ) 37 42 } 38 43 39 - // loading 40 - // = 41 - if ( 42 - !view || 43 - (view.isLoading && !view.isRefreshing) || 44 - view.params.user !== name 45 - ) { 44 + if (!view.hasLoaded) { 46 45 return ( 47 46 <View> 48 47 <ActivityIndicator /> ··· 66 65 67 66 // loaded 68 67 // = 69 - const renderItem = ({item}: {item: FollowerItem}) => <User item={item} /> 68 + const renderItem = ({item}: {item: FollowerItem}) => ( 69 + <User key={item.did} item={item} /> 70 + ) 70 71 return ( 71 - <View> 72 - <FlatList 73 - data={view.followers} 74 - keyExtractor={item => item._reactKey} 75 - renderItem={renderItem} 76 - contentContainerStyle={{paddingBottom: 200}} 77 - /> 78 - </View> 72 + <FlatList 73 + data={view.followers} 74 + keyExtractor={item => item.did} 75 + refreshing={view.isRefreshing} 76 + onRefresh={onRefresh} 77 + onEndReached={onEndReached} 78 + renderItem={renderItem} 79 + initialNumToRender={15} 80 + ListFooterComponent={() => ( 81 + <View style={styles.footer}> 82 + {view.isLoading && <ActivityIndicator />} 83 + </View> 84 + )} 85 + extraData={view.isLoading} 86 + /> 79 87 ) 80 88 }) 81 89 ··· 127 135 paddingRight: 10, 128 136 paddingTop: 10, 129 137 paddingBottom: 10, 138 + }, 139 + footer: { 140 + height: 200, 141 + paddingTop: 20, 130 142 }, 131 143 })
+3 -4
src/view/shell/mobile/index.tsx
··· 396 396 /> 397 397 <Animated.View 398 398 style={[ 399 - s.flex1, 399 + {height: '100%'}, 400 400 screenBg, 401 401 current 402 402 ? [ ··· 543 543 const styles = StyleSheet.create({ 544 544 outerContainer: { 545 545 height: '100%', 546 - flex: 1, 547 546 }, 548 547 innerContainer: { 549 - flex: 1, 548 + height: '100%', 550 549 }, 551 550 screenContainer: { 552 - flex: 1, 551 + height: '100%', 553 552 }, 554 553 screenMask: { 555 554 position: 'absolute',