Update mutewords and hiddenposts to use the new sdk

This commit is contained in:
Paul Frazee
2024-03-09 12:45:26 -08:00
parent 2f8573b339
commit 68d4d4142c
11 changed files with 60 additions and 1104 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ export function TagMenu({
const displayTag = '#' + tag
const isMuted = Boolean(
(preferences?.mutedWords?.find(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
+1 -1
View File
@@ -50,7 +50,7 @@ export function TagMenu({
const {mutateAsync: removeMutedWord, variables: optimisticRemove} =
useRemoveMutedWordMutation()
const isMuted = Boolean(
(preferences?.mutedWords?.find(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
+2 -2
View File
@@ -233,8 +233,8 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
</Trans>
</Text>
</View>
) : preferences.mutedWords.length ? (
[...preferences.mutedWords]
) : preferences.moderationPrefs.mutedWords.length ? (
[...preferences.moderationPrefs.mutedWords]
.reverse()
.map((word, i) => (
<MutedWordRow
@@ -94,15 +94,12 @@ function ModerationDetailsDialogInner({
name = _(msg`Account Muted`)
description = _(msg`You have muted this user.`)
}
// TODO
// } else if (cause.type === 'muted-word') {
// return {
// icon: EyeSlash,
// name: _(msg`Post hidden by muted word`),
// description: _(
// msg`You've chosen to hide a word or tag within this post.`,
// ),
// }
} else if (modcause.type === 'mute-word') {
name = _(msg`Post Hidden by Muted Word`)
description = _(msg`You've chosen to hide a word or tag within this post.`)
} else if (modcause.type === 'hidden') {
name = _(msg`Post Hidden by You`)
description = _(msg`You have hidden this post.`)
} else if (modcause.type === 'label') {
name = desc.name
description = desc.description
@@ -1,692 +0,0 @@
import {describe, it, expect} from '@jest/globals'
import {RichText} from '@atproto/api'
import {hasMutedWord} from '../moderatePost_wrapped'
describe(`hasMutedWord`, () => {
describe(`tags`, () => {
it(`match: outline tag`, () => {
const rt = new RichText({
text: `This is a post #inlineTag`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'outlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: inline tag`, () => {
const rt = new RichText({
text: `This is a post #inlineTag`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: content target matches inline tag`, () => {
const rt = new RichText({
text: `This is a post #inlineTag`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`no match: only tag targets`, () => {
const rt = new RichText({
text: `This is a post`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
})
describe(`early exits`, () => {
it(`match: single character 希`, () => {
/**
* @see https://bsky.app/profile/mukuuji.bsky.social/post/3klji4fvsdk2c
*/
const rt = new RichText({
text: `改善希望です`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: '希', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`no match: long muted word, short post`, () => {
const rt = new RichText({
text: `hey`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'politics', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
it(`match: exact text`, () => {
const rt = new RichText({
text: `javascript`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'javascript', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`general content`, () => {
it(`match: word within post`, () => {
const rt = new RichText({
text: `This is a post about javascript`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'javascript', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`no match: partial word`, () => {
const rt = new RichText({
text: `Use your brain, Eric`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'ai', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
it(`match: multiline`, () => {
const rt = new RichText({
text: `Use your\n\tbrain, Eric`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: 'brain', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: :)`, () => {
const rt = new RichText({
text: `So happy :)`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: `:)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`punctuation semi-fuzzy`, () => {
describe(`yay!`, () => {
const rt = new RichText({
text: `We're federating, yay!`,
})
rt.detectFacetsWithoutResolution()
it(`match: yay!`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'yay!', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: yay`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'yay', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`y!ppee!!`, () => {
const rt = new RichText({
text: `We're federating, y!ppee!!`,
})
rt.detectFacetsWithoutResolution()
it(`match: y!ppee`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'y!ppee', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
// single exclamation point, source has double
it(`no match: y!ppee!`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'y!ppee!', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`Why so S@assy?`, () => {
const rt = new RichText({
text: `Why so S@assy?`,
})
rt.detectFacetsWithoutResolution()
it(`match: S@assy`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'S@assy', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: s@assy`, () => {
const match = hasMutedWord({
mutedWords: [{value: 's@assy', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`New York Times`, () => {
const rt = new RichText({
text: `New York Times`,
})
rt.detectFacetsWithoutResolution()
// case insensitive
it(`match: new york times`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'new york times', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`!command`, () => {
const rt = new RichText({
text: `Idk maybe a bot !command`,
})
rt.detectFacetsWithoutResolution()
it(`match: !command`, () => {
const match = hasMutedWord({
mutedWords: [{value: `!command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: command`, () => {
const match = hasMutedWord({
mutedWords: [{value: `command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`no match: !command`, () => {
const rt = new RichText({
text: `Idk maybe a bot command`,
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord({
mutedWords: [{value: `!command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
})
describe(`e/acc`, () => {
const rt = new RichText({
text: `I'm e/acc pilled`,
})
rt.detectFacetsWithoutResolution()
it(`match: e/acc`, () => {
const match = hasMutedWord({
mutedWords: [{value: `e/acc`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: acc`, () => {
const match = hasMutedWord({
mutedWords: [{value: `acc`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`super-bad`, () => {
const rt = new RichText({
text: `I'm super-bad`,
})
rt.detectFacetsWithoutResolution()
it(`match: super-bad`, () => {
const match = hasMutedWord({
mutedWords: [{value: `super-bad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: super`, () => {
const match = hasMutedWord({
mutedWords: [{value: `super`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: super bad`, () => {
const match = hasMutedWord({
mutedWords: [{value: `super bad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: superbad`, () => {
const match = hasMutedWord({
mutedWords: [{value: `superbad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
})
describe(`idk_what_this_would_be`, () => {
const rt = new RichText({
text: `Weird post with idk_what_this_would_be`,
})
rt.detectFacetsWithoutResolution()
it(`match: idk what this would be`, () => {
const match = hasMutedWord({
mutedWords: [{value: `idk what this would be`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`no match: idk what this would be for`, () => {
// extra word
const match = hasMutedWord({
mutedWords: [
{value: `idk what this would be for`, targets: ['content']},
],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
it(`match: idk`, () => {
// extra word
const match = hasMutedWord({
mutedWords: [{value: `idk`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: idkwhatthiswouldbe`, () => {
const match = hasMutedWord({
mutedWords: [{value: `idkwhatthiswouldbe`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(false)
})
})
describe(`parentheses`, () => {
const rt = new RichText({
text: `Post with context(iykyk)`,
})
rt.detectFacetsWithoutResolution()
it(`match: context(iykyk)`, () => {
const match = hasMutedWord({
mutedWords: [{value: `context(iykyk)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: context`, () => {
const match = hasMutedWord({
mutedWords: [{value: `context`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: iykyk`, () => {
const match = hasMutedWord({
mutedWords: [{value: `iykyk`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: (iykyk)`, () => {
const match = hasMutedWord({
mutedWords: [{value: `(iykyk)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
describe(`🦋`, () => {
const rt = new RichText({
text: `Post with 🦋`,
})
rt.detectFacetsWithoutResolution()
it(`match: 🦋`, () => {
const match = hasMutedWord({
mutedWords: [{value: `🦋`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
})
describe(`phrases`, () => {
describe(`I like turtles, or how I learned to stop worrying and love the internet.`, () => {
const rt = new RichText({
text: `I like turtles, or how I learned to stop worrying and love the internet.`,
})
rt.detectFacetsWithoutResolution()
it(`match: stop worrying`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'stop worrying', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`match: turtles, or how`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'turtles, or how', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
})
describe(`languages without spaces`, () => {
// I love turtles, or how I learned to stop worrying and love the internet
describe(`私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`, () => {
const rt = new RichText({
text: `私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`,
})
rt.detectFacetsWithoutResolution()
// internet
it(`match: インターネット`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'インターネット', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
languages: ['ja'],
isOwnPost: false,
})
expect(match).toBe(true)
})
})
})
describe(`doesn't mute own post`, () => {
it(`does mute if it isn't own post`, () => {
const rt = new RichText({
text: `Mute words!`,
})
const match = hasMutedWord({
mutedWords: [{value: 'words', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: false,
})
expect(match).toBe(true)
})
it(`doesn't mute own post when muted word is in text`, () => {
const rt = new RichText({
text: `Mute words!`,
})
const match = hasMutedWord({
mutedWords: [{value: 'words', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: true,
})
expect(match).toBe(false)
})
it(`doesn't mute own post when muted word is in tags`, () => {
const rt = new RichText({
text: `Mute #words!`,
})
const match = hasMutedWord({
mutedWords: [{value: 'words', targets: ['tags']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
isOwnPost: true,
})
expect(match).toBe(false)
})
})
})
+8 -377
View File
@@ -1,385 +1,16 @@
import {
// AppBskyEmbedRecord,
// AppBskyEmbedRecordWithMedia,
moderatePost,
AppBskyActorDefs,
// AppBskyFeedPost,
AppBskyRichtextFacet,
// AppBskyEmbedImages,
// AppBskyEmbedExternal,
} from '@atproto/api'
import {moderatePost} from '@atproto/api'
// TODO
// do we think we'll need this again?
// if not we can ditch it!
// -prf
type ModeratePost = typeof moderatePost
type Options = Parameters<ModeratePost>[1] & {
hiddenPosts?: string[]
mutedWords?: AppBskyActorDefs.MutedWord[]
}
const REGEX = {
LEADING_TRAILING_PUNCTUATION: /(?:^\p{P}+|\p{P}+$)/gu,
ESCAPE: /[[\]{}()*+?.\\^$|\s]/g,
SEPARATORS: /[\/\-\\—\(\)\[\]\_]+/g,
WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g,
}
/**
* List of 2-letter lang codes for languages that either don't use spaces, or
* don't use spaces in a way conducive to word-based filtering.
*
* For these, we use a simple `String.includes` to check for a match.
*/
const LANGUAGE_EXCEPTIONS = [
'ja', // Japanese
'zh', // Chinese
'ko', // Korean
'th', // Thai
'vi', // Vietnamese
]
export function hasMutedWord({
mutedWords,
text,
facets,
outlineTags,
languages,
isOwnPost,
}: {
mutedWords: AppBskyActorDefs.MutedWord[]
text: string
facets?: AppBskyRichtextFacet.Main[]
outlineTags?: string[]
languages?: string[]
isOwnPost: boolean
}) {
if (isOwnPost) return false
const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '')
const tags = ([] as string[])
.concat(outlineTags || [])
.concat(
facets
?.filter(facet => {
return facet.features.find(feature =>
AppBskyRichtextFacet.isTag(feature),
)
})
.map(t => t.features[0].tag as string) || [],
)
.map(t => t.toLowerCase())
for (const mute of mutedWords) {
const mutedWord = mute.value.toLowerCase()
const postText = text.toLowerCase()
// `content` applies to tags as well
if (tags.includes(mutedWord)) return true
// rest of the checks are for `content` only
if (!mute.targets.includes('content')) continue
// single character or other exception, has to use includes
if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord))
return true
// too long
if (mutedWord.length > postText.length) continue
// exact match
if (mutedWord === postText) return true
// any muted phrase with space or punctuation
if (/(?:\s|\p{P})+?/u.test(mutedWord) && postText.includes(mutedWord))
return true
// check individual character groups
const words = postText.split(REGEX.WORD_BOUNDARY)
for (const word of words) {
if (word === mutedWord) return true
// compare word without leading/trailing punctuation, but allow internal
// punctuation (such as `s@ssy`)
const wordTrimmedPunctuation = word.replace(
REGEX.LEADING_TRAILING_PUNCTUATION,
'',
)
if (mutedWord === wordTrimmedPunctuation) return true
if (mutedWord.length > wordTrimmedPunctuation.length) continue
// handle hyphenated, slash separated words, etc
if (REGEX.SEPARATORS.test(wordTrimmedPunctuation)) {
// check against full normalized phrase
const wordNormalizedSeparators = wordTrimmedPunctuation.replace(
REGEX.SEPARATORS,
' ',
)
const mutedWordNormalizedSeparators = mutedWord.replace(
REGEX.SEPARATORS,
' ',
)
// hyphenated (or other sep) to spaced words
if (wordNormalizedSeparators === mutedWordNormalizedSeparators)
return true
/* Disabled for now e.g. `super-cool` to `supercool`
const wordNormalizedCompressed = wordNormalizedSeparators.replace(
REGEX.WORD_BOUNDARY,
'',
)
const mutedWordNormalizedCompressed =
mutedWordNormalizedSeparators.replace(/\s+?/g, '')
// hyphenated (or other sep) to non-hyphenated contiguous word
if (mutedWordNormalizedCompressed === wordNormalizedCompressed)
return true
*/
// then individual parts of separated phrases/words
const wordParts = wordTrimmedPunctuation.split(REGEX.SEPARATORS)
for (const wp of wordParts) {
// still retain internal punctuation
if (wp === mutedWord) return true
}
}
}
}
return false
}
type Options = Parameters<ModeratePost>[1]
export function moderatePost_wrapped(
subject: Parameters<ModeratePost>[0],
opts: Options,
) {
const moderation = moderatePost(subject, opts)
moderation.addHidden(opts.hiddenPosts?.includes(subject.uri) || false)
return moderation
/* TODO
const {hiddenPosts = [], mutedWords = [], ...options} = opts
const moderations = moderatePost(subject, options)
const isOwnPost = subject.author.did === opts.userDid
if (hiddenPosts.includes(subject.uri)) {
moderations.content.filter = true
moderations.content.blur = true
if (!moderations.content.cause) {
moderations.content.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'post-hidden',
source: {type: 'user'},
priority: 1,
}
}
}
if (AppBskyFeedPost.isRecord(subject.record)) {
let muted = hasMutedWord({
mutedWords,
text: subject.record.text,
facets: subject.record.facets || [],
outlineTags: subject.record.tags || [],
languages: subject.record.langs,
isOwnPost,
})
if (
subject.record.embed &&
AppBskyEmbedImages.isMain(subject.record.embed)
) {
for (const image of subject.record.embed.images) {
muted =
muted ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: subject.record.langs,
isOwnPost,
})
}
}
if (muted) {
moderations.content.filter = true
moderations.content.blur = true
if (!moderations.content.cause) {
moderations.content.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'muted-word',
source: {type: 'user'},
priority: 1,
}
}
}
}
if (subject.embed) {
let embedHidden = false
let embedMuted = false
let externalMuted = false
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
embedHidden = hiddenPosts.includes(subject.embed.record.uri)
}
if (
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
) {
embedHidden = hiddenPosts.includes(subject.embed.record.record.uri)
}
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
const embeddedPost = subject.embed.record.value
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: embeddedPost.text,
facets: embeddedPost.facets,
outlineTags: embeddedPost.tags,
languages: embeddedPost.langs,
isOwnPost,
})
if (AppBskyEmbedImages.isMain(embeddedPost.embed)) {
for (const image of embeddedPost.embed.images) {
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: embeddedPost.langs,
isOwnPost,
})
}
}
if (AppBskyEmbedExternal.isMain(embeddedPost.embed)) {
const {external} = embeddedPost.embed
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: external.title + ' ' + external.description,
facets: [],
outlineTags: [],
languages: [],
isOwnPost,
})
}
if (AppBskyEmbedRecordWithMedia.isMain(embeddedPost.embed)) {
if (AppBskyEmbedExternal.isMain(embeddedPost.embed.media)) {
const {external} = embeddedPost.embed.media
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: external.title + ' ' + external.description,
facets: [],
outlineTags: [],
languages: [],
isOwnPost,
})
}
if (AppBskyEmbedImages.isMain(embeddedPost.embed.media)) {
for (const image of embeddedPost.embed.media.images) {
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: AppBskyFeedPost.isRecord(embeddedPost.record)
? embeddedPost.langs
: [],
isOwnPost,
})
}
}
}
}
}
if (AppBskyEmbedExternal.isView(subject.embed)) {
const {external} = subject.embed
externalMuted =
externalMuted ||
hasMutedWord({
mutedWords,
text: external.title + ' ' + external.description,
facets: [],
outlineTags: [],
languages: [],
isOwnPost,
})
}
if (
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
) {
if (AppBskyFeedPost.isRecord(subject.embed.record.record.value)) {
const post = subject.embed.record.record.value
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: post.text,
facets: post.facets,
outlineTags: post.tags,
languages: post.langs,
isOwnPost,
})
}
if (AppBskyEmbedImages.isView(subject.embed.media)) {
for (const image of subject.embed.media.images) {
embedMuted =
embedMuted ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: AppBskyFeedPost.isRecord(subject.record)
? subject.record.langs
: [],
isOwnPost,
})
}
}
}
if (embedHidden) {
moderations.embed.filter = true
moderations.embed.blur = true
if (!moderations.embed.cause) {
moderations.embed.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'post-hidden',
source: {type: 'user'},
priority: 1,
}
}
} else if (externalMuted || embedMuted) {
moderations.content.filter = true
moderations.content.blur = true
if (!moderations.content.cause) {
moderations.content.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'muted-word',
source: {type: 'user'},
priority: 1,
}
}
}
}
return moderations
*/
return moderatePost(subject, opts)
}
@@ -84,17 +84,15 @@ export function useModerationCauseDescription(
}
}
}
// TODO
// if (cause.type === 'muted-word') {
// return {
// icon: EyeSlash,
// name: _(msg`Post hidden by muted word`),
// description: _(
// msg`You've chosen to hide a word or tag within this post.`,
// ),
// }
// }
// @ts-ignore Temporary extension to the moderation system -prf
if (cause.type === 'mute-word') {
return {
icon: EyeSlash,
name: _(msg`Post Hidden by Muted Word`),
description: _(
msg`You've chosen to hide a word or tag within this post.`,
),
}
}
if (cause.type === 'hidden') {
return {
icon: EyeSlash,
+27
View File
@@ -25,6 +25,7 @@ import {
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {useLabelersDetailedInfoQuery} from '#/state/queries/labeler'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Divider} from '#/components/Divider'
@@ -34,6 +35,7 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import {Button} from '#/components/Button'
import {InlineLink, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {getModerationServiceTitle} from '#/lib/moderation'
@@ -132,10 +134,12 @@ export function ModerationScreenInner({
preferences: UsePreferencesQueryResponse
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
}) {
const {_} = useLingui()
const t = useTheme()
const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics()
const {gtMobile} = useBreakpoints()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
useFocusEffect(
React.useCallback(() => {
@@ -186,6 +190,29 @@ export function ModerationScreenInner({
a.overflow_hidden,
t.atoms.bg_contrast_25,
]}>
<Button
testID="mutewordsBtn"
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.p_lg,
a.gap_sm,
]}
label={_(msg`Muted words & tags`)}
onPress={() => mutedWordsDialogControl.open()}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<Group size="md" style={[t.atoms.text_contrast_medium]} />
<Text style={[a.text_md]}>
<Trans>Muted words & tags</Trans>
</Text>
</View>
<ChevronRight
size="sm"
style={[t.atoms.text_contrast_low, a.self_end]}
/>
</Button>
<Divider />
<Link
testID="moderationlistsBtn"
style={[
+2 -2
View File
@@ -38,11 +38,11 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
adultContentEnabled: false,
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
labelers: [],
mutedWords: [],
hiddenPosts: [],
},
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
userAge: 13, // TODO(pwi)
interests: {tags: []},
mutedWords: [],
hiddenPosts: [],
}
+2 -9
View File
@@ -20,7 +20,6 @@ import {
DEFAULT_LOGGED_OUT_PREFERENCES,
} from '#/state/queries/preferences/const'
import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
import {useLabelDefinitions} from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
@@ -78,7 +77,6 @@ export function useModerationOpts() {
const {currentAccount} = useSession()
const prefs = usePreferencesQuery()
const {labelDefs} = useLabelDefinitions()
const hiddenPosts = useHiddenPosts()
const opts = useMemo<ModerationOpts | undefined>(() => {
if (override) {
return override
@@ -86,17 +84,12 @@ export function useModerationOpts() {
if (!prefs.data) {
return
}
const moderationPrefs = prefs.data.moderationPrefs
return {
userDid: currentAccount?.did,
prefs: {
...moderationPrefs,
hiddenPosts,
},
mutedWords: prefs.data.mutedWords || [], // TODO
prefs: prefs.data.moderationPrefs,
labelDefs,
}
}, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
}, [override, currentAccount, labelDefs, prefs.data])
return opts
}
+2
View File
@@ -242,6 +242,8 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
labels: {[label[0]]: visibility[0] as LabelPreference},
},
],
mutedWords: [],
hiddenPosts: [],
},
labelDefs: {
'did:plc:fake-labeler': [