diff options
Diffstat (limited to 'src/state/models/feeds/post.ts')
-rw-r--r-- | src/state/models/feeds/post.ts | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts new file mode 100644 index 000000000..0c411d448 --- /dev/null +++ b/src/state/models/feeds/post.ts @@ -0,0 +1,265 @@ +import {makeAutoObservable} from 'mobx' +import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {updateDataOptimistically} from 'lib/async/revertible' +import {PostLabelInfo, PostModeration} from 'lib/labeling/types' +import {FeedViewPostsSlice} from 'lib/api/feed-manip' +import { + getEmbedLabels, + getEmbedMuted, + getEmbedMutedByList, + getEmbedBlocking, + getEmbedBlockedBy, + getPostModeration, + filterAccountLabels, + filterProfileLabels, + mergePostModerations, +} from 'lib/labeling/helpers' + +type FeedViewPost = AppBskyFeedDefs.FeedViewPost +type ReasonRepost = AppBskyFeedDefs.ReasonRepost +type PostView = AppBskyFeedDefs.PostView + +let _idCounter = 0 + +export class PostsFeedItemModel { + // ui state + _reactKey: string = '' + + // data + post: PostView + postRecord?: AppBskyFeedPost.Record + reply?: FeedViewPost['reply'] + reason?: FeedViewPost['reason'] + richText?: RichText + + constructor( + public rootStore: RootStoreModel, + reactKey: string, + v: FeedViewPost, + ) { + this._reactKey = reactKey + this.post = v.post + if (AppBskyFeedPost.isRecord(this.post.record)) { + const valid = AppBskyFeedPost.validateRecord(this.post.record) + if (valid.success) { + this.postRecord = this.post.record + this.richText = new RichText(this.postRecord, {cleanNewlines: true}) + } else { + this.postRecord = undefined + this.richText = undefined + rootStore.log.warn( + 'Received an invalid app.bsky.feed.post record', + valid.error, + ) + } + } else { + this.postRecord = undefined + this.richText = undefined + rootStore.log.warn( + 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', + this.post.record, + ) + } + this.reply = v.reply + this.reason = v.reason + makeAutoObservable(this, {rootStore: false}) + } + + get rootUri(): string { + if (this.reply?.root.uri) { + return this.reply.root.uri + } + return this.post.uri + } + + get isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + + get labelInfo(): PostLabelInfo { + return { + postLabels: (this.post.labels || []).concat( + getEmbedLabels(this.post.embed), + ), + accountLabels: filterAccountLabels(this.post.author.labels), + profileLabels: filterProfileLabels(this.post.author.labels), + isMuted: + this.post.author.viewer?.muted || + getEmbedMuted(this.post.embed) || + false, + mutedByList: + this.post.author.viewer?.mutedByList || + getEmbedMutedByList(this.post.embed), + isBlocking: + !!this.post.author.viewer?.blocking || + getEmbedBlocking(this.post.embed) || + false, + isBlockedBy: + !!this.post.author.viewer?.blockedBy || + getEmbedBlockedBy(this.post.embed) || + false, + } + } + + get moderation(): PostModeration { + return getPostModeration(this.rootStore, this.labelInfo) + } + + copy(v: FeedViewPost) { + this.post = v.post + this.reply = v.reply + this.reason = v.reason + } + + copyMetrics(v: FeedViewPost) { + this.post.replyCount = v.post.replyCount + this.post.repostCount = v.post.repostCount + this.post.likeCount = v.post.likeCount + this.post.viewer = v.post.viewer + } + + get reasonRepost(): ReasonRepost | undefined { + if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') { + return this.reason as ReasonRepost + } + } + + async toggleLike() { + this.post.viewer = this.post.viewer || {} + if (this.post.viewer.like) { + const url = this.post.viewer.like + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) - 1 + this.post.viewer!.like = undefined + }, + () => this.rootStore.agent.deleteLike(url), + ) + } else { + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) + 1 + this.post.viewer!.like = 'pending' + }, + () => this.rootStore.agent.like(this.post.uri, this.post.cid), + res => { + this.post.viewer!.like = res.uri + }, + ) + } + } + + async toggleRepost() { + this.post.viewer = this.post.viewer || {} + if (this.post.viewer?.repost) { + const url = this.post.viewer.repost + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) - 1 + this.post.viewer!.repost = undefined + }, + () => this.rootStore.agent.deleteRepost(url), + ) + } else { + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) + 1 + this.post.viewer!.repost = 'pending' + }, + () => this.rootStore.agent.repost(this.post.uri, this.post.cid), + res => { + this.post.viewer!.repost = res.uri + }, + ) + } + } + + async toggleThreadMute() { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(this.rootUri) + } else { + this.rootStore.mutedThreads.uris.add(this.rootUri) + } + } + + async delete() { + await this.rootStore.agent.deletePost(this.post.uri) + this.rootStore.emitPostDeleted(this.post.uri) + } +} + +export class PostsFeedSliceModel { + // ui state + _reactKey: string = '' + + // data + items: PostsFeedItemModel[] = [] + + constructor( + public rootStore: RootStoreModel, + reactKey: string, + slice: FeedViewPostsSlice, + ) { + this._reactKey = reactKey + for (const item of slice.items) { + this.items.push( + new PostsFeedItemModel(rootStore, `slice-${_idCounter++}`, item), + ) + } + makeAutoObservable(this, {rootStore: false}) + } + + get uri() { + if (this.isReply) { + return this.items[1].post.uri + } + return this.items[0].post.uri + } + + get isThread() { + return ( + this.items.length > 1 && + this.items.every( + item => item.post.author.did === this.items[0].post.author.did, + ) + ) + } + + get isReply() { + return this.items.length > 1 && !this.isThread + } + + get rootItem() { + if (this.isReply) { + return this.items[1] + } + return this.items[0] + } + + get moderation() { + return mergePostModerations(this.items.map(item => item.moderation)) + } + + containsUri(uri: string) { + return !!this.items.find(item => item.post.uri === uri) + } + + isThreadParentAt(i: number) { + if (this.items.length === 1) { + return false + } + return i < this.items.length - 1 + } + + isThreadChildAt(i: number) { + if (this.items.length === 1) { + return false + } + return i > 0 + } +} |