diff options
Diffstat (limited to 'src/state/models/feeds')
-rw-r--r-- | src/state/models/feeds/custom-feed.ts | 21 | ||||
-rw-r--r-- | src/state/models/feeds/multi-feed.ts | 2 | ||||
-rw-r--r-- | src/state/models/feeds/post.ts | 222 | ||||
-rw-r--r-- | src/state/models/feeds/posts-slice.ts | 78 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 23 |
5 files changed, 204 insertions, 142 deletions
diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 8fc1eb1ec..1303952ea 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -3,6 +3,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' import {updateDataOptimistically} from 'lib/async/revertible' +import {track} from 'lib/analytics/analytics' export class CustomFeedModel { // data @@ -56,11 +57,23 @@ export class CustomFeedModel { // = 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() { - 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() { @@ -80,6 +93,8 @@ export class CustomFeedModel { ) } catch (e: any) { this.rootStore.log.error('Failed to like feed', e) + } finally { + track('CustomFeed:Like') } } @@ -100,6 +115,8 @@ export class CustomFeedModel { ) } catch (e: any) { this.rootStore.log.error('Failed to unlike feed', e) + } finally { + track('CustomFeed:Unlike') } } diff --git a/src/state/models/feeds/multi-feed.ts b/src/state/models/feeds/multi-feed.ts index c2ca8d72f..1fc57a86b 100644 --- a/src/state/models/feeds/multi-feed.ts +++ b/src/state/models/feeds/multi-feed.ts @@ -4,7 +4,7 @@ import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' import {CustomFeedModel} from './custom-feed' import {PostsFeedModel} from './posts' -import {PostsFeedSliceModel} from './post' +import {PostsFeedSliceModel} from './posts-slice' const FEED_PAGE_SIZE = 10 const FEEDS_PAGE_SIZE = 3 diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts index 18a90ee82..8e3c9b03e 100644 --- a/src/state/models/feeds/post.ts +++ b/src/state/models/feeds/post.ts @@ -1,34 +1,35 @@ 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 {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, + getPostModeration, } from 'lib/labeling/helpers' +import {track} from 'lib/analytics/analytics' 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 + postRecord?: FeedPost.Record reply?: FeedViewPost['reply'] reason?: FeedViewPost['reason'] richText?: RichText @@ -40,8 +41,8 @@ export class PostsFeedItemModel { ) { this._reactKey = reactKey this.post = v.post - if (AppBskyFeedPost.isRecord(this.post.record)) { - const valid = AppBskyFeedPost.validateRecord(this.post.record) + 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}) @@ -66,6 +67,14 @@ export class PostsFeedItemModel { makeAutoObservable(this, {rootStore: false}) } + get uri() { + return this.post.uri + } + + get parentUri() { + return this.postRecord?.reply?.parent.uri + } + get rootUri(): string { if (typeof this.reply?.root.uri === 'string') { return this.reply.root.uri @@ -127,139 +136,94 @@ export class PostsFeedItemModel { 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 - }, - ) + try { + if (this.post.viewer.like) { + // unlike + 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 { + // like + 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 + }, + ) + } + } catch (error) { + this.rootStore.log.error('Failed to toggle like', error) + } finally { + track(this.post.viewer.like ? 'Post:Unlike' : 'Post:Like') } } 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 - }, - ) + try { + 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 + }, + ) + } + } catch (error) { + this.rootStore.log.error('Failed to toggle repost', error) + } finally { + track(this.post.viewer.repost ? 'Post:Unrepost' : 'Post:Repost') } } async toggleThreadMute() { - if (this.isThreadMuted) { - this.rootStore.mutedThreads.uris.delete(this.rootUri) - } else { - this.rootStore.mutedThreads.uris.add(this.rootUri) + try { + if (this.isThreadMuted) { + this.rootStore.mutedThreads.uris.delete(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() { - 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 + try { + 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) + } finally { + track('Post:Delete') } - return i > 0 } } diff --git a/src/state/models/feeds/posts-slice.ts b/src/state/models/feeds/posts-slice.ts new file mode 100644 index 000000000..239bc5b6a --- /dev/null +++ b/src/state/models/feeds/posts-slice.ts @@ -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 + } +} diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 2c6f89c35..cd5e3c056 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -9,11 +9,17 @@ import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' import {cleanError} from 'lib/strings/errors' 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 let _idCounter = 0 +type QueryParams = + | GetTimeline.QueryParams + | GetAuthorFeed.QueryParams + | GetCustomFeed.QueryParams + export class PostsFeedModel { // state isLoading = false @@ -24,7 +30,7 @@ export class PostsFeedModel { isBlockedBy = false error = '' loadMoreError = '' - params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams + params: QueryParams hasMore = true loadMoreCursor: string | undefined pollCursor: string | undefined @@ -43,10 +49,7 @@ export class PostsFeedModel { constructor( public rootStore: RootStoreModel, public feedType: 'home' | 'author' | 'custom', - params: - | GetTimeline.QueryParams - | GetAuthorFeed.QueryParams - | GetCustomFeed.QueryParams, + params: QueryParams, ) { makeAutoObservable( this, @@ -218,6 +221,9 @@ export class PostsFeedModel { } } finally { this.lock.release() + if (this.feedType === 'custom') { + track('CustomFeed:LoadMore') + } } }) @@ -416,10 +422,7 @@ export class PostsFeedModel { } protected async _getFeed( - params: - | GetTimeline.QueryParams - | GetAuthorFeed.QueryParams - | GetCustomFeed.QueryParams, + params: QueryParams, ): Promise< GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response > { |