From 20d463ff2f5a112473f75a21595b3d89b8dfc0b0 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 18 Mar 2024 12:46:28 -0700 Subject: 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 * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey * 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 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey --- src/view/screens/DebugMod.tsx | 923 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 src/view/screens/DebugMod.tsx (limited to 'src/view/screens/DebugMod.tsx') diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx new file mode 100644 index 000000000..64f2376a4 --- /dev/null +++ b/src/view/screens/DebugMod.tsx @@ -0,0 +1,923 @@ +import React from 'react' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {View} from 'react-native' +import { + LABELS, + mock, + moderatePost, + moderateProfile, + ModerationOpts, + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyFeedPost, + LabelPreference, + ModerationDecision, + ModerationBehavior, + RichText, + ComAtprotoLabelDefs, + interpretLabelValueDefinition, +} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {moderationOptsOverrideContext} from '#/state/queries/preferences' +import {useSession} from '#/state/session' +import {FeedNotification} from '#/state/queries/notifications/types' +import { + groupNotifications, + shouldFilterNotif, +} from '#/state/queries/notifications/util' + +import {atoms as a, useTheme} from '#/alf' +import {CenteredView, ScrollView} from '#/view/com/util/Views' +import {H1, H3, P, Text} from '#/components/Typography' +import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' +import * as Toggle from '#/components/forms/Toggle' +import * as ToggleButton from '#/components/forms/ToggleButton' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import { + ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom, + ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, +} from '#/components/icons/Chevron' +import {ScreenHider} from '../../components/moderation/ScreenHider' +import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard' +import {ProfileCard} from '../com/profile/ProfileCard' +import {FeedItem} from '../com/posts/FeedItem' +import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem' +import {PostThreadItem} from '../com/post-thread/PostThreadItem' +import {Divider} from '#/components/Divider' + +const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys( + LABELS, +) as (keyof typeof LABELS)[] + +export const DebugModScreen = ({}: NativeStackScreenProps< + CommonNavigatorParams, + 'DebugMod' +>) => { + const t = useTheme() + const [scenario, setScenario] = React.useState(['label']) + const [scenarioSwitches, setScenarioSwitches] = React.useState([]) + const [label, setLabel] = React.useState([LABEL_VALUES[0]]) + const [target, setTarget] = React.useState(['account']) + const [visibility, setVisiblity] = React.useState(['warn']) + const [customLabelDef, setCustomLabelDef] = + React.useState({ + identifier: 'custom', + blurs: 'content', + severity: 'alert', + defaultSetting: 'warn', + locales: [ + { + lang: 'en', + name: 'Custom label', + description: 'A custom label created in this test environment', + }, + ], + }) + const [view, setView] = React.useState(['post']) + const labelStrings = useGlobalLabelStrings() + const {currentAccount} = useSession() + + const isTargetMe = + scenario[0] === 'label' && scenarioSwitches.includes('targetMe') + const isSelfLabel = + scenario[0] === 'label' && scenarioSwitches.includes('selfLabel') + const noAdult = + scenario[0] === 'label' && scenarioSwitches.includes('noAdult') + const isLoggedOut = + scenario[0] === 'label' && scenarioSwitches.includes('loggedOut') + const isFollowing = scenarioSwitches.includes('following') + + const did = + isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test' + + const profile = React.useMemo(() => { + const mockedProfile = mock.profileViewBasic({ + handle: `bob.test`, + displayName: 'Bob Robertson', + description: 'User with this as their bio', + labels: + scenario[0] === 'label' && target[0] === 'account' + ? [ + mock.label({ + src: isSelfLabel ? did : undefined, + val: label[0], + uri: `at://${did}/`, + }), + ] + : scenario[0] === 'label' && target[0] === 'profile' + ? [ + mock.label({ + src: isSelfLabel ? did : undefined, + val: label[0], + uri: `at://${did}/app.bsky.actor.profile/self`, + }), + ] + : undefined, + viewer: mock.actorViewerState({ + following: isFollowing + ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234` + : undefined, + muted: scenario[0] === 'mute', + mutedByList: undefined, + blockedBy: undefined, + blocking: + scenario[0] === 'block' + ? `at://did:web:alice.test/app.bsky.actor.block/fake` + : undefined, + blockingByList: undefined, + }), + }) + mockedProfile.did = did + mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png' + mockedProfile.banner = + 'https://bsky.social/about/images/social-card-default-gradient.png' + return mockedProfile + }, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount]) + + const post = React.useMemo(() => { + return mock.postView({ + record: mock.post({ + text: "This is the body of the post. It's where the text goes. You get the idea.", + }), + author: profile, + labels: + scenario[0] === 'label' && target[0] === 'post' + ? [ + mock.label({ + src: isSelfLabel ? did : undefined, + val: label[0], + uri: `at://${did}/app.bsky.feed.post/fake`, + }), + ] + : undefined, + embed: + target[0] === 'embed' + ? mock.embedRecordView({ + record: mock.post({ + text: 'Embed', + }), + labels: + scenario[0] === 'label' && target[0] === 'embed' + ? [ + mock.label({ + src: isSelfLabel ? did : undefined, + val: label[0], + uri: `at://${did}/app.bsky.feed.post/fake`, + }), + ] + : undefined, + author: profile, + }) + : { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: + 'https://bsky.social/about/images/social-card-default-gradient.png', + fullsize: + 'https://bsky.social/about/images/social-card-default-gradient.png', + alt: '', + }, + ], + }, + }) + }, [scenario, label, target, profile, isSelfLabel, did]) + + const replyNotif = React.useMemo(() => { + const notif = mock.replyNotification({ + record: mock.post({ + text: "This is the body of the post. It's where the text goes. You get the idea.", + reply: { + parent: { + uri: `at://${did}/app.bsky.feed.post/fake-parent`, + cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq', + }, + root: { + uri: `at://${did}/app.bsky.feed.post/fake-parent`, + cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq', + }, + }, + }), + author: profile, + labels: + scenario[0] === 'label' && target[0] === 'post' + ? [ + mock.label({ + src: isSelfLabel ? did : undefined, + val: label[0], + uri: `at://${did}/app.bsky.feed.post/fake`, + }), + ] + : undefined, + }) + const [item] = groupNotifications([notif]) + item.subject = mock.postView({ + record: notif.record as AppBskyFeedPost.Record, + author: profile, + labels: notif.labels, + }) + return item + }, [scenario, label, target, profile, isSelfLabel, did]) + + const followNotif = React.useMemo(() => { + const notif = mock.followNotification({ + author: profile, + subjectDid: currentAccount?.did || '', + }) + const [item] = groupNotifications([notif]) + return item + }, [profile, currentAccount]) + + const modOpts = React.useMemo(() => { + return { + userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test', + prefs: { + adultContentEnabled: !noAdult, + labels: { + [label[0]]: visibility[0] as LabelPreference, + }, + labelers: [ + { + did: 'did:plc:fake-labeler', + labels: {[label[0]]: visibility[0] as LabelPreference}, + }, + ], + mutedWords: [], + hiddenPosts: [], + }, + labelDefs: { + 'did:plc:fake-labeler': [ + interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'), + ], + }, + } + }, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef]) + + const profileModeration = React.useMemo(() => { + return moderateProfile(profile, modOpts) + }, [profile, modOpts]) + const postModeration = React.useMemo(() => { + return moderatePost(post, modOpts) + }, [post, modOpts]) + + return ( + + + +

Moderation states

+ + + + + Label + + + Block + + + Mute + + + + {scenario[0] === 'label' && ( + <> + + + + {LABEL_VALUES.map(labelValue => { + let targetFixed = target[0] + if ( + targetFixed !== 'account' && + targetFixed !== 'profile' + ) { + targetFixed = 'content' + } + const disabled = + isSelfLabel && + LABELS[labelValue].flags.includes('no-self') + return ( + + + {labelValue} + + ) + })} + + + Custom label + + + + + {label[0] === 'custom' ? ( + + ) : ( + <> + + + + )} + + + + + + + + + Target is me + + + + Following target + + + + Self label + + + + Adult disabled + + + + Logged out + + + + + {LABELS[label[0] as keyof typeof LABELS]?.configurable !== + false && ( + + + Preference + + + + + + Hide + + + + Warn + + + + Ignore + + + + + )} + + + + + + + Target + + + + + + + Account + + + + Profile + + + + Post + + + + Embed + + + + + + + + )} + + + + + + + + Post + + + Notifications + + + Account + + + Data + + + + + {view[0] === 'post' && ( + <> + + + + + + + + + + )} + + {view[0] === 'notifications' && ( + <> + + + + + + + )} + + {view[0] === 'account' && ( + <> + + + + + + + )} + + {view[0] === 'data' && ( + <> + + + + + + + )} + + + +
+
+
+ ) +} + +function Heading({title, subtitle}: {title: string; subtitle?: string}) { + const t = useTheme() + return ( +

+ {title}{' '} + {!!subtitle && ( +

{subtitle}

+ )} + + ) +} + +function CustomLabelForm({ + def, + setDef, +}: { + def: ComAtprotoLabelDefs.LabelValueDefinition + setDef: React.Dispatch< + React.SetStateAction + > +}) { + const t = useTheme() + return ( + + + + Blurs + + + setDef(v => ({...v, blurs: values[0]}))}> + + + + Content + + + + Media + + + + None + + + + + + + + Severity + + + setDef(v => ({...v, severity: values[0]}))}> + + + + Alert + + + + Inform + + + + None + + + + + + + ) +} + +function Toggler({label, children}: React.PropsWithChildren<{label: string}>) { + const t = useTheme() + const [show, setShow] = React.useState(false) + return ( + + + + {show && children} + + + ) +} + +function SmallToggler({ + label, + children, +}: React.PropsWithChildren<{label: string}>) { + const [show, setShow] = React.useState(false) + return ( + + + + + {show && children} + + ) +} + +function DataView({label, data}: {label: string; data: any}) { + return ( + + + {JSON.stringify(data, null, 2)} + + + ) +} + +function ModerationUIView({ + mod, + label, +}: { + mod: ModerationDecision + label: string +}) { + return ( + + + {[ + 'profileList', + 'profileView', + 'avatar', + 'banner', + 'displayName', + 'contentList', + 'contentView', + 'contentMedia', + ].map(key => { + const ui = mod.ui(key as keyof ModerationBehavior) + return ( + + {key} + + + + + + + ) + })} + + + ) +} + +function Spacer() { + return +} + +function MockPostFeedItem({ + post, + moderation, +}: { + post: AppBskyFeedDefs.PostView + moderation: ModerationDecision +}) { + const t = useTheme() + if (moderation.ui('contentList').filter) { + return ( +

+ Filtered from the feed +

+ ) + } + return ( + + ) +} + +function MockPostThreadItem({ + post, + reply, +}: { + post: AppBskyFeedDefs.PostView + moderation: ModerationDecision + reply?: boolean +}) { + return ( + {}} + /> + ) +} + +function MockNotifItem({ + notif, + moderationOpts, +}: { + notif: FeedNotification + moderationOpts: ModerationOpts +}) { + const t = useTheme() + if (shouldFilterNotif(notif.notification, moderationOpts)) { + return ( +

+ Filtered from the feed +

+ ) + } + return +} + +function MockAccountCard({ + profile, + moderation, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + moderation: ModerationDecision +}) { + const t = useTheme() + + if (moderation.ui('profileList').filter) { + return ( +

+ Filtered from the listing +

+ ) + } + + return +} + +function MockAccountScreen({ + profile, + moderation, + moderationOpts, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + moderation: ModerationDecision + moderationOpts: ModerationOpts +}) { + const t = useTheme() + const {_} = useLingui() + return ( + + + + + + ) +} + +function Flag({v, label}: {v: boolean | undefined; label: string}) { + const t = useTheme() + return ( + + + {v && } + +

{label}

+
+ ) +} -- cgit 1.4.1