Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Remove post thread (#1920)

* Delete post-thread model

* Remove post-thread-item

* Remove unused types

authored by

Eric Bailey and committed by
GitHub
d8b26edb 6616b2bf

+3 -485
-131
src/state/models/content/post-thread-item.ts
··· 1 - import {makeAutoObservable} from 'mobx' 2 - import { 3 - AppBskyFeedPost as FeedPost, 4 - AppBskyFeedDefs, 5 - RichText, 6 - PostModeration, 7 - } from '@atproto/api' 8 - import {RootStoreModel} from '../root-store' 9 - import {PostsFeedItemModel} from '../feeds/post' 10 - 11 - type PostView = AppBskyFeedDefs.PostView 12 - 13 - // NOTE: this model uses the same data as PostsFeedItemModel, but is used for 14 - // rendering a single post in a thread view, and has additional state 15 - // for rendering the thread view, but calls the same data methods 16 - // as PostsFeedItemModel 17 - // TODO: refactor as an extension or subclass of PostsFeedItemModel 18 - export class PostThreadItemModel { 19 - // ui state 20 - _reactKey: string = '' 21 - _depth = 0 22 - _isHighlightedPost = false 23 - _showParentReplyLine = false 24 - _showChildReplyLine = false 25 - _hasMore = false 26 - 27 - // data 28 - data: PostsFeedItemModel 29 - post: PostView 30 - postRecord?: FeedPost.Record 31 - richText?: RichText 32 - parent?: 33 - | PostThreadItemModel 34 - | AppBskyFeedDefs.NotFoundPost 35 - | AppBskyFeedDefs.BlockedPost 36 - replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[] 37 - 38 - constructor( 39 - public rootStore: RootStoreModel, 40 - v: AppBskyFeedDefs.ThreadViewPost, 41 - ) { 42 - this._reactKey = `thread-${v.post.uri}` 43 - this.data = new PostsFeedItemModel(rootStore, this._reactKey, v) 44 - this.post = this.data.post 45 - this.postRecord = this.data.postRecord 46 - this.richText = this.data.richText 47 - // replies and parent are handled via assignTreeModels 48 - makeAutoObservable(this, {rootStore: false}) 49 - } 50 - 51 - get uri() { 52 - return this.post.uri 53 - } 54 - 55 - get parentUri() { 56 - return this.postRecord?.reply?.parent.uri 57 - } 58 - 59 - get rootUri(): string { 60 - if (this.postRecord?.reply?.root.uri) { 61 - return this.postRecord.reply.root.uri 62 - } 63 - return this.post.uri 64 - } 65 - 66 - get moderation(): PostModeration { 67 - return this.data.moderation 68 - } 69 - 70 - assignTreeModels( 71 - v: AppBskyFeedDefs.ThreadViewPost, 72 - highlightedPostUri: string, 73 - includeParent = true, 74 - includeChildren = true, 75 - ) { 76 - // parents 77 - if (includeParent && v.parent) { 78 - if (AppBskyFeedDefs.isThreadViewPost(v.parent)) { 79 - const parentModel = new PostThreadItemModel(this.rootStore, v.parent) 80 - parentModel._depth = this._depth - 1 81 - parentModel._showChildReplyLine = true 82 - if (v.parent.parent) { 83 - parentModel._showParentReplyLine = true 84 - parentModel.assignTreeModels( 85 - v.parent, 86 - highlightedPostUri, 87 - true, 88 - false, 89 - ) 90 - } 91 - this.parent = parentModel 92 - } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) { 93 - this.parent = v.parent 94 - } else if (AppBskyFeedDefs.isBlockedPost(v.parent)) { 95 - this.parent = v.parent 96 - } 97 - } 98 - // replies 99 - if (includeChildren && v.replies) { 100 - const replies = [] 101 - for (const item of v.replies) { 102 - if (AppBskyFeedDefs.isThreadViewPost(item)) { 103 - const itemModel = new PostThreadItemModel(this.rootStore, item) 104 - itemModel._depth = this._depth + 1 105 - itemModel._showParentReplyLine = 106 - itemModel.parentUri !== highlightedPostUri 107 - if (item.replies?.length) { 108 - itemModel._showChildReplyLine = true 109 - itemModel.assignTreeModels(item, highlightedPostUri, false, true) 110 - } 111 - replies.push(itemModel) 112 - } else if (AppBskyFeedDefs.isNotFoundPost(item)) { 113 - replies.push(item) 114 - } 115 - } 116 - this.replies = replies 117 - } 118 - } 119 - 120 - async toggleLike() { 121 - this.data.toggleLike() 122 - } 123 - 124 - async toggleRepost() { 125 - this.data.toggleRepost() 126 - } 127 - 128 - async delete() { 129 - this.data.delete() 130 - } 131 - }
-342
src/state/models/content/post-thread.ts
··· 1 - import {makeAutoObservable, runInAction} from 'mobx' 2 - import { 3 - AppBskyFeedGetPostThread as GetPostThread, 4 - AppBskyFeedDefs, 5 - AppBskyFeedPost, 6 - PostModeration, 7 - } from '@atproto/api' 8 - import {AtUri} from '@atproto/api' 9 - import {RootStoreModel} from '../root-store' 10 - import * as apilib from 'lib/api/index' 11 - import {cleanError} from 'lib/strings/errors' 12 - import {ThreadViewPreference} from '../ui/preferences' 13 - import {PostThreadItemModel} from './post-thread-item' 14 - import {logger} from '#/logger' 15 - 16 - export class PostThreadModel { 17 - // state 18 - isLoading = false 19 - isLoadingFromCache = false 20 - isFromCache = false 21 - isRefreshing = false 22 - hasLoaded = false 23 - error = '' 24 - notFound = false 25 - resolvedUri = '' 26 - params: GetPostThread.QueryParams 27 - 28 - // data 29 - thread?: PostThreadItemModel | null = null 30 - isBlocked = false 31 - 32 - constructor( 33 - public rootStore: RootStoreModel, 34 - params: GetPostThread.QueryParams, 35 - ) { 36 - makeAutoObservable( 37 - this, 38 - { 39 - rootStore: false, 40 - params: false, 41 - }, 42 - {autoBind: true}, 43 - ) 44 - this.params = params 45 - } 46 - 47 - static fromPostView( 48 - rootStore: RootStoreModel, 49 - postView: AppBskyFeedDefs.PostView, 50 - ) { 51 - const model = new PostThreadModel(rootStore, {uri: postView.uri}) 52 - model.resolvedUri = postView.uri 53 - model.hasLoaded = true 54 - model.thread = new PostThreadItemModel(rootStore, { 55 - post: postView, 56 - }) 57 - return model 58 - } 59 - 60 - get hasContent() { 61 - return !!this.thread 62 - } 63 - 64 - get hasError() { 65 - return this.error !== '' 66 - } 67 - 68 - get rootUri(): string { 69 - if (this.thread) { 70 - if (this.thread.postRecord?.reply?.root.uri) { 71 - return this.thread.postRecord.reply.root.uri 72 - } 73 - } 74 - return this.resolvedUri 75 - } 76 - 77 - get isCachedPostAReply() { 78 - if (AppBskyFeedPost.isRecord(this.thread?.post.record)) { 79 - return !!this.thread?.post.record.reply 80 - } 81 - return false 82 - } 83 - 84 - // public api 85 - // = 86 - 87 - /** 88 - * Load for first render 89 - */ 90 - async setup() { 91 - if (!this.resolvedUri) { 92 - await this._resolveUri() 93 - } 94 - 95 - if (this.hasContent) { 96 - await this.update() 97 - } else { 98 - const precache = this.rootStore.posts.cache.get(this.resolvedUri) 99 - if (precache) { 100 - await this._loadPrecached(precache) 101 - } else { 102 - await this._load() 103 - } 104 - } 105 - } 106 - 107 - /** 108 - * Register any event listeners. Returns a cleanup function. 109 - */ 110 - registerListeners() { 111 - const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this)) 112 - return () => sub.remove() 113 - } 114 - 115 - /** 116 - * Reset and load 117 - */ 118 - async refresh() { 119 - await this._load(true) 120 - } 121 - 122 - /** 123 - * Update content in-place 124 - */ 125 - async update() { 126 - // NOTE: it currently seems that a full load-and-replace works fine for this 127 - // if the UI loses its place or has jarring re-arrangements, replace this 128 - // with a more in-place update 129 - this._load() 130 - } 131 - 132 - /** 133 - * Refreshes when posts are deleted 134 - */ 135 - onPostDeleted(_uri: string) { 136 - this.refresh() 137 - } 138 - 139 - // state transitions 140 - // = 141 - 142 - _xLoading(isRefreshing = false) { 143 - this.isLoading = true 144 - this.isRefreshing = isRefreshing 145 - this.error = '' 146 - this.notFound = false 147 - } 148 - 149 - _xIdle(err?: any) { 150 - this.isLoading = false 151 - this.isRefreshing = false 152 - this.hasLoaded = true 153 - this.error = cleanError(err) 154 - if (err) { 155 - logger.error('Failed to fetch post thread', {error: err}) 156 - } 157 - this.notFound = err instanceof GetPostThread.NotFoundError 158 - } 159 - 160 - // loader functions 161 - // = 162 - 163 - async _resolveUri() { 164 - const urip = new AtUri(this.params.uri) 165 - if (!urip.host.startsWith('did:')) { 166 - try { 167 - urip.host = await apilib.resolveName(this.rootStore, urip.host) 168 - } catch (e: any) { 169 - runInAction(() => { 170 - this.error = e.toString() 171 - }) 172 - } 173 - } 174 - runInAction(() => { 175 - this.resolvedUri = urip.toString() 176 - }) 177 - } 178 - 179 - async _loadPrecached(precache: AppBskyFeedDefs.PostView) { 180 - // start with the cached version 181 - this.isLoadingFromCache = true 182 - this.isFromCache = true 183 - this._replaceAll({ 184 - success: true, 185 - headers: {}, 186 - data: { 187 - thread: { 188 - post: precache, 189 - }, 190 - }, 191 - }) 192 - this._xIdle() 193 - 194 - // then update in the background 195 - try { 196 - const res = await this.rootStore.agent.getPostThread( 197 - Object.assign({}, this.params, {uri: this.resolvedUri}), 198 - ) 199 - this._replaceAll(res) 200 - } catch (e: any) { 201 - console.log(e) 202 - this._xIdle(e) 203 - } finally { 204 - runInAction(() => { 205 - this.isLoadingFromCache = false 206 - }) 207 - } 208 - } 209 - 210 - async _load(isRefreshing = false) { 211 - if (this.hasLoaded && !isRefreshing) { 212 - return 213 - } 214 - this._xLoading(isRefreshing) 215 - try { 216 - const res = await this.rootStore.agent.getPostThread( 217 - Object.assign({}, this.params, {uri: this.resolvedUri}), 218 - ) 219 - this._replaceAll(res) 220 - this._xIdle() 221 - } catch (e: any) { 222 - console.log(e) 223 - this._xIdle(e) 224 - } 225 - } 226 - 227 - _replaceAll(res: GetPostThread.Response) { 228 - this.isBlocked = AppBskyFeedDefs.isBlockedPost(res.data.thread) 229 - if (this.isBlocked) { 230 - return 231 - } 232 - pruneReplies(res.data.thread) 233 - const thread = new PostThreadItemModel( 234 - this.rootStore, 235 - res.data.thread as AppBskyFeedDefs.ThreadViewPost, 236 - ) 237 - thread._isHighlightedPost = true 238 - thread.assignTreeModels( 239 - res.data.thread as AppBskyFeedDefs.ThreadViewPost, 240 - thread.uri, 241 - ) 242 - sortThread(thread, this.rootStore.preferences.thread) 243 - this.thread = thread 244 - } 245 - } 246 - 247 - type MaybePost = 248 - | AppBskyFeedDefs.ThreadViewPost 249 - | AppBskyFeedDefs.NotFoundPost 250 - | AppBskyFeedDefs.BlockedPost 251 - | {[k: string]: unknown; $type: string} 252 - function pruneReplies(post: MaybePost) { 253 - if (post.replies) { 254 - post.replies = (post.replies as MaybePost[]).filter((reply: MaybePost) => { 255 - if (reply.blocked) { 256 - return false 257 - } 258 - pruneReplies(reply) 259 - return true 260 - }) 261 - } 262 - } 263 - 264 - type MaybeThreadItem = 265 - | PostThreadItemModel 266 - | AppBskyFeedDefs.NotFoundPost 267 - | AppBskyFeedDefs.BlockedPost 268 - function sortThread(item: MaybeThreadItem, opts: ThreadViewPreference) { 269 - if ('notFound' in item) { 270 - return 271 - } 272 - item = item as PostThreadItemModel 273 - if (item.replies) { 274 - item.replies.sort((a: MaybeThreadItem, b: MaybeThreadItem) => { 275 - if ('notFound' in a && a.notFound) { 276 - return 1 277 - } 278 - if ('notFound' in b && b.notFound) { 279 - return -1 280 - } 281 - item = item as PostThreadItemModel 282 - a = a as PostThreadItemModel 283 - b = b as PostThreadItemModel 284 - const aIsByOp = a.post.author.did === item.post.author.did 285 - const bIsByOp = b.post.author.did === item.post.author.did 286 - if (aIsByOp && bIsByOp) { 287 - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest 288 - } else if (aIsByOp) { 289 - return -1 // op's own reply 290 - } else if (bIsByOp) { 291 - return 1 // op's own reply 292 - } 293 - // put moderated content down at the bottom 294 - if (modScore(a.moderation) !== modScore(b.moderation)) { 295 - return modScore(a.moderation) - modScore(b.moderation) 296 - } 297 - if (opts.prioritizeFollowedUsers) { 298 - const af = a.post.author.viewer?.following 299 - const bf = b.post.author.viewer?.following 300 - if (af && !bf) { 301 - return -1 302 - } else if (!af && bf) { 303 - return 1 304 - } 305 - } 306 - if (opts.sort === 'oldest') { 307 - return a.post.indexedAt.localeCompare(b.post.indexedAt) 308 - } else if (opts.sort === 'newest') { 309 - return b.post.indexedAt.localeCompare(a.post.indexedAt) 310 - } else if (opts.sort === 'most-likes') { 311 - if (a.post.likeCount === b.post.likeCount) { 312 - return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest 313 - } else { 314 - return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes 315 - } 316 - } else if (opts.sort === 'random') { 317 - return 0.5 - Math.random() // this is vaguely criminal but we can get away with it 318 - } 319 - return b.post.indexedAt.localeCompare(a.post.indexedAt) 320 - }) 321 - item.replies.forEach(reply => sortThread(reply, opts)) 322 - } 323 - } 324 - 325 - function modScore(mod: PostModeration): number { 326 - if (mod.content.blur && mod.content.noOverride) { 327 - return 5 328 - } 329 - if (mod.content.blur) { 330 - return 4 331 - } 332 - if (mod.content.alert) { 333 - return 3 334 - } 335 - if (mod.embed.blur && mod.embed.noOverride) { 336 - return 2 337 - } 338 - if (mod.embed.blur) { 339 - return 1 340 - } 341 - return 0 342 - }
-9
src/state/models/ui/preferences.ts
··· 1 1 import {makeAutoObservable} from 'mobx' 2 2 import { 3 3 LabelPreference as APILabelPreference, 4 - BskyFeedViewPreference, 5 4 BskyThreadViewPreference, 6 5 } from '@atproto/api' 7 6 import {isObj, hasProp} from 'lib/type-guards' ··· 10 9 11 10 // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf 12 11 export type LabelPreference = APILabelPreference | 'show' 13 - export type FeedViewPreference = BskyFeedViewPreference & { 14 - lab_mergeFeedEnabled?: boolean | undefined 15 - } 16 12 export type ThreadViewPreference = BskyThreadViewPreference & { 17 13 lab_treeViewEnabled?: boolean | undefined 18 14 } ··· 35 31 contentLabels = new LabelPreferencesModel() 36 32 savedFeeds: string[] = [] 37 33 pinnedFeeds: string[] = [] 38 - thread: ThreadViewPreference = { 39 - sort: 'oldest', 40 - prioritizeFollowedUsers: true, 41 - lab_treeViewEnabled: false, // experimental 42 - } 43 34 44 35 constructor(public rootStore: RootStoreModel) { 45 36 makeAutoObservable(this, {}, {autoBind: true})
+3 -3
src/state/queries/post-thread.ts
··· 4 4 AppBskyFeedGetPostThread, 5 5 } from '@atproto/api' 6 6 import {useQuery} from '@tanstack/react-query' 7 - import {useSession} from '../session' 8 - import {ThreadViewPreference} from '../models/ui/preferences' 7 + import {useSession} from '#/state/session' 8 + import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 9 9 10 10 export const RQKEY = (uri: string) => ['post-thread', uri] 11 11 type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] ··· 72 72 73 73 export function sortThread( 74 74 node: ThreadNode, 75 - opts: ThreadViewPreference, 75 + opts: UsePreferencesQueryResponse['threadViewPrefs'], 76 76 ): ThreadNode { 77 77 if (node.type !== 'post') { 78 78 return node