about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/content/post-thread-item.ts6
-rw-r--r--src/state/models/content/post-thread.ts56
-rw-r--r--src/state/models/content/profile.ts26
-rw-r--r--src/state/models/discovery/foafs.ts8
-rw-r--r--src/state/models/discovery/suggested-actors.ts9
-rw-r--r--src/state/models/feeds/notifications.ts47
-rw-r--r--src/state/models/feeds/post.ts42
-rw-r--r--src/state/models/feeds/posts-slice.ts16
-rw-r--r--src/state/models/ui/preferences.ts55
-rw-r--r--src/state/models/ui/shell.ts9
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