Labeling & moderation updates [DRAFT] (#1057)
* First pass moving to the new labeling sdk (it compiles) * Correct behaviors around interpreting label moderation * Improve moderation state rendering * Improve hiders and alerts * Improve handling of mutes * Improve profile warnings * Add profile blurring to profile header * Add blocks to test cases * Render labels on profile cards, do not filter * Filter profiles from suggestions using moderation * Apply profile blurring to ProfileCard * Handle blocked and deleted quote posts * Temporarily translate content filtering settings to new labels * Fix types * Tune ContentHider & PostHider click targets * Put a warning on profilecard label pills * Fix screenhider learnmore link on mobile * Enforce no-override on user avatar * Dont enumerate profile blur-media labels in alerts * Fixes to muted posts (esp quotes of muted users) * Fixes to account/profile warnings * Bump @atproto/api@0.5.0 * Bump @atproto/api@0.5.1 * Fix tests * 1.43 * Remove log * Bump @atproto/api@0.5.2zio/stable
parent
3ae5a6b631
commit
b154d3ea21
|
@ -75,26 +75,35 @@ async function main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const user of [
|
for (const user of [
|
||||||
'csam-account',
|
'dmca-account',
|
||||||
'csam-profile',
|
'dmca-profile',
|
||||||
'csam-posts',
|
'dmca-posts',
|
||||||
'porn-account',
|
'porn-account',
|
||||||
'porn-profile',
|
'porn-profile',
|
||||||
'porn-posts',
|
'porn-posts',
|
||||||
'nudity-account',
|
'nudity-account',
|
||||||
'nudity-profile',
|
'nudity-profile',
|
||||||
'nudity-posts',
|
'nudity-posts',
|
||||||
|
'scam-account',
|
||||||
|
'scam-profile',
|
||||||
|
'scam-posts',
|
||||||
'unknown-account',
|
'unknown-account',
|
||||||
'unknown-profile',
|
'unknown-profile',
|
||||||
'unknown-posts',
|
'unknown-posts',
|
||||||
'always-filter-account',
|
'hide-account',
|
||||||
'always-filter-profile',
|
'hide-profile',
|
||||||
'always-filter-posts',
|
'hide-posts',
|
||||||
'always-warn-account',
|
'no-promote-account',
|
||||||
'always-warn-profile',
|
'no-promote-profile',
|
||||||
'always-warn-posts',
|
'no-promote-posts',
|
||||||
|
'warn-account',
|
||||||
|
'warn-profile',
|
||||||
|
'warn-posts',
|
||||||
'muted-account',
|
'muted-account',
|
||||||
'muted-by-list-account',
|
'muted-by-list-account',
|
||||||
|
'blocking-account',
|
||||||
|
'blockedby-account',
|
||||||
|
'mutual-block-account',
|
||||||
]) {
|
]) {
|
||||||
await server.mocker.createUser(user)
|
await server.mocker.createUser(user)
|
||||||
await server.mocker.follow('alice', user)
|
await server.mocker.follow('alice', user)
|
||||||
|
@ -108,25 +117,25 @@ async function main() {
|
||||||
await server.mocker.like(user, anchorPost)
|
await server.mocker.like(user, anchorPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.mocker.labelAccount('csam', 'csam-account')
|
await server.mocker.labelAccount('dmca-violation', 'dmca-account')
|
||||||
await server.mocker.labelProfile('csam', 'csam-profile')
|
await server.mocker.labelProfile('dmca-violation', 'dmca-profile')
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'csam',
|
'dmca-violation',
|
||||||
await server.mocker.createPost('csam-posts', 'csam post'),
|
await server.mocker.createPost('dmca-posts', 'dmca post'),
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'csam',
|
'dmca-violation',
|
||||||
await server.mocker.createQuotePost(
|
await server.mocker.createQuotePost(
|
||||||
'csam-posts',
|
'dmca-posts',
|
||||||
'csam quote post',
|
'dmca quote post',
|
||||||
anchorPost,
|
anchorPost,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'csam',
|
'dmca-violation',
|
||||||
await server.mocker.createReply(
|
await server.mocker.createReply(
|
||||||
'csam-posts',
|
'dmca-posts',
|
||||||
'csam reply',
|
'dmca reply',
|
||||||
anchorPost,
|
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(
|
await server.mocker.labelAccount(
|
||||||
'not-a-real-label',
|
'not-a-real-label',
|
||||||
'unknown-account',
|
'unknown-account',
|
||||||
|
@ -206,54 +238,74 @@ async function main() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await server.mocker.labelAccount('!filter', 'always-filter-account')
|
await server.mocker.labelAccount('!hide', 'hide-account')
|
||||||
await server.mocker.labelProfile('!filter', 'always-filter-profile')
|
await server.mocker.labelProfile('!hide', 'hide-profile')
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'!filter',
|
'!hide',
|
||||||
await server.mocker.createPost(
|
await server.mocker.createPost('hide-posts', 'hide post'),
|
||||||
'always-filter-posts',
|
|
||||||
'always-filter post',
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'!filter',
|
'!hide',
|
||||||
await server.mocker.createQuotePost(
|
await server.mocker.createQuotePost(
|
||||||
'always-filter-posts',
|
'hide-posts',
|
||||||
'always-filter quote post',
|
'hide quote post',
|
||||||
anchorPost,
|
anchorPost,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'!filter',
|
'!hide',
|
||||||
await server.mocker.createReply(
|
await server.mocker.createReply(
|
||||||
'always-filter-posts',
|
'hide-posts',
|
||||||
'always-filter reply',
|
'hide reply',
|
||||||
anchorPost,
|
anchorPost,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await server.mocker.labelAccount('!warn', 'always-warn-account')
|
await server.mocker.labelAccount('!no-promote', 'no-promote-account')
|
||||||
await server.mocker.labelProfile('!warn', 'always-warn-profile')
|
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(
|
await server.mocker.labelPost(
|
||||||
'!warn',
|
'!warn',
|
||||||
await server.mocker.createPost(
|
await server.mocker.createPost('warn-posts', 'warn post'),
|
||||||
'always-warn-posts',
|
|
||||||
'always-warn post',
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'!warn',
|
'!warn',
|
||||||
await server.mocker.createQuotePost(
|
await server.mocker.createQuotePost(
|
||||||
'always-warn-posts',
|
'warn-posts',
|
||||||
'always-warn quote post',
|
'warn quote post',
|
||||||
anchorPost,
|
anchorPost,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await server.mocker.labelPost(
|
await server.mocker.labelPost(
|
||||||
'!warn',
|
'!warn',
|
||||||
await server.mocker.createReply(
|
await server.mocker.createReply(
|
||||||
'always-warn-posts',
|
'warn-posts',
|
||||||
'always-warn reply',
|
'warn reply',
|
||||||
anchorPost,
|
anchorPost,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -291,6 +343,85 @@ async function main() {
|
||||||
'account reply',
|
'account reply',
|
||||||
anchorPost,
|
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')
|
console.log('Ready')
|
||||||
|
|
|
@ -53,7 +53,7 @@ describe('Profile screen', () => {
|
||||||
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
||||||
'alice.test',
|
'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 () => {
|
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 () => {
|
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.id('profileHeaderDropdownBtn')).tap()
|
||||||
await element(by.text('Mute Account')).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.id('profileHeaderDropdownBtn')).tap()
|
||||||
await element(by.text('Unmute Account')).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 () => {
|
it('Can report another user', async () => {
|
||||||
|
|
4
app.json
4
app.json
|
@ -4,7 +4,7 @@
|
||||||
"slug": "bluesky",
|
"slug": "bluesky",
|
||||||
"scheme": "bluesky",
|
"scheme": "bluesky",
|
||||||
"owner": "blueskysocial",
|
"owner": "blueskysocial",
|
||||||
"version": "1.42.0",
|
"version": "1.43.0",
|
||||||
"runtimeVersion": {
|
"runtimeVersion": {
|
||||||
"policy": "appVersion"
|
"policy": "appVersion"
|
||||||
},
|
},
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"versionCode": 28,
|
"versionCode": 29,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
|
|
|
@ -146,6 +146,7 @@ class Mocker {
|
||||||
}
|
}
|
||||||
return await agent.post({
|
return await agent.post({
|
||||||
text,
|
text,
|
||||||
|
langs: ['en'],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -162,6 +163,7 @@ class Mocker {
|
||||||
return await agent.post({
|
return await agent.post({
|
||||||
text,
|
text,
|
||||||
embed: {$type: 'app.bsky.embed.record', record: {uri, cid}},
|
embed: {$type: 'app.bsky.embed.record', record: {uri, cid}},
|
||||||
|
langs: ['en'],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -178,6 +180,7 @@ class Mocker {
|
||||||
return await agent.post({
|
return await agent.post({
|
||||||
text,
|
text,
|
||||||
reply: {root: {uri, cid}, parent: {uri, cid}},
|
reply: {root: {uri, cid}, parent: {uri, cid}},
|
||||||
|
langs: ['en'],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.42.0",
|
"version": "1.43.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "is-ci || husky install",
|
"prepare": "is-ci || husky install",
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.4.3",
|
"@atproto/api": "^0.5.2",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@expo/html-elements": "^0.4.2",
|
"@expo/html-elements": "^0.4.2",
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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[]
|
||||||
|
}
|
|
@ -1,10 +1,19 @@
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
|
import {describeModerationCause} from '../moderation'
|
||||||
|
|
||||||
// \u2705 = ✅
|
// \u2705 = ✅
|
||||||
// \u2713 = ✓
|
// \u2713 = ✓
|
||||||
// \u2714 = ✔
|
// \u2714 = ✔
|
||||||
// \u2611 = ☑
|
// \u2611 = ☑
|
||||||
const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu
|
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') {
|
if (typeof str === 'string') {
|
||||||
return str.replace(CHECK_MARKS_RE, '').trim()
|
return str.replace(CHECK_MARKS_RE, '').trim()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {
|
||||||
AppBskyFeedPost as FeedPost,
|
AppBskyFeedPost as FeedPost,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
RichText,
|
RichText,
|
||||||
|
PostModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
|
|
||||||
import {PostsFeedItemModel} from '../feeds/post'
|
import {PostsFeedItemModel} from '../feeds/post'
|
||||||
|
|
||||||
type PostView = AppBskyFeedDefs.PostView
|
type PostView = AppBskyFeedDefs.PostView
|
||||||
|
@ -67,10 +67,6 @@ export class PostThreadItemModel {
|
||||||
return this.data.isThreadMuted
|
return this.data.isThreadMuted
|
||||||
}
|
}
|
||||||
|
|
||||||
get labelInfo(): PostLabelInfo {
|
|
||||||
return this.data.labelInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
get moderation(): PostModeration {
|
get moderation(): PostModeration {
|
||||||
return this.data.moderation
|
return this.data.moderation
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedGetPostThread as GetPostThread,
|
AppBskyFeedGetPostThread as GetPostThread,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
|
PostModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
|
@ -231,7 +232,6 @@ export class PostThreadModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pruneReplies(res.data.thread)
|
pruneReplies(res.data.thread)
|
||||||
sortThread(res.data.thread)
|
|
||||||
const thread = new PostThreadItemModel(
|
const thread = new PostThreadItemModel(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
||||||
|
@ -241,6 +241,7 @@ export class PostThreadModel {
|
||||||
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
||||||
thread.uri,
|
thread.uri,
|
||||||
)
|
)
|
||||||
|
sortThread(thread)
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,24 +263,28 @@ function pruneReplies(post: MaybePost) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortThread(post: MaybePost) {
|
type MaybeThreadItem =
|
||||||
if (post.notFound) {
|
| PostThreadItemModel
|
||||||
|
| AppBskyFeedDefs.NotFoundPost
|
||||||
|
| AppBskyFeedDefs.BlockedPost
|
||||||
|
function sortThread(item: MaybeThreadItem) {
|
||||||
|
if ('notFound' in item) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post = post as AppBskyFeedDefs.ThreadViewPost
|
item = item as PostThreadItemModel
|
||||||
if (post.replies) {
|
if (item.replies) {
|
||||||
post.replies.sort((a: MaybePost, b: MaybePost) => {
|
item.replies.sort((a: MaybeThreadItem, b: MaybeThreadItem) => {
|
||||||
post = post as AppBskyFeedDefs.ThreadViewPost
|
if ('notFound' in a && a.notFound) {
|
||||||
if (a.notFound) {
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if (b.notFound) {
|
if ('notFound' in b && b.notFound) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
a = a as AppBskyFeedDefs.ThreadViewPost
|
item = item as PostThreadItemModel
|
||||||
b = b as AppBskyFeedDefs.ThreadViewPost
|
a = a as PostThreadItemModel
|
||||||
const aIsByOp = a.post.author.did === post.post.author.did
|
b = b as PostThreadItemModel
|
||||||
const bIsByOp = b.post.author.did === post.post.author.did
|
const aIsByOp = a.post.author.did === item.post.author.did
|
||||||
|
const bIsByOp = b.post.author.did === item.post.author.did
|
||||||
if (aIsByOp && bIsByOp) {
|
if (aIsByOp && bIsByOp) {
|
||||||
return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
|
return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
|
||||||
} else if (aIsByOp) {
|
} else if (aIsByOp) {
|
||||||
|
@ -287,8 +292,31 @@ function sortThread(post: MaybePost) {
|
||||||
} else if (bIsByOp) {
|
} else if (bIsByOp) {
|
||||||
return 1 // op's own reply
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,18 +6,14 @@ import {
|
||||||
AppBskyActorGetProfile as GetProfile,
|
AppBskyActorGetProfile as GetProfile,
|
||||||
AppBskyActorProfile,
|
AppBskyActorProfile,
|
||||||
RichText,
|
RichText,
|
||||||
|
moderateProfile,
|
||||||
|
ProfileModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {FollowState} from '../cache/my-follows'
|
import {FollowState} from '../cache/my-follows'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
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'
|
import {track} from 'lib/analytics/analytics'
|
||||||
|
|
||||||
export class ProfileViewerModel {
|
export class ProfileViewerModel {
|
||||||
|
@ -26,7 +22,8 @@ export class ProfileViewerModel {
|
||||||
following?: string
|
following?: string
|
||||||
followedBy?: string
|
followedBy?: string
|
||||||
blockedBy?: boolean
|
blockedBy?: boolean
|
||||||
blocking?: string
|
blocking?: string;
|
||||||
|
[key: string]: unknown
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
@ -53,7 +50,8 @@ export class ProfileModel {
|
||||||
followsCount: number = 0
|
followsCount: number = 0
|
||||||
postsCount: number = 0
|
postsCount: number = 0
|
||||||
labels?: ComAtprotoLabelDefs.Label[] = undefined
|
labels?: ComAtprotoLabelDefs.Label[] = undefined
|
||||||
viewer = new ProfileViewerModel()
|
viewer = new ProfileViewerModel();
|
||||||
|
[key: string]: unknown
|
||||||
|
|
||||||
// added data
|
// added data
|
||||||
descriptionRichText?: RichText = new RichText({text: ''})
|
descriptionRichText?: RichText = new RichText({text: ''})
|
||||||
|
@ -85,18 +83,8 @@ export class ProfileModel {
|
||||||
return this.hasLoaded && !this.hasContent
|
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 {
|
get moderation(): ProfileModeration {
|
||||||
return getProfileModeration(this.rootStore, this.labelInfo)
|
return moderateProfile(this, this.rootStore.preferences.moderationOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// public api
|
// public api
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
AppBskyGraphGetFollows as GetFollows,
|
AppBskyGraphGetFollows as GetFollows,
|
||||||
|
moderateProfile,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import sampleSize from 'lodash.samplesize'
|
import sampleSize from 'lodash.samplesize'
|
||||||
|
@ -52,6 +53,13 @@ export class FoafsModel {
|
||||||
cursor,
|
cursor,
|
||||||
limit: 100,
|
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)
|
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
|
||||||
if (!res.data.cursor) {
|
if (!res.data.cursor) {
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
@ -69,7 +69,12 @@ export class SuggestedActorsModel {
|
||||||
limit: 25,
|
limit: 25,
|
||||||
cursor: this.loadMoreCursor,
|
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)
|
this.rootStore.me.follows.hydrateProfiles(actors)
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
AppBskyFeedLike,
|
AppBskyFeedLike,
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
ComAtprotoLabelDefs,
|
ComAtprotoLabelDefs,
|
||||||
|
moderatePost,
|
||||||
|
moderateProfile,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
|
@ -15,16 +17,6 @@ import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {PostThreadModel} from '../content/post-thread'
|
import {PostThreadModel} from '../content/post-thread'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
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 GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
@ -100,27 +92,19 @@ export class NotificationsFeedItemModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get labelInfo(): PostLabelInfo {
|
get shouldFilter(): boolean {
|
||||||
const addedInfo = this.additionalPost?.thread?.labelInfo
|
if (this.additionalPost?.thread) {
|
||||||
return {
|
const postMod = moderatePost(
|
||||||
postLabels: (this.labels || []).concat(addedInfo?.postLabels || []),
|
this.additionalPost.thread.data.post,
|
||||||
accountLabels: filterAccountLabels(this.author.labels).concat(
|
this.rootStore.preferences.moderationOpts,
|
||||||
addedInfo?.accountLabels || [],
|
)
|
||||||
),
|
return postMod.content.filter || false
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
const profileMod = moderateProfile(
|
||||||
|
this.author,
|
||||||
get moderation(): PostModeration {
|
this.rootStore.preferences.moderationOpts,
|
||||||
return getPostModeration(this.rootStore, this.labelInfo)
|
)
|
||||||
|
return profileMod.account.filter || false
|
||||||
}
|
}
|
||||||
|
|
||||||
get numUnreadInGroup(): number {
|
get numUnreadInGroup(): number {
|
||||||
|
@ -565,8 +549,7 @@ export class NotificationsFeedModel {
|
||||||
): NotificationsFeedItemModel[] {
|
): NotificationsFeedItemModel[] {
|
||||||
return items
|
return items
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
const hideByLabel =
|
const hideByLabel = item.shouldFilter
|
||||||
item.moderation.list.behavior === ModerationBehaviorCode.Hide
|
|
||||||
let mutedThread = !!(
|
let mutedThread = !!(
|
||||||
item.reasonSubjectRootUri &&
|
item.reasonSubjectRootUri &&
|
||||||
this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
|
this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
|
||||||
|
|
|
@ -3,21 +3,13 @@ import {
|
||||||
AppBskyFeedPost as FeedPost,
|
AppBskyFeedPost as FeedPost,
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
RichText,
|
RichText,
|
||||||
|
moderatePost,
|
||||||
|
PostModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {updateDataOptimistically} from 'lib/async/revertible'
|
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 {track} from 'lib/analytics/analytics'
|
||||||
|
import {hackAddDeletedEmbed} from 'lib/api/hack-add-deleted-embed'
|
||||||
|
|
||||||
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
||||||
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
|
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
|
||||||
|
@ -44,6 +36,7 @@ export class PostsFeedItemModel {
|
||||||
if (FeedPost.isRecord(this.post.record)) {
|
if (FeedPost.isRecord(this.post.record)) {
|
||||||
const valid = FeedPost.validateRecord(this.post.record)
|
const valid = FeedPost.validateRecord(this.post.record)
|
||||||
if (valid.success) {
|
if (valid.success) {
|
||||||
|
hackAddDeletedEmbed(this.post)
|
||||||
this.postRecord = this.post.record
|
this.postRecord = this.post.record
|
||||||
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,33 +79,8 @@ export class PostsFeedItemModel {
|
||||||
return this.rootStore.mutedThreads.uris.has(this.rootUri)
|
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 {
|
get moderation(): PostModeration {
|
||||||
return getPostModeration(this.rootStore, this.labelInfo)
|
return moderatePost(this.post, this.rootStore.preferences.moderationOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(v: FeedViewPost) {
|
copy(v: FeedViewPost) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
||||||
import {mergePostModerations} from 'lib/labeling/helpers'
|
|
||||||
import {PostsFeedItemModel} from './post'
|
import {PostsFeedItemModel} from './post'
|
||||||
|
|
||||||
let _idCounter = 0
|
let _idCounter = 0
|
||||||
|
@ -55,7 +54,20 @@ export class PostsFeedSliceModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get moderation() {
|
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) {
|
containsUri(uri: string) {
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
|
import {LabelPreference as APILabelPreference} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {RootStoreModel} from '../root-store'
|
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 {LabelValGroup} from 'lib/labeling/types'
|
||||||
import {getLabelValueGroup} from 'lib/labeling/helpers'
|
import {getLabelValueGroup} from 'lib/labeling/helpers'
|
||||||
import {
|
import {
|
||||||
|
@ -16,7 +21,8 @@ import {DEFAULT_FEEDS} from 'lib/constants'
|
||||||
import {isIOS, deviceLocales} from 'platform/detection'
|
import {isIOS, deviceLocales} from 'platform/detection'
|
||||||
import {LANGUAGES} from '../../../locale/languages'
|
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 = [
|
const LABEL_GROUPS = [
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'nudity',
|
'nudity',
|
||||||
|
@ -408,6 +414,43 @@ export class PreferencesModel {
|
||||||
return res
|
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[]) {
|
async setSavedFeeds(saved: string[], pinned: string[]) {
|
||||||
const oldSaved = this.savedFeeds
|
const oldSaved = this.savedFeeds
|
||||||
const oldPinned = this.pinnedFeeds
|
const oldPinned = this.pinnedFeeds
|
||||||
|
@ -485,3 +528,11 @@ export class PreferencesModel {
|
||||||
this.requireAltTextEnabled = !this.requireAltTextEnabled
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {AppBskyEmbedRecord} from '@atproto/api'
|
import {AppBskyEmbedRecord, ModerationUI} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {ProfileModel} from '../content/profile'
|
import {ProfileModel} from '../content/profile'
|
||||||
|
@ -42,6 +42,12 @@ export interface ServerInputModal {
|
||||||
onSelect: (url: string) => void
|
onSelect: (url: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModerationDetailsModal {
|
||||||
|
name: 'moderation-details'
|
||||||
|
context: 'account' | 'content'
|
||||||
|
moderation: ModerationUI
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReportPostModal {
|
export interface ReportPostModal {
|
||||||
name: 'report-post'
|
name: 'report-post'
|
||||||
postUri: string
|
postUri: string
|
||||||
|
@ -146,6 +152,7 @@ export type Modal =
|
||||||
| PreferencesHomeFeed
|
| PreferencesHomeFeed
|
||||||
|
|
||||||
// Moderation
|
// Moderation
|
||||||
|
| ModerationDetailsModal
|
||||||
| ReportAccountModal
|
| ReportAccountModal
|
||||||
| ReportPostModal
|
| ReportPostModal
|
||||||
| CreateOrEditMuteListModal
|
| CreateOrEditMuteListModal
|
||||||
|
|
|
@ -30,6 +30,7 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||||
import * as OnboardingModal from './OnboardingModal'
|
import * as OnboardingModal from './OnboardingModal'
|
||||||
|
import * as ModerationDetailsModal from './ModerationDetails'
|
||||||
|
|
||||||
const DEFAULT_SNAPPOINTS = ['90%']
|
const DEFAULT_SNAPPOINTS = ['90%']
|
||||||
|
|
||||||
|
@ -136,6 +137,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'onboarding') {
|
} else if (activeModal?.name === 'onboarding') {
|
||||||
snapPoints = OnboardingModal.snapPoints
|
snapPoints = OnboardingModal.snapPoints
|
||||||
element = <OnboardingModal.Component />
|
element = <OnboardingModal.Component />
|
||||||
|
} else if (activeModal?.name === 'moderation-details') {
|
||||||
|
snapPoints = ModerationDetailsModal.snapPoints
|
||||||
|
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
import * as OnboardingModal from './OnboardingModal'
|
import * as OnboardingModal from './OnboardingModal'
|
||||||
|
import * as ModerationDetailsModal from './ModerationDetails'
|
||||||
|
|
||||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||||
|
|
||||||
|
@ -110,6 +111,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <PreferencesHomeFeed.Component />
|
element = <PreferencesHomeFeed.Component />
|
||||||
} else if (modal.name === 'onboarding') {
|
} else if (modal.name === 'onboarding') {
|
||||||
element = <OnboardingModal.Component />
|
element = <OnboardingModal.Component />
|
||||||
|
} else if (modal.name === 'moderation-details') {
|
||||||
|
element = <ModerationDetailsModal.Component {...modal} />
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{' '}
|
||||||
|
<TextLink
|
||||||
|
type="2xl"
|
||||||
|
href={listUriToHref(list.uri)}
|
||||||
|
text={list.name}
|
||||||
|
style={pal.link}
|
||||||
|
/>{' '}
|
||||||
|
list which you have muted.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = moderation.cause.labelDef.strings[context].en.name
|
||||||
|
description = moderation.cause.labelDef.strings[context].en.description
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="moderationDetailsModal" style={[styles.container, pal.view]}>
|
||||||
|
<Text type="title-xl" style={[pal.text, styles.title]}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text type="2xl" style={[pal.text, styles.description]}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={styles.btn}
|
||||||
|
onPress={() => store.shell.closeModal()}>
|
||||||
|
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
|
||||||
|
Okay
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -7,7 +7,11 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {
|
||||||
|
AppBskyEmbedImages,
|
||||||
|
ProfileModeration,
|
||||||
|
moderateProfile,
|
||||||
|
} from '@atproto/api'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
|
@ -31,11 +35,6 @@ import {Link, TextLink} from '../util/Link'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
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 {formatCount} from '../util/numeric/format'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
|
||||||
|
@ -99,9 +98,9 @@ export const FeedItem = observer(function ({
|
||||||
handle: item.author.handle,
|
handle: item.author.handle,
|
||||||
displayName: item.author.displayName,
|
displayName: item.author.displayName,
|
||||||
avatar: item.author.avatar,
|
avatar: item.author.avatar,
|
||||||
moderation: getProfileModeration(
|
moderation: moderateProfile(
|
||||||
store,
|
item.author,
|
||||||
getProfileViewBasicLabelInfo(item.author),
|
store.preferences.moderationOpts,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...(item.additional?.map(({author}) => {
|
...(item.additional?.map(({author}) => {
|
||||||
|
@ -111,10 +110,7 @@ export const FeedItem = observer(function ({
|
||||||
handle: author.handle,
|
handle: author.handle,
|
||||||
displayName: author.displayName,
|
displayName: author.displayName,
|
||||||
avatar: author.avatar,
|
avatar: author.avatar,
|
||||||
moderation: getProfileModeration(
|
moderation: moderateProfile(author, store.preferences.moderationOpts),
|
||||||
store,
|
|
||||||
getProfileViewBasicLabelInfo(author),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}) || []),
|
}) || []),
|
||||||
]
|
]
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
import {PostHider} from '../util/moderation/PostHider'
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
import {ImageHider} from '../util/moderation/ImageHider'
|
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
@ -159,10 +159,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
|
|
||||||
if (item._isHighlightedPost) {
|
if (item._isHighlightedPost) {
|
||||||
return (
|
return (
|
||||||
<PostHider
|
<Link
|
||||||
testID={`postThreadItem-by-${item.post.author.handle}`}
|
testID={`postThreadItem-by-${item.post.author.handle}`}
|
||||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}>
|
||||||
moderation={item.moderation.thread}>
|
|
||||||
<PostSandboxWarning />
|
<PostSandboxWarning />
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
|
@ -227,7 +226,16 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||||
<ContentHider moderation={item.moderation.view}>
|
<ContentHider
|
||||||
|
moderation={item.moderation.content}
|
||||||
|
ignoreMute
|
||||||
|
style={styles.contentHider}
|
||||||
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
|
<PostAlerts
|
||||||
|
moderation={item.moderation.content}
|
||||||
|
includeMute
|
||||||
|
style={styles.alert}
|
||||||
|
/>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -242,9 +250,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<ImageHider moderation={item.moderation.view} style={s.mb10}>
|
{item.post.embed && (
|
||||||
|
<ContentHider moderation={item.moderation.embed} style={s.mb10}>
|
||||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||||
</ImageHider>
|
</ContentHider>
|
||||||
|
)}
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
<ExpandedPostDetails
|
<ExpandedPostDetails
|
||||||
post={item.post}
|
post={item.post}
|
||||||
|
@ -311,7 +321,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PostHider>
|
</Link>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -325,7 +335,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
pal.view,
|
pal.view,
|
||||||
item._showParentReplyLine && styles.noTopBorder,
|
item._showParentReplyLine && styles.noTopBorder,
|
||||||
]}
|
]}
|
||||||
moderation={item.moderation.thread}>
|
moderation={item.moderation.content}>
|
||||||
{item._showParentReplyLine && (
|
{item._showParentReplyLine && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -360,9 +370,10 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
/>
|
/>
|
||||||
<ContentHider
|
<PostAlerts
|
||||||
moderation={item.moderation.thread}
|
moderation={item.moderation.content}
|
||||||
containerStyle={styles.contentHider}>
|
style={styles.alert}
|
||||||
|
/>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View style={styles.postTextContainer}>
|
<View style={styles.postTextContainer}>
|
||||||
<RichText
|
<RichText
|
||||||
|
@ -373,9 +384,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<ImageHider style={s.mb10} moderation={item.moderation.thread}>
|
{item.post.embed && (
|
||||||
|
<ContentHider style={s.mb10} moderation={item.moderation.embed}>
|
||||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||||
</ImageHider>
|
</ContentHider>
|
||||||
|
)}
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<View style={[pal.borderDark, styles.translateLink]}>
|
<View style={[pal.borderDark, styles.translateLink]}>
|
||||||
<Link href={translatorUrl} title="Translate">
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
@ -385,7 +398,6 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ContentHider>
|
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
itemCid={itemCid}
|
itemCid={itemCid}
|
||||||
|
@ -515,6 +527,9 @@ const styles = StyleSheet.create({
|
||||||
paddingRight: 5,
|
paddingRight: 5,
|
||||||
maxWidth: 240,
|
maxWidth: 240,
|
||||||
},
|
},
|
||||||
|
alert: {
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
postTextContainer: {
|
postTextContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -531,7 +546,10 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
contentHider: {
|
contentHider: {
|
||||||
marginTop: 4,
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
contentHiderChild: {
|
||||||
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
expandedInfo: {
|
expandedInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
@ -19,9 +19,8 @@ import {UserInfoText} from '../util/UserInfoText'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
import {ImageHider} from '../util/moderation/ImageHider'
|
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
@ -206,10 +205,7 @@ const PostLoaded = observer(
|
||||||
}, [item, setDeleted, store])
|
}, [item, setDeleted, store])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostHider
|
<Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}>
|
||||||
href={itemHref}
|
|
||||||
style={[styles.outer, pal.view, pal.border, style]}
|
|
||||||
moderation={item.moderation.list}>
|
|
||||||
{showReplyLine && <View style={styles.replyLine} />}
|
{showReplyLine && <View style={styles.replyLine} />}
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
|
@ -251,8 +247,13 @@ const PostLoaded = observer(
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ContentHider
|
<ContentHider
|
||||||
moderation={item.moderation.list}
|
moderation={item.moderation.content}
|
||||||
containerStyle={styles.contentHider}>
|
style={styles.contentHider}
|
||||||
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
|
<PostAlerts
|
||||||
|
moderation={item.moderation.content}
|
||||||
|
style={styles.alert}
|
||||||
|
/>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View style={styles.postTextContainer}>
|
<View style={styles.postTextContainer}>
|
||||||
<RichText
|
<RichText
|
||||||
|
@ -264,9 +265,9 @@ const PostLoaded = observer(
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<ImageHider moderation={item.moderation.list} style={s.mb10}>
|
<ContentHider moderation={item.moderation.embed} style={s.mb10}>
|
||||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||||
</ImageHider>
|
</ContentHider>
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<View style={[pal.borderDark, styles.translateLink]}>
|
<View style={[pal.borderDark, styles.translateLink]}>
|
||||||
<Link href={translatorUrl} title="Translate">
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
@ -302,7 +303,7 @@ const PostLoaded = observer(
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PostHider>
|
</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -323,6 +324,9 @@ const styles = StyleSheet.create({
|
||||||
layoutContent: {
|
layoutContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
alert: {
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
postTextContainer: {
|
postTextContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -341,6 +345,9 @@ const styles = StyleSheet.create({
|
||||||
borderLeftColor: colors.gray2,
|
borderLeftColor: colors.gray2,
|
||||||
},
|
},
|
||||||
contentHider: {
|
contentHider: {
|
||||||
marginTop: 4,
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
contentHiderChild: {
|
||||||
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,16 +8,14 @@ import {
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {PostsFeedItemModel} from 'state/models/feeds/post'
|
import {PostsFeedItemModel} from 'state/models/feeds/post'
|
||||||
import {ModerationBehaviorCode} from 'lib/labeling/types'
|
|
||||||
import {Link, DesktopWebTextLink} from '../util/Link'
|
import {Link, DesktopWebTextLink} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
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 {RichText} from '../util/text/RichText'
|
||||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
@ -35,13 +33,11 @@ export const FeedItem = observer(function ({
|
||||||
item,
|
item,
|
||||||
isThreadChild,
|
isThreadChild,
|
||||||
isThreadParent,
|
isThreadParent,
|
||||||
ignoreMuteFor,
|
|
||||||
}: {
|
}: {
|
||||||
item: PostsFeedItemModel
|
item: PostsFeedItemModel
|
||||||
isThreadChild?: boolean
|
isThreadChild?: boolean
|
||||||
isThreadParent?: boolean
|
isThreadParent?: boolean
|
||||||
showReplyLine?: boolean
|
showReplyLine?: boolean
|
||||||
ignoreMuteFor?: string
|
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -147,26 +143,17 @@ export const FeedItem = observer(function ({
|
||||||
isThreadParent ? styles.outerNoBottom : undefined,
|
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) {
|
if (!record || deleted) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostHider
|
<Link
|
||||||
testID={`feedItem-by-${item.post.author.handle}`}
|
testID={`feedItem-by-${item.post.author.handle}`}
|
||||||
style={outerStyles}
|
style={outerStyles}
|
||||||
href={itemHref}
|
href={itemHref}
|
||||||
moderation={moderation}>
|
noFeedback
|
||||||
|
accessible={false}>
|
||||||
{isThreadChild && (
|
{isThreadChild && (
|
||||||
<View
|
<View
|
||||||
style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
|
style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
|
||||||
|
@ -255,8 +242,14 @@ export const FeedItem = observer(function ({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ContentHider
|
<ContentHider
|
||||||
moderation={moderation}
|
moderation={item.moderation.content}
|
||||||
containerStyle={styles.contentHider}>
|
ignoreMute
|
||||||
|
style={styles.contentHider}
|
||||||
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
|
<PostAlerts
|
||||||
|
moderation={item.moderation.content}
|
||||||
|
style={styles.alert}
|
||||||
|
/>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View style={styles.postTextContainer}>
|
<View style={styles.postTextContainer}>
|
||||||
<RichText
|
<RichText
|
||||||
|
@ -267,9 +260,11 @@ export const FeedItem = observer(function ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<ImageHider moderation={item.moderation.list} style={styles.embed}>
|
<ContentHider
|
||||||
|
moderation={item.moderation.embed}
|
||||||
|
style={styles.embed}>
|
||||||
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
||||||
</ImageHider>
|
</ContentHider>
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<View style={[pal.borderDark, styles.translateLink]}>
|
<View style={[pal.borderDark, styles.translateLink]}>
|
||||||
<Link href={translatorUrl} title="Translate">
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
@ -306,7 +301,7 @@ export const FeedItem = observer(function ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PostHider>
|
</Link>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -358,6 +353,10 @@ const styles = StyleSheet.create({
|
||||||
layoutContent: {
|
layoutContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
alert: {
|
||||||
|
marginTop: 6,
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
postTextContainer: {
|
postTextContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -365,7 +364,10 @@ const styles = StyleSheet.create({
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
},
|
},
|
||||||
contentHider: {
|
contentHider: {
|
||||||
marginTop: 4,
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
contentHiderChild: {
|
||||||
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
embed: {
|
embed: {
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
|
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
|
@ -7,21 +8,20 @@ import {Text} from '../util/text/Text'
|
||||||
import Svg, {Circle, Line} from 'react-native-svg'
|
import Svg, {Circle, Line} from 'react-native-svg'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {ModerationBehaviorCode} from 'lib/labeling/types'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
|
||||||
export function FeedSlice({
|
export const FeedSlice = observer(
|
||||||
|
({
|
||||||
slice,
|
slice,
|
||||||
ignoreMuteFor,
|
ignoreFilterFor,
|
||||||
}: {
|
}: {
|
||||||
slice: PostsFeedSliceModel
|
slice: PostsFeedSliceModel
|
||||||
ignoreMuteFor?: string
|
ignoreFilterFor?: string
|
||||||
}) {
|
}) => {
|
||||||
if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
|
if (slice.shouldFilter(ignoreFilterFor)) {
|
||||||
if (!ignoreMuteFor && !slice.moderation.list.noOverride) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (slice.isThread && slice.items.length > 3) {
|
if (slice.isThread && slice.items.length > 3) {
|
||||||
const last = slice.items.length - 1
|
const last = slice.items.length - 1
|
||||||
return (
|
return (
|
||||||
|
@ -31,14 +31,12 @@ export function FeedSlice({
|
||||||
item={slice.items[0]}
|
item={slice.items[0]}
|
||||||
isThreadParent={slice.isThreadParentAt(0)}
|
isThreadParent={slice.isThreadParentAt(0)}
|
||||||
isThreadChild={slice.isThreadChildAt(0)}
|
isThreadChild={slice.isThreadChildAt(0)}
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
|
||||||
/>
|
/>
|
||||||
<FeedItem
|
<FeedItem
|
||||||
key={slice.items[1]._reactKey}
|
key={slice.items[1]._reactKey}
|
||||||
item={slice.items[1]}
|
item={slice.items[1]}
|
||||||
isThreadParent={slice.isThreadParentAt(1)}
|
isThreadParent={slice.isThreadParentAt(1)}
|
||||||
isThreadChild={slice.isThreadChildAt(1)}
|
isThreadChild={slice.isThreadChildAt(1)}
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
|
||||||
/>
|
/>
|
||||||
<ViewFullThread slice={slice} />
|
<ViewFullThread slice={slice} />
|
||||||
<FeedItem
|
<FeedItem
|
||||||
|
@ -46,7 +44,6 @@ export function FeedSlice({
|
||||||
item={slice.items[last]}
|
item={slice.items[last]}
|
||||||
isThreadParent={slice.isThreadParentAt(last)}
|
isThreadParent={slice.isThreadParentAt(last)}
|
||||||
isThreadChild={slice.isThreadChildAt(last)}
|
isThreadChild={slice.isThreadChildAt(last)}
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -60,12 +57,12 @@ export function FeedSlice({
|
||||||
item={item}
|
item={item}
|
||||||
isThreadParent={slice.isThreadParentAt(i)}
|
isThreadParent={slice.isThreadParentAt(i)}
|
||||||
isThreadChild={slice.isThreadChildAt(i)}
|
isThreadChild={slice.isThreadChildAt(i)}
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
|
function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {
|
||||||
|
AppBskyActorDefs,
|
||||||
|
moderateProfile,
|
||||||
|
ProfileModeration,
|
||||||
|
} from '@atproto/api'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
@ -11,12 +15,11 @@ import {useStores} from 'state/index'
|
||||||
import {FollowButton} from './FollowButton'
|
import {FollowButton} from './FollowButton'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
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 {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {
|
||||||
|
describeModerationCause,
|
||||||
|
getProfileModerationCauses,
|
||||||
|
} from 'lib/moderation'
|
||||||
|
|
||||||
export const ProfileCard = observer(
|
export const ProfileCard = observer(
|
||||||
({
|
({
|
||||||
|
@ -25,7 +28,6 @@ export const ProfileCard = observer(
|
||||||
noBg,
|
noBg,
|
||||||
noBorder,
|
noBorder,
|
||||||
followers,
|
followers,
|
||||||
overrideModeration,
|
|
||||||
renderButton,
|
renderButton,
|
||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
|
@ -33,7 +35,6 @@ export const ProfileCard = observer(
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
overrideModeration?: boolean
|
|
||||||
renderButton?: (
|
renderButton?: (
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
profile: AppBskyActorDefs.ProfileViewBasic,
|
||||||
) => React.ReactNode
|
) => React.ReactNode
|
||||||
|
@ -41,18 +42,11 @@ export const ProfileCard = observer(
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
const moderation = getProfileModeration(
|
const moderation = moderateProfile(
|
||||||
store,
|
profile,
|
||||||
getProfileViewBasicLabelInfo(profile),
|
store.preferences.moderationOpts,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
moderation.list.behavior === ModerationBehaviorCode.Hide &&
|
|
||||||
!overrideModeration
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
@ -82,20 +76,17 @@ export const ProfileCard = observer(
|
||||||
lineHeight={1.2}>
|
lineHeight={1.2}>
|
||||||
{sanitizeDisplayName(
|
{sanitizeDisplayName(
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.profile,
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
{sanitizeHandle(profile.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
{!!profile.viewer?.followedBy && (
|
<ProfileCardPills
|
||||||
<View style={s.flexRow}>
|
followedBy={!!profile.viewer?.followedBy}
|
||||||
<View style={[s.mt5, pal.btn, styles.pill]}>
|
moderation={moderation}
|
||||||
<Text type="xs" style={pal.text}>
|
/>
|
||||||
Follows You
|
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
{renderButton ? (
|
{renderButton ? (
|
||||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||||
|
@ -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 (
|
||||||
|
<View style={styles.pills}>
|
||||||
|
{followedBy && (
|
||||||
|
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||||
|
<Text type="xs" style={pal.text}>
|
||||||
|
Follows You
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{causes.map(cause => {
|
||||||
|
const desc = describeModerationCause(cause, 'account')
|
||||||
|
return (
|
||||||
|
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||||
|
<Text type="xs" style={pal.text}>
|
||||||
|
{cause?.type === 'label' ? '⚠' : ''}
|
||||||
|
{desc.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const FollowersList = observer(
|
const FollowersList = observer(
|
||||||
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
|
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -125,9 +154,9 @@ const FollowersList = observer(
|
||||||
const followersWithMods = followers
|
const followersWithMods = followers
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
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 (
|
return (
|
||||||
<View style={styles.followedBy}>
|
<View style={styles.followedBy}>
|
||||||
|
@ -218,6 +247,12 @@ const styles = StyleSheet.create({
|
||||||
paddingRight: 10,
|
paddingRight: 10,
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
},
|
},
|
||||||
|
pills: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
columnGap: 6,
|
||||||
|
rowGap: 2,
|
||||||
|
},
|
||||||
pill: {
|
pill: {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
|
|
|
@ -21,15 +21,13 @@ import * as Toast from '../util/Toast'
|
||||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {ThemedText} from '../util/text/ThemedText'
|
import {ThemedText} from '../util/text/ThemedText'
|
||||||
import {TextLink} from '../util/Link'
|
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
|
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {listUriToHref} from 'lib/strings/url-helpers'
|
|
||||||
import {isDesktopWeb, isNative} from 'platform/detection'
|
import {isDesktopWeb, isNative} from 'platform/detection'
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
import {FollowState} from 'state/models/cache/my-follows'
|
||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from 'lib/sharing'
|
||||||
|
@ -116,7 +114,10 @@ const ProfileHeaderLoaded = observer(
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
const onPressAvi = React.useCallback(() => {
|
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.shell.openLightbox(new ProfileImageLightbox(view))
|
||||||
}
|
}
|
||||||
}, [store, view])
|
}, [store, view])
|
||||||
|
@ -434,6 +435,7 @@ const ProfileHeaderLoaded = observer(
|
||||||
style={[pal.text, styles.title]}>
|
style={[pal.text, styles.title]}>
|
||||||
{sanitizeDisplayName(
|
{sanitizeDisplayName(
|
||||||
view.displayName || sanitizeHandle(view.handle),
|
view.displayName || sanitizeHandle(view.handle),
|
||||||
|
view.moderation.profile,
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
@ -494,7 +496,9 @@ const ProfileHeaderLoaded = observer(
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{view.descriptionRichText ? (
|
{view.description &&
|
||||||
|
view.descriptionRichText &&
|
||||||
|
!view.moderation.profile.blur ? (
|
||||||
<RichText
|
<RichText
|
||||||
testID="profileHeaderDescription"
|
testID="profileHeaderDescription"
|
||||||
style={[styles.description, pal.text]}
|
style={[styles.description, pal.text]}
|
||||||
|
@ -504,52 +508,7 @@ const ProfileHeaderLoaded = observer(
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ProfileHeaderWarnings moderation={view.moderation.view} />
|
<ProfileHeaderAlerts moderation={view.moderation} />
|
||||||
<View style={styles.moderationLines}>
|
|
||||||
{view.viewer.blocking ? (
|
|
||||||
<View
|
|
||||||
testID="profileHeaderBlockedNotice"
|
|
||||||
style={[styles.moderationNotice, pal.viewLight]}>
|
|
||||||
<FontAwesomeIcon icon="ban" style={[pal.text]} />
|
|
||||||
<Text type="lg-medium" style={pal.text}>
|
|
||||||
Account blocked
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : view.viewer.muted ? (
|
|
||||||
<View
|
|
||||||
testID="profileHeaderMutedNotice"
|
|
||||||
style={[styles.moderationNotice, pal.viewLight]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'eye-slash']}
|
|
||||||
style={[pal.text]}
|
|
||||||
/>
|
|
||||||
<Text type="lg-medium" style={pal.text}>
|
|
||||||
Account muted{' '}
|
|
||||||
{view.viewer.mutedByList && (
|
|
||||||
<Text type="lg-medium" style={pal.text}>
|
|
||||||
by{' '}
|
|
||||||
<TextLink
|
|
||||||
type="lg-medium"
|
|
||||||
style={pal.link}
|
|
||||||
href={listUriToHref(view.viewer.mutedByList.uri)}
|
|
||||||
text={view.viewer.mutedByList.name}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{view.viewer.blockedBy && (
|
|
||||||
<View
|
|
||||||
testID="profileHeaderBlockedNotice"
|
|
||||||
style={[styles.moderationNotice, pal.viewLight]}>
|
|
||||||
<FontAwesomeIcon icon="ban" style={[pal.text]} />
|
|
||||||
<Text type="lg-medium" style={pal.text}>
|
|
||||||
This account has blocked you
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
{!isDesktopWeb && !hideBackButton && (
|
{!isDesktopWeb && !hideBackButton && (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
|
@ -693,19 +652,6 @@ const styles = StyleSheet.create({
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
moderationLines: {
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
|
|
||||||
moderationNotice: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 14,
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
|
|
||||||
br40: {borderRadius: 40},
|
br40: {borderRadius: 40},
|
||||||
br50: {borderRadius: 50},
|
br50: {borderRadius: 50},
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
|
||||||
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {HighPriorityImage} from 'view/com/util/images/Image'
|
import {HighPriorityImage} from 'view/com/util/images/Image'
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
|
@ -13,7 +14,6 @@ import {colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {AvatarModeration} from 'lib/labeling/types'
|
|
||||||
import {UserPreviewLink} from './UserPreviewLink'
|
import {UserPreviewLink} from './UserPreviewLink'
|
||||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ interface BaseUserAvatarProps {
|
||||||
type?: Type
|
type?: Type
|
||||||
size: number
|
size: number
|
||||||
avatar?: string | null
|
avatar?: string | null
|
||||||
moderation?: AvatarModeration
|
moderation?: ModerationUI
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserAvatarProps extends BaseUserAvatarProps {
|
interface UserAvatarProps extends BaseUserAvatarProps {
|
||||||
|
@ -213,20 +213,20 @@ export function UserAvatar({
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const warning = useMemo(() => {
|
const alert = useMemo(() => {
|
||||||
if (!moderation?.warn) {
|
if (!moderation?.alert) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={[styles.warningIconContainer, pal.view]}>
|
<View style={[styles.alertIconContainer, pal.view]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="exclamation-circle"
|
icon="exclamation-circle"
|
||||||
style={styles.warningIcon}
|
style={styles.alertIcon}
|
||||||
size={Math.floor(size / 3)}
|
size={Math.floor(size / 3)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [moderation?.warn, size, pal])
|
}, [moderation?.alert, size, pal])
|
||||||
|
|
||||||
// onSelectNewAvatar is only passed as prop on the EditProfile component
|
// onSelectNewAvatar is only passed as prop on the EditProfile component
|
||||||
return onSelectNewAvatar ? (
|
return onSelectNewAvatar ? (
|
||||||
|
@ -259,12 +259,12 @@ export function UserAvatar({
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar}}
|
||||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||||
/>
|
/>
|
||||||
{warning}
|
{alert}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={{width: size, height: size}}>
|
<View style={{width: size, height: size}}>
|
||||||
<DefaultAvatar type={type} size={size} />
|
<DefaultAvatar type={type} size={size} />
|
||||||
{warning}
|
{alert}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -289,13 +289,13 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: colors.gray5,
|
backgroundColor: colors.gray5,
|
||||||
},
|
},
|
||||||
warningIconContainer: {
|
alertIconContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
},
|
},
|
||||||
warningIcon: {
|
alertIcon: {
|
||||||
color: colors.red3,
|
color: colors.red3,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
|
@ -10,7 +11,6 @@ import {
|
||||||
useCameraPermission,
|
useCameraPermission,
|
||||||
} from 'lib/hooks/usePermissions'
|
} from 'lib/hooks/usePermissions'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {AvatarModeration} from 'lib/labeling/types'
|
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
||||||
|
@ -21,7 +21,7 @@ export function UserBanner({
|
||||||
onSelectNewBanner,
|
onSelectNewBanner,
|
||||||
}: {
|
}: {
|
||||||
banner?: string | null
|
banner?: string | null
|
||||||
moderation?: AvatarModeration
|
moderation?: ModerationUI
|
||||||
onSelectNewBanner?: (img: RNImage | null) => void
|
onSelectNewBanner?: (img: RNImage | null) => void
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {addStyle} from 'lib/styles'
|
import {InfoCircleIcon} from 'lib/icons'
|
||||||
import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
|
import {describeModerationCause} from 'lib/moderation'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
export function ContentHider({
|
export function ContentHider({
|
||||||
testID,
|
testID,
|
||||||
moderation,
|
moderation,
|
||||||
|
ignoreMute,
|
||||||
style,
|
style,
|
||||||
containerStyle,
|
childContainerStyle,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
testID?: string
|
testID?: string
|
||||||
moderation: ModerationBehavior
|
moderation: ModerationUI
|
||||||
|
ignoreMute?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
containerStyle?: StyleProp<ViewStyle>
|
childContainerStyle?: StyleProp<ViewStyle>
|
||||||
}>) {
|
}>) {
|
||||||
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const onPressShow = React.useCallback(() => {
|
|
||||||
setOverride(true)
|
|
||||||
}, [setOverride])
|
|
||||||
const onPressHide = React.useCallback(() => {
|
|
||||||
setOverride(false)
|
|
||||||
}, [setOverride])
|
|
||||||
|
|
||||||
if (
|
if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
|
||||||
moderation.behavior === ModerationBehaviorCode.Show ||
|
|
||||||
moderation.behavior === ModerationBehaviorCode.Warn ||
|
|
||||||
moderation.behavior === ModerationBehaviorCode.WarnImages
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -38,73 +34,61 @@ export function ContentHider({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
const desc = describeModerationCause(moderation.cause, 'content')
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, pal.view, pal.border, containerStyle]}>
|
<View testID={testID} style={style}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={override ? onPressHide : onPressShow}
|
onPress={() => {
|
||||||
accessibilityLabel={override ? 'Hide post' : 'Show post'}
|
if (!moderation.noOverride) {
|
||||||
// TODO: The text labelling should be split up so controls have unique roles
|
setOverride(v => !v)
|
||||||
accessibilityHint={
|
|
||||||
override
|
|
||||||
? 'Re-hide post'
|
|
||||||
: 'Shows post hidden based on your moderation settings'
|
|
||||||
}
|
}
|
||||||
style={[
|
}}
|
||||||
styles.description,
|
accessibilityRole="button"
|
||||||
pal.viewLight,
|
accessibilityHint={override ? 'Hide the content' : 'Show the content'}
|
||||||
override && styles.descriptionOpen,
|
accessibilityLabel=""
|
||||||
]}>
|
style={[styles.cover, pal.viewLight]}>
|
||||||
<Text type="md" style={pal.textLight}>
|
<Pressable
|
||||||
{moderation.reason || 'Content warning'}
|
onPress={() => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'moderation-details',
|
||||||
|
context: 'content',
|
||||||
|
moderation,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Learn more about this warning"
|
||||||
|
accessibilityHint="">
|
||||||
|
<InfoCircleIcon size={18} style={pal.text} />
|
||||||
|
</Pressable>
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
{desc.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!moderation.noOverride && (
|
||||||
<View style={styles.showBtn}>
|
<View style={styles.showBtn}>
|
||||||
<Text type="md-medium" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
{override ? 'Hide' : 'Show'}
|
{override ? 'Hide' : 'Show'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
|
||||||
{override && (
|
|
||||||
<View style={[styles.childrenContainer, pal.border]}>
|
|
||||||
<View testID={testID} style={addStyle(style, styles.child)}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
|
</Pressable>
|
||||||
|
{override && <View style={childContainerStyle}>{children}</View>}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
cover: {
|
||||||
marginBottom: 10,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 4,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
paddingLeft: 14,
|
paddingLeft: 14,
|
||||||
paddingRight: 18,
|
paddingRight: isDesktopWeb ? 18 : 22,
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
descriptionOpen: {
|
|
||||||
borderBottomLeftRadius: 0,
|
|
||||||
borderBottomRightRadius: 0,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
},
|
||||||
showBtn: {
|
showBtn: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
childrenContainer: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingTop: 8,
|
|
||||||
},
|
|
||||||
child: {},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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<ViewStyle>
|
|
||||||
}>) {
|
|
||||||
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 (
|
|
||||||
<View testID={testID} style={style}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View testID={testID} style={style}>
|
|
||||||
<View style={[styles.cover, pal.viewLight]}>
|
|
||||||
<Pressable
|
|
||||||
onPress={onPressToggle}
|
|
||||||
style={[styles.toggleBtn]}
|
|
||||||
accessibilityLabel="Show image"
|
|
||||||
accessibilityHint="">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={override ? 'eye' : ['far', 'eye-slash']}
|
|
||||||
size={24}
|
|
||||||
style={pal.text as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
<Text type="lg" style={pal.text}>
|
|
||||||
{moderation.reason || 'Content warning'}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.flex1} />
|
|
||||||
<Text type="xl-bold" style={pal.link}>
|
|
||||||
{override ? 'Hide' : 'Show'}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
{override && children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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<ViewStyle>
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'moderation-details',
|
||||||
|
context: 'content',
|
||||||
|
moderation,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Learn more about this warning"
|
||||||
|
accessibilityHint=""
|
||||||
|
style={[styles.container, pal.viewLight, style]}>
|
||||||
|
<InfoCircleIcon style={pal.text} size={18} />
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
{desc.name}
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||||
|
Learn More
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingLeft: 14,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
learnMoreBtn: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,17 +1,20 @@
|
||||||
import React, {ComponentProps} from 'react'
|
import React, {ComponentProps} from 'react'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, Pressable, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Link} from '../Link'
|
import {Link} from '../Link'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {addStyle} from 'lib/styles'
|
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<typeof Link> {
|
interface Props extends ComponentProps<typeof Link> {
|
||||||
// testID?: string
|
// testID?: string
|
||||||
// href?: string
|
// href?: string
|
||||||
// style: StyleProp<ViewStyle>
|
// style: StyleProp<ViewStyle>
|
||||||
moderation: ModerationBehavior
|
moderation: ModerationUI
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostHider({
|
export function PostHider({
|
||||||
|
@ -22,50 +25,11 @@ export function PostHider({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const bg = override ? pal.viewLight : pal.view
|
|
||||||
|
|
||||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
if (!moderation.blur) {
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moderation.behavior === ModerationBehaviorCode.Warn) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={[styles.description, bg, pal.border]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'eye-slash']}
|
|
||||||
style={[styles.icon, pal.text]}
|
|
||||||
/>
|
|
||||||
<Text type="md" style={pal.textLight}>
|
|
||||||
{moderation.reason || 'Content warning'}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.showBtn}
|
|
||||||
onPress={() => setOverride(v => !v)}
|
|
||||||
accessibilityRole="button">
|
|
||||||
<Text type="md" style={pal.link}>
|
|
||||||
{override ? 'Hide' : 'Show'} post
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
{override && (
|
|
||||||
<View style={[styles.childrenContainer, pal.border, bg]}>
|
|
||||||
<Link
|
|
||||||
testID={testID}
|
|
||||||
style={addStyle(style, styles.child)}
|
|
||||||
href={href}
|
|
||||||
noFeedback>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: any further label enforcement should occur in ContentContainer
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
@ -79,26 +43,77 @@ export function PostHider({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const desc = describeModerationCause(moderation.cause, 'content')
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
if (!moderation.noOverride) {
|
||||||
|
setOverride(v => !v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityHint={override ? 'Hide the content' : 'Show the content'}
|
||||||
|
accessibilityLabel=""
|
||||||
|
style={[styles.description, pal.viewLight]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'moderation-details',
|
||||||
|
context: 'content',
|
||||||
|
moderation,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Learn more about this warning"
|
||||||
|
accessibilityHint="">
|
||||||
|
<InfoCircleIcon size={18} style={pal.text} />
|
||||||
|
</Pressable>
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
{desc.name}
|
||||||
|
</Text>
|
||||||
|
{!moderation.noOverride && (
|
||||||
|
<Text type="xl" style={[styles.showBtn, pal.link]}>
|
||||||
|
{override ? 'Hide' : 'Show'}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
{override && (
|
||||||
|
<View style={[styles.childrenContainer, pal.border, pal.viewLight]}>
|
||||||
|
<Link
|
||||||
|
testID={testID}
|
||||||
|
style={addStyle(style, styles.child)}
|
||||||
|
href={href}
|
||||||
|
noFeedback>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
description: {
|
description: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 18,
|
paddingLeft: 18,
|
||||||
borderTopWidth: 1,
|
paddingRight: isDesktopWeb ? 18 : 22,
|
||||||
},
|
marginTop: 1,
|
||||||
icon: {
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
},
|
||||||
showBtn: {
|
showBtn: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
childrenContainer: {
|
childrenContainer: {
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 4,
|
||||||
paddingBottom: 6,
|
paddingBottom: 6,
|
||||||
},
|
},
|
||||||
child: {
|
child: {
|
||||||
borderWidth: 1,
|
borderWidth: 0,
|
||||||
borderRadius: 12,
|
borderTopWidth: 0,
|
||||||
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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<ViewStyle>
|
||||||
|
}) {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
|
||||||
|
const causes = getProfileModerationCauses(moderation)
|
||||||
|
if (!causes.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.grid}>
|
||||||
|
{causes.map(cause => {
|
||||||
|
const desc = describeModerationCause(cause, 'account')
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
testID="profileHeaderAlert"
|
||||||
|
key={desc.name}
|
||||||
|
onPress={() => {
|
||||||
|
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]}>
|
||||||
|
<InfoCircleIcon style={pal.text} size={24} />
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
{desc.name}
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||||
|
Learn More
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
grid: {
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
learnMoreBtn: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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 (
|
|
||||||
<View style={[styles.container, palErr.border, palErr.view]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="circle-exclamation"
|
|
||||||
style={palErr.text as FontAwesomeIconStyle}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<Text style={palErr.text}>
|
|
||||||
This account has been flagged: {moderation.reason}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 10,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,16 +1,24 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {Button} from '../forms/Button'
|
import {Button} from '../forms/Button'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
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({
|
export function ScreenHider({
|
||||||
testID,
|
testID,
|
||||||
|
@ -22,24 +30,17 @@ export function ScreenHider({
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
testID?: string
|
testID?: string
|
||||||
screenDescription: string
|
screenDescription: string
|
||||||
moderation: ModerationBehavior
|
moderation: ModerationUI
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
containerStyle?: StyleProp<ViewStyle>
|
containerStyle?: StyleProp<ViewStyle>
|
||||||
}>) {
|
}>) {
|
||||||
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
if (!moderation.blur || override) {
|
||||||
if (navigation.canGoBack()) {
|
|
||||||
navigation.goBack()
|
|
||||||
} else {
|
|
||||||
navigation.navigate('Home')
|
|
||||||
}
|
|
||||||
}, [navigation])
|
|
||||||
|
|
||||||
if (moderation.behavior !== ModerationBehaviorCode.Hide || override) {
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -47,6 +48,7 @@ export function ScreenHider({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const desc = describeModerationCause(moderation.cause, 'account')
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, pal.view, containerStyle]}>
|
<View style={[styles.container, pal.view, containerStyle]}>
|
||||||
<View style={styles.iconContainer}>
|
<View style={styles.iconContainer}>
|
||||||
|
@ -63,11 +65,38 @@ export function ScreenHider({
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
||||||
This {screenDescription} has been flagged:{' '}
|
This {screenDescription} has been flagged:{' '}
|
||||||
{moderation.reason || 'Content warning'}
|
<Text type="2xl-medium" style={pal.text}>
|
||||||
|
{desc.name}
|
||||||
|
</Text>
|
||||||
|
.{' '}
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
onPress={() => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'moderation-details',
|
||||||
|
context: 'account',
|
||||||
|
moderation,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Learn more about this warning"
|
||||||
|
accessibilityHint="">
|
||||||
|
<Text type="2xl" style={pal.link}>
|
||||||
|
Learn More
|
||||||
|
</Text>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
</Text>
|
</Text>
|
||||||
{!isDesktopWeb && <View style={styles.spacer} />}
|
{!isDesktopWeb && <View style={styles.spacer} />}
|
||||||
<View style={styles.btnContainer}>
|
<View style={styles.btnContainer}>
|
||||||
<Button type="inverted" onPress={onPressBack} style={styles.btn}>
|
<Button
|
||||||
|
type="inverted"
|
||||||
|
onPress={() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Home')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={styles.btn}>
|
||||||
<Text type="button-lg" style={pal.textInverted}>
|
<Text type="button-lg" style={pal.textInverted}>
|
||||||
Go back
|
Go back
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api'
|
import {
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyFeedPost,
|
||||||
|
AppBskyEmbedImages,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
} from '@atproto/api'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {PostMeta} from '../PostMeta'
|
import {PostMeta} from '../PostMeta'
|
||||||
import {Link} from '../Link'
|
import {Link} from '../Link'
|
||||||
|
@ -9,6 +14,55 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {ComposerOptsQuote} from 'state/models/ui/shell'
|
import {ComposerOptsQuote} from 'state/models/ui/shell'
|
||||||
import {PostEmbeds} from '.'
|
import {PostEmbeds} from '.'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {InfoCircleIcon} from 'lib/icons'
|
||||||
|
|
||||||
|
export function MaybeQuoteEmbed({
|
||||||
|
embed,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
embed: AppBskyEmbedRecord.View
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
if (
|
||||||
|
AppBskyEmbedRecord.isViewRecord(embed.record) &&
|
||||||
|
AppBskyFeedPost.isRecord(embed.record.value) &&
|
||||||
|
AppBskyFeedPost.validateRecord(embed.record.value).success
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<QuoteEmbed
|
||||||
|
quote={{
|
||||||
|
author: embed.record.author,
|
||||||
|
cid: embed.record.cid,
|
||||||
|
uri: embed.record.uri,
|
||||||
|
indexedAt: embed.record.indexedAt,
|
||||||
|
text: embed.record.value.text,
|
||||||
|
embeds: embed.record.embeds,
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||||
|
<InfoCircleIcon size={18} style={pal.text} />
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
Blocked
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||||
|
<InfoCircleIcon size={18} style={pal.text} />
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
Deleted
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export function QuoteEmbed({
|
export function QuoteEmbed({
|
||||||
quote,
|
quote,
|
||||||
|
@ -76,4 +130,14 @@ const styles = StyleSheet.create({
|
||||||
paddingLeft: 13,
|
paddingLeft: 13,
|
||||||
paddingRight: 8,
|
paddingRight: 8,
|
||||||
},
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 8,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
AppBskyFeedPost,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
@ -24,7 +23,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {YoutubeEmbed} from './YoutubeEmbed'
|
import {YoutubeEmbed} from './YoutubeEmbed'
|
||||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||||
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
|
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
|
||||||
import QuoteEmbed from './QuoteEmbed'
|
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||||
import {CustomFeedEmbed} from './CustomFeedEmbed'
|
import {CustomFeedEmbed} from './CustomFeedEmbed'
|
||||||
import {ListEmbed} from './ListEmbed'
|
import {ListEmbed} from './ListEmbed'
|
||||||
|
@ -49,25 +48,11 @@ export function PostEmbeds({
|
||||||
|
|
||||||
// quote post with media
|
// quote post with media
|
||||||
// =
|
// =
|
||||||
if (
|
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||||
AppBskyEmbedRecordWithMedia.isView(embed) &&
|
|
||||||
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
|
|
||||||
AppBskyFeedPost.isRecord(embed.record.record.value) &&
|
|
||||||
AppBskyFeedPost.validateRecord(embed.record.record.value).success
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.stackContainer, style]}>
|
<View style={[styles.stackContainer, style]}>
|
||||||
<PostEmbeds embed={embed.media} />
|
<PostEmbeds embed={embed.media} />
|
||||||
<QuoteEmbed
|
<MaybeQuoteEmbed embed={embed.record} />
|
||||||
quote={{
|
|
||||||
author: embed.record.record.author,
|
|
||||||
cid: embed.record.record.cid,
|
|
||||||
uri: embed.record.record.uri,
|
|
||||||
indexedAt: embed.record.record.indexedAt,
|
|
||||||
text: embed.record.record.value.text,
|
|
||||||
embeds: embed.record.record.embeds,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,25 +60,7 @@ export function PostEmbeds({
|
||||||
// quote post
|
// quote post
|
||||||
// =
|
// =
|
||||||
if (AppBskyEmbedRecord.isView(embed)) {
|
if (AppBskyEmbedRecord.isView(embed)) {
|
||||||
if (
|
return <MaybeQuoteEmbed embed={embed} style={style} />
|
||||||
AppBskyEmbedRecord.isViewRecord(embed.record) &&
|
|
||||||
AppBskyFeedPost.isRecord(embed.record.value) &&
|
|
||||||
AppBskyFeedPost.validateRecord(embed.record.value).success
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<QuoteEmbed
|
|
||||||
quote={{
|
|
||||||
author: embed.record.author,
|
|
||||||
cid: embed.record.cid,
|
|
||||||
uri: embed.record.uri,
|
|
||||||
indexedAt: embed.record.indexedAt,
|
|
||||||
text: embed.record.value.text,
|
|
||||||
embeds: embed.record.embeds,
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// image embed
|
// image embed
|
||||||
|
|
|
@ -66,7 +66,6 @@ export const ModerationBlockedAccounts = withAuthRequired(
|
||||||
testID={`blockedAccount-${index}`}
|
testID={`blockedAccount-${index}`}
|
||||||
key={item.did}
|
key={item.did}
|
||||||
profile={item}
|
profile={item}
|
||||||
overrideModeration
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -63,7 +63,6 @@ export const ModerationMutedAccounts = withAuthRequired(
|
||||||
testID={`mutedAccount-${index}`}
|
testID={`mutedAccount-${index}`}
|
||||||
key={item.did}
|
key={item.did}
|
||||||
profile={item}
|
profile={item}
|
||||||
overrideModeration
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -232,7 +232,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
)
|
)
|
||||||
} else if (item instanceof PostsFeedSliceModel) {
|
} else if (item instanceof PostsFeedSliceModel) {
|
||||||
return (
|
return (
|
||||||
<FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
|
<FeedSlice slice={item} ignoreFilterFor={uiState.profile.did} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
testID="profileView"
|
testID="profileView"
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
screenDescription="profile"
|
screenDescription="profile"
|
||||||
moderation={uiState.profile.moderation.view}>
|
moderation={uiState.profile.moderation.account}>
|
||||||
{uiState.profile.hasError ? (
|
{uiState.profile.hasError ? (
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
testID="profileErrorScreen"
|
testID="profileErrorScreen"
|
||||||
|
|
|
@ -40,10 +40,10 @@
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
|
|
||||||
"@atproto/api@^0.4.3":
|
"@atproto/api@^0.5.2":
|
||||||
version "0.4.3"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.4.3.tgz#d7e478bf7009df2adaf1ac6051eb3e3fea185c90"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.5.2.tgz#4ba5c57a8737092216e8274ef7ef63384f03d3c6"
|
||||||
integrity sha512-8LdREwmoA58YQDrLS0rohd7cHokhoiXfyYEeNtNlkdO0w/2QpkUCQ1PgPBP2kIRM9PhOEkKp7W3Sn8Te9Qq8jg==
|
integrity sha512-AAYz/IL52efZzaIx6Q8wV20MPK3/P5Dpm2qrFeh+rZHaP4KGPJaqLLYnK+SG9ZPQAHtvc18ZftNQJHB2CZsEbw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "*"
|
"@atproto/common-web" "*"
|
||||||
"@atproto/uri" "*"
|
"@atproto/uri" "*"
|
||||||
|
|
Loading…
Reference in New Issue