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:
parent
d5ebbeb3fc
commit
20d463ff2f
165 changed files with 7034 additions and 5009 deletions
923
src/view/screens/DebugMod.tsx
Normal file
923
src/view/screens/DebugMod.tsx
Normal file
|
@ -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<string[]>(['label'])
|
||||
const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
|
||||
const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
|
||||
const [target, setTarget] = React.useState<string[]>(['account'])
|
||||
const [visibility, setVisiblity] = React.useState<string[]>(['warn'])
|
||||
const [customLabelDef, setCustomLabelDef] =
|
||||
React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
|
||||
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<string[]>(['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 (
|
||||
<moderationOptsOverrideContext.Provider value={modOpts}>
|
||||
<ScrollView>
|
||||
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
|
||||
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
|
||||
|
||||
<Heading title="" subtitle="Scenario" />
|
||||
<ToggleButton.Group
|
||||
label="Scenario"
|
||||
values={scenario}
|
||||
onChange={setScenario}>
|
||||
<ToggleButton.Button name="label" label="Label">
|
||||
Label
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="block" label="Block">
|
||||
Block
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="mute" label="Mute">
|
||||
Mute
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
{scenario[0] === 'label' && (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.mt_lg,
|
||||
a.mb_lg,
|
||||
a.p_lg,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Toggle"
|
||||
type="radio"
|
||||
values={label}
|
||||
onChange={setLabel}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
{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 (
|
||||
<Toggle.Item
|
||||
key={labelValue}
|
||||
name={labelValue}
|
||||
label={labelStrings[labelValue].name}
|
||||
disabled={disabled}
|
||||
style={disabled ? {opacity: 0.5} : undefined}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>{labelValue}</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
)
|
||||
})}
|
||||
<Toggle.Item
|
||||
name="custom"
|
||||
label="Custom label"
|
||||
disabled={isSelfLabel}
|
||||
style={isSelfLabel ? {opacity: 0.5} : undefined}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Custom label</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
{label[0] === 'custom' ? (
|
||||
<CustomLabelForm
|
||||
def={customLabelDef}
|
||||
setDef={setCustomLabelDef}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View style={{height: 10}} />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={{height: 10}} />
|
||||
|
||||
<SmallToggler label="Advanced">
|
||||
<Toggle.Group
|
||||
label="Toggle"
|
||||
type="checkbox"
|
||||
values={scenarioSwitches}
|
||||
onChange={setScenarioSwitches}>
|
||||
<View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
|
||||
<Toggle.Item name="targetMe" label="Target is me">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Target is me</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="following" label="Following target">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Following target</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="selfLabel" label="Self label">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Self label</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="noAdult" label="Adult disabled">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Adult disabled</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="loggedOut" label="Logged out">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Logged out</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
{LABELS[label[0] as keyof typeof LABELS]?.configurable !==
|
||||
false && (
|
||||
<View style={[a.mt_md]}>
|
||||
<Text
|
||||
style={[a.font_bold, a.text_xs, t.atoms.text, a.pb_sm]}>
|
||||
Preference
|
||||
</Text>
|
||||
<Toggle.Group
|
||||
label="Preference"
|
||||
type="radio"
|
||||
values={visibility}
|
||||
onChange={setVisiblity}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.flex_wrap,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Toggle.Item name="hide" label="Hide">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Hide</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="warn" label="Warn">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Warn</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="ignore" label="Ignore">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Ignore</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
)}
|
||||
</SmallToggler>
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_xs,
|
||||
t.atoms.text,
|
||||
a.pl_md,
|
||||
a.pb_xs,
|
||||
]}>
|
||||
Target
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Target"
|
||||
type="radio"
|
||||
values={target}
|
||||
onChange={setTarget}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
<Toggle.Item name="account" label="Account">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Account</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="profile" label="Profile">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Profile</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="post" label="Post">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Post</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="embed" label="Embed">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Embed</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Heading title="" subtitle="Results" />
|
||||
|
||||
<ToggleButton.Group label="Results" values={view} onChange={setView}>
|
||||
<ToggleButton.Button name="post" label="Post">
|
||||
Post
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="notifications" label="Notifications">
|
||||
Notifications
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="account" label="Account">
|
||||
Account
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="data" label="Data">
|
||||
Data
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.mt_lg,
|
||||
a.p_md,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
{view[0] === 'post' && (
|
||||
<>
|
||||
<Heading title="Post" subtitle="in feed" />
|
||||
<MockPostFeedItem post={post} moderation={postModeration} />
|
||||
|
||||
<Heading title="Post" subtitle="viewed directly" />
|
||||
<MockPostThreadItem post={post} moderation={postModeration} />
|
||||
|
||||
<Heading title="Post" subtitle="reply in thread" />
|
||||
<MockPostThreadItem
|
||||
post={post}
|
||||
moderation={postModeration}
|
||||
reply
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'notifications' && (
|
||||
<>
|
||||
<Heading title="Notification" subtitle="quote or reply" />
|
||||
<MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
|
||||
<View style={{height: 20}} />
|
||||
<Heading title="Notification" subtitle="follow or like" />
|
||||
<MockNotifItem notif={followNotif} moderationOpts={modOpts} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'account' && (
|
||||
<>
|
||||
<Heading title="Account" subtitle="in listing" />
|
||||
<MockAccountCard
|
||||
profile={profile}
|
||||
moderation={profileModeration}
|
||||
/>
|
||||
|
||||
<Heading title="Account" subtitle="viewing directly" />
|
||||
<MockAccountScreen
|
||||
profile={profile}
|
||||
moderation={profileModeration}
|
||||
moderationOpts={modOpts}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'data' && (
|
||||
<>
|
||||
<ModerationUIView
|
||||
label="Profile Moderation UI"
|
||||
mod={profileModeration}
|
||||
/>
|
||||
<ModerationUIView
|
||||
label="Post Moderation UI"
|
||||
mod={postModeration}
|
||||
/>
|
||||
<DataView
|
||||
label={label[0]}
|
||||
data={LABELS[label[0] as keyof typeof LABELS]}
|
||||
/>
|
||||
<DataView
|
||||
label="Profile Moderation Data"
|
||||
data={profileModeration}
|
||||
/>
|
||||
<DataView label="Post Moderation Data" data={postModeration} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{height: 400}} />
|
||||
</CenteredView>
|
||||
</ScrollView>
|
||||
</moderationOptsOverrideContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Heading({title, subtitle}: {title: string; subtitle?: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<H3 style={[a.text_3xl, a.font_bold, a.pb_md]}>
|
||||
{title}{' '}
|
||||
{!!subtitle && (
|
||||
<H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
|
||||
)}
|
||||
</H3>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomLabelForm({
|
||||
def,
|
||||
setDef,
|
||||
}: {
|
||||
def: ComAtprotoLabelDefs.LabelValueDefinition
|
||||
setDef: React.Dispatch<
|
||||
React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
|
||||
>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.flex_wrap,
|
||||
a.gap_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_md,
|
||||
a.p_md,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<View>
|
||||
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
|
||||
Blurs
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Blurs"
|
||||
type="radio"
|
||||
values={[def.blurs]}
|
||||
onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
<Toggle.Item name="content" label="Content">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Content</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="media" label="Media">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Media</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="none" label="None">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>None</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
|
||||
Severity
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Severity"
|
||||
type="radio"
|
||||
values={[def.severity]}
|
||||
onChange={values => setDef(v => ({...v, severity: values[0]}))}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
|
||||
<Toggle.Item name="alert" label="Alert">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Alert</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="inform" label="Inform">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Inform</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="none" label="None">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>None</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
|
||||
const t = useTheme()
|
||||
const [show, setShow] = React.useState(false)
|
||||
return (
|
||||
<View style={a.mb_md}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.border_contrast_medium,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
label="Toggle visibility"
|
||||
size="small"
|
||||
onPress={() => setShow(!show)}>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={show ? ChevronTop : ChevronBottom}
|
||||
position="right"
|
||||
/>
|
||||
</Button>
|
||||
{show && children}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SmallToggler({
|
||||
label,
|
||||
children,
|
||||
}: React.PropsWithChildren<{label: string}>) {
|
||||
const [show, setShow] = React.useState(false)
|
||||
return (
|
||||
<View>
|
||||
<View style={[a.flex_row]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
label="Toggle visibility"
|
||||
size="tiny"
|
||||
onPress={() => setShow(!show)}>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={show ? ChevronTop : ChevronBottom}
|
||||
position="right"
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
{show && children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function DataView({label, data}: {label: string; data: any}) {
|
||||
return (
|
||||
<Toggler label={label}>
|
||||
<Text style={[{fontFamily: 'monospace'}, a.p_md]}>
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</Text>
|
||||
</Toggler>
|
||||
)
|
||||
}
|
||||
|
||||
function ModerationUIView({
|
||||
mod,
|
||||
label,
|
||||
}: {
|
||||
mod: ModerationDecision
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<Toggler label={label}>
|
||||
<View style={a.p_lg}>
|
||||
{[
|
||||
'profileList',
|
||||
'profileView',
|
||||
'avatar',
|
||||
'banner',
|
||||
'displayName',
|
||||
'contentList',
|
||||
'contentView',
|
||||
'contentMedia',
|
||||
].map(key => {
|
||||
const ui = mod.ui(key as keyof ModerationBehavior)
|
||||
return (
|
||||
<View key={key} style={[a.flex_row, a.gap_md]}>
|
||||
<Text style={[a.font_bold, {width: 100}]}>{key}</Text>
|
||||
<Flag v={ui.filter} label="Filter" />
|
||||
<Flag v={ui.blur} label="Blur" />
|
||||
<Flag v={ui.alert} label="Alert" />
|
||||
<Flag v={ui.inform} label="Inform" />
|
||||
<Flag v={ui.noOverride} label="No-override" />
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</Toggler>
|
||||
)
|
||||
}
|
||||
|
||||
function Spacer() {
|
||||
return <View style={{height: 30}} />
|
||||
}
|
||||
|
||||
function MockPostFeedItem({
|
||||
post,
|
||||
moderation,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
moderation: ModerationDecision
|
||||
}) {
|
||||
const t = useTheme()
|
||||
if (moderation.ui('contentList').filter) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
|
||||
Filtered from the feed
|
||||
</P>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FeedItem
|
||||
post={post}
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
moderation={moderation}
|
||||
reason={undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MockPostThreadItem({
|
||||
post,
|
||||
reply,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
moderation: ModerationDecision
|
||||
reply?: boolean
|
||||
}) {
|
||||
return (
|
||||
<PostThreadItem
|
||||
// @ts-ignore
|
||||
post={post}
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
depth={reply ? 1 : 0}
|
||||
isHighlightedPost={!reply}
|
||||
treeView={false}
|
||||
prevPost={undefined}
|
||||
nextPost={undefined}
|
||||
hasPrecedingItem={false}
|
||||
onPostReply={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MockNotifItem({
|
||||
notif,
|
||||
moderationOpts,
|
||||
}: {
|
||||
notif: FeedNotification
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
if (shouldFilterNotif(notif.notification, moderationOpts)) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
|
||||
Filtered from the feed
|
||||
</P>
|
||||
)
|
||||
}
|
||||
return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
|
||||
}
|
||||
|
||||
function MockAccountCard({
|
||||
profile,
|
||||
moderation,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (moderation.ui('profileList').filter) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
|
||||
Filtered from the listing
|
||||
</P>
|
||||
)
|
||||
}
|
||||
|
||||
return <ProfileCard profile={profile} />
|
||||
}
|
||||
|
||||
function MockAccountScreen({
|
||||
profile,
|
||||
moderation,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
|
||||
<ScreenHider
|
||||
style={{}}
|
||||
screenDescription={_(msg`profile`)}
|
||||
modui={moderation.ui('profileView')}>
|
||||
<ProfileHeaderStandard
|
||||
// @ts-ignore ProfileViewBasic is close enough -prf
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
descriptionRT={new RichText({text: profile.description as string})}
|
||||
/>
|
||||
</ScreenHider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Flag({v, label}: {v: boolean | undefined; label: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_xs,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
{
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
]}>
|
||||
{v && <Check size="xs" fill={t.palette.contrast_900} />}
|
||||
</View>
|
||||
<P style={a.text_xs}>{label}</P>
|
||||
</View>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue