[APP-705] Metrics revamp pt2 (#896)
* export track function from analytics.tsx * fix create account tracking * fix tracking sign in * add custom feed events * fix type errors * refactor create post event * add profile follow & unfollow events * refactor PostsFeedSliceModel into its own file * refactor PostThreadItemModel into its own file * reorganize code a lil bit * refactor post-thread-item to use post-feed-item model under the hood * add post events * add post reply tracking * track custom feed load more * track list subscribe and unsubscribe
This commit is contained in:
parent
bfaa6d73f3
commit
a8bbaa06c7
22 changed files with 436 additions and 386 deletions
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
141
src/state/models/content/post-thread-item.ts
Normal file
141
src/state/models/content/post-thread-item.ts
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue