diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/user-followers-view.ts | 62 | ||||
-rw-r--r-- | src/view/com/profile/ProfileFollowers.tsx | 66 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 7 |
3 files changed, 74 insertions, 61 deletions
diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts index 6cb8dfeb6..9daaf35a4 100644 --- a/src/state/models/user-followers-view.ts +++ b/src/state/models/user-followers-view.ts @@ -5,9 +5,9 @@ import { } from '@atproto/api' import {RootStoreModel} from './root-store' -export type FollowerItem = GetFollowers.Follower & { - _reactKey: string -} +const PAGE_SIZE = 30 + +export type FollowerItem = GetFollowers.Follower export class UserFollowersViewModel { // state @@ -16,6 +16,9 @@ export class UserFollowersViewModel { hasLoaded = false error = '' params: GetFollowers.QueryParams + hasMore = true + loadMoreCursor?: string + private _loadMorePromise: Promise<void> | undefined // data subject: ActorRef.WithInfo = { @@ -55,16 +58,17 @@ export class UserFollowersViewModel { // public api // = - async setup() { - await this._fetch() - } - async refresh() { - await this._fetch(true) + return this.loadMore(true) } - async loadMore() { - // TODO + async loadMore(isRefreshing = false) { + if (this._loadMorePromise) { + return this._loadMorePromise + } + this._loadMorePromise = this._loadMore(isRefreshing) + await this._loadMorePromise + this._loadMorePromise = undefined } // state transitions @@ -89,32 +93,30 @@ export class UserFollowersViewModel { // loader functions // = - private async _fetch(isRefreshing = false) { + private async _loadMore(isRefreshing = false) { + if (!this.hasMore) { + return + } this._xLoading(isRefreshing) try { - const res = await this.rootStore.api.app.bsky.graph.getFollowers( - this.params, - ) - this._replaceAll(res) + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + before: this.loadMoreCursor, + }) + if (this.isRefreshing) { + this.followers = [] + } + const res = await this.rootStore.api.app.bsky.graph.getFollowers(params) + await this._appendAll(res) this._xIdle() } catch (e: any) { - this._xIdle(`Failed to load feed: ${e.toString()}`) - } - } - - private _replaceAll(res: GetFollowers.Response) { - this.subject.did = res.data.subject.did - this.subject.handle = res.data.subject.handle - this.subject.displayName = res.data.subject.displayName - this.subject.avatar = res.data.subject.avatar - this.followers.length = 0 - let counter = 0 - for (const item of res.data.followers) { - this._append({_reactKey: `item-${counter++}`, ...item}) + this._xIdle(e) } } - private _append(item: FollowerItem) { - this.followers.push(item) + private async _appendAll(res: GetFollowers.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.followers = this.followers.concat(res.data.followers) } } diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 56a4646ec..b1dfbe996 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -10,7 +10,7 @@ import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' +import {s} from '../../lib/styles' import {usePalette} from '../../lib/hooks/usePalette' export const ProfileFollowers = observer(function ProfileFollowers({ @@ -19,30 +19,29 @@ export const ProfileFollowers = observer(function ProfileFollowers({ name: string }) { const store = useStores() - const [view, setView] = React.useState<UserFollowersViewModel | undefined>() + const view = React.useMemo( + () => new UserFollowersViewModel(store, {user: name}), + [store, name], + ) useEffect(() => { - if (view?.params.user === name) { - return // no change needed? or trigger refresh? - } - const newView = new UserFollowersViewModel(store, {user: name}) - setView(newView) - newView - .setup() + view + .loadMore() .catch(err => store.log.error('Failed to fetch user followers', err)) - }, [name, view?.params.user, store]) + }, [view, store.log]) const onRefresh = () => { - view?.refresh() + view.refresh() + } + const onEndReached = () => { + view + .loadMore() + .catch(err => + view?.rootStore.log.error('Failed to load more followers', err), + ) } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.user !== name - ) { + if (!view.hasLoaded) { return ( <View> <ActivityIndicator /> @@ -66,16 +65,25 @@ export const ProfileFollowers = observer(function ProfileFollowers({ // loaded // = - const renderItem = ({item}: {item: FollowerItem}) => <User item={item} /> + const renderItem = ({item}: {item: FollowerItem}) => ( + <User key={item.did} item={item} /> + ) return ( - <View> - <FlatList - data={view.followers} - keyExtractor={item => item._reactKey} - renderItem={renderItem} - contentContainerStyle={{paddingBottom: 200}} - /> - </View> + <FlatList + data={view.followers} + keyExtractor={item => item.did} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + ListFooterComponent={() => ( + <View style={styles.footer}> + {view.isLoading && <ActivityIndicator />} + </View> + )} + extraData={view.isLoading} + /> ) }) @@ -128,4 +136,8 @@ const styles = StyleSheet.create({ paddingTop: 10, paddingBottom: 10, }, + footer: { + height: 200, + paddingTop: 20, + }, }) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index b1d00adc1..c4ca7b9f5 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -396,7 +396,7 @@ export const MobileShell: React.FC = observer(() => { /> <Animated.View style={[ - s.flex1, + {height: '100%'}, screenBg, current ? [ @@ -543,13 +543,12 @@ function constructScreenRenderDesc(nav: NavigationModel): { const styles = StyleSheet.create({ outerContainer: { height: '100%', - flex: 1, }, innerContainer: { - flex: 1, + height: '100%', }, screenContainer: { - flex: 1, + height: '100%', }, screenMask: { position: 'absolute', |