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 follows listing

+73 -65
+33 -31
src/state/models/user-follows-view.ts
··· 1 1 import {makeAutoObservable} from 'mobx' 2 2 import { 3 - AppBskyGraphGetFollows as GetFollows, 3 + AppBskyGraphGetFollowers as GetFollows, 4 4 AppBskyActorRef as ActorRef, 5 5 } from '@atproto/api' 6 6 import {RootStoreModel} from './root-store' 7 7 8 - export type FollowItem = GetFollows.Follow & { 9 - _reactKey: string 10 - } 8 + const PAGE_SIZE = 30 9 + 10 + export type FollowItem = GetFollows.Follow 11 11 12 12 export class UserFollowsViewModel { 13 13 // state ··· 16 16 hasLoaded = false 17 17 error = '' 18 18 params: GetFollows.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.getFollows( 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.follows = [] 108 + } 109 + const res = await this.rootStore.api.app.bsky.graph.getFollows(params) 110 + await this._appendAll(res) 99 111 this._xIdle() 100 112 } catch (e: any) { 101 - this._xIdle(`Failed to load feed: ${e.toString()}`) 102 - } 103 - } 104 - 105 - private _replaceAll(res: GetFollows.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.follows.length = 0 111 - let counter = 0 112 - for (const item of res.data.follows) { 113 - this._append({_reactKey: `item-${counter++}`, ...item}) 113 + this._xIdle(e) 114 114 } 115 115 } 116 116 117 - private _append(item: FollowItem) { 118 - this.follows.push(item) 117 + private async _appendAll(res: GetFollows.Response) { 118 + this.loadMoreCursor = res.data.cursor 119 + this.hasMore = !!this.loadMoreCursor 120 + this.follows = this.follows.concat(res.data.follows) 119 121 } 120 122 }
+40 -34
src/view/com/profile/ProfileFollows.tsx
··· 5 5 UserFollowsViewModel, 6 6 FollowItem, 7 7 } from '../../../state/models/user-follows-view' 8 - import {useStores} from '../../../state' 9 8 import {Link} from '../util/Link' 10 9 import {Text} from '../util/text/Text' 11 10 import {ErrorMessage} from '../util/error/ErrorMessage' 12 11 import {UserAvatar} from '../util/UserAvatar' 12 + import {useStores} from '../../../state' 13 13 import {s} from '../../lib/styles' 14 14 import {usePalette} from '../../lib/hooks/usePalette' 15 15 ··· 19 19 name: string 20 20 }) { 21 21 const store = useStores() 22 - const [view, setView] = React.useState<UserFollowsViewModel | undefined>() 22 + const view = React.useMemo( 23 + () => new UserFollowsViewModel(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 UserFollowsViewModel(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 follows', 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 follows', 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: FollowItem}) => <User item={item} /> 68 + const renderItem = ({item}: {item: FollowItem}) => ( 69 + <User key={item.did} item={item} /> 70 + ) 70 71 return ( 71 - <View> 72 - <FlatList 73 - data={view.follows} 74 - keyExtractor={item => item._reactKey} 75 - renderItem={renderItem} 76 - contentContainerStyle={{paddingBottom: 200}} 77 - /> 78 - </View> 72 + <FlatList 73 + data={view.follows} 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 ··· 100 108 <Text style={[s.bold, pal.text]}> 101 109 {item.displayName || item.handle} 102 110 </Text> 103 - <Text type="sm" style={pal.textLight}> 111 + <Text type="sm" style={[pal.textLight]}> 104 112 @{item.handle} 105 113 </Text> 106 114 </View> ··· 122 130 paddingTop: 10, 123 131 paddingBottom: 10, 124 132 }, 125 - avi: { 126 - width: 40, 127 - height: 40, 128 - borderRadius: 20, 129 - resizeMode: 'cover', 130 - }, 131 133 layoutContent: { 132 134 flex: 1, 133 135 paddingRight: 10, 134 136 paddingTop: 10, 135 137 paddingBottom: 10, 138 + }, 139 + footer: { 140 + height: 200, 141 + paddingTop: 20, 136 142 }, 137 143 })