diff options
Diffstat (limited to 'src/state/models/feeds')
-rw-r--r-- | src/state/models/feeds/notifications.ts | 77 | ||||
-rw-r--r-- | src/state/models/feeds/post.ts | 46 | ||||
-rw-r--r-- | src/state/models/feeds/posts-slice.ts | 34 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 64 |
4 files changed, 85 insertions, 136 deletions
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 05e2ef0db..50a411379 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -8,6 +8,8 @@ import { AppBskyFeedLike, AppBskyGraphFollow, ComAtprotoLabelDefs, + moderatePost, + moderateProfile, } from '@atproto/api' import AwaitLock from 'await-lock' import chunk from 'lodash.chunk' @@ -15,24 +17,12 @@ import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' import {PostThreadModel} from '../content/post-thread' import {cleanError} from 'lib/strings/errors' -import { - PostLabelInfo, - PostModeration, - ModerationBehaviorCode, -} from 'lib/labeling/types' -import { - getPostModeration, - filterAccountLabels, - filterProfileLabels, -} from 'lib/labeling/helpers' const GROUPABLE_REASONS = ['like', 'repost', 'follow'] const PAGE_SIZE = 30 const MS_1HR = 1e3 * 60 * 60 const MS_2DAY = MS_1HR * 48 -let _idCounter = 0 - export const MAX_VISIBLE_NOTIFS = 30 export interface GroupedNotification extends ListNotifications.Notification { @@ -100,27 +90,19 @@ export class NotificationsFeedItemModel { } } - get labelInfo(): PostLabelInfo { - const addedInfo = this.additionalPost?.thread?.labelInfo - return { - postLabels: (this.labels || []).concat(addedInfo?.postLabels || []), - accountLabels: filterAccountLabels(this.author.labels).concat( - addedInfo?.accountLabels || [], - ), - profileLabels: filterProfileLabels(this.author.labels).concat( - addedInfo?.profileLabels || [], - ), - isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false, - mutedByList: this.author.viewer?.mutedByList || addedInfo?.mutedByList, - isBlocking: - !!this.author.viewer?.blocking || addedInfo?.isBlocking || false, - isBlockedBy: - !!this.author.viewer?.blockedBy || addedInfo?.isBlockedBy || false, + get shouldFilter(): boolean { + if (this.additionalPost?.thread) { + const postMod = moderatePost( + this.additionalPost.thread.data.post, + this.rootStore.preferences.moderationOpts, + ) + return postMod.content.filter || false } - } - - get moderation(): PostModeration { - return getPostModeration(this.rootStore, this.labelInfo) + const profileMod = moderateProfile( + this.author, + this.rootStore.preferences.moderationOpts, + ) + return profileMod.account.filter || false } get numUnreadInGroup(): number { @@ -259,6 +241,12 @@ export class NotificationsFeedModel { loadMoreError = '' hasMore = true loadMoreCursor?: string + + /** + * The last time notifications were seen. Refers to either the + * user's machine clock or the value of the `indexedAt` property on their + * latest notification, whichever was greater at the time of viewing. + */ lastSync?: Date // used to linearize async modifications to state @@ -345,9 +333,6 @@ export class NotificationsFeedModel { limit: PAGE_SIZE, }) await this._replaceAll(res) - runInAction(() => { - this.lastSync = new Date() - }) this._setQueued(undefined) this._countUnread() this._xIdle() @@ -503,7 +488,9 @@ export class NotificationsFeedModel { const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({ uris: [addedUri], }) - notif.setAdditionalData(postsRes.data.posts[0]) + const post = postsRes.data.posts[0] + notif.setAdditionalData(post) + this.rootStore.posts.set(post.uri, post) } const filtered = this._filterNotifications([notif]) return filtered[0] @@ -539,9 +526,17 @@ export class NotificationsFeedModel { // = async _replaceAll(res: ListNotifications.Response) { - if (res.data.notifications[0]) { - this.mostRecentNotificationUri = res.data.notifications[0].uri + const latest = res.data.notifications[0] + + if (latest) { + const now = new Date() + const lastIndexed = new Date(latest.indexedAt) + const nowOrLastIndexed = now > lastIndexed ? now : lastIndexed + + this.mostRecentNotificationUri = latest.uri + this.lastSync = nowOrLastIndexed } + return this._appendAll(res, true) } @@ -563,8 +558,7 @@ export class NotificationsFeedModel { ): NotificationsFeedItemModel[] { return items .filter(item => { - const hideByLabel = - item.moderation.list.behavior === ModerationBehaviorCode.Hide + const hideByLabel = item.shouldFilter let mutedThread = !!( item.reasonSubjectRootUri && this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri) @@ -588,7 +582,7 @@ export class NotificationsFeedModel { for (const item of items) { const itemModel = new NotificationsFeedItemModel( this.rootStore, - `item-${_idCounter++}`, + `notification-${item.uri}`, item, ) const uri = itemModel.additionalDataUri @@ -611,6 +605,7 @@ export class NotificationsFeedModel { ), ) for (const post of postsChunks.flat()) { + this.rootStore.posts.set(post.uri, post) const models = addedPostMap.get(post.uri) if (models?.length) { for (const model of models) { diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts index 47039c72a..ae4f29105 100644 --- a/src/state/models/feeds/post.ts +++ b/src/state/models/feeds/post.ts @@ -3,21 +3,13 @@ import { AppBskyFeedPost as FeedPost, AppBskyFeedDefs, RichText, + moderatePost, + PostModeration, } from '@atproto/api' import {RootStoreModel} from '../root-store' 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' import {track} from 'lib/analytics/analytics' +import {hackAddDeletedEmbed} from 'lib/api/hack-add-deleted-embed' type FeedViewPost = AppBskyFeedDefs.FeedViewPost type ReasonRepost = AppBskyFeedDefs.ReasonRepost @@ -36,14 +28,15 @@ export class PostsFeedItemModel { constructor( public rootStore: RootStoreModel, - reactKey: string, + _reactKey: string, v: FeedViewPost, ) { - this._reactKey = reactKey + this._reactKey = _reactKey this.post = v.post if (FeedPost.isRecord(this.post.record)) { const valid = FeedPost.validateRecord(this.post.record) if (valid.success) { + hackAddDeletedEmbed(this.post) this.postRecord = this.post.record this.richText = new RichText(this.postRecord, {cleanNewlines: true}) } else { @@ -86,33 +79,8 @@ export class PostsFeedItemModel { 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) + return moderatePost(this.post, this.rootStore.preferences.moderationOpts) } copy(v: FeedViewPost) { diff --git a/src/state/models/feeds/posts-slice.ts b/src/state/models/feeds/posts-slice.ts index 239bc5b6a..16e4eef15 100644 --- a/src/state/models/feeds/posts-slice.ts +++ b/src/state/models/feeds/posts-slice.ts @@ -1,11 +1,8 @@ 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 = '' @@ -13,15 +10,15 @@ export class PostsFeedSliceModel { // data items: PostsFeedItemModel[] = [] - constructor( - public rootStore: RootStoreModel, - reactKey: string, - slice: FeedViewPostsSlice, - ) { - this._reactKey = reactKey - for (const item of slice.items) { + constructor(public rootStore: RootStoreModel, slice: FeedViewPostsSlice) { + this._reactKey = slice._reactKey + for (let i = 0; i < slice.items.length; i++) { this.items.push( - new PostsFeedItemModel(rootStore, `slice-${_idCounter++}`, item), + new PostsFeedItemModel( + rootStore, + `${this._reactKey} - ${i}`, + slice.items[i], + ), ) } makeAutoObservable(this, {rootStore: false}) @@ -55,7 +52,20 @@ export class PostsFeedSliceModel { } get moderation() { - return mergePostModerations(this.items.map(item => item.moderation)) + // prefer the most stringent item + const topItem = this.items.find(item => item.moderation.content.filter) + if (topItem) { + return topItem.moderation + } + // otherwise just use the first one + return this.items[0].moderation + } + + shouldFilter(ignoreFilterForDid: string | undefined): boolean { + const mods = this.items + .filter(item => item.post.author.did !== ignoreFilterForDid) + .map(item => item.moderation) + return !!mods.find(mod => mod.content.filter) } containsUri(uri: string) { diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 4e6633d38..6facc27ad 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -8,12 +8,11 @@ import AwaitLock from 'await-lock' 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 {FeedTuner} from 'lib/api/feed-manip' import {PostsFeedSliceModel} from './posts-slice' import {track} from 'lib/analytics/analytics' const PAGE_SIZE = 30 -let _idCounter = 0 type QueryParams = | GetTimeline.QueryParams @@ -75,24 +74,6 @@ export class PostsFeedModel { return this.hasLoaded && !this.hasContent } - get nonReplyFeed() { - if (this.feedType === 'author') { - return this.slices.filter(slice => { - const params = this.params as GetAuthorFeed.QueryParams - const item = slice.rootItem - const isRepost = - item?.reasonRepost?.by?.handle === params.actor || - item?.reasonRepost?.by?.did === params.actor - const allow = - !item.postRecord?.reply || // not a reply - isRepost // but allow if it's a repost - return allow - }) - } else { - return this.slices - } - } - setHasNewLatest(v: boolean) { this.hasNewLatest = v } @@ -282,31 +263,26 @@ export class PostsFeedModel { return } const res = await this._getFeed({limit: 1}) - this.setHasNewLatest(res.data.feed[0]?.post.uri !== this.pollCursor) + if (res.data.feed[0]) { + const slices = this.tuner.tune(res.data.feed, this.feedTuners) + if (slices[0]) { + const sliceModel = new PostsFeedSliceModel(this.rootStore, slices[0]) + if (sliceModel.moderation.content.filter) { + return + } + this.setHasNewLatest(sliceModel.uri !== this.pollCursor) + } + } } /** - * Fetches the given post and adds it to the top - * Used by the composer to add their new posts + * Updates the UI after the user has created a post */ - async addPostToTop(uri: string) { + onPostCreated() { if (!this.slices.length) { return this.refresh() - } - try { - const res = await this.rootStore.agent.app.bsky.feed.getPosts({ - uris: [uri], - }) - const toPrepend = new PostsFeedSliceModel( - this.rootStore, - uri, - new FeedViewPostsSlice(res.data.posts.map(post => ({post}))), - ) - runInAction(() => { - this.slices = [toPrepend].concat(this.slices) - }) - } catch (e) { - this.rootStore.log.error('Failed to load post to prepend', {e}) + } else { + this.setHasNewLatest(true) } } @@ -374,16 +350,15 @@ export class PostsFeedModel { this.rootStore.me.follows.hydrateProfiles( res.data.feed.map(item => item.post.author), ) + for (const item of res.data.feed) { + this.rootStore.posts.fromFeedItem(item) + } const slices = this.tuner.tune(res.data.feed, this.feedTuners) const toAppend: PostsFeedSliceModel[] = [] for (const slice of slices) { - const sliceModel = new PostsFeedSliceModel( - this.rootStore, - `item-${_idCounter++}`, - slice, - ) + const sliceModel = new PostsFeedSliceModel(this.rootStore, slice) toAppend.push(sliceModel) } runInAction(() => { @@ -405,6 +380,7 @@ export class PostsFeedModel { res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, ) { for (const item of res.data.feed) { + this.rootStore.posts.fromFeedItem(item) const existingSlice = this.slices.find(slice => slice.containsUri(item.post.uri), ) |