diff options
Diffstat (limited to 'src/state/models/content')
-rw-r--r-- | src/state/models/content/list.ts | 3 | ||||
-rw-r--r-- | src/state/models/content/post-thread-item.ts | 141 | ||||
-rw-r--r-- | src/state/models/content/post-thread.ts | 227 | ||||
-rw-r--r-- | src/state/models/content/profile.ts | 9 |
4 files changed, 154 insertions, 226 deletions
diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts index 038e9fc30..d5c9e649e 100644 --- a/src/state/models/content/list.ts +++ b/src/state/models/content/list.ts @@ -11,6 +11,7 @@ import {RootStoreModel} from '../root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {bundleAsync} from 'lib/async/bundle' +import {track} from 'lib/analytics/analytics' const PAGE_SIZE = 30 @@ -222,6 +223,7 @@ export class ListModel { await this.rootStore.agent.app.bsky.graph.muteActorList({ list: this.list.uri, }) + track('Lists:Subscribe') await this.refresh() } @@ -232,6 +234,7 @@ export class ListModel { await this.rootStore.agent.app.bsky.graph.unmuteActorList({ list: this.list.uri, }) + track('Lists:Unsubscribe') await this.refresh() } diff --git a/src/state/models/content/post-thread-item.ts b/src/state/models/content/post-thread-item.ts new file mode 100644 index 000000000..c33415507 --- /dev/null +++ b/src/state/models/content/post-thread-item.ts @@ -0,0 +1,141 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyFeedPost as FeedPost, + AppBskyFeedDefs, + RichText, +} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {PostLabelInfo, PostModeration} from 'lib/labeling/types' +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 isThreadMuted() { + return this.rootStore.mutedThreads.uris.has(this.rootUri) + } + + get labelInfo(): PostLabelInfo { + return this.data.labelInfo + } + + 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 && replies.length === 0 + 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 toggleThreadMute() { + this.data.toggleThreadMute() + } + + async delete() { + this.data.delete() + } +} diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index 577b76e01..0a67c783e 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -1,238 +1,13 @@ import {makeAutoObservable, runInAction} from 'mobx' import { AppBskyFeedGetPostThread as GetPostThread, - AppBskyFeedPost as FeedPost, AppBskyFeedDefs, - RichText, } 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 {updateDataOptimistically} from 'lib/async/revertible' -import {PostLabelInfo, PostModeration} from 'lib/labeling/types' -import { - getEmbedLabels, - getEmbedMuted, - getEmbedMutedByList, - getEmbedBlocking, - getEmbedBlockedBy, - filterAccountLabels, - filterProfileLabels, - getPostModeration, -} from 'lib/labeling/helpers' - -export class PostThreadItemModel { - // ui state - _reactKey: string = '' - _depth = 0 - _isHighlightedPost = false - _showParentReplyLine = false - _showChildReplyLine = false - _hasMore = false - - // data - post: AppBskyFeedDefs.PostView - postRecord?: FeedPost.Record - parent?: - | PostThreadItemModel - | AppBskyFeedDefs.NotFoundPost - | AppBskyFeedDefs.BlockedPost - replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[] - richText?: RichText - - 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.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) - } - - constructor( - public rootStore: RootStoreModel, - v: AppBskyFeedDefs.ThreadViewPost, - ) { - this._reactKey = `thread-${v.post.uri}` - this.post = v.post - if (FeedPost.isRecord(this.post.record)) { - const valid = FeedPost.validateRecord(this.post.record) - if (valid.success) { - this.postRecord = this.post.record - this.richText = new RichText(this.postRecord, {cleanNewlines: true}) - } else { - rootStore.log.warn( - 'Received an invalid app.bsky.feed.post record', - valid.error, - ) - } - } else { - rootStore.log.warn( - 'app.bsky.feed.getPostThread served an unexpected record type', - this.post.record, - ) - } - // replies and parent are handled via assignTreeModels - makeAutoObservable(this, {rootStore: false}) - } - - 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 && replies.length === 0 - 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.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) - } -} +import {PostThreadItemModel} from './post-thread-item' export class PostThreadModel { // state diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index 9d8378f79..34b2ea28e 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -18,6 +18,7 @@ import { filterAccountLabels, filterProfileLabels, } from 'lib/labeling/helpers' +import {track} from 'lib/analytics/analytics' export class ProfileViewerModel { muted?: boolean @@ -127,19 +128,27 @@ export class ProfileModel { } if (followUri) { + // unfollow await this.rootStore.agent.deleteFollow(followUri) runInAction(() => { this.followersCount-- this.viewer.following = undefined this.rootStore.me.follows.removeFollow(this.did) }) + track('Profile:Unfollow', { + username: this.handle, + }) } else { + // follow const res = await this.rootStore.agent.follow(this.did) runInAction(() => { this.followersCount++ this.viewer.following = res.uri this.rootStore.me.follows.addFollow(this.did, res.uri) }) + track('Profile:Follow', { + username: this.handle, + }) } } |