Remove post thread (#1920)
* Delete post-thread model * Remove post-thread-item * Remove unused typeszio/stable
parent
6616b2bff0
commit
d8b26edb56
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue