3p moderation services [WIP] (#2550)

* Add modservice screen and profile-header-card

* Drop the guidelines for now

* Remove ununsed constants

* Add label & label group descriptions

* Not found state

* Reorg, add icon

* Subheader

* Header

* Complete header

* Clean up

* Add all groups

* Fix scroll view

* Dialogs side quest

* Remove log

* Add (WIP) debug mod page

* Dialog solution

* Add note

* Clean up and reorganize localized moderation strings

* Memoize

* Add example

* Add first ReportDialog screen

* Report dialog step 2

* Submit

* Integrate updates

* Move moderation screen

* Migrate buttons

* Migrate everything

* Rough sketch

* Fix types

* Update atoms values

* Abstract ModerationServiceCard

* Hook up data to settings page

* Handle subscription

* Rough enablement

* Rough enablement

* Some validation, fixes

* More work on the mod debug screen

* Hook up data

* Update invalidation

* Hook up data to ReportDialog

* Fix native error

* Refactor/rewrite the entire moderation-application system

* Fix toggles

* Add copyright and other option to report

* Handle reports on profile vs content

* Little cleanup

* Get post hiding back in gear

* Better loading flow on Mod screen

* Clean up Mod screen

* Clean up ProfileMod screen

* Handle muting correctly

* Update enablement on ProfileMod screen

* Improve Moderation screen and dialog

* Styling, handle disabled labelers

* Rework list of labels on own content

* Use moderateNotification()

* ReportDialog updates

* Fix button overflow

* Simplify the ProfileModerationService ui

* Mod screen design

* Move moderation card from the profile header to a tab

* Small tweaks to the moderation screen

* Enable toggle on mod page

* Add notifs to debugmod and dont filter notifs from followed users

* Add moderator-service profile view

* Wire up more of the modservice data to profiles

* A bunch of speculative non-working UI

* Cleanup: delete old code

* Update ModerationDetailsDialog

* Update ReportDialog

* Update LabelsOnMe dialog

* Handle ReportDialog load better

* Rename LabelsOnMeDialog, fix close

* Experiment to put labeling under a tab of a normal profile

* Moderator variation of profile

* Remove dead code and start moving toward latest modsdk

* Remove a bunch of now-dead label strings

* Update ModDebug to be a bit more intuitive and support custom labels

* Minor ui tweaks

* Improve consistency of display name blurring

* Fix profile-card warning rendering

* More debugmod UI tuning

* Update to use new labeler semantics

* Delete some dead code and do some refactoring

* Update profile to pull from labeler definition

* Implement new label config controls (wip)

* Tweak ui

* Implement preference controls on labelers

* Rework label pref ui

* Get moderation screen working

* Add asyncstorage query persistence

* Implement label handling

* Small cleanup

* Implement Likes dialog

* Fix: remove text outside of text element

* Cleanup

* Fix likes dialog on mobile

* Implement the label appeal flow

* Get report flow working again with temporarily fixed report options

* Update onboarding

* Enforce limit of ten labeler subscriptions

* Fix type errors

* Fix lint errors

* Improve types of RQ

* Some work on Likes dialog, needs discussion

* Bit of ReportDialog cleanup

* Replace non-single-path SVG

* Update nudity descriptions

* Update to use new sdk updates

* Add adult-content-enabled behavior to label config

* Use the default setting of custom labels

* Handle global moderation label prefs with the global settings

* Fix missing postAuthor

* Fix empty moderation page

* Add mutewords control back to Mod screen

* Tweak adult setting styles

* Remove deprecated global labels

* Handle underage users on mod screen

* Adjust font sizes

* Swap in RichText

* Like button improvements

* Tweaks to Labeler profile

* Design tweaks for mod pref dialog

* Add tertiary button color

* Switch moderation UIs to tertiary color

* Update mutewords and hiddenposts to use the new sdk

* Add test-environment mod authority

* Switch 'gore' to 'graphic-media'

* Move nudity out of the adult content control

* Remove focus styles from buttons - let the browser behavior handle it

* Fixes to the adult content age-gating in moderaiton

* Ditch tertiary button color, lighten secondary button

* Fix some colors

* Remove focused overrides from toggles

* Liked by screen

* Rework the moderationlabelpref

* Fix optimistic like

* Cleanup

* Change how onboarding handles adult content enabled/disabled

* Add special handling of the mod authorities

* Tweaks

* Update the default labeler avatar to a shield

* Add route to go server

* Avoid dups due to bad config

* Fix attrs

* Fix: dont try to detect link/label mismatches on post meta

* Correctly show the label behavior when adult content is disabled

* Readd the local hiddenPosts handling

* WIP

* Fix bad merge

* Conten hider design tweaks

* Fix text string breakage

* Adjust source text in ContentHider

* Fix link bug

* Design tweaks to ContentHider and ModDetailsDialog

* Adjust spacing of inform badges

* Adjust spacing of embeds in posts

* Style tweaks to post/profile alerts

* Labels on me and dialog

* Remove bad focus styles from post dropdown

* Better spacing solution

* Tune moderation UIs

* Moderation UI tweaks for mobile

* Move labelers query on Mod screen

* Update to use new SDK appLabelers semantics

* Implement report submission

* Replace the report modal entirely with the report dialog

* Add @ to mod details dialog handle

* Bump SDK package

* Remove silly type

* Add to AWS build CI

* Fix ToggleButton overflow

* Clean up ModServiceCard, rename to LabelingServiceCard

* Hackfix to translate gore labels to graphic-media

* Tune content hider sizing on web desktop

* Handle self labels

* Fix spacing below text-only posts

* Fix: send appeals to the right labeler

* Give mod page links interactive states

* Fix references

* Remove focus handling

* Remove remnant

* Remove the like count from the subscribed labeler listing

* Bump @atproto/api@0.11.1

* Remove extra @

* Fix: persist labels to local storage to reduce coverage gaps

* update dipendencies

* revert dipendencies

* Add some explainers on how blocking affects labelers

* Tweak copy

* Fix underline color in header

* Fix profile menu

* Handle card overflow

* Remove metrics from header

* Mute 'account' not 'user'

* Show metrics if self

* Show the labels tab on logged out view

* Fix bad merge

* Use purple theming on labelers

* Tighten space on LabelerCard

* Set staleTime to 6hrs for labeler details

* Memoize the memoizers

* Drop staleTime to 60s

* Move label defs into a context to reduce recomputes

* Submit view tweaks

* Move labeler fetch below auth

* Mitigation: hardcode the bluesky moderation labeler name

* Bump sdk

* Add missing translated string

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Add missing translated string

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Hailey's fix for incorrect profile tabs

Co-authored-by: Hailey <me@haileyok.com>

* Feedback

* Fix borders, add bottom space

* Hailey's fix pt 2

Co-authored-by: Hailey <me@haileyok.com>

* Fix post tabs

* Integrate feedback pt 1

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 2

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 3

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 4

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 5

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 6

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 7

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 8

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Format

* Integrate new bday modal

* Use public agent for getServices

* Update casing

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
Paul Frazee 2024-03-18 12:46:28 -07:00 committed by GitHub
parent d5ebbeb3fc
commit 20d463ff2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
165 changed files with 7034 additions and 5009 deletions

View file

@ -29,26 +29,20 @@ export const DEFAULT_PROD_FEEDS = {
export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
birthDate: new Date('2022-11-17'), // TODO(pwi)
adultContentEnabled: false,
feeds: {
saved: [],
pinned: [],
unpinned: [],
},
// labels are undefined until set by user
contentLabels: {
nsfw: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nsfw,
nudity: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nudity,
suggestive: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.suggestive,
gore: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.gore,
hate: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.hate,
spam: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.spam,
impersonation: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.impersonation,
moderationPrefs: {
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: [],
}

View file

@ -1,29 +1,27 @@
import {useMemo} from 'react'
import {useMemo, createContext, useContext} from 'react'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {
LabelPreference,
BskyFeedViewPreference,
ModerationOpts,
AppBskyActorDefs,
} from '@atproto/api'
import {track} from '#/lib/analytics/analytics'
import {getAge} from '#/lib/strings/time'
import {useSession, getAgent} from '#/state/session'
import {DEFAULT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
import {getAgent, useSession} from '#/state/session'
import {
ConfigurableLabelGroup,
UsePreferencesQueryResponse,
ThreadViewPreferences,
} from '#/state/queries/preferences/types'
import {temp__migrateLabelPref} from '#/state/queries/preferences/util'
import {
DEFAULT_HOME_FEED_PREFS,
DEFAULT_THREAD_VIEW_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation'
import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {saveLabelers} from '#/state/session/agent-config'
export * from '#/state/queries/preferences/types'
export * from '#/state/queries/preferences/moderation'
@ -44,6 +42,13 @@ export function usePreferencesQuery() {
return DEFAULT_LOGGED_OUT_PREFERENCES
} else {
const res = await agent.getPreferences()
// save to local storage to ensure there are labels on initial requests
saveLabelers(
agent.session.did,
res.moderationPrefs.labelers.map(l => l.did),
)
const preferences: UsePreferencesQueryResponse = {
...res,
feeds: {
@ -54,32 +59,6 @@ export function usePreferencesQuery() {
return !res.feeds.pinned?.includes(f)
}) || [],
},
// labels are undefined until set by user
contentLabels: {
nsfw: temp__migrateLabelPref(
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
),
nudity: temp__migrateLabelPref(
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
),
suggestive: temp__migrateLabelPref(
res.contentLabels?.suggestive ||
DEFAULT_LABEL_PREFERENCES.suggestive,
),
gore: temp__migrateLabelPref(
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
),
hate: temp__migrateLabelPref(
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
),
spam: temp__migrateLabelPref(
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
),
impersonation: temp__migrateLabelPref(
res.contentLabels?.impersonation ||
DEFAULT_LABEL_PREFERENCES.impersonation,
),
},
feedViewPrefs: {
...DEFAULT_HOME_FEED_PREFS,
...(res.feedViewPrefs.home || {}),
@ -96,25 +75,30 @@ export function usePreferencesQuery() {
})
}
// used in the moderation state devtool
export const moderationOptsOverrideContext = createContext<
ModerationOpts | undefined
>(undefined)
export function useModerationOpts() {
const override = useContext(moderationOptsOverrideContext)
const {currentAccount} = useSession()
const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => {
const {labelDefs} = useLabelDefinitions()
const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs
const opts = useMemo<ModerationOpts | undefined>(() => {
if (override) {
return override
}
if (!prefs.data) {
return
}
const moderationOpts = getModerationOpts({
userDid: currentAccount?.did || '',
preferences: prefs.data,
})
return {
...moderationOpts,
hiddenPosts,
mutedWords: prefs.data.mutedWords || [],
userDid: currentAccount?.did,
prefs: {...prefs.data.moderationPrefs, hiddenPosts: hiddenPosts || []},
labelDefs,
}
}, [currentAccount?.did, prefs.data, hiddenPosts])
}, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
return opts
}
@ -138,10 +122,32 @@ export function usePreferencesSetContentLabelMutation() {
return useMutation<
void,
unknown,
{labelGroup: ConfigurableLabelGroup; visibility: LabelPreference}
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
>({
mutationFn: async ({labelGroup, visibility}) => {
await getAgent().setContentLabelPref(labelGroup, visibility)
mutationFn: async ({label, visibility, labelerDid}) => {
await getAgent().setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useSetContentLabelMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
label,
visibility,
labelerDid,
}: {
label: string
visibility: LabelPreference
labelerDid?: string
}) => {
await getAgent().setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,

View file

@ -1,181 +1,53 @@
import React from 'react'
import {
LabelPreference,
ComAtprotoLabelDefs,
ModerationOpts,
DEFAULT_LABEL_SETTINGS,
BskyAgent,
interpretLabelValueDefinitions,
} from '@atproto/api'
import {
LabelGroup,
ConfigurableLabelGroup,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
export type Label = ComAtprotoLabelDefs.Label
export type LabelGroupConfig = {
id: LabelGroup
title: string
isAdultImagery?: boolean
subtitle?: string
warning: string
values: string[]
}
export const DEFAULT_LABEL_PREFERENCES: Record<
ConfigurableLabelGroup,
LabelPreference
> = {
nsfw: 'hide',
nudity: 'warn',
suggestive: 'warn',
gore: 'warn',
hate: 'hide',
spam: 'hide',
impersonation: 'hide',
}
import {usePreferencesQuery} from './index'
import {useLabelersDetailedInfoQuery} from '../labeler'
/**
* More strict than our default settings for logged in users.
*
* TODO(pwi)
*/
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: Record<
ConfigurableLabelGroup,
LabelPreference
> = {
nsfw: 'hide',
nudity: 'hide',
suggestive: 'hide',
gore: 'hide',
hate: 'hide',
spam: 'hide',
impersonation: 'hide',
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS =
Object.fromEntries(
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
)
export function useMyLabelersQuery() {
const prefs = usePreferencesQuery()
const dids = Array.from(
new Set(
BskyAgent.appLabelers.concat(
prefs.data?.moderationPrefs.labelers.map(l => l.did) || [],
),
),
)
const labelers = useLabelersDetailedInfoQuery({dids})
const isLoading = prefs.isLoading || labelers.isLoading
const error = prefs.error || labelers.error
return React.useMemo(() => {
return {
isLoading,
error,
data: labelers.data,
}
}, [labelers, isLoading, error])
}
export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
id: 'illegal',
title: 'Illegal Content',
warning: 'Illegal Content',
values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
}
export const ALWAYS_FILTER_LABEL_GROUP: LabelGroupConfig = {
id: 'always-filter',
title: 'Content Warning',
warning: 'Content Warning',
values: ['!filter'],
}
export const ALWAYS_WARN_LABEL_GROUP: LabelGroupConfig = {
id: 'always-warn',
title: 'Content Warning',
warning: 'Content Warning',
values: ['!warn', 'account-security'],
}
export const UNKNOWN_LABEL_GROUP: LabelGroupConfig = {
id: 'unknown',
title: 'Unknown Label',
warning: 'Content Warning',
values: [],
}
export const CONFIGURABLE_LABEL_GROUPS: Record<
ConfigurableLabelGroup,
LabelGroupConfig
> = {
nsfw: {
id: 'nsfw',
title: 'Explicit Sexual Images',
subtitle: 'i.e. pornography',
warning: 'Sexually Explicit',
values: ['porn', 'nsfl'],
isAdultImagery: true,
},
nudity: {
id: 'nudity',
title: 'Other Nudity',
subtitle: 'Including non-sexual and artistic',
warning: 'Nudity',
values: ['nudity'],
isAdultImagery: true,
},
suggestive: {
id: 'suggestive',
title: 'Sexually Suggestive',
subtitle: 'Does not include nudity',
warning: 'Sexually Suggestive',
values: ['sexual'],
isAdultImagery: true,
},
gore: {
id: 'gore',
title: 'Violent / Bloody',
subtitle: 'Gore, self-harm, torture',
warning: 'Violence',
values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
isAdultImagery: true,
},
hate: {
id: 'hate',
title: 'Hate Group Iconography',
subtitle: 'Images of terror groups, articles covering events, etc.',
warning: 'Hate Groups',
values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
},
spam: {
id: 'spam',
title: 'Spam',
subtitle: 'Excessive unwanted interactions',
warning: 'Spam',
values: ['spam'],
},
impersonation: {
id: 'impersonation',
title: 'Impersonation',
subtitle: 'Accounts falsely claiming to be people or orgs',
warning: 'Impersonation',
values: ['impersonation'],
},
}
export function getModerationOpts({
userDid,
preferences,
}: {
userDid: string
preferences: UsePreferencesQueryResponse
}): ModerationOpts {
return {
userDid: userDid,
adultContentEnabled: preferences.adultContentEnabled,
labels: {
porn: preferences.contentLabels.nsfw,
sexual: preferences.contentLabels.suggestive,
nudity: preferences.contentLabels.nudity,
nsfl: preferences.contentLabels.gore,
corpse: preferences.contentLabels.gore,
gore: preferences.contentLabels.gore,
torture: preferences.contentLabels.gore,
'self-harm': preferences.contentLabels.gore,
'intolerant-race': preferences.contentLabels.hate,
'intolerant-gender': preferences.contentLabels.hate,
'intolerant-sexual-orientation': preferences.contentLabels.hate,
'intolerant-religion': preferences.contentLabels.hate,
intolerant: preferences.contentLabels.hate,
'icon-intolerant': preferences.contentLabels.hate,
spam: preferences.contentLabels.spam,
impersonation: preferences.contentLabels.impersonation,
scam: 'warn',
},
labelers: [
{
labeler: {
did: '',
displayName: 'Bluesky Social',
},
labels: {},
},
],
}
export function useLabelDefinitionsQuery() {
const labelers = useMyLabelersQuery()
return React.useMemo(() => {
return {
labelDefs: Object.fromEntries(
(labelers.data || []).map(labeler => [
labeler.creator.did,
interpretLabelValueDefinitions(labeler),
]),
),
labelers: labelers.data || [],
}
}, [labelers])
}

View file

@ -1,46 +1,13 @@
import {
BskyPreferences,
LabelPreference,
BskyThreadViewPreference,
BskyFeedViewPreference,
} from '@atproto/api'
export const configurableAdultLabelGroups = [
'nsfw',
'nudity',
'suggestive',
'gore',
] as const
export const configurableOtherLabelGroups = [
'hate',
'spam',
'impersonation',
] as const
export const configurableLabelGroups = [
...configurableAdultLabelGroups,
...configurableOtherLabelGroups,
] as const
export type ConfigurableLabelGroup = (typeof configurableLabelGroups)[number]
export type LabelGroup =
| ConfigurableLabelGroup
| 'illegal'
| 'always-filter'
| 'always-warn'
| 'unknown'
export type UsePreferencesQueryResponse = Omit<
BskyPreferences,
'contentLabels' | 'feedViewPrefs' | 'feeds'
> & {
/*
* Content labels previously included 'show', which has been deprecated in
* favor of 'ignore'. The API can return legacy data from the database, and
* we clean up the data in `usePreferencesQuery`.
*/
contentLabels: Record<ConfigurableLabelGroup, LabelPreference>
feedViewPrefs: BskyFeedViewPreference & {
lab_mergeFeedEnabled?: boolean
}

View file

@ -1,16 +0,0 @@
import {LabelPreference} from '@atproto/api'
/**
* Content labels previously included 'show', which has been deprecated in
* favor of 'ignore'. The API can return legacy data from the database, and
* we clean up the data in `usePreferencesQuery`.
*
* @deprecated
*/
export function temp__migrateLabelPref(
pref: LabelPreference | 'show',
): LabelPreference {
// @ts-ignore
if (pref === 'show') return 'ignore'
return pref
}