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 reposted-by list

+73 -86
+33 -55
src/state/models/reposted-by-view.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 2 import {AtUri} from '../../third-party/uri' 3 - import { 4 - AppBskyFeedGetRepostedBy as GetRepostedBy, 5 - AppBskySystemDeclRef, 6 - } from '@atproto/api' 7 - type DeclRef = AppBskySystemDeclRef.Main 3 + import {AppBskyFeedGetRepostedBy as GetRepostedBy} from '@atproto/api' 8 4 import {RootStoreModel} from './root-store' 9 5 10 - export class RepostedByViewItemModel implements GetRepostedBy.RepostedBy { 11 - // ui state 12 - _reactKey: string = '' 13 - 14 - // data 15 - did: string = '' 16 - handle: string = '' 17 - displayName: string = '' 18 - avatar?: string 19 - declaration: DeclRef = {cid: '', actorType: ''} 20 - createdAt?: string 21 - indexedAt: string = '' 6 + const PAGE_SIZE = 30 22 7 23 - constructor(reactKey: string, v: RepostedByItem) { 24 - makeAutoObservable(this) 25 - this._reactKey = reactKey 26 - Object.assign(this, v) 27 - } 28 - } 8 + export type RepostedByItem = GetRepostedBy.RepostedBy 29 9 30 10 export class RepostedByViewModel { 31 11 // state ··· 35 15 error = '' 36 16 resolvedUri = '' 37 17 params: GetRepostedBy.QueryParams 18 + hasMore = true 19 + loadMoreCursor?: string 20 + private _loadMorePromise: Promise<void> | undefined 38 21 39 22 // data 40 23 uri: string = '' 41 - repostedBy: RepostedByViewItemModel[] = [] 24 + repostedBy: RepostedByItem[] = [] 42 25 43 26 constructor( 44 27 public rootStore: RootStoreModel, ··· 70 53 // public api 71 54 // = 72 55 73 - async setup() { 74 - if (!this.resolvedUri) { 75 - await this._resolveUri() 76 - } 77 - await this._fetch() 78 - } 79 - 80 56 async refresh() { 81 - await this._fetch(true) 57 + return this.loadMore(true) 82 58 } 83 59 84 - async loadMore() { 85 - // TODO 60 + async loadMore(isRefreshing = false) { 61 + if (this._loadMorePromise) { 62 + return this._loadMorePromise 63 + } 64 + if (!this.resolvedUri) { 65 + await this._resolveUri() 66 + } 67 + this._loadMorePromise = this._loadMore(isRefreshing) 68 + await this._loadMorePromise 69 + this._loadMorePromise = undefined 86 70 } 87 71 88 72 // state transitions ··· 121 105 }) 122 106 } 123 107 124 - private async _fetch(isRefreshing = false) { 108 + private async _loadMore(isRefreshing = false) { 125 109 this._xLoading(isRefreshing) 126 110 try { 127 - const res = await this.rootStore.api.app.bsky.feed.getRepostedBy( 128 - Object.assign({}, this.params, {uri: this.resolvedUri}), 129 - ) 130 - this._replaceAll(res) 111 + const params = Object.assign({}, this.params, { 112 + uri: this.resolvedUri, 113 + limit: PAGE_SIZE, 114 + before: this.loadMoreCursor, 115 + }) 116 + if (this.isRefreshing) { 117 + this.repostedBy = [] 118 + } 119 + const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params) 120 + await this._appendAll(res) 131 121 this._xIdle() 132 122 } catch (e: any) { 133 123 this._xIdle(e) 134 124 } 135 125 } 136 126 137 - private async _refresh() { 138 - this._xLoading(true) 139 - // TODO: refetch and update items 140 - this._xIdle() 141 - } 142 - 143 - private _replaceAll(res: GetRepostedBy.Response) { 144 - this.repostedBy.length = 0 145 - let counter = 0 146 - for (const item of res.data.repostedBy) { 147 - this._append(counter++, item) 148 - } 149 - } 150 - 151 - private _append(keyId: number, item: RepostedByItem) { 152 - this.repostedBy.push(new RepostedByViewItemModel(`item-${keyId}`, item)) 127 + private _appendAll(res: GetRepostedBy.Response) { 128 + this.loadMoreCursor = res.data.cursor 129 + this.hasMore = !!this.loadMoreCursor 130 + this.repostedBy = this.repostedBy.concat(res.data.repostedBy) 153 131 } 154 132 }
+40 -31
src/view/com/post-thread/PostRepostedBy.tsx
··· 3 3 import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' 4 4 import { 5 5 RepostedByViewModel, 6 - RepostedByViewItemModel, 6 + RepostedByItem, 7 7 } from '../../../state/models/reposted-by-view' 8 8 import {UserAvatar} from '../util/UserAvatar' 9 9 import {ErrorMessage} from '../util/error/ErrorMessage' ··· 18 18 uri: string 19 19 }) { 20 20 const store = useStores() 21 - // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment 22 - const [view, setView] = React.useState<RepostedByViewModel | undefined>() 21 + const view = React.useMemo( 22 + () => new RepostedByViewModel(store, {uri}), 23 + [store, uri], 24 + ) 23 25 24 26 useEffect(() => { 25 - if (view?.params.uri === uri) { 26 - return // no change needed? or trigger refresh? 27 - } 28 - const newView = new RepostedByViewModel(store, {uri}) 29 - setView(newView) 30 - newView 31 - .setup() 32 - .catch(err => store.log.error('Failed to fetch reposted by', err)) 33 - }, [uri, view?.params.uri, store]) 27 + view 28 + .loadMore() 29 + .catch(err => store.log.error('Failed to fetch user followers', err)) 30 + }, [view, store.log]) 34 31 35 32 const onRefresh = () => { 36 - view?.refresh() 33 + view.refresh() 34 + } 35 + const onEndReached = () => { 36 + view 37 + .loadMore() 38 + .catch(err => 39 + view?.rootStore.log.error('Failed to load more followers', err), 40 + ) 37 41 } 38 42 39 - // loading 40 - // = 41 - if ( 42 - !view || 43 - (view.isLoading && !view.isRefreshing) || 44 - view.params.uri !== uri 45 - ) { 43 + if (!view.hasLoaded) { 46 44 return ( 47 45 <View> 48 46 <ActivityIndicator /> ··· 66 64 67 65 // loaded 68 66 // = 69 - const renderItem = ({item}: {item: RepostedByViewItemModel}) => ( 70 - <RepostedByItem item={item} /> 67 + const renderItem = ({item}: {item: RepostedByItem}) => ( 68 + <RepostedByItemCom item={item} /> 71 69 ) 72 70 return ( 73 - <View> 74 - <FlatList 75 - data={view.repostedBy} 76 - keyExtractor={item => item._reactKey} 77 - renderItem={renderItem} 78 - contentContainerStyle={{paddingBottom: 200}} 79 - /> 80 - </View> 71 + <FlatList 72 + data={view.repostedBy} 73 + keyExtractor={item => item.did} 74 + refreshing={view.isRefreshing} 75 + onRefresh={onRefresh} 76 + onEndReached={onEndReached} 77 + renderItem={renderItem} 78 + initialNumToRender={15} 79 + ListFooterComponent={() => ( 80 + <View style={styles.footer}> 81 + {view.isLoading && <ActivityIndicator />} 82 + </View> 83 + )} 84 + extraData={view.isLoading} 85 + /> 81 86 ) 82 87 }) 83 88 84 - const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => { 89 + const RepostedByItemCom = ({item}: {item: RepostedByItem}) => { 85 90 return ( 86 91 <Link 87 92 style={styles.outer} ··· 131 136 paddingRight: 10, 132 137 paddingTop: 10, 133 138 paddingBottom: 10, 139 + }, 140 + footer: { 141 + height: 200, 142 + paddingTop: 20, 134 143 }, 135 144 })