[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 unsubscribezio/stable
parent
bfaa6d73f3
commit
a8bbaa06c7
|
@ -16,6 +16,8 @@ const segmentClient = createClient({
|
||||||
trackAppLifecycleEvents: false,
|
trackAppLifecycleEvents: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
|
||||||
|
|
||||||
export function useAnalytics() {
|
export function useAnalytics() {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const methods: ClientMethods = useAnalyticsOrig()
|
const methods: ClientMethods = useAnalyticsOrig()
|
||||||
|
|
|
@ -11,6 +11,7 @@ interface TrackPropertiesMap {
|
||||||
// LOGIN / SIGN UP events
|
// LOGIN / SIGN UP events
|
||||||
'Sign In': {resumedSession: boolean} // CAN BE SERVER
|
'Sign In': {resumedSession: boolean} // CAN BE SERVER
|
||||||
'Create Account': {} // CAN BE SERVER
|
'Create Account': {} // CAN BE SERVER
|
||||||
|
'Try Create Account': {}
|
||||||
'Signin:PressedForgotPassword': {}
|
'Signin:PressedForgotPassword': {}
|
||||||
'Signin:PressedSelectService': {}
|
'Signin:PressedSelectService': {}
|
||||||
// COMPOSER / CREATE POST events
|
// COMPOSER / CREATE POST events
|
||||||
|
@ -30,12 +31,28 @@ interface TrackPropertiesMap {
|
||||||
// FEED events
|
// FEED events
|
||||||
'Feed:onRefresh': {}
|
'Feed:onRefresh': {}
|
||||||
'Feed:onEndReached': {}
|
'Feed:onEndReached': {}
|
||||||
|
// POST events
|
||||||
|
'Post:Like': {} // CAN BE SERVER
|
||||||
|
'Post:Unlike': {} // CAN BE SERVER
|
||||||
|
'Post:Repost': {} // CAN BE SERVER
|
||||||
|
'Post:Unrepost': {} // CAN BE SERVER
|
||||||
|
'Post:Delete': {} // CAN BE SERVER
|
||||||
|
'Post:ThreadMute': {} // CAN BE SERVER
|
||||||
|
'Post:ThreadUnmute': {} // CAN BE SERVER
|
||||||
|
'Post:Reply': {} // CAN BE SERVER
|
||||||
// FEED ITEM events
|
// FEED ITEM events
|
||||||
'FeedItem:PostReply': {} // CAN BE SERVER
|
'FeedItem:PostReply': {} // CAN BE SERVER
|
||||||
'FeedItem:PostRepost': {} // CAN BE SERVER
|
'FeedItem:PostRepost': {} // CAN BE SERVER
|
||||||
'FeedItem:PostLike': {} // CAN BE SERVER
|
'FeedItem:PostLike': {} // CAN BE SERVER
|
||||||
'FeedItem:PostDelete': {} // CAN BE SERVER
|
'FeedItem:PostDelete': {} // CAN BE SERVER
|
||||||
'FeedItem:ThreadMute': {} // CAN BE SERVER
|
'FeedItem:ThreadMute': {} // CAN BE SERVER
|
||||||
|
// PROFILE events
|
||||||
|
'Profile:Follow': {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
'Profile:Unfollow': {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
// PROFILE HEADER events
|
// PROFILE HEADER events
|
||||||
'ProfileHeader:EditProfileButtonClicked': {}
|
'ProfileHeader:EditProfileButtonClicked': {}
|
||||||
'ProfileHeader:FollowersButtonClicked': {}
|
'ProfileHeader:FollowersButtonClicked': {}
|
||||||
|
@ -72,7 +89,28 @@ interface TrackPropertiesMap {
|
||||||
'Lists:onEndReached': {}
|
'Lists:onEndReached': {}
|
||||||
'CreateMuteList:AvatarSelected': {}
|
'CreateMuteList:AvatarSelected': {}
|
||||||
'CreateMuteList:Save': {} // CAN BE SERVER
|
'CreateMuteList:Save': {} // CAN BE SERVER
|
||||||
|
'Lists:Subscribe': {} // CAN BE SERVER
|
||||||
|
'Lists:Unsubscribe': {} // CAN BE SERVER
|
||||||
// CUSTOM FEED events
|
// CUSTOM FEED events
|
||||||
|
'CustomFeed:Save': {}
|
||||||
|
'CustomFeed:Unsave': {}
|
||||||
|
'CustomFeed:Like': {}
|
||||||
|
'CustomFeed:Unlike': {}
|
||||||
|
'CustomFeed:Share': {}
|
||||||
|
'CustomFeed:Pin': {
|
||||||
|
uri: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'CustomFeed:Unpin': {
|
||||||
|
uri: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'CustomFeed:Reorder': {
|
||||||
|
uri: string
|
||||||
|
name: string
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
'CustomFeed:LoadMore': {}
|
||||||
'MultiFeed:onEndReached': {}
|
'MultiFeed:onEndReached': {}
|
||||||
'MultiFeed:onRefresh': {}
|
'MultiFeed:onRefresh': {}
|
||||||
// MODERATION events
|
// MODERATION events
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {RootStoreModel} from '../root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
|
@ -222,6 +223,7 @@ export class ListModel {
|
||||||
await this.rootStore.agent.app.bsky.graph.muteActorList({
|
await this.rootStore.agent.app.bsky.graph.muteActorList({
|
||||||
list: this.list.uri,
|
list: this.list.uri,
|
||||||
})
|
})
|
||||||
|
track('Lists:Subscribe')
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +234,7 @@ export class ListModel {
|
||||||
await this.rootStore.agent.app.bsky.graph.unmuteActorList({
|
await this.rootStore.agent.app.bsky.graph.unmuteActorList({
|
||||||
list: this.list.uri,
|
list: this.list.uri,
|
||||||
})
|
})
|
||||||
|
track('Lists:Unsubscribe')
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedGetPostThread as GetPostThread,
|
AppBskyFeedGetPostThread as GetPostThread,
|
||||||
AppBskyFeedPost as FeedPost,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
RichText,
|
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {updateDataOptimistically} from 'lib/async/revertible'
|
import {PostThreadItemModel} from './post-thread-item'
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PostThreadModel {
|
export class PostThreadModel {
|
||||||
// state
|
// state
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
filterAccountLabels,
|
filterAccountLabels,
|
||||||
filterProfileLabels,
|
filterProfileLabels,
|
||||||
} from 'lib/labeling/helpers'
|
} from 'lib/labeling/helpers'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
export class ProfileViewerModel {
|
export class ProfileViewerModel {
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
|
@ -127,19 +128,27 @@ export class ProfileModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (followUri) {
|
if (followUri) {
|
||||||
|
// unfollow
|
||||||
await this.rootStore.agent.deleteFollow(followUri)
|
await this.rootStore.agent.deleteFollow(followUri)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount--
|
this.followersCount--
|
||||||
this.viewer.following = undefined
|
this.viewer.following = undefined
|
||||||
this.rootStore.me.follows.removeFollow(this.did)
|
this.rootStore.me.follows.removeFollow(this.did)
|
||||||
})
|
})
|
||||||
|
track('Profile:Unfollow', {
|
||||||
|
username: this.handle,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// follow
|
||||||
const res = await this.rootStore.agent.follow(this.did)
|
const res = await this.rootStore.agent.follow(this.did)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount++
|
this.followersCount++
|
||||||
this.viewer.following = res.uri
|
this.viewer.following = res.uri
|
||||||
this.rootStore.me.follows.addFollow(this.did, res.uri)
|
this.rootStore.me.follows.addFollow(this.did, res.uri)
|
||||||
})
|
})
|
||||||
|
track('Profile:Follow', {
|
||||||
|
username: this.handle,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {RootStoreModel} from 'state/models/root-store'
|
import {RootStoreModel} from 'state/models/root-store'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {updateDataOptimistically} from 'lib/async/revertible'
|
import {updateDataOptimistically} from 'lib/async/revertible'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
export class CustomFeedModel {
|
export class CustomFeedModel {
|
||||||
// data
|
// data
|
||||||
|
@ -56,11 +57,23 @@ export class CustomFeedModel {
|
||||||
// =
|
// =
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
await this.rootStore.preferences.addSavedFeed(this.uri)
|
try {
|
||||||
|
await this.rootStore.preferences.addSavedFeed(this.uri)
|
||||||
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to save feed', error)
|
||||||
|
} finally {
|
||||||
|
track('CustomFeed:Save')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async unsave() {
|
async unsave() {
|
||||||
await this.rootStore.preferences.removeSavedFeed(this.uri)
|
try {
|
||||||
|
await this.rootStore.preferences.removeSavedFeed(this.uri)
|
||||||
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to unsave feed', error)
|
||||||
|
} finally {
|
||||||
|
track('CustomFeed:Unsave')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async like() {
|
async like() {
|
||||||
|
@ -80,6 +93,8 @@ export class CustomFeedModel {
|
||||||
)
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.rootStore.log.error('Failed to like feed', e)
|
this.rootStore.log.error('Failed to like feed', e)
|
||||||
|
} finally {
|
||||||
|
track('CustomFeed:Like')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +115,8 @@ export class CustomFeedModel {
|
||||||
)
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.rootStore.log.error('Failed to unlike feed', e)
|
this.rootStore.log.error('Failed to unlike feed', e)
|
||||||
|
} finally {
|
||||||
|
track('CustomFeed:Unlike')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {CustomFeedModel} from './custom-feed'
|
import {CustomFeedModel} from './custom-feed'
|
||||||
import {PostsFeedModel} from './posts'
|
import {PostsFeedModel} from './posts'
|
||||||
import {PostsFeedSliceModel} from './post'
|
import {PostsFeedSliceModel} from './posts-slice'
|
||||||
|
|
||||||
const FEED_PAGE_SIZE = 10
|
const FEED_PAGE_SIZE = 10
|
||||||
const FEEDS_PAGE_SIZE = 3
|
const FEEDS_PAGE_SIZE = 3
|
||||||
|
|
|
@ -1,34 +1,35 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api'
|
import {
|
||||||
|
AppBskyFeedPost as FeedPost,
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
RichText,
|
||||||
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {updateDataOptimistically} from 'lib/async/revertible'
|
import {updateDataOptimistically} from 'lib/async/revertible'
|
||||||
import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
|
import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
|
||||||
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
|
||||||
import {
|
import {
|
||||||
getEmbedLabels,
|
getEmbedLabels,
|
||||||
getEmbedMuted,
|
getEmbedMuted,
|
||||||
getEmbedMutedByList,
|
getEmbedMutedByList,
|
||||||
getEmbedBlocking,
|
getEmbedBlocking,
|
||||||
getEmbedBlockedBy,
|
getEmbedBlockedBy,
|
||||||
getPostModeration,
|
|
||||||
filterAccountLabels,
|
filterAccountLabels,
|
||||||
filterProfileLabels,
|
filterProfileLabels,
|
||||||
mergePostModerations,
|
getPostModeration,
|
||||||
} from 'lib/labeling/helpers'
|
} from 'lib/labeling/helpers'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
||||||
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
|
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
|
||||||
type PostView = AppBskyFeedDefs.PostView
|
type PostView = AppBskyFeedDefs.PostView
|
||||||
|
|
||||||
let _idCounter = 0
|
|
||||||
|
|
||||||
export class PostsFeedItemModel {
|
export class PostsFeedItemModel {
|
||||||
// ui state
|
// ui state
|
||||||
_reactKey: string = ''
|
_reactKey: string = ''
|
||||||
|
|
||||||
// data
|
// data
|
||||||
post: PostView
|
post: PostView
|
||||||
postRecord?: AppBskyFeedPost.Record
|
postRecord?: FeedPost.Record
|
||||||
reply?: FeedViewPost['reply']
|
reply?: FeedViewPost['reply']
|
||||||
reason?: FeedViewPost['reason']
|
reason?: FeedViewPost['reason']
|
||||||
richText?: RichText
|
richText?: RichText
|
||||||
|
@ -40,8 +41,8 @@ export class PostsFeedItemModel {
|
||||||
) {
|
) {
|
||||||
this._reactKey = reactKey
|
this._reactKey = reactKey
|
||||||
this.post = v.post
|
this.post = v.post
|
||||||
if (AppBskyFeedPost.isRecord(this.post.record)) {
|
if (FeedPost.isRecord(this.post.record)) {
|
||||||
const valid = AppBskyFeedPost.validateRecord(this.post.record)
|
const valid = FeedPost.validateRecord(this.post.record)
|
||||||
if (valid.success) {
|
if (valid.success) {
|
||||||
this.postRecord = this.post.record
|
this.postRecord = this.post.record
|
||||||
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
||||||
|
@ -66,6 +67,14 @@ export class PostsFeedItemModel {
|
||||||
makeAutoObservable(this, {rootStore: false})
|
makeAutoObservable(this, {rootStore: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get uri() {
|
||||||
|
return this.post.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
get parentUri() {
|
||||||
|
return this.postRecord?.reply?.parent.uri
|
||||||
|
}
|
||||||
|
|
||||||
get rootUri(): string {
|
get rootUri(): string {
|
||||||
if (typeof this.reply?.root.uri === 'string') {
|
if (typeof this.reply?.root.uri === 'string') {
|
||||||
return this.reply.root.uri
|
return this.reply.root.uri
|
||||||
|
@ -127,139 +136,94 @@ export class PostsFeedItemModel {
|
||||||
|
|
||||||
async toggleLike() {
|
async toggleLike() {
|
||||||
this.post.viewer = this.post.viewer || {}
|
this.post.viewer = this.post.viewer || {}
|
||||||
if (this.post.viewer.like) {
|
try {
|
||||||
const url = this.post.viewer.like
|
if (this.post.viewer.like) {
|
||||||
await updateDataOptimistically(
|
// unlike
|
||||||
this.post,
|
const url = this.post.viewer.like
|
||||||
() => {
|
await updateDataOptimistically(
|
||||||
this.post.likeCount = (this.post.likeCount || 0) - 1
|
this.post,
|
||||||
this.post.viewer!.like = undefined
|
() => {
|
||||||
},
|
this.post.likeCount = (this.post.likeCount || 0) - 1
|
||||||
() => this.rootStore.agent.deleteLike(url),
|
this.post.viewer!.like = undefined
|
||||||
)
|
},
|
||||||
} else {
|
() => this.rootStore.agent.deleteLike(url),
|
||||||
await updateDataOptimistically(
|
)
|
||||||
this.post,
|
} else {
|
||||||
() => {
|
// like
|
||||||
this.post.likeCount = (this.post.likeCount || 0) + 1
|
await updateDataOptimistically(
|
||||||
this.post.viewer!.like = 'pending'
|
this.post,
|
||||||
},
|
() => {
|
||||||
() => this.rootStore.agent.like(this.post.uri, this.post.cid),
|
this.post.likeCount = (this.post.likeCount || 0) + 1
|
||||||
res => {
|
this.post.viewer!.like = 'pending'
|
||||||
this.post.viewer!.like = res.uri
|
},
|
||||||
},
|
() => this.rootStore.agent.like(this.post.uri, this.post.cid),
|
||||||
)
|
res => {
|
||||||
|
this.post.viewer!.like = res.uri
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to toggle like', error)
|
||||||
|
} finally {
|
||||||
|
track(this.post.viewer.like ? 'Post:Unlike' : 'Post:Like')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleRepost() {
|
async toggleRepost() {
|
||||||
this.post.viewer = this.post.viewer || {}
|
this.post.viewer = this.post.viewer || {}
|
||||||
if (this.post.viewer?.repost) {
|
try {
|
||||||
const url = this.post.viewer.repost
|
if (this.post.viewer?.repost) {
|
||||||
await updateDataOptimistically(
|
const url = this.post.viewer.repost
|
||||||
this.post,
|
await updateDataOptimistically(
|
||||||
() => {
|
this.post,
|
||||||
this.post.repostCount = (this.post.repostCount || 0) - 1
|
() => {
|
||||||
this.post.viewer!.repost = undefined
|
this.post.repostCount = (this.post.repostCount || 0) - 1
|
||||||
},
|
this.post.viewer!.repost = undefined
|
||||||
() => this.rootStore.agent.deleteRepost(url),
|
},
|
||||||
)
|
() => this.rootStore.agent.deleteRepost(url),
|
||||||
} else {
|
)
|
||||||
await updateDataOptimistically(
|
} else {
|
||||||
this.post,
|
await updateDataOptimistically(
|
||||||
() => {
|
this.post,
|
||||||
this.post.repostCount = (this.post.repostCount || 0) + 1
|
() => {
|
||||||
this.post.viewer!.repost = 'pending'
|
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.rootStore.agent.repost(this.post.uri, this.post.cid),
|
||||||
this.post.viewer!.repost = res.uri
|
res => {
|
||||||
},
|
this.post.viewer!.repost = res.uri
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to toggle repost', error)
|
||||||
|
} finally {
|
||||||
|
track(this.post.viewer.repost ? 'Post:Unrepost' : 'Post:Repost')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleThreadMute() {
|
async toggleThreadMute() {
|
||||||
if (this.isThreadMuted) {
|
try {
|
||||||
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
if (this.isThreadMuted) {
|
||||||
} else {
|
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
||||||
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
} else {
|
||||||
|
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to toggle thread mute', error)
|
||||||
|
} finally {
|
||||||
|
track(this.isThreadMuted ? 'Post:ThreadUnmute' : 'Post:ThreadMute')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
await this.rootStore.agent.deletePost(this.post.uri)
|
try {
|
||||||
this.rootStore.emitPostDeleted(this.post.uri)
|
await this.rootStore.agent.deletePost(this.post.uri)
|
||||||
}
|
this.rootStore.emitPostDeleted(this.post.uri)
|
||||||
}
|
} catch (error) {
|
||||||
|
this.rootStore.log.error('Failed to delete post', error)
|
||||||
export class PostsFeedSliceModel {
|
} finally {
|
||||||
// ui state
|
track('Post:Delete')
|
||||||
_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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import {makeAutoObservable} from 'mobx'
|
||||||
|
import {RootStoreModel} from '../root-store'
|
||||||
|
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
||||||
|
import {mergePostModerations} from 'lib/labeling/helpers'
|
||||||
|
import {PostsFeedItemModel} from './post'
|
||||||
|
|
||||||
|
let _idCounter = 0
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,11 +9,17 @@ import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
|
import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
|
||||||
import {PostsFeedSliceModel} from './post'
|
import {PostsFeedSliceModel} from './posts-slice'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
let _idCounter = 0
|
let _idCounter = 0
|
||||||
|
|
||||||
|
type QueryParams =
|
||||||
|
| GetTimeline.QueryParams
|
||||||
|
| GetAuthorFeed.QueryParams
|
||||||
|
| GetCustomFeed.QueryParams
|
||||||
|
|
||||||
export class PostsFeedModel {
|
export class PostsFeedModel {
|
||||||
// state
|
// state
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
@ -24,7 +30,7 @@ export class PostsFeedModel {
|
||||||
isBlockedBy = false
|
isBlockedBy = false
|
||||||
error = ''
|
error = ''
|
||||||
loadMoreError = ''
|
loadMoreError = ''
|
||||||
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
|
params: QueryParams
|
||||||
hasMore = true
|
hasMore = true
|
||||||
loadMoreCursor: string | undefined
|
loadMoreCursor: string | undefined
|
||||||
pollCursor: string | undefined
|
pollCursor: string | undefined
|
||||||
|
@ -43,10 +49,7 @@ export class PostsFeedModel {
|
||||||
constructor(
|
constructor(
|
||||||
public rootStore: RootStoreModel,
|
public rootStore: RootStoreModel,
|
||||||
public feedType: 'home' | 'author' | 'custom',
|
public feedType: 'home' | 'author' | 'custom',
|
||||||
params:
|
params: QueryParams,
|
||||||
| GetTimeline.QueryParams
|
|
||||||
| GetAuthorFeed.QueryParams
|
|
||||||
| GetCustomFeed.QueryParams,
|
|
||||||
) {
|
) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
|
@ -218,6 +221,9 @@ export class PostsFeedModel {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.lock.release()
|
this.lock.release()
|
||||||
|
if (this.feedType === 'custom') {
|
||||||
|
track('CustomFeed:LoadMore')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -416,10 +422,7 @@ export class PostsFeedModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _getFeed(
|
protected async _getFeed(
|
||||||
params:
|
params: QueryParams,
|
||||||
| GetTimeline.QueryParams
|
|
||||||
| GetAuthorFeed.QueryParams
|
|
||||||
| GetCustomFeed.QueryParams,
|
|
||||||
): Promise<
|
): Promise<
|
||||||
GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response
|
GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response
|
||||||
> {
|
> {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as EmailValidator from 'email-validator'
|
||||||
import {createFullHandle} from 'lib/strings/handles'
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {getAge} from 'lib/strings/time'
|
import {getAge} from 'lib/strings/time'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
||||||
|
|
||||||
|
@ -117,6 +118,8 @@ export class CreateAccountModel {
|
||||||
this.setIsProcessing(false)
|
this.setIsProcessing(false)
|
||||||
this.setError(cleanError(errMsg))
|
this.setError(cleanError(errMsg))
|
||||||
throw e
|
throw e
|
||||||
|
} finally {
|
||||||
|
track('Create Account')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {RootStoreModel} from '../root-store'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {CustomFeedModel} from '../feeds/custom-feed'
|
import {CustomFeedModel} from '../feeds/custom-feed'
|
||||||
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
export class SavedFeedsModel {
|
export class SavedFeedsModel {
|
||||||
// state
|
// state
|
||||||
|
@ -143,8 +144,16 @@ export class SavedFeedsModel {
|
||||||
|
|
||||||
async togglePinnedFeed(feed: CustomFeedModel) {
|
async togglePinnedFeed(feed: CustomFeedModel) {
|
||||||
if (!this.isPinned(feed)) {
|
if (!this.isPinned(feed)) {
|
||||||
|
track('CustomFeed:Pin', {
|
||||||
|
name: feed.data.displayName,
|
||||||
|
uri: feed.uri,
|
||||||
|
})
|
||||||
return this.rootStore.preferences.addPinnedFeed(feed.uri)
|
return this.rootStore.preferences.addPinnedFeed(feed.uri)
|
||||||
} else {
|
} else {
|
||||||
|
track('CustomFeed:Unpin', {
|
||||||
|
name: feed.data.displayName,
|
||||||
|
uri: feed.uri,
|
||||||
|
})
|
||||||
return this.rootStore.preferences.removePinnedFeed(feed.uri)
|
return this.rootStore.preferences.removePinnedFeed(feed.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +194,11 @@ export class SavedFeedsModel {
|
||||||
this.rootStore.preferences.savedFeeds,
|
this.rootStore.preferences.savedFeeds,
|
||||||
pinned,
|
pinned,
|
||||||
)
|
)
|
||||||
|
track('CustomFeed:Reorder', {
|
||||||
|
name: item.data.displayName,
|
||||||
|
uri: item.uri,
|
||||||
|
index: pinned.indexOf(item.uri),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// state transitions
|
// state transitions
|
||||||
|
|
|
@ -56,9 +56,10 @@ export const CreateAccount = observer(
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await model.submit()
|
await model.submit()
|
||||||
track('Create Account')
|
|
||||||
} catch {
|
} catch {
|
||||||
// dont need to handle here
|
// dont need to handle here
|
||||||
|
} finally {
|
||||||
|
track('Try Create Account')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [model, track])
|
}, [model, track])
|
||||||
|
|
|
@ -327,7 +327,6 @@ const LoginForm = ({
|
||||||
identifier: fullIdent,
|
identifier: fullIdent,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
track('Sign In', {resumedSession: false})
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errMsg = e.toString()
|
const errMsg = e.toString()
|
||||||
store.log.warn('Failed to login', e)
|
store.log.warn('Failed to login', e)
|
||||||
|
@ -341,6 +340,8 @@ const LoginForm = ({
|
||||||
} else {
|
} else {
|
||||||
setError(cleanError(errMsg))
|
setError(cleanError(errMsg))
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
track('Sign In', {resumedSession: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,9 +169,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
knownHandles: autocompleteView.knownHandles,
|
knownHandles: autocompleteView.knownHandles,
|
||||||
langs: store.preferences.postLanguages,
|
langs: store.preferences.postLanguages,
|
||||||
})
|
})
|
||||||
track('Create Post', {
|
|
||||||
imageCount: gallery.size,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (extLink) {
|
if (extLink) {
|
||||||
setExtLink({
|
setExtLink({
|
||||||
|
@ -183,6 +180,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
setError(cleanError(e.message))
|
setError(cleanError(e.message))
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
return
|
return
|
||||||
|
} finally {
|
||||||
|
track('Create Post', {
|
||||||
|
imageCount: gallery.size,
|
||||||
|
})
|
||||||
|
if (replyTo && replyTo.uri) track('Post:Reply')
|
||||||
}
|
}
|
||||||
if (!replyTo) {
|
if (!replyTo) {
|
||||||
store.me.mainFeed.addPostToTop(createdPost.uri)
|
store.me.mainFeed.addPostToTop(createdPost.uri)
|
||||||
|
|
|
@ -9,10 +9,8 @@ import {
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {AppBskyFeedDefs} from '@atproto/api'
|
import {AppBskyFeedDefs} from '@atproto/api'
|
||||||
import {CenteredView, FlatList} from '../util/Views'
|
import {CenteredView, FlatList} from '../util/Views'
|
||||||
import {
|
import {PostThreadModel} from 'state/models/content/post-thread'
|
||||||
PostThreadModel,
|
import {PostThreadItemModel} from 'state/models/content/post-thread-item'
|
||||||
PostThreadItemModel,
|
|
||||||
} from 'state/models/content/post-thread'
|
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {PostThreadItemModel} from 'state/models/content/post-thread'
|
import {PostThreadItemModel} from 'state/models/content/post-thread-item'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
|
|
@ -13,10 +13,8 @@ import {observer} from 'mobx-react-lite'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {
|
import {PostThreadModel} from 'state/models/content/post-thread'
|
||||||
PostThreadModel,
|
import {PostThreadItemModel} from 'state/models/content/post-thread-item'
|
||||||
PostThreadItemModel,
|
|
||||||
} from 'state/models/content/post-thread'
|
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {PostsFeedSliceModel} from 'state/models/feeds/post'
|
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
|
|
@ -31,12 +31,14 @@ import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
||||||
import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton'
|
import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton'
|
||||||
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||||
import {EmptyState} from 'view/com/util/EmptyState'
|
import {EmptyState} from 'view/com/util/EmptyState'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
|
||||||
export const CustomFeedScreen = withAuthRequired(
|
export const CustomFeedScreen = withAuthRequired(
|
||||||
observer(({route}: Props) => {
|
observer(({route}: Props) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {track} = useAnalytics()
|
||||||
const {rkey, name} = route.params
|
const {rkey, name} = route.params
|
||||||
const uri = useMemo(
|
const uri = useMemo(
|
||||||
() => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
|
() => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
|
||||||
|
@ -99,7 +101,8 @@ export const CustomFeedScreen = withAuthRequired(
|
||||||
const onPressShare = React.useCallback(() => {
|
const onPressShare = React.useCallback(() => {
|
||||||
const url = toShareUrl(`/profile/${name}/feed/${rkey}`)
|
const url = toShareUrl(`/profile/${name}/feed/${rkey}`)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
}, [name, rkey])
|
track('CustomFeed:Share')
|
||||||
|
}, [name, rkey, track])
|
||||||
|
|
||||||
const onScrollToTop = React.useCallback(() => {
|
const onScrollToTop = React.useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {CenteredView} from '../com/util/Views'
|
||||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||||
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
|
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {PostsFeedSliceModel} from 'state/models/feeds/post'
|
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
|
||||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||||
import {FeedSlice} from '../com/posts/FeedSlice'
|
import {FeedSlice} from '../com/posts/FeedSlice'
|
||||||
import {ListCard} from 'view/com/lists/ListCard'
|
import {ListCard} from 'view/com/lists/ListCard'
|
||||||
|
|
Loading…
Reference in New Issue