Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Performance fixes with new getPosts (#525)

* Update notifications to fetch in a batch using getPosts

* Improve search perf with getPosts

* Bump @atproto/api@0.2.9

* Just use post uri for key

authored by

Paul Frazee and committed by
GitHub
1b356556 da8af38d

+97 -81
+1 -1
package.json
··· 22 22 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" 23 23 }, 24 24 "dependencies": { 25 - "@atproto/api": "0.2.8", 25 + "@atproto/api": "0.2.9", 26 26 "@bam.tech/react-native-image-resizer": "^3.0.4", 27 27 "@braintree/sanitize-url": "^6.0.2", 28 28 "@expo/webpack-config": "^18.0.1",
+21 -37
src/state/models/content/post-thread.ts
··· 11 11 import {cleanError} from 'lib/strings/errors' 12 12 import {updateDataOptimistically} from 'lib/async/revertible' 13 13 14 - function* reactKeyGenerator(): Generator<string> { 15 - let counter = 0 16 - while (true) { 17 - yield `item-${counter++}` 18 - } 19 - } 20 - 21 14 export class PostThreadItemModel { 22 15 // ui state 23 16 _reactKey: string = '' ··· 55 48 56 49 constructor( 57 50 public rootStore: RootStoreModel, 58 - reactKey: string, 59 51 v: AppBskyFeedDefs.ThreadViewPost, 60 52 ) { 61 - this._reactKey = reactKey 53 + this._reactKey = `thread-${v.post.uri}` 62 54 this.post = v.post 63 55 if (FeedPost.isRecord(this.post.record)) { 64 56 const valid = FeedPost.validateRecord(this.post.record) ··· 82 74 } 83 75 84 76 assignTreeModels( 85 - keyGen: Generator<string>, 86 77 v: AppBskyFeedDefs.ThreadViewPost, 87 78 higlightedPostUri: string, 88 79 includeParent = true, ··· 91 82 // parents 92 83 if (includeParent && v.parent) { 93 84 if (AppBskyFeedDefs.isThreadViewPost(v.parent)) { 94 - const parentModel = new PostThreadItemModel( 95 - this.rootStore, 96 - keyGen.next().value, 97 - v.parent, 98 - ) 85 + const parentModel = new PostThreadItemModel(this.rootStore, v.parent) 99 86 parentModel._depth = this._depth - 1 100 87 parentModel._showChildReplyLine = true 101 88 if (v.parent.parent) { 102 89 parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri 103 - parentModel.assignTreeModels( 104 - keyGen, 105 - v.parent, 106 - higlightedPostUri, 107 - true, 108 - false, 109 - ) 90 + parentModel.assignTreeModels(v.parent, higlightedPostUri, true, false) 110 91 } 111 92 this.parent = parentModel 112 93 } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) { ··· 118 99 const replies = [] 119 100 for (const item of v.replies) { 120 101 if (AppBskyFeedDefs.isThreadViewPost(item)) { 121 - const itemModel = new PostThreadItemModel( 122 - this.rootStore, 123 - keyGen.next().value, 124 - item, 125 - ) 102 + const itemModel = new PostThreadItemModel(this.rootStore, item) 126 103 itemModel._depth = this._depth + 1 127 104 itemModel._showParentReplyLine = 128 105 itemModel.parentUri !== higlightedPostUri 129 106 if (item.replies?.length) { 130 107 itemModel._showChildReplyLine = true 131 - itemModel.assignTreeModels( 132 - keyGen, 133 - item, 134 - higlightedPostUri, 135 - false, 136 - true, 137 - ) 108 + itemModel.assignTreeModels(item, higlightedPostUri, false, true) 138 109 } 139 110 replies.push(itemModel) 140 111 } else if (AppBskyFeedDefs.isNotFoundPost(item)) { ··· 241 212 this.params = params 242 213 } 243 214 215 + static fromPostView( 216 + rootStore: RootStoreModel, 217 + postView: AppBskyFeedDefs.PostView, 218 + ) { 219 + const model = new PostThreadModel(rootStore, {uri: postView.uri}) 220 + model.resolvedUri = postView.uri 221 + model.hasLoaded = true 222 + model.thread = new PostThreadItemModel(rootStore, { 223 + post: postView, 224 + }) 225 + return model 226 + } 227 + 244 228 get hasContent() { 245 229 return typeof this.thread !== 'undefined' 246 230 } ··· 360 344 } 361 345 362 346 async _load(isRefreshing = false) { 347 + if (this.hasLoaded && !isRefreshing) { 348 + return 349 + } 363 350 this._xLoading(isRefreshing) 364 351 try { 365 352 const res = await this.rootStore.agent.getPostThread( ··· 374 361 375 362 _replaceAll(res: GetPostThread.Response) { 376 363 sortThread(res.data.thread) 377 - const keyGen = reactKeyGenerator() 378 364 const thread = new PostThreadItemModel( 379 365 this.rootStore, 380 - keyGen.next().value, 381 366 res.data.thread as AppBskyFeedDefs.ThreadViewPost, 382 367 ) 383 368 thread._isHighlightedPost = true 384 369 thread.assignTreeModels( 385 - keyGen, 386 370 res.data.thread as AppBskyFeedDefs.ThreadViewPost, 387 371 thread.uri, 388 372 )
+44 -32
src/state/models/feeds/notifications.ts
··· 2 2 import { 3 3 AppBskyNotificationListNotifications as ListNotifications, 4 4 AppBskyActorDefs, 5 + AppBskyFeedDefs, 5 6 AppBskyFeedPost, 6 7 AppBskyFeedRepost, 7 8 AppBskyFeedLike, ··· 146 147 return false 147 148 } 148 149 150 + get additionaDataUri(): string | undefined { 151 + if (this.isReply || this.isQuote || this.isMention) { 152 + return this.uri 153 + } else if (this.isLike || this.isRepost) { 154 + return this.subjectUri 155 + } 156 + } 157 + 149 158 get subjectUri(): string { 150 159 if (this.reasonSubject) { 151 160 return this.reasonSubject ··· 193 202 ) 194 203 } 195 204 196 - async fetchAdditionalData() { 197 - if (!this.needsAdditionalData) { 198 - return 199 - } 200 - let postUri 201 - if (this.isReply || this.isQuote || this.isMention) { 202 - postUri = this.uri 203 - } else if (this.isLike || this.isRepost) { 204 - postUri = this.subjectUri 205 - } 206 - if (postUri) { 207 - this.additionalPost = new PostThreadModel(this.rootStore, { 208 - uri: postUri, 209 - depth: 0, 210 - }) 211 - await this.additionalPost.setup().catch(e => { 212 - this.rootStore.log.error( 213 - 'Failed to load post needed by notification', 214 - e, 215 - ) 216 - }) 217 - } 205 + setAdditionalData(additionalPost: AppBskyFeedDefs.PostView) { 206 + this.additionalPost = PostThreadModel.fromPostView( 207 + this.rootStore, 208 + additionalPost, 209 + ) 218 210 } 219 211 } 220 212 ··· 464 456 'mostRecent', 465 457 res.data.notifications[0], 466 458 ) 467 - await notif.fetchAdditionalData() 459 + const addedUri = notif.additionaDataUri 460 + if (addedUri) { 461 + const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({ 462 + uris: [addedUri], 463 + }) 464 + notif.setAdditionalData(postsRes.data.posts[0]) 465 + } 468 466 const filtered = this._filterNotifications([notif]) 469 467 return filtered[0] 470 468 } ··· 536 534 async _fetchItemModels( 537 535 items: ListNotifications.Notification[], 538 536 ): Promise<NotificationsFeedItemModel[]> { 539 - const promises = [] 537 + // construct item models and track who needs more data 540 538 const itemModels: NotificationsFeedItemModel[] = [] 539 + const addedPostMap = new Map<string, NotificationsFeedItemModel[]>() 541 540 for (const item of items) { 542 541 const itemModel = new NotificationsFeedItemModel( 543 542 this.rootStore, 544 543 `item-${_idCounter++}`, 545 544 item, 546 545 ) 547 - if (itemModel.needsAdditionalData) { 548 - promises.push(itemModel.fetchAdditionalData()) 546 + const uri = itemModel.additionaDataUri 547 + if (uri) { 548 + const models = addedPostMap.get(uri) || [] 549 + models.push(itemModel) 550 + addedPostMap.set(uri, models) 549 551 } 550 552 itemModels.push(itemModel) 551 553 } 552 - await Promise.all(promises).catch(e => { 553 - this.rootStore.log.error( 554 - 'Uncaught failure during notifications _processNotifications()', 555 - e, 556 - ) 557 - }) 554 + 555 + // fetch additional data 556 + if (addedPostMap.size > 0) { 557 + const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({ 558 + uris: Array.from(addedPostMap.keys()), 559 + }) 560 + for (const post of postsRes.data.posts) { 561 + const models = addedPostMap.get(post.uri) 562 + if (models?.length) { 563 + for (const model of models) { 564 + model.setAdditionalData(post) 565 + } 566 + } 567 + } 568 + } 569 + 558 570 return itemModels 559 571 } 560 572
+19 -4
src/state/models/ui/search.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 2 import {searchProfiles, searchPosts} from 'lib/api/search' 3 - import {AppBskyActorDefs} from '@atproto/api' 3 + import {PostThreadModel} from '../content/post-thread' 4 + import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' 4 5 import {RootStoreModel} from '../root-store' 5 6 6 7 export class SearchUIModel { 7 8 isPostsLoading = false 8 9 isProfilesLoading = false 9 10 query: string = '' 10 - postUris: string[] = [] 11 + posts: PostThreadModel[] = [] 11 12 profiles: AppBskyActorDefs.ProfileView[] = [] 12 13 13 14 constructor(public rootStore: RootStoreModel) { ··· 15 16 } 16 17 17 18 async fetch(q: string) { 18 - this.postUris = [] 19 + this.posts = [] 19 20 this.profiles = [] 20 21 this.query = q 21 22 if (!q.trim()) { ··· 29 30 searchPosts(q).catch(_e => []), 30 31 searchProfiles(q).catch(_e => []), 31 32 ]) 33 + 34 + let posts: AppBskyFeedDefs.PostView[] = [] 35 + if (postsSearch?.length) { 36 + do { 37 + const res = await this.rootStore.agent.app.bsky.feed.getPosts({ 38 + uris: postsSearch 39 + .splice(0, 25) 40 + .map(p => `at://${p.user.did}/${p.tid}`), 41 + }) 42 + posts = posts.concat(res.data.posts) 43 + } while (postsSearch.length) 44 + } 32 45 runInAction(() => { 33 - this.postUris = postsSearch?.map(p => `at://${p.user.did}/${p.tid}`) || [] 46 + this.posts = posts.map(post => 47 + PostThreadModel.fromPostView(this.rootStore, post), 48 + ) 34 49 this.isPostsLoading = false 35 50 }) 36 51
+8 -3
src/view/com/search/SearchResults.tsx
··· 49 49 ) 50 50 } 51 51 52 - if (model.postUris.length === 0) { 52 + if (model.posts.length === 0) { 53 53 return ( 54 54 <CenteredView> 55 55 <Text type="xl" style={[styles.empty, pal.text]}> ··· 61 61 62 62 return ( 63 63 <ScrollView style={pal.view}> 64 - {model.postUris.map(uri => ( 65 - <Post key={uri} uri={uri} hideError /> 64 + {model.posts.map(post => ( 65 + <Post 66 + key={post.resolvedUri} 67 + uri={post.resolvedUri} 68 + initView={post} 69 + hideError 70 + /> 66 71 ))} 67 72 <View style={s.footerSpacer} /> 68 73 <View style={s.footerSpacer} />
+4 -4
yarn.lock
··· 30 30 tlds "^1.234.0" 31 31 typed-emitter "^2.1.0" 32 32 33 - "@atproto/api@0.2.8": 34 - version "0.2.8" 35 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.8.tgz#92ed413804ecb43aaa45ec18afc93d6f2b28a689" 36 - integrity sha512-LfPgtf3UNg2W/AxHkJMJrLNT9QAD6bi16Sw5Zt3mgANrDnHWGygA7gRpeNdgVI+kFEhQfrIItemJvWLIB9BJDQ== 33 + "@atproto/api@0.2.9": 34 + version "0.2.9" 35 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.9.tgz#08e29da66d1a9001d9d3ce427548c1760d805e99" 36 + integrity sha512-r00IqidX2YF3VUEa4MUO2Vxqp3+QhI1cSNcWgzT4LsANapzrwdDTM+rY2Ejp9na3F+unO4SWRW3o434cVmG5gw== 37 37 dependencies: 38 38 "@atproto/common-web" "*" 39 39 "@atproto/uri" "*"