diff options
Diffstat (limited to 'src/state/models')
-rw-r--r-- | src/state/models/content/post-thread-item.ts | 6 | ||||
-rw-r--r-- | src/state/models/content/post-thread.ts | 56 | ||||
-rw-r--r-- | src/state/models/content/profile.ts | 26 | ||||
-rw-r--r-- | src/state/models/discovery/foafs.ts | 8 | ||||
-rw-r--r-- | src/state/models/discovery/suggested-actors.ts | 9 | ||||
-rw-r--r-- | src/state/models/feeds/notifications.ts | 47 | ||||
-rw-r--r-- | src/state/models/feeds/post.ts | 42 | ||||
-rw-r--r-- | src/state/models/feeds/posts-slice.ts | 16 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 55 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 9 |
10 files changed, 160 insertions, 114 deletions
diff --git a/src/state/models/content/post-thread-item.ts b/src/state/models/content/post-thread-item.ts index 14aa607ed..141b4f937 100644 --- a/src/state/models/content/post-thread-item.ts +++ b/src/state/models/content/post-thread-item.ts @@ -3,9 +3,9 @@ import { AppBskyFeedPost as FeedPost, AppBskyFeedDefs, RichText, + PostModeration, } from '@atproto/api' import {RootStoreModel} from '../root-store' -import {PostLabelInfo, PostModeration} from 'lib/labeling/types' import {PostsFeedItemModel} from '../feeds/post' type PostView = AppBskyFeedDefs.PostView @@ -67,10 +67,6 @@ export class PostThreadItemModel { return this.data.isThreadMuted } - get labelInfo(): PostLabelInfo { - return this.data.labelInfo - } - get moderation(): PostModeration { return this.data.moderation } diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index c500174a5..85ed13cb4 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import { AppBskyFeedGetPostThread as GetPostThread, AppBskyFeedDefs, + PostModeration, } from '@atproto/api' import {AtUri} from '@atproto/api' import {RootStoreModel} from '../root-store' @@ -231,7 +232,6 @@ export class PostThreadModel { return } pruneReplies(res.data.thread) - sortThread(res.data.thread) const thread = new PostThreadItemModel( this.rootStore, res.data.thread as AppBskyFeedDefs.ThreadViewPost, @@ -241,6 +241,7 @@ export class PostThreadModel { res.data.thread as AppBskyFeedDefs.ThreadViewPost, thread.uri, ) + sortThread(thread) this.thread = thread } } @@ -262,24 +263,28 @@ function pruneReplies(post: MaybePost) { } } -function sortThread(post: MaybePost) { - if (post.notFound) { +type MaybeThreadItem = + | PostThreadItemModel + | AppBskyFeedDefs.NotFoundPost + | AppBskyFeedDefs.BlockedPost +function sortThread(item: MaybeThreadItem) { + if ('notFound' in item) { return } - post = post as AppBskyFeedDefs.ThreadViewPost - if (post.replies) { - post.replies.sort((a: MaybePost, b: MaybePost) => { - post = post as AppBskyFeedDefs.ThreadViewPost - if (a.notFound) { + item = item as PostThreadItemModel + if (item.replies) { + item.replies.sort((a: MaybeThreadItem, b: MaybeThreadItem) => { + if ('notFound' in a && a.notFound) { return 1 } - if (b.notFound) { + if ('notFound' in b && b.notFound) { return -1 } - a = a as AppBskyFeedDefs.ThreadViewPost - b = b as AppBskyFeedDefs.ThreadViewPost - const aIsByOp = a.post.author.did === post.post.author.did - const bIsByOp = b.post.author.did === post.post.author.did + item = item as PostThreadItemModel + a = a as PostThreadItemModel + b = b as PostThreadItemModel + const aIsByOp = a.post.author.did === item.post.author.did + const bIsByOp = b.post.author.did === item.post.author.did if (aIsByOp && bIsByOp) { return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest } else if (aIsByOp) { @@ -287,8 +292,31 @@ function sortThread(post: MaybePost) { } else if (bIsByOp) { return 1 // op's own reply } + // put moderated content down at the bottom + if (modScore(a.moderation) !== modScore(b.moderation)) { + return modScore(a.moderation) - modScore(b.moderation) + } return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest }) - post.replies.forEach(reply => sortThread(reply)) + item.replies.forEach(reply => sortThread(reply)) + } +} + +function modScore(mod: PostModeration): number { + if (mod.content.blur && mod.content.noOverride) { + return 5 + } + if (mod.content.blur) { + return 4 + } + if (mod.content.alert) { + return 3 + } + if (mod.embed.blur && mod.embed.noOverride) { + return 2 + } + if (mod.embed.blur) { + return 1 } + return 0 } diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index 2ea4ada6e..26fa6008c 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -6,18 +6,14 @@ import { AppBskyActorGetProfile as GetProfile, AppBskyActorProfile, RichText, + moderateProfile, + ProfileModeration, } from '@atproto/api' import {RootStoreModel} from '../root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' import {FollowState} from '../cache/my-follows' import {Image as RNImage} from 'react-native-image-crop-picker' -import {ProfileLabelInfo, ProfileModeration} from 'lib/labeling/types' -import { - getProfileModeration, - filterAccountLabels, - filterProfileLabels, -} from 'lib/labeling/helpers' import {track} from 'lib/analytics/analytics' export class ProfileViewerModel { @@ -26,7 +22,8 @@ export class ProfileViewerModel { following?: string followedBy?: string blockedBy?: boolean - blocking?: string + blocking?: string; + [key: string]: unknown constructor() { makeAutoObservable(this) @@ -53,7 +50,8 @@ export class ProfileModel { followsCount: number = 0 postsCount: number = 0 labels?: ComAtprotoLabelDefs.Label[] = undefined - viewer = new ProfileViewerModel() + viewer = new ProfileViewerModel(); + [key: string]: unknown // added data descriptionRichText?: RichText = new RichText({text: ''}) @@ -85,18 +83,8 @@ export class ProfileModel { return this.hasLoaded && !this.hasContent } - get labelInfo(): ProfileLabelInfo { - return { - accountLabels: filterAccountLabels(this.labels), - profileLabels: filterProfileLabels(this.labels), - isMuted: this.viewer?.muted || false, - isBlocking: !!this.viewer?.blocking || false, - isBlockedBy: !!this.viewer?.blockedBy || false, - } - } - get moderation(): ProfileModeration { - return getProfileModeration(this.rootStore, this.labelInfo) + return moderateProfile(this, this.rootStore.preferences.moderationOpts) } // public api diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts index 4b25ed4af..4bcb2bdd1 100644 --- a/src/state/models/discovery/foafs.ts +++ b/src/state/models/discovery/foafs.ts @@ -1,6 +1,7 @@ import { AppBskyActorDefs, AppBskyGraphGetFollows as GetFollows, + moderateProfile, } from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import sampleSize from 'lodash.samplesize' @@ -52,6 +53,13 @@ export class FoafsModel { cursor, limit: 100, }) + res.data.follows = res.data.follows.filter( + profile => + !moderateProfile( + profile, + this.rootStore.preferences.moderationOpts, + ).account.filter, + ) this.rootStore.me.follows.hydrateProfiles(res.data.follows) if (!res.data.cursor) { break diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts index 50faae614..533e14eab 100644 --- a/src/state/models/discovery/suggested-actors.ts +++ b/src/state/models/discovery/suggested-actors.ts @@ -1,5 +1,5 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorDefs} from '@atproto/api' +import {AppBskyActorDefs, moderateProfile} from '@atproto/api' import {RootStoreModel} from '../root-store' import {cleanError} from 'lib/strings/errors' import {bundleAsync} from 'lib/async/bundle' @@ -69,7 +69,12 @@ export class SuggestedActorsModel { limit: 25, cursor: this.loadMoreCursor, }) - const {actors, cursor} = res.data + let {actors, cursor} = res.data + actors = actors.filter( + actor => + !moderateProfile(actor, this.rootStore.preferences.moderationOpts) + .account.filter, + ) this.rootStore.me.follows.hydrateProfiles(actors) runInAction(() => { diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index b7ac3a53b..5f170062d 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,16 +17,6 @@ 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 @@ -100,27 +92,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 { @@ -565,8 +549,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) diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts index 47039c72a..68cc3de4c 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 @@ -44,6 +36,7 @@ export class PostsFeedItemModel { 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..c02faed3b 100644 --- a/src/state/models/feeds/posts-slice.ts +++ b/src/state/models/feeds/posts-slice.ts @@ -1,7 +1,6 @@ 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 @@ -55,7 +54,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/ui/preferences.ts b/src/state/models/ui/preferences.ts index e1c0b1f71..a892d8d34 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -1,9 +1,14 @@ import {makeAutoObservable, runInAction} from 'mobx' +import {LabelPreference as APILabelPreference} from '@atproto/api' import AwaitLock from 'await-lock' import isEqual from 'lodash.isequal' import {isObj, hasProp} from 'lib/type-guards' import {RootStoreModel} from '../root-store' -import {ComAtprotoLabelDefs, AppBskyActorDefs} from '@atproto/api' +import { + ComAtprotoLabelDefs, + AppBskyActorDefs, + ModerationOpts, +} from '@atproto/api' import {LabelValGroup} from 'lib/labeling/types' import {getLabelValueGroup} from 'lib/labeling/helpers' import { @@ -16,7 +21,8 @@ import {DEFAULT_FEEDS} from 'lib/constants' import {isIOS, deviceLocales} from 'platform/detection' import {LANGUAGES} from '../../../locale/languages' -export type LabelPreference = 'show' | 'warn' | 'hide' +// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf +export type LabelPreference = APILabelPreference | 'show' const LABEL_GROUPS = [ 'nsfw', 'nudity', @@ -408,6 +414,43 @@ export class PreferencesModel { return res } + get moderationOpts(): ModerationOpts { + return { + userDid: this.rootStore.session.currentSession?.did || '', + adultContentEnabled: this.adultContentEnabled, + labelerSettings: [ + { + labeler: { + did: '', + displayName: 'Bluesky Social', + }, + settings: { + // TEMP translate old settings until this UI can be migrated -prf + porn: tempfixLabelPref(this.contentLabels.nsfw), + sexual: tempfixLabelPref(this.contentLabels.suggestive), + nudity: tempfixLabelPref(this.contentLabels.nudity), + nsfl: tempfixLabelPref(this.contentLabels.gore), + corpse: tempfixLabelPref(this.contentLabels.gore), + gore: tempfixLabelPref(this.contentLabels.gore), + torture: tempfixLabelPref(this.contentLabels.gore), + 'self-harm': tempfixLabelPref(this.contentLabels.gore), + 'intolerant-race': tempfixLabelPref(this.contentLabels.hate), + 'intolerant-gender': tempfixLabelPref(this.contentLabels.hate), + 'intolerant-sexual-orientation': tempfixLabelPref( + this.contentLabels.hate, + ), + 'intolerant-religion': tempfixLabelPref(this.contentLabels.hate), + intolerant: tempfixLabelPref(this.contentLabels.hate), + 'icon-intolerant': tempfixLabelPref(this.contentLabels.hate), + spam: tempfixLabelPref(this.contentLabels.spam), + impersonation: tempfixLabelPref(this.contentLabels.impersonation), + scam: 'warn', + }, + }, + ], + } + } + async setSavedFeeds(saved: string[], pinned: string[]) { const oldSaved = this.savedFeeds const oldPinned = this.pinnedFeeds @@ -485,3 +528,11 @@ export class PreferencesModel { this.requireAltTextEnabled = !this.requireAltTextEnabled } } + +// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf +function tempfixLabelPref(pref: LabelPreference): APILabelPreference { + if (pref === 'show') { + return 'ignore' + } + return pref +} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index e33a34acf..476277592 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -1,4 +1,4 @@ -import {AppBskyEmbedRecord} from '@atproto/api' +import {AppBskyEmbedRecord, ModerationUI} from '@atproto/api' import {RootStoreModel} from '../root-store' import {makeAutoObservable, runInAction} from 'mobx' import {ProfileModel} from '../content/profile' @@ -42,6 +42,12 @@ export interface ServerInputModal { onSelect: (url: string) => void } +export interface ModerationDetailsModal { + name: 'moderation-details' + context: 'account' | 'content' + moderation: ModerationUI +} + export interface ReportPostModal { name: 'report-post' postUri: string @@ -146,6 +152,7 @@ export type Modal = | PreferencesHomeFeed // Moderation + | ModerationDetailsModal | ReportAccountModal | ReportPostModal | CreateOrEditMuteListModal |