diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts index 341f0f80..40f12b43 100644 --- a/src/state/models/user-follows-view.ts +++ b/src/state/models/user-follows-view.ts @@ -1,13 +1,13 @@ import {makeAutoObservable} from 'mobx' import { - AppBskyGraphGetFollows as GetFollows, + AppBskyGraphGetFollowers as GetFollows, AppBskyActorRef as ActorRef, } from '@atproto/api' import {RootStoreModel} from './root-store' -export type FollowItem = GetFollows.Follow & { - _reactKey: string -} +const PAGE_SIZE = 30 + +export type FollowItem = GetFollows.Follow export class UserFollowsViewModel { // state @@ -16,6 +16,9 @@ export class UserFollowsViewModel { hasLoaded = false error = '' params: GetFollows.QueryParams + hasMore = true + loadMoreCursor?: string + private _loadMorePromise: Promise | undefined // data subject: ActorRef.WithInfo = { @@ -55,16 +58,17 @@ export class UserFollowsViewModel { // 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 UserFollowsViewModel { // 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.getFollows( - this.params, - ) - this._replaceAll(res) + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + before: this.loadMoreCursor, + }) + if (this.isRefreshing) { + this.follows = [] + } + const res = await this.rootStore.api.app.bsky.graph.getFollows(params) + await this._appendAll(res) this._xIdle() } catch (e: any) { - this._xIdle(`Failed to load feed: ${e.toString()}`) + this._xIdle(e) } } - private _replaceAll(res: GetFollows.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.follows.length = 0 - let counter = 0 - for (const item of res.data.follows) { - this._append({_reactKey: `item-${counter++}`, ...item}) - } - } - - private _append(item: FollowItem) { - this.follows.push(item) + private async _appendAll(res: GetFollows.Response) { + this.loadMoreCursor = res.data.cursor + this.hasMore = !!this.loadMoreCursor + this.follows = this.follows.concat(res.data.follows) } } diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 4fbabff6..fca12d11 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -5,11 +5,11 @@ import { UserFollowsViewModel, FollowItem, } from '../../../state/models/user-follows-view' -import {useStores} from '../../../state' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' +import {useStores} from '../../../state' import {s} from '../../lib/styles' import {usePalette} from '../../lib/hooks/usePalette' @@ -19,30 +19,29 @@ export const ProfileFollows = observer(function ProfileFollows({ name: string }) { const store = useStores() - const [view, setView] = React.useState() + const view = React.useMemo( + () => new UserFollowsViewModel(store, {user: name}), + [store, name], + ) useEffect(() => { - if (view?.params.user === name) { - return // no change needed? or trigger refresh? - } - const newView = new UserFollowsViewModel(store, {user: name}) - setView(newView) - newView - .setup() + view + .loadMore() .catch(err => store.log.error('Failed to fetch user follows', 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 follows', err), + ) } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.user !== name - ) { + if (!view.hasLoaded) { return ( @@ -66,16 +65,25 @@ export const ProfileFollows = observer(function ProfileFollows({ // loaded // = - const renderItem = ({item}: {item: FollowItem}) => + const renderItem = ({item}: {item: FollowItem}) => ( + + ) return ( - - item._reactKey} - renderItem={renderItem} - contentContainerStyle={{paddingBottom: 200}} - /> - + item.did} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + ListFooterComponent={() => ( + + {view.isLoading && } + + )} + extraData={view.isLoading} + /> ) }) @@ -100,7 +108,7 @@ const User = ({item}: {item: FollowItem}) => { {item.displayName || item.handle} - + @{item.handle} @@ -122,16 +130,14 @@ const styles = StyleSheet.create({ paddingTop: 10, paddingBottom: 10, }, - avi: { - width: 40, - height: 40, - borderRadius: 20, - resizeMode: 'cover', - }, layoutContent: { flex: 1, paddingRight: 10, paddingTop: 10, paddingBottom: 10, }, + footer: { + height: 200, + paddingTop: 20, + }, })