Remove post thread (#1920)

* Delete post-thread model

* Remove post-thread-item

* Remove unused types
zio/stable
Eric Bailey 2023-11-15 19:18:45 -06:00 committed by GitHub
parent 6616b2bff0
commit d8b26edb56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 3 additions and 485 deletions

View File

@ -1,131 +0,0 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyFeedPost as FeedPost,
AppBskyFeedDefs,
RichText,
PostModeration,
} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {PostsFeedItemModel} from '../feeds/post'
type PostView = AppBskyFeedDefs.PostView
// NOTE: this model uses the same data as PostsFeedItemModel, but is used for
// rendering a single post in a thread view, and has additional state
// for rendering the thread view, but calls the same data methods
// as PostsFeedItemModel
// TODO: refactor as an extension or subclass of PostsFeedItemModel
export class PostThreadItemModel {
// ui state
_reactKey: string = ''
_depth = 0
_isHighlightedPost = false
_showParentReplyLine = false
_showChildReplyLine = false
_hasMore = false
// data
data: PostsFeedItemModel
post: PostView
postRecord?: FeedPost.Record
richText?: RichText
parent?:
| PostThreadItemModel
| AppBskyFeedDefs.NotFoundPost
| AppBskyFeedDefs.BlockedPost
replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[]
constructor(
public rootStore: RootStoreModel,
v: AppBskyFeedDefs.ThreadViewPost,
) {
this._reactKey = `thread-${v.post.uri}`
this.data = new PostsFeedItemModel(rootStore, this._reactKey, v)
this.post = this.data.post
this.postRecord = this.data.postRecord
this.richText = this.data.richText
// replies and parent are handled via assignTreeModels
makeAutoObservable(this, {rootStore: false})
}
get uri() {
return this.post.uri
}
get parentUri() {
return this.postRecord?.reply?.parent.uri
}
get rootUri(): string {
if (this.postRecord?.reply?.root.uri) {
return this.postRecord.reply.root.uri
}
return this.post.uri
}
get moderation(): PostModeration {
return this.data.moderation
}
assignTreeModels(
v: AppBskyFeedDefs.ThreadViewPost,
highlightedPostUri: string,
includeParent = true,
includeChildren = true,
) {
// parents
if (includeParent && v.parent) {
if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
const parentModel = new PostThreadItemModel(this.rootStore, v.parent)
parentModel._depth = this._depth - 1
parentModel._showChildReplyLine = true
if (v.parent.parent) {
parentModel._showParentReplyLine = true
parentModel.assignTreeModels(
v.parent,
highlightedPostUri,
true,
false,
)
}
this.parent = parentModel
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
this.parent = v.parent
} else if (AppBskyFeedDefs.isBlockedPost(v.parent)) {
this.parent = v.parent
}
}
// replies
if (includeChildren && v.replies) {
const replies = []
for (const item of v.replies) {
if (AppBskyFeedDefs.isThreadViewPost(item)) {
const itemModel = new PostThreadItemModel(this.rootStore, item)
itemModel._depth = this._depth + 1
itemModel._showParentReplyLine =
itemModel.parentUri !== highlightedPostUri
if (item.replies?.length) {
itemModel._showChildReplyLine = true
itemModel.assignTreeModels(item, highlightedPostUri, false, true)
}
replies.push(itemModel)
} else if (AppBskyFeedDefs.isNotFoundPost(item)) {
replies.push(item)
}
}
this.replies = replies
}
}
async toggleLike() {
this.data.toggleLike()
}
async toggleRepost() {
this.data.toggleRepost()
}
async delete() {
this.data.delete()
}
}

View File

@ -1,342 +0,0 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyFeedGetPostThread as GetPostThread,
AppBskyFeedDefs,
AppBskyFeedPost,
PostModeration,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {ThreadViewPreference} from '../ui/preferences'
import {PostThreadItemModel} from './post-thread-item'
import {logger} from '#/logger'
export class PostThreadModel {
// state
isLoading = false
isLoadingFromCache = false
isFromCache = false
isRefreshing = false
hasLoaded = false
error = ''
notFound = false
resolvedUri = ''
params: GetPostThread.QueryParams
// data
thread?: PostThreadItemModel | null = null
isBlocked = false
constructor(
public rootStore: RootStoreModel,
params: GetPostThread.QueryParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
}
static fromPostView(
rootStore: RootStoreModel,
postView: AppBskyFeedDefs.PostView,
) {
const model = new PostThreadModel(rootStore, {uri: postView.uri})
model.resolvedUri = postView.uri
model.hasLoaded = true
model.thread = new PostThreadItemModel(rootStore, {
post: postView,
})
return model
}
get hasContent() {
return !!this.thread
}
get hasError() {
return this.error !== ''
}
get rootUri(): string {
if (this.thread) {
if (this.thread.postRecord?.reply?.root.uri) {
return this.thread.postRecord.reply.root.uri
}
}
return this.resolvedUri
}
get isCachedPostAReply() {
if (AppBskyFeedPost.isRecord(this.thread?.post.record)) {
return !!this.thread?.post.record.reply
}
return false
}
// public api
// =
/**
* Load for first render
*/
async setup() {
if (!this.resolvedUri) {
await this._resolveUri()
}
if (this.hasContent) {
await this.update()
} else {
const precache = this.rootStore.posts.cache.get(this.resolvedUri)
if (precache) {
await this._loadPrecached(precache)
} else {
await this._load()
}
}
}
/**
* Register any event listeners. Returns a cleanup function.
*/
registerListeners() {
const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this))
return () => sub.remove()
}
/**
* Reset and load
*/
async refresh() {
await this._load(true)
}
/**
* Update content in-place
*/
async update() {
// NOTE: it currently seems that a full load-and-replace works fine for this
// if the UI loses its place or has jarring re-arrangements, replace this
// with a more in-place update
this._load()
}
/**
* Refreshes when posts are deleted
*/
onPostDeleted(_uri: string) {
this.refresh()
}
// state transitions
// =
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
this.notFound = false
}
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = cleanError(err)
if (err) {
logger.error('Failed to fetch post thread', {error: err})
}
this.notFound = err instanceof GetPostThread.NotFoundError
}
// loader functions
// =
async _resolveUri() {
const urip = new AtUri(this.params.uri)
if (!urip.host.startsWith('did:')) {
try {
urip.host = await apilib.resolveName(this.rootStore, urip.host)
} catch (e: any) {
runInAction(() => {
this.error = e.toString()
})
}
}
runInAction(() => {
this.resolvedUri = urip.toString()
})
}
async _loadPrecached(precache: AppBskyFeedDefs.PostView) {
// start with the cached version
this.isLoadingFromCache = true
this.isFromCache = true
this._replaceAll({
success: true,
headers: {},
data: {
thread: {
post: precache,
},
},
})
this._xIdle()
// then update in the background
try {
const res = await this.rootStore.agent.getPostThread(
Object.assign({}, this.params, {uri: this.resolvedUri}),
)
this._replaceAll(res)
} catch (e: any) {
console.log(e)
this._xIdle(e)
} finally {
runInAction(() => {
this.isLoadingFromCache = false
})
}
}
async _load(isRefreshing = false) {
if (this.hasLoaded && !isRefreshing) {
return
}
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.agent.getPostThread(
Object.assign({}, this.params, {uri: this.resolvedUri}),
)
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
console.log(e)
this._xIdle(e)
}
}
_replaceAll(res: GetPostThread.Response) {
this.isBlocked = AppBskyFeedDefs.isBlockedPost(res.data.thread)
if (this.isBlocked) {
return
}
pruneReplies(res.data.thread)
const thread = new PostThreadItemModel(
this.rootStore,
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
)
thread._isHighlightedPost = true
thread.assignTreeModels(
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
thread.uri,
)
sortThread(thread, this.rootStore.preferences.thread)
this.thread = thread
}
}
type MaybePost =
| AppBskyFeedDefs.ThreadViewPost
| AppBskyFeedDefs.NotFoundPost
| AppBskyFeedDefs.BlockedPost
| {[k: string]: unknown; $type: string}
function pruneReplies(post: MaybePost) {
if (post.replies) {
post.replies = (post.replies as MaybePost[]).filter((reply: MaybePost) => {
if (reply.blocked) {
return false
}
pruneReplies(reply)
return true
})
}
}
type MaybeThreadItem =
| PostThreadItemModel
| AppBskyFeedDefs.NotFoundPost
| AppBskyFeedDefs.BlockedPost
function sortThread(item: MaybeThreadItem, opts: ThreadViewPreference) {
if ('notFound' in item) {
return
}
item = item as PostThreadItemModel
if (item.replies) {
item.replies.sort((a: MaybeThreadItem, b: MaybeThreadItem) => {
if ('notFound' in a && a.notFound) {
return 1
}
if ('notFound' in b && b.notFound) {
return -1
}
item = item as PostThreadItemModel
a = a as PostThreadItemModel
b = b as PostThreadItemModel
const aIsByOp = a.post.author.did === item.post.author.did
const bIsByOp = b.post.author.did === item.post.author.did
if (aIsByOp && bIsByOp) {
return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
} else if (aIsByOp) {
return -1 // op's own reply
} else if (bIsByOp) {
return 1 // op's own reply
}
// put moderated content down at the bottom
if (modScore(a.moderation) !== modScore(b.moderation)) {
return modScore(a.moderation) - modScore(b.moderation)
}
if (opts.prioritizeFollowedUsers) {
const af = a.post.author.viewer?.following
const bf = b.post.author.viewer?.following
if (af && !bf) {
return -1
} else if (!af && bf) {
return 1
}
}
if (opts.sort === 'oldest') {
return a.post.indexedAt.localeCompare(b.post.indexedAt)
} else if (opts.sort === 'newest') {
return b.post.indexedAt.localeCompare(a.post.indexedAt)
} else if (opts.sort === 'most-likes') {
if (a.post.likeCount === b.post.likeCount) {
return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
} else {
return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
}
} else if (opts.sort === 'random') {
return 0.5 - Math.random() // this is vaguely criminal but we can get away with it
}
return b.post.indexedAt.localeCompare(a.post.indexedAt)
})
item.replies.forEach(reply => sortThread(reply, opts))
}
}
function modScore(mod: PostModeration): number {
if (mod.content.blur && mod.content.noOverride) {
return 5
}
if (mod.content.blur) {
return 4
}
if (mod.content.alert) {
return 3
}
if (mod.embed.blur && mod.embed.noOverride) {
return 2
}
if (mod.embed.blur) {
return 1
}
return 0
}

View File

@ -1,7 +1,6 @@
import {makeAutoObservable} from 'mobx'
import {
LabelPreference as APILabelPreference,
BskyFeedViewPreference,
BskyThreadViewPreference,
} from '@atproto/api'
import {isObj, hasProp} from 'lib/type-guards'
@ -10,9 +9,6 @@ import {ModerationOpts} from '@atproto/api'
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
export type LabelPreference = APILabelPreference | 'show'
export type FeedViewPreference = BskyFeedViewPreference & {
lab_mergeFeedEnabled?: boolean | undefined
}
export type ThreadViewPreference = BskyThreadViewPreference & {
lab_treeViewEnabled?: boolean | undefined
}
@ -35,11 +31,6 @@ export class PreferencesModel {
contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = []
pinnedFeeds: string[] = []
thread: ThreadViewPreference = {
sort: 'oldest',
prioritizeFollowedUsers: true,
lab_treeViewEnabled: false, // experimental
}
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {}, {autoBind: true})

View File

@ -4,8 +4,8 @@ import {
AppBskyFeedGetPostThread,
} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {useSession} from '../session'
import {ThreadViewPreference} from '../models/ui/preferences'
import {useSession} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
export const RQKEY = (uri: string) => ['post-thread', uri]
type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
@ -72,7 +72,7 @@ export function usePostThreadQuery(uri: string | undefined) {
export function sortThread(
node: ThreadNode,
opts: ThreadViewPreference,
opts: UsePreferencesQueryResponse['threadViewPrefs'],
): ThreadNode {
if (node.type !== 'post') {
return node