Add tags and mute words (#2968)
* Add bare minimum hashtags support (#2804) * Add bare minimum hashtags support As atproto/api already parses hashtags, this is as simple as hooking it up like link segments. This is "bare minimum" because: - Opening hashtag "#foo" is actually just a search for "foo" right now to work around #2491. - There is no integration in the composer. This hasn't stopped people from using hashtags already, and can be added later. - This change itself only had to hook things up - thank you for having already put the hashtag parsing in place. * Remove workaround for hash search not working now that it's fixed * Add RichTextTag and TagMenu * Sketch * Remove hackfix * Some cleanup * Sketch web * Mobile design * Mobile handling of tags search * Web only * Fix navigation woes * Use new callback * Hook it up * Integrate muted tags * Fix dropdown styles * Type error * Use close callback * Fix styles * Cleanup, install latest sdk * Quick muted words screen * Targets * Dir structure * Icons, list view * Move to dialog * Add removal confirmation * Swap copy * Improve checkboxees * Update matching, add tests * Moderate embeds * Create global dialogs concept again to prevent flashing * Add access from moderation screen * Highlight tags on native * Add web highlighting * Add close to web modal * Adjust close color * Rename toggles and adjust logic * Icon update * Load states * Improve regex * Improve regex * Improve regex * Revert link test * Hyphenated words * Improve matching * Enhance * Some tweaks * Muted words modal changes * Handle invalid handles, handle long tags * Remove main regex * Better test * Space/punct check drop to includes * Lowercase post text before comparison * Add better real world test case --------- Co-authored-by: Kisaragi Hiu <mail@kisaragi-hiu.com>
This commit is contained in:
parent
c8582924e2
commit
58aaad704a
49 changed files with 1983 additions and 39 deletions
578
src/lib/__tests__/moderatePost_wrapped.test.ts
Normal file
578
src/lib/__tests__/moderatePost_wrapped.test.ts
Normal file
|
@ -0,0 +1,578 @@
|
|||
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(
|
||||
[{value: 'outlineTag', targets: ['tag']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
['outlineTag'],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: inline tag`, () => {
|
||||
const rt = new RichText({
|
||||
text: `This is a post #inlineTag`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'inlineTag', targets: ['tag']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
['outlineTag'],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'inlineTag', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
['outlineTag'],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`no match: only tag targets`, () => {
|
||||
const rt = new RichText({
|
||||
text: `This is a post`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'inlineTag', targets: ['tag']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: '希', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`no match: long muted word, short post`, () => {
|
||||
const rt = new RichText({
|
||||
text: `hey`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'politics', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(false)
|
||||
})
|
||||
|
||||
it(`match: exact text`, () => {
|
||||
const rt = new RichText({
|
||||
text: `javascript`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'javascript', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'javascript', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`no match: partial word`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Use your brain, Eric`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'ai', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(false)
|
||||
})
|
||||
|
||||
it(`match: multiline`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Use your\n\tbrain, Eric`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: 'brain', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: :)`, () => {
|
||||
const rt = new RichText({
|
||||
text: `So happy :)`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: `:)`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'yay!', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: yay`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: 'yay', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'y!ppee', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
// single exclamation point, source has double
|
||||
it(`no match: y!ppee!`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: 'y!ppee!', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'S@assy', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: s@assy`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: 's@assy', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'new york times', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`!command`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Idk maybe a bot !command`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
it(`match: !command`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `!command`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: command`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `command`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`no match: !command`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Idk maybe a bot command`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
const match = hasMutedWord(
|
||||
[{value: `!command`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: `e/acc`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: acc`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `acc`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: `super-bad`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: super`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `super`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: super bad`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `super bad`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: superbad`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `superbad`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: `idk what this would be`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`no match: idk what this would be for`, () => {
|
||||
// extra word
|
||||
const match = hasMutedWord(
|
||||
[{value: `idk what this would be for`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(false)
|
||||
})
|
||||
|
||||
it(`match: idk`, () => {
|
||||
// extra word
|
||||
const match = hasMutedWord(
|
||||
[{value: `idk`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: idkwhatthiswouldbe`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `idkwhatthiswouldbe`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`parentheses`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Post with context(iykyk)`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
it(`match: context(iykyk)`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `context(iykyk)`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: context`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `context`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: iykyk`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `iykyk`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: (iykyk)`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `(iykyk)`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`🦋`, () => {
|
||||
const rt = new RichText({
|
||||
text: `Post with 🦋`,
|
||||
})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
|
||||
it(`match: 🦋`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: `🦋`, targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
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(
|
||||
[{value: 'stop worrying', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
|
||||
it(`match: turtles, or how`, () => {
|
||||
const match = hasMutedWord(
|
||||
[{value: 'turtles, or how', targets: ['content']}],
|
||||
rt.text,
|
||||
rt.facets,
|
||||
[],
|
||||
)
|
||||
|
||||
expect(match).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue