diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts
index f553fc686f..79c3185714 100644
--- a/__e2e__/mock-server.ts
+++ b/__e2e__/mock-server.ts
@@ -75,26 +75,35 @@ async function main() {
)
for (const user of [
- 'csam-account',
- 'csam-profile',
- 'csam-posts',
+ 'dmca-account',
+ 'dmca-profile',
+ 'dmca-posts',
'porn-account',
'porn-profile',
'porn-posts',
'nudity-account',
'nudity-profile',
'nudity-posts',
+ 'scam-account',
+ 'scam-profile',
+ 'scam-posts',
'unknown-account',
'unknown-profile',
'unknown-posts',
- 'always-filter-account',
- 'always-filter-profile',
- 'always-filter-posts',
- 'always-warn-account',
- 'always-warn-profile',
- 'always-warn-posts',
+ 'hide-account',
+ 'hide-profile',
+ 'hide-posts',
+ 'no-promote-account',
+ 'no-promote-profile',
+ 'no-promote-posts',
+ 'warn-account',
+ 'warn-profile',
+ 'warn-posts',
'muted-account',
'muted-by-list-account',
+ 'blocking-account',
+ 'blockedby-account',
+ 'mutual-block-account',
]) {
await server.mocker.createUser(user)
await server.mocker.follow('alice', user)
@@ -108,25 +117,25 @@ async function main() {
await server.mocker.like(user, anchorPost)
}
- await server.mocker.labelAccount('csam', 'csam-account')
- await server.mocker.labelProfile('csam', 'csam-profile')
+ await server.mocker.labelAccount('dmca-violation', 'dmca-account')
+ await server.mocker.labelProfile('dmca-violation', 'dmca-profile')
await server.mocker.labelPost(
- 'csam',
- await server.mocker.createPost('csam-posts', 'csam post'),
+ 'dmca-violation',
+ await server.mocker.createPost('dmca-posts', 'dmca post'),
)
await server.mocker.labelPost(
- 'csam',
+ 'dmca-violation',
await server.mocker.createQuotePost(
- 'csam-posts',
- 'csam quote post',
+ 'dmca-posts',
+ 'dmca quote post',
anchorPost,
),
)
await server.mocker.labelPost(
- 'csam',
+ 'dmca-violation',
await server.mocker.createReply(
- 'csam-posts',
- 'csam reply',
+ 'dmca-posts',
+ 'dmca reply',
anchorPost,
),
)
@@ -177,6 +186,29 @@ async function main() {
),
)
+ await server.mocker.labelAccount('scam', 'scam-account')
+ await server.mocker.labelProfile('scam', 'scam-profile')
+ await server.mocker.labelPost(
+ 'scam',
+ await server.mocker.createPost('scam-posts', 'scam post'),
+ )
+ await server.mocker.labelPost(
+ 'scam',
+ await server.mocker.createQuotePost(
+ 'scam-posts',
+ 'scam quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ 'scam',
+ await server.mocker.createReply(
+ 'scam-posts',
+ 'scam reply',
+ anchorPost,
+ ),
+ )
+
await server.mocker.labelAccount(
'not-a-real-label',
'unknown-account',
@@ -206,54 +238,74 @@ async function main() {
),
)
- await server.mocker.labelAccount('!filter', 'always-filter-account')
- await server.mocker.labelProfile('!filter', 'always-filter-profile')
+ await server.mocker.labelAccount('!hide', 'hide-account')
+ await server.mocker.labelProfile('!hide', 'hide-profile')
await server.mocker.labelPost(
- '!filter',
- await server.mocker.createPost(
- 'always-filter-posts',
- 'always-filter post',
- ),
+ '!hide',
+ await server.mocker.createPost('hide-posts', 'hide post'),
)
await server.mocker.labelPost(
- '!filter',
+ '!hide',
await server.mocker.createQuotePost(
- 'always-filter-posts',
- 'always-filter quote post',
+ 'hide-posts',
+ 'hide quote post',
anchorPost,
),
)
await server.mocker.labelPost(
- '!filter',
+ '!hide',
await server.mocker.createReply(
- 'always-filter-posts',
- 'always-filter reply',
+ 'hide-posts',
+ 'hide reply',
anchorPost,
),
)
- await server.mocker.labelAccount('!warn', 'always-warn-account')
- await server.mocker.labelProfile('!warn', 'always-warn-profile')
+ await server.mocker.labelAccount('!no-promote', 'no-promote-account')
+ await server.mocker.labelProfile('!no-promote', 'no-promote-profile')
+ await server.mocker.labelPost(
+ '!no-promote',
+ await server.mocker.createPost(
+ 'no-promote-posts',
+ 'no-promote post',
+ ),
+ )
+ await server.mocker.labelPost(
+ '!no-promote',
+ await server.mocker.createQuotePost(
+ 'no-promote-posts',
+ 'no-promote quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ '!no-promote',
+ await server.mocker.createReply(
+ 'no-promote-posts',
+ 'no-promote reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount('!warn', 'warn-account')
+ await server.mocker.labelProfile('!warn', 'warn-profile')
await server.mocker.labelPost(
'!warn',
- await server.mocker.createPost(
- 'always-warn-posts',
- 'always-warn post',
- ),
+ await server.mocker.createPost('warn-posts', 'warn post'),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createQuotePost(
- 'always-warn-posts',
- 'always-warn quote post',
+ 'warn-posts',
+ 'warn quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createReply(
- 'always-warn-posts',
- 'always-warn reply',
+ 'warn-posts',
+ 'warn reply',
anchorPost,
),
)
@@ -291,6 +343,85 @@ async function main() {
'account reply',
anchorPost,
)
+
+ await server.mocker.createPost('blocking-account', 'blocking post')
+ await server.mocker.createQuotePost(
+ 'blocking-account',
+ 'blocking quote post',
+ anchorPost,
+ )
+ await server.mocker.createReply(
+ 'blocking-account',
+ 'blocking reply',
+ anchorPost,
+ )
+ await server.mocker.users.alice.agent.app.bsky.graph.block.create(
+ {
+ repo: server.mocker.users.alice.did,
+ },
+ {
+ subject: server.mocker.users['blocking-account'].did,
+ createdAt: new Date().toISOString(),
+ },
+ )
+
+ await server.mocker.createPost('blockedby-account', 'blockedby post')
+ await server.mocker.createQuotePost(
+ 'blockedby-account',
+ 'blockedby quote post',
+ anchorPost,
+ )
+ await server.mocker.createReply(
+ 'blockedby-account',
+ 'blockedby reply',
+ anchorPost,
+ )
+ await server.mocker.users[
+ 'blockedby-account'
+ ].agent.app.bsky.graph.block.create(
+ {
+ repo: server.mocker.users['blockedby-account'].did,
+ },
+ {
+ subject: server.mocker.users.alice.did,
+ createdAt: new Date().toISOString(),
+ },
+ )
+
+ await server.mocker.createPost(
+ 'mutual-block-account',
+ 'mutual-block post',
+ )
+ await server.mocker.createQuotePost(
+ 'mutual-block-account',
+ 'mutual-block quote post',
+ anchorPost,
+ )
+ await server.mocker.createReply(
+ 'mutual-block-account',
+ 'mutual-block reply',
+ anchorPost,
+ )
+ await server.mocker.users.alice.agent.app.bsky.graph.block.create(
+ {
+ repo: server.mocker.users.alice.did,
+ },
+ {
+ subject: server.mocker.users['mutual-block-account'].did,
+ createdAt: new Date().toISOString(),
+ },
+ )
+ await server.mocker.users[
+ 'mutual-block-account'
+ ].agent.app.bsky.graph.block.create(
+ {
+ repo: server.mocker.users['mutual-block-account'].did,
+ },
+ {
+ subject: server.mocker.users.alice.did,
+ createdAt: new Date().toISOString(),
+ },
+ )
}
}
console.log('Ready')
diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts
index 6c6d6db9cb..7d2b5c3639 100644
--- a/__e2e__/tests/profile-screen.test.ts
+++ b/__e2e__/tests/profile-screen.test.ts
@@ -53,7 +53,7 @@ describe('Profile screen', () => {
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
'alice.test',
)
- await expect(element(by.id('profileHeaderDescription'))).toHaveText('')
+ await expect(element(by.id('profileHeaderDescription'))).not.toExist()
})
it('Set avi and banner via the edit profile modal', async () => {
@@ -107,13 +107,13 @@ describe('Profile screen', () => {
})
it('Can mute/unmute another user', async () => {
- await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
+ await expect(element(by.id('profileHeaderAlert'))).not.toExist()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Mute Account')).tap()
- await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible()
+ await expect(element(by.id('profileHeaderAlert'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Unmute Account')).tap()
- await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
+ await expect(element(by.id('profileHeaderAlert'))).not.toExist()
})
it('Can report another user', async () => {
diff --git a/app.json b/app.json
index e32cd9ca60..cf3af39df5 100644
--- a/app.json
+++ b/app.json
@@ -4,7 +4,7 @@
"slug": "bluesky",
"scheme": "bluesky",
"owner": "blueskysocial",
- "version": "1.42.0",
+ "version": "1.43.0",
"runtimeVersion": {
"policy": "appVersion"
},
@@ -43,7 +43,7 @@
"backgroundColor": "#ffffff"
},
"android": {
- "versionCode": 28,
+ "versionCode": 29,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 34c3e8bc41..21d90b93b7 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -146,6 +146,7 @@ class Mocker {
}
return await agent.post({
text,
+ langs: ['en'],
createdAt: new Date().toISOString(),
})
}
@@ -162,6 +163,7 @@ class Mocker {
return await agent.post({
text,
embed: {$type: 'app.bsky.embed.record', record: {uri, cid}},
+ langs: ['en'],
createdAt: new Date().toISOString(),
})
}
@@ -178,6 +180,7 @@ class Mocker {
return await agent.post({
text,
reply: {root: {uri, cid}, parent: {uri, cid}},
+ langs: ['en'],
createdAt: new Date().toISOString(),
})
}
diff --git a/package.json b/package.json
index 05b08b3da1..1da54bb275 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.42.0",
+ "version": "1.43.0",
"private": true,
"scripts": {
"prepare": "is-ci || husky install",
@@ -24,7 +24,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
},
"dependencies": {
- "@atproto/api": "^0.4.3",
+ "@atproto/api": "^0.5.2",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@expo/html-elements": "^0.4.2",
diff --git a/src/lib/api/hack-add-deleted-embed.ts b/src/lib/api/hack-add-deleted-embed.ts
new file mode 100644
index 0000000000..59aad21a21
--- /dev/null
+++ b/src/lib/api/hack-add-deleted-embed.ts
@@ -0,0 +1,24 @@
+import {
+ AppBskyFeedDefs,
+ AppBskyFeedPost,
+ ComAtprotoRepoStrongRef,
+} from '@atproto/api'
+
+/**
+ * HACK
+ * The server doesnt seem to be correctly giving the notFound view yet
+ * so I'm adding it manually for now
+ * -prf
+ */
+export function hackAddDeletedEmbed(post: AppBskyFeedDefs.PostView) {
+ const record = post.record as AppBskyFeedPost.Record
+ if (record.embed?.$type === 'app.bsky.embed.record' && !post.embed) {
+ post.embed = {
+ $type: 'app.bsky.embed.record#view',
+ record: {
+ $type: 'app.bsky.embed.record#viewNotFound',
+ uri: (record.embed.record as ComAtprotoRepoStrongRef.Main).uri,
+ },
+ }
+ }
+}
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
new file mode 100644
index 0000000000..758e3de3a5
--- /dev/null
+++ b/src/lib/moderation.ts
@@ -0,0 +1,77 @@
+import {ModerationCause, ProfileModeration} from '@atproto/api'
+
+export interface ModerationCauseDescription {
+ name: string
+ description: string
+}
+
+export function describeModerationCause(
+ cause: ModerationCause | undefined,
+ context: 'account' | 'content',
+): ModerationCauseDescription {
+ if (!cause) {
+ return {
+ name: 'Content Warning',
+ description:
+ 'Moderator has chosen to set a general warning on the content.',
+ }
+ }
+ if (cause.type === 'blocking') {
+ return {
+ name: 'Blocked User',
+ description: 'You have blocked this user. You cannot view their content.',
+ }
+ }
+ if (cause.type === 'blocked-by') {
+ return {
+ name: 'Blocking You',
+ description: 'This user has blocked you. You cannot view their content.',
+ }
+ }
+ if (cause.type === 'muted') {
+ if (cause.source.type === 'user') {
+ return {
+ name: context === 'account' ? 'Muted User' : 'Post by muted user',
+ description: 'You have muted this user',
+ }
+ } else {
+ return {
+ name:
+ context === 'account'
+ ? `Muted by "${cause.source.list.name}"`
+ : `Post by muted user ("${cause.source.list.name}")`,
+ description: 'You have muted this user',
+ }
+ }
+ }
+ return cause.labelDef.strings[context].en
+}
+
+export function getProfileModerationCauses(
+ moderation: ProfileModeration,
+): ModerationCause[] {
+ /*
+ Gather everything on profile and account that blurs or alerts
+ */
+ return [
+ moderation.decisions.profile.cause,
+ ...moderation.decisions.profile.additionalCauses,
+ moderation.decisions.account.cause,
+ ...moderation.decisions.account.additionalCauses,
+ ].filter(cause => {
+ if (!cause) {
+ return false
+ }
+ if (cause?.type === 'label') {
+ if (
+ cause.labelDef.onwarn === 'blur' ||
+ cause.labelDef.onwarn === 'alert'
+ ) {
+ return true
+ } else {
+ return false
+ }
+ }
+ return true
+ }) as ModerationCause[]
+}
diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts
index b98153732d..29b7c3b50f 100644
--- a/src/lib/strings/display-names.ts
+++ b/src/lib/strings/display-names.ts
@@ -1,10 +1,19 @@
+import {ModerationUI} from '@atproto/api'
+import {describeModerationCause} from '../moderation'
+
// \u2705 = ✅
// \u2713 = ✓
// \u2714 = ✔
// \u2611 = ☑
const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu
-export function sanitizeDisplayName(str: string): string {
+export function sanitizeDisplayName(
+ str: string,
+ moderation?: ModerationUI,
+): string {
+ if (moderation?.blur) {
+ return `⚠${describeModerationCause(moderation.cause, 'account').name}`
+ }
if (typeof str === 'string') {
return str.replace(CHECK_MARKS_RE, '').trim()
}
diff --git a/src/state/models/content/post-thread-item.ts b/src/state/models/content/post-thread-item.ts
index 14aa607ed5..141b4f9376 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 c500174a5c..85ed13cb45 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 2ea4ada6e5..26fa6008c8 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 4b25ed4af8..4bcb2bdd14 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 50faae6148..533e14eabe 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 b7ac3a53b6..5f170062d6 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 47039c72ab..68cc3de4c5 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 239bc5b6a7..c02faed3b2 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 e1c0b1f71a..a892d8d349 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 e33a34acf6..4762775929 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
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 00d0616165..469492971b 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -30,6 +30,7 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
import * as OnboardingModal from './OnboardingModal'
+import * as ModerationDetailsModal from './ModerationDetails'
const DEFAULT_SNAPPOINTS = ['90%']
@@ -136,6 +137,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'onboarding') {
snapPoints = OnboardingModal.snapPoints
element =
+ } else if (activeModal?.name === 'moderation-details') {
+ snapPoints = ModerationDetailsModal.snapPoints
+ element =
} else {
return null
}
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 39cdbd868b..df13dfed29 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as OnboardingModal from './OnboardingModal'
+import * as ModerationDetailsModal from './ModerationDetails'
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
@@ -110,6 +111,8 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'onboarding') {
element =
+ } else if (modal.name === 'moderation-details') {
+ element =
} else {
return null
}
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
new file mode 100644
index 0000000000..abeb2fdf48
--- /dev/null
+++ b/src/view/com/modals/ModerationDetails.tsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {ModerationUI} from '@atproto/api'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {TextLink} from '../util/Link'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {listUriToHref} from 'lib/strings/url-helpers'
+import {Button} from '../util/forms/Button'
+
+export const snapPoints = [300]
+
+export function Component({
+ context,
+ moderation,
+}: {
+ context: 'account' | 'content'
+ moderation: ModerationUI
+}) {
+ const store = useStores()
+ const pal = usePalette('default')
+
+ let name
+ let description
+ if (!moderation.cause) {
+ name = 'Content Warning'
+ description =
+ 'Moderator has chosen to set a general warning on the content.'
+ } else if (moderation.cause.type === 'blocking') {
+ name = 'Account Blocked'
+ description = 'You have blocked this user. You cannot view their content.'
+ } else if (moderation.cause.type === 'blocked-by') {
+ name = 'Account Blocks You'
+ description = 'This user has blocked you. You cannot view their content.'
+ } else if (moderation.cause.type === 'muted') {
+ if (moderation.cause.source.type === 'user') {
+ name = 'Account Muted'
+ description = 'You have muted this user.'
+ } else {
+ const list = moderation.cause.source.list
+ name = <>Account Muted by List>
+ description = (
+ <>
+ This user is included the{' '}
+ {' '}
+ list which you have muted.
+ >
+ )
+ }
+ } else {
+ name = moderation.cause.labelDef.strings[context].en.name
+ description = moderation.cause.labelDef.strings[context].en.description
+ }
+
+ return (
+
+
+ {name}
+
+
+ {description}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: isDesktopWeb ? 0 : 14,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ },
+ btn: {
+ paddingVertical: 14,
+ marginTop: isDesktopWeb ? 40 : 0,
+ marginBottom: isDesktopWeb ? 0 : 40,
+ },
+})
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 7b9f0715bb..ce9f5bc0d9 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -7,7 +7,11 @@ import {
StyleSheet,
View,
} from 'react-native'
-import {AppBskyEmbedImages} from '@atproto/api'
+import {
+ AppBskyEmbedImages,
+ ProfileModeration,
+ moderateProfile,
+} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {
FontAwesomeIcon,
@@ -31,11 +35,6 @@ import {Link, TextLink} from '../util/Link'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
-import {
- getProfileViewBasicLabelInfo,
- getProfileModeration,
-} from 'lib/labeling/helpers'
-import {ProfileModeration} from 'lib/labeling/types'
import {formatCount} from '../util/numeric/format'
import {makeProfileLink} from 'lib/routes/links'
@@ -99,9 +98,9 @@ export const FeedItem = observer(function ({
handle: item.author.handle,
displayName: item.author.displayName,
avatar: item.author.avatar,
- moderation: getProfileModeration(
- store,
- getProfileViewBasicLabelInfo(item.author),
+ moderation: moderateProfile(
+ item.author,
+ store.preferences.moderationOpts,
),
},
...(item.additional?.map(({author}) => {
@@ -111,10 +110,7 @@ export const FeedItem = observer(function ({
handle: author.handle,
displayName: author.displayName,
avatar: author.avatar,
- moderation: getProfileModeration(
- store,
- getProfileViewBasicLabelInfo(author),
- ),
+ moderation: moderateProfile(author, store.preferences.moderationOpts),
}
}) || []),
]
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index edf8d7749b..b5469c6fbf 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -26,7 +26,7 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
-import {ImageHider} from '../util/moderation/ImageHider'
+import {PostAlerts} from '../util/moderation/PostAlerts'
import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
@@ -159,10 +159,9 @@ export const PostThreadItem = observer(function PostThreadItem({
if (item._isHighlightedPost) {
return (
-
+ style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}>
@@ -227,7 +226,16 @@ export const PostThreadItem = observer(function PostThreadItem({
-
+
+
{item.richText?.text ? (
) : undefined}
-
-
-
+ {item.post.embed && (
+
+
+
+ )}
-
+
)
} else {
return (
@@ -325,7 +335,7 @@ export const PostThreadItem = observer(function PostThreadItem({
pal.view,
item._showParentReplyLine && styles.noTopBorder,
]}
- moderation={item.moderation.thread}>
+ moderation={item.moderation.content}>
{item._showParentReplyLine && (
-
- {item.richText?.text ? (
-
-
-
- ) : undefined}
-
+
+ {item.richText?.text ? (
+
+
+
+ ) : undefined}
+ {item.post.embed && (
+
-
- {needsTranslation && (
-
-
-
- Translate this post
-
-
-
- )}
-
+
+ )}
+ {needsTranslation && (
+
+
+
+ Translate this post
+
+
+
+ )}
+
{showReplyLine && }
@@ -251,8 +247,13 @@ const PostLoaded = observer(
)}
+ moderation={item.moderation.content}
+ style={styles.contentHider}
+ childContainerStyle={styles.contentHiderChild}>
+
{item.richText?.text ? (
) : undefined}
-
+
-
+
{needsTranslation && (
@@ -302,7 +303,7 @@ const PostLoaded = observer(
/>
-
+
)
},
)
@@ -323,6 +324,9 @@ const styles = StyleSheet.create({
layoutContent: {
flex: 1,
},
+ alert: {
+ marginBottom: 6,
+ },
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',
@@ -341,6 +345,9 @@ const styles = StyleSheet.create({
borderLeftColor: colors.gray2,
},
contentHider: {
- marginTop: 4,
+ marginBottom: 6,
+ },
+ contentHiderChild: {
+ marginTop: 6,
},
})
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 75c3211458..9d2bc72bc1 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -8,16 +8,14 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {PostsFeedItemModel} from 'state/models/feeds/post'
-import {ModerationBehaviorCode} from 'lib/labeling/types'
import {Link, DesktopWebTextLink} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
-import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
-import {ImageHider} from '../util/moderation/ImageHider'
+import {PostAlerts} from '../util/moderation/PostAlerts'
import {RichText} from '../util/text/RichText'
import {PostSandboxWarning} from '../util/PostSandboxWarning'
import * as Toast from '../util/Toast'
@@ -35,13 +33,11 @@ export const FeedItem = observer(function ({
item,
isThreadChild,
isThreadParent,
- ignoreMuteFor,
}: {
item: PostsFeedItemModel
isThreadChild?: boolean
isThreadParent?: boolean
showReplyLine?: boolean
- ignoreMuteFor?: string
}) {
const store = useStores()
const pal = usePalette('default')
@@ -147,26 +143,17 @@ export const FeedItem = observer(function ({
isThreadParent ? styles.outerNoBottom : undefined,
]
- // moderation override
- let moderation = item.moderation.list
- if (
- ignoreMuteFor === item.post.author.did &&
- moderation.isMute &&
- !moderation.noOverride
- ) {
- moderation = {behavior: ModerationBehaviorCode.Show}
- }
-
if (!record || deleted) {
return
}
return (
-
+ noFeedback
+ accessible={false}>
{isThreadChild && (
)}
+ moderation={item.moderation.content}
+ ignoreMute
+ style={styles.contentHider}
+ childContainerStyle={styles.contentHiderChild}>
+
{item.richText?.text ? (
) : undefined}
-
+
-
+
{needsTranslation && (
@@ -306,7 +301,7 @@ export const FeedItem = observer(function ({
/>
-
+
)
})
@@ -358,6 +353,10 @@ const styles = StyleSheet.create({
layoutContent: {
flex: 1,
},
+ alert: {
+ marginTop: 6,
+ marginBottom: 6,
+ },
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',
@@ -365,7 +364,10 @@ const styles = StyleSheet.create({
paddingBottom: 4,
},
contentHider: {
- marginTop: 4,
+ marginBottom: 6,
+ },
+ contentHiderChild: {
+ marginTop: 6,
},
embed: {
marginBottom: 6,
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index b73d4a99de..6345f777f2 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -1,5 +1,6 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
import {AtUri} from '@atproto/api'
import {Link} from '../util/Link'
@@ -7,65 +8,61 @@ import {Text} from '../util/text/Text'
import Svg, {Circle, Line} from 'react-native-svg'
import {FeedItem} from './FeedItem'
import {usePalette} from 'lib/hooks/usePalette'
-import {ModerationBehaviorCode} from 'lib/labeling/types'
import {makeProfileLink} from 'lib/routes/links'
-export function FeedSlice({
- slice,
- ignoreMuteFor,
-}: {
- slice: PostsFeedSliceModel
- ignoreMuteFor?: string
-}) {
- if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
- if (!ignoreMuteFor && !slice.moderation.list.noOverride) {
+export const FeedSlice = observer(
+ ({
+ slice,
+ ignoreFilterFor,
+ }: {
+ slice: PostsFeedSliceModel
+ ignoreFilterFor?: string
+ }) => {
+ if (slice.shouldFilter(ignoreFilterFor)) {
return null
}
- }
- if (slice.isThread && slice.items.length > 3) {
- const last = slice.items.length - 1
+
+ if (slice.isThread && slice.items.length > 3) {
+ const last = slice.items.length - 1
+ return (
+ <>
+
+
+
+
+ >
+ )
+ }
+
return (
<>
-
-
-
-
+ {slice.items.map((item, i) => (
+
+ ))}
>
)
- }
-
- return (
- <>
- {slice.items.map((item, i) => (
-
- ))}
- >
- )
-}
+ },
+)
function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
const pal = usePalette('default')
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 946e0f2ab8..ba0c59def8 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,7 +1,11 @@
import * as React from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
-import {AppBskyActorDefs} from '@atproto/api'
+import {
+ AppBskyActorDefs,
+ moderateProfile,
+ ProfileModeration,
+} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
@@ -11,12 +15,11 @@ import {useStores} from 'state/index'
import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
-import {
- getProfileViewBasicLabelInfo,
- getProfileModeration,
-} from 'lib/labeling/helpers'
-import {ModerationBehaviorCode} from 'lib/labeling/types'
import {makeProfileLink} from 'lib/routes/links'
+import {
+ describeModerationCause,
+ getProfileModerationCauses,
+} from 'lib/moderation'
export const ProfileCard = observer(
({
@@ -25,7 +28,6 @@ export const ProfileCard = observer(
noBg,
noBorder,
followers,
- overrideModeration,
renderButton,
}: {
testID?: string
@@ -33,7 +35,6 @@ export const ProfileCard = observer(
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
- overrideModeration?: boolean
renderButton?: (
profile: AppBskyActorDefs.ProfileViewBasic,
) => React.ReactNode
@@ -41,18 +42,11 @@ export const ProfileCard = observer(
const store = useStores()
const pal = usePalette('default')
- const moderation = getProfileModeration(
- store,
- getProfileViewBasicLabelInfo(profile),
+ const moderation = moderateProfile(
+ profile,
+ store.preferences.moderationOpts,
)
- if (
- moderation.list.behavior === ModerationBehaviorCode.Hide &&
- !overrideModeration
- ) {
- return null
- }
-
return (
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
+ moderation.profile,
)}
{sanitizeHandle(profile.handle, '@')}
- {!!profile.viewer?.followedBy && (
-
-
-
- Follows You
-
-
-
- )}
+
+ {!!profile.viewer?.followedBy && }
{renderButton ? (
{renderButton(profile)}
@@ -114,6 +105,44 @@ export const ProfileCard = observer(
},
)
+function ProfileCardPills({
+ followedBy,
+ moderation,
+}: {
+ followedBy: boolean
+ moderation: ProfileModeration
+}) {
+ const pal = usePalette('default')
+
+ const causes = getProfileModerationCauses(moderation)
+ if (!followedBy && !causes.length) {
+ return null
+ }
+
+ return (
+
+ {followedBy && (
+
+
+ Follows You
+
+
+ )}
+ {causes.map(cause => {
+ const desc = describeModerationCause(cause, 'account')
+ return (
+
+
+ {cause?.type === 'label' ? '⚠' : ''}
+ {desc.name}
+
+
+ )
+ })}
+
+ )
+}
+
const FollowersList = observer(
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
const store = useStores()
@@ -125,9 +154,9 @@ const FollowersList = observer(
const followersWithMods = followers
.map(f => ({
f,
- mod: getProfileModeration(store, getProfileViewBasicLabelInfo(f)),
+ mod: moderateProfile(f, store.preferences.moderationOpts),
}))
- .filter(({mod}) => mod.list.behavior !== ModerationBehaviorCode.Hide)
+ .filter(({mod}) => !mod.account.filter)
return (
@@ -218,6 +247,12 @@ const styles = StyleSheet.create({
paddingRight: 10,
paddingBottom: 10,
},
+ pills: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ columnGap: 6,
+ rowGap: 2,
+ },
pill: {
borderRadius: 4,
paddingHorizontal: 6,
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index a372f0d81f..f8531d76cf 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -21,15 +21,13 @@ import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
import {ThemedText} from '../util/text/ThemedText'
-import {TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
-import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
+import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
-import {listUriToHref} from 'lib/strings/url-helpers'
import {isDesktopWeb, isNative} from 'platform/detection'
import {FollowState} from 'state/models/cache/my-follows'
import {shareUrl} from 'lib/sharing'
@@ -116,7 +114,10 @@ const ProfileHeaderLoaded = observer(
}, [navigation])
const onPressAvi = React.useCallback(() => {
- if (view.avatar) {
+ if (
+ view.avatar &&
+ !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
+ ) {
store.shell.openLightbox(new ProfileImageLightbox(view))
}
}, [store, view])
@@ -434,6 +435,7 @@ const ProfileHeaderLoaded = observer(
style={[pal.text, styles.title]}>
{sanitizeDisplayName(
view.displayName || sanitizeHandle(view.handle),
+ view.moderation.profile,
)}
@@ -494,7 +496,9 @@ const ProfileHeaderLoaded = observer(
- {view.descriptionRichText ? (
+ {view.description &&
+ view.descriptionRichText &&
+ !view.moderation.profile.blur ? (
)}
-
-
- {view.viewer.blocking ? (
-
-
-
- Account blocked
-
-
- ) : view.viewer.muted ? (
-
-
-
- Account muted{' '}
- {view.viewer.mutedByList && (
-
- by{' '}
-
-
- )}
-
-
- ) : undefined}
- {view.viewer.blockedBy && (
-
-
-
- This account has blocked you
-
-
- )}
-
+
{!isDesktopWeb && !hideBackButton && (
{
- if (!moderation?.warn) {
+ const alert = useMemo(() => {
+ if (!moderation?.alert) {
return null
}
return (
-
+
)
- }, [moderation?.warn, size, pal])
+ }, [moderation?.alert, size, pal])
// onSelectNewAvatar is only passed as prop on the EditProfile component
return onSelectNewAvatar ? (
@@ -259,12 +259,12 @@ export function UserAvatar({
source={{uri: avatar}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
- {warning}
+ {alert}
) : (
- {warning}
+ {alert}
)
}
@@ -289,13 +289,13 @@ const styles = StyleSheet.create({
justifyContent: 'center',
backgroundColor: colors.gray5,
},
- warningIconContainer: {
+ alertIconContainer: {
position: 'absolute',
right: 0,
bottom: 0,
borderRadius: 100,
},
- warningIcon: {
+ alertIcon: {
color: colors.red3,
},
})
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index b7e91b5dd3..7c5c583c2f 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,6 +1,7 @@
import React, {useMemo} from 'react'
import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ModerationUI} from '@atproto/api'
import {Image} from 'expo-image'
import {colors} from 'lib/styles'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
@@ -10,7 +11,6 @@ import {
useCameraPermission,
} from 'lib/hooks/usePermissions'
import {usePalette} from 'lib/hooks/usePalette'
-import {AvatarModeration} from 'lib/labeling/types'
import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
@@ -21,7 +21,7 @@ export function UserBanner({
onSelectNewBanner,
}: {
banner?: string | null
- moderation?: AvatarModeration
+ moderation?: ModerationUI
onSelectNewBanner?: (img: RNImage | null) => void
}) {
const store = useStores()
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index ac5c8395d4..6be2f8be01 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -1,36 +1,32 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
+import {ModerationUI} from '@atproto/api'
import {Text} from '../text/Text'
-import {addStyle} from 'lib/styles'
-import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
+import {InfoCircleIcon} from 'lib/icons'
+import {describeModerationCause} from 'lib/moderation'
+import {useStores} from 'state/index'
+import {isDesktopWeb} from 'platform/detection'
export function ContentHider({
testID,
moderation,
+ ignoreMute,
style,
- containerStyle,
+ childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
- moderation: ModerationBehavior
+ moderation: ModerationUI
+ ignoreMute?: boolean
style?: StyleProp
- containerStyle?: StyleProp
+ childContainerStyle?: StyleProp
}>) {
+ const store = useStores()
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
- const onPressShow = React.useCallback(() => {
- setOverride(true)
- }, [setOverride])
- const onPressHide = React.useCallback(() => {
- setOverride(false)
- }, [setOverride])
- if (
- moderation.behavior === ModerationBehaviorCode.Show ||
- moderation.behavior === ModerationBehaviorCode.Warn ||
- moderation.behavior === ModerationBehaviorCode.WarnImages
- ) {
+ if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
return (
{children}
@@ -38,73 +34,61 @@ export function ContentHider({
)
}
- if (moderation.behavior === ModerationBehaviorCode.Hide) {
- return null
- }
-
+ const desc = describeModerationCause(moderation.cause, 'content')
return (
-
+
-
- {moderation.reason || 'Content warning'}
+ onPress={() => {
+ if (!moderation.noOverride) {
+ setOverride(v => !v)
+ }
+ }}
+ accessibilityRole="button"
+ accessibilityHint={override ? 'Hide the content' : 'Show the content'}
+ accessibilityLabel=""
+ style={[styles.cover, pal.viewLight]}>
+ {
+ store.shell.openModal({
+ name: 'moderation-details',
+ context: 'content',
+ moderation,
+ })
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="Learn more about this warning"
+ accessibilityHint="">
+
+
+
+ {desc.name}
-
-
- {override ? 'Hide' : 'Show'}
-
-
-
- {override && (
-
-
- {children}
+ {!moderation.noOverride && (
+
+
+ {override ? 'Hide' : 'Show'}
+
-
- )}
+ )}
+
+ {override && {children}}
)
}
const styles = StyleSheet.create({
- container: {
- marginBottom: 10,
- borderWidth: 1,
- borderRadius: 12,
- },
- description: {
+ cover: {
flexDirection: 'row',
alignItems: 'center',
+ gap: 4,
+ borderRadius: 8,
+ marginTop: 4,
paddingVertical: 14,
paddingLeft: 14,
- paddingRight: 18,
- borderRadius: 12,
- },
- descriptionOpen: {
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 0,
- },
- icon: {
- marginRight: 10,
+ paddingRight: isDesktopWeb ? 18 : 22,
},
showBtn: {
marginLeft: 'auto',
+ alignSelf: 'center',
},
- childrenContainer: {
- paddingHorizontal: 12,
- paddingTop: 8,
- },
- child: {},
})
diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx
deleted file mode 100644
index 40c9d0a216..0000000000
--- a/src/view/com/util/moderation/ImageHider.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from 'react'
-import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from '../text/Text'
-import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
-import {isDesktopWeb} from 'platform/detection'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
-
-export function ImageHider({
- testID,
- moderation,
- style,
- children,
-}: React.PropsWithChildren<{
- testID?: string
- moderation: ModerationBehavior
- style?: StyleProp
-}>) {
- const pal = usePalette('default')
- const [override, setOverride] = React.useState(false)
- const onPressToggle = React.useCallback(() => {
- setOverride(v => !v)
- }, [setOverride])
-
- if (moderation.behavior === ModerationBehaviorCode.Hide) {
- return null
- }
-
- if (moderation.behavior !== ModerationBehaviorCode.WarnImages) {
- return (
-
- {children}
-
- )
- }
-
- return (
-
-
-
-
-
- {moderation.reason || 'Content warning'}
-
-
-
- {override ? 'Hide' : 'Show'}
-
-
-
- {override && children}
-
- )
-}
-
-const styles = StyleSheet.create({
- cover: {
- borderRadius: 8,
- marginTop: 4,
- },
- toggleBtn: {
- flexDirection: 'row',
- gap: 8,
- alignItems: 'center',
- paddingHorizontal: isDesktopWeb ? 24 : 20,
- paddingVertical: isDesktopWeb ? 20 : 18,
- },
- flex1: {
- flex: 1,
- },
-})
diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx
new file mode 100644
index 0000000000..45937c2d8a
--- /dev/null
+++ b/src/view/com/util/moderation/PostAlerts.tsx
@@ -0,0 +1,68 @@
+import React from 'react'
+import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
+import {ModerationUI} from '@atproto/api'
+import {Text} from '../text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {InfoCircleIcon} from 'lib/icons'
+import {describeModerationCause} from 'lib/moderation'
+import {useStores} from 'state/index'
+
+export function PostAlerts({
+ moderation,
+ includeMute,
+ style,
+}: {
+ moderation: ModerationUI
+ includeMute?: boolean
+ style?: StyleProp
+}) {
+ const store = useStores()
+ const pal = usePalette('default')
+
+ const shouldAlert =
+ !!moderation.cause &&
+ (moderation.alert ||
+ (includeMute && moderation.blur && moderation.cause?.type === 'muted'))
+ if (!shouldAlert) {
+ return null
+ }
+
+ const desc = describeModerationCause(moderation.cause, 'content')
+ return (
+ {
+ store.shell.openModal({
+ name: 'moderation-details',
+ context: 'content',
+ moderation,
+ })
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="Learn more about this warning"
+ accessibilityHint=""
+ style={[styles.container, pal.viewLight, style]}>
+
+
+ {desc.name}
+
+
+ Learn More
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ paddingVertical: 8,
+ paddingLeft: 14,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ },
+ learnMoreBtn: {
+ marginLeft: 'auto',
+ },
+})
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index f2b6dbddd6..dc74d3e39e 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -1,17 +1,20 @@
import React, {ComponentProps} from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {StyleSheet, Pressable, View} from 'react-native'
+import {ModerationUI} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {Link} from '../Link'
import {Text} from '../text/Text'
import {addStyle} from 'lib/styles'
-import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
+import {describeModerationCause} from 'lib/moderation'
+import {InfoCircleIcon} from 'lib/icons'
+import {useStores} from 'state/index'
+import {isDesktopWeb} from 'platform/detection'
interface Props extends ComponentProps {
// testID?: string
// href?: string
// style: StyleProp
- moderation: ModerationBehavior
+ moderation: ModerationUI
}
export function PostHider({
@@ -22,60 +25,71 @@ export function PostHider({
children,
...props
}: Props) {
+ const store = useStores()
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
- const bg = override ? pal.viewLight : pal.view
- if (moderation.behavior === ModerationBehaviorCode.Hide) {
- return null
- }
-
- if (moderation.behavior === ModerationBehaviorCode.Warn) {
+ if (!moderation.blur) {
return (
- <>
-
-
-
- {moderation.reason || 'Content warning'}
-
- setOverride(v => !v)}
- accessibilityRole="button">
-
- {override ? 'Hide' : 'Show'} post
-
-
-
- {override && (
-
-
- {children}
-
-
- )}
- >
+
+ {children}
+
)
}
- // NOTE: any further label enforcement should occur in ContentContainer
+ const desc = describeModerationCause(moderation.cause, 'content')
return (
-
- {children}
-
+ <>
+ {
+ if (!moderation.noOverride) {
+ setOverride(v => !v)
+ }
+ }}
+ accessibilityRole="button"
+ accessibilityHint={override ? 'Hide the content' : 'Show the content'}
+ accessibilityLabel=""
+ style={[styles.description, pal.viewLight]}>
+ {
+ store.shell.openModal({
+ name: 'moderation-details',
+ context: 'content',
+ moderation,
+ })
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="Learn more about this warning"
+ accessibilityHint="">
+
+
+
+ {desc.name}
+
+ {!moderation.noOverride && (
+
+ {override ? 'Hide' : 'Show'}
+
+ )}
+
+ {override && (
+
+
+ {children}
+
+
+ )}
+ >
)
}
@@ -83,22 +97,23 @@ const styles = StyleSheet.create({
description: {
flexDirection: 'row',
alignItems: 'center',
+ gap: 4,
paddingVertical: 14,
- paddingHorizontal: 18,
- borderTopWidth: 1,
- },
- icon: {
- marginRight: 10,
+ paddingLeft: 18,
+ paddingRight: isDesktopWeb ? 18 : 22,
+ marginTop: 1,
},
showBtn: {
marginLeft: 'auto',
+ alignSelf: 'center',
},
childrenContainer: {
- paddingHorizontal: 6,
+ paddingHorizontal: 4,
paddingBottom: 6,
},
child: {
- borderWidth: 1,
- borderRadius: 12,
+ borderWidth: 0,
+ borderTopWidth: 0,
+ borderRadius: 8,
},
})
diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
new file mode 100644
index 0000000000..3cc3b5b9e2
--- /dev/null
+++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
@@ -0,0 +1,76 @@
+import React from 'react'
+import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {ProfileModeration} from '@atproto/api'
+import {Text} from '../text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {InfoCircleIcon} from 'lib/icons'
+import {
+ describeModerationCause,
+ getProfileModerationCauses,
+} from 'lib/moderation'
+import {useStores} from 'state/index'
+
+export function ProfileHeaderAlerts({
+ moderation,
+ style,
+}: {
+ moderation: ProfileModeration
+ style?: StyleProp
+}) {
+ const store = useStores()
+ const pal = usePalette('default')
+
+ const causes = getProfileModerationCauses(moderation)
+ if (!causes.length) {
+ return null
+ }
+
+ return (
+
+ {causes.map(cause => {
+ const desc = describeModerationCause(cause, 'account')
+ return (
+ {
+ store.shell.openModal({
+ name: 'moderation-details',
+ context: 'content',
+ moderation: {cause},
+ })
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="Learn more about this warning"
+ accessibilityHint=""
+ style={[styles.container, pal.viewLight, style]}>
+
+
+ {desc.name}
+
+
+ Learn More
+
+
+ )
+ })}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ grid: {
+ gap: 4,
+ },
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ },
+ learnMoreBtn: {
+ marginLeft: 'auto',
+ },
+})
diff --git a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
deleted file mode 100644
index 7a1a8e295b..0000000000
--- a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
-
-export function ProfileHeaderWarnings({
- moderation,
-}: {
- moderation: ModerationBehavior
-}) {
- const palErr = usePalette('error')
- if (moderation.behavior === ModerationBehaviorCode.Show) {
- return null
- }
- return (
-
-
-
- This account has been flagged: {moderation.reason}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- borderWidth: 1,
- borderRadius: 6,
- paddingHorizontal: 10,
- paddingVertical: 8,
- },
-})
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
index 2e7b07e1a3..b76b1101c9 100644
--- a/src/view/com/util/moderation/ScreenHider.tsx
+++ b/src/view/com/util/moderation/ScreenHider.tsx
@@ -1,16 +1,24 @@
import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {
+ TouchableWithoutFeedback,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
+import {ModerationUI} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {NavigationProp} from 'lib/routes/types'
import {Text} from '../text/Text'
import {Button} from '../forms/Button'
import {isDesktopWeb} from 'platform/detection'
-import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
+import {describeModerationCause} from 'lib/moderation'
+import {useStores} from 'state/index'
export function ScreenHider({
testID,
@@ -22,24 +30,17 @@ export function ScreenHider({
}: React.PropsWithChildren<{
testID?: string
screenDescription: string
- moderation: ModerationBehavior
+ moderation: ModerationUI
style?: StyleProp
containerStyle?: StyleProp
}>) {
+ const store = useStores()
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const [override, setOverride] = React.useState(false)
const navigation = useNavigation()
- const onPressBack = React.useCallback(() => {
- if (navigation.canGoBack()) {
- navigation.goBack()
- } else {
- navigation.navigate('Home')
- }
- }, [navigation])
-
- if (moderation.behavior !== ModerationBehaviorCode.Hide || override) {
+ if (!moderation.blur || override) {
return (
{children}
@@ -47,6 +48,7 @@ export function ScreenHider({
)
}
+ const desc = describeModerationCause(moderation.cause, 'account')
return (
@@ -63,11 +65,38 @@ export function ScreenHider({
This {screenDescription} has been flagged:{' '}
- {moderation.reason || 'Content warning'}
+
+ {desc.name}
+
+ .{' '}
+ {
+ store.shell.openModal({
+ name: 'moderation-details',
+ context: 'account',
+ moderation,
+ })
+ }}
+ accessibilityRole="button"
+ accessibilityLabel="Learn more about this warning"
+ accessibilityHint="">
+
+ Learn More
+
+
{!isDesktopWeb && }
-