diff options
author | Paul Frazee <pfrazee@gmail.com> | 2024-03-18 12:46:28 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-18 12:46:28 -0700 |
commit | 20d463ff2f5a112473f75a21595b3d89b8dfc0b0 (patch) | |
tree | 371aa26309374de3b9deef001c99f41bff1057d1 /src/screens/Moderation/index.tsx | |
parent | d5ebbeb3fc2802e80f55162996b6baabbf764f9c (diff) | |
download | voidsky-20d463ff2f5a112473f75a21595b3d89b8dfc0b0.tar.zst |
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>
Diffstat (limited to 'src/screens/Moderation/index.tsx')
-rw-r--r-- | src/screens/Moderation/index.tsx | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx new file mode 100644 index 000000000..26fa9ec77 --- /dev/null +++ b/src/screens/Moderation/index.tsx @@ -0,0 +1,560 @@ +import React from 'react' +import {View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {ComAtprotoLabelDefs} from '@atproto/api' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {LABELS} from '@atproto/api' +import {useSafeAreaFrame} from 'react-native-safe-area-context' + +import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' +import {CenteredView} from '#/view/com/util/Views' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {useAnalytics} from 'lib/analytics/analytics' +import {useSetMinimalShellMode} from '#/state/shell' +import {useSession} from '#/state/session' +import { + useProfileQuery, + useProfileUpdateMutation, +} from '#/state/queries/profile' +import {ScrollView} from '#/view/com/util/Views' + +import { + UsePreferencesQueryResponse, + useMyLabelersQuery, + usePreferencesQuery, + usePreferencesSetAdultContentMutation, +} from '#/state/queries/preferences' + +import {getLabelingServiceTitle} from '#/lib/moderation' +import {logger} from '#/logger' +import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf' +import {Divider} from '#/components/Divider' +import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' +import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' +import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' +import {Text} from '#/components/Typography' +import * as Toggle from '#/components/forms/Toggle' +import {InlineLink, Link} from '#/components/Link' +import {Button, ButtonText} from '#/components/Button' +import {Loader} from '#/components/Loader' +import * as LabelingService from '#/components/LabelingServiceCard' +import {GlobalModerationLabelPref} from '#/components/moderation/GlobalModerationLabelPref' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Props as SVGIconProps} from '#/components/icons/common' +import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import * as Dialog from '#/components/Dialog' + +function ErrorState({error}: {error: string}) { + const t = useTheme() + return ( + <View style={[a.p_xl]}> + <Text + style={[ + a.text_md, + a.leading_normal, + a.pb_md, + t.atoms.text_contrast_medium, + ]}> + <Trans> + Hmmmm, it seems we're having trouble loading this data. See below for + more details. If this issue persists, please contact us. + </Trans> + </Text> + <View + style={[ + a.relative, + a.py_md, + a.px_lg, + a.rounded_md, + a.mb_2xl, + t.atoms.bg_contrast_25, + ]}> + <Text style={[a.text_md, a.leading_normal]}>{error}</Text> + </View> + </View> + ) +} + +export function ModerationScreen( + _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, +) { + const t = useTheme() + const {_} = useLingui() + const { + isLoading: isPreferencesLoading, + error: preferencesError, + data: preferences, + } = usePreferencesQuery() + const {gtMobile} = useBreakpoints() + const {height} = useSafeAreaFrame() + + const isLoading = isPreferencesLoading + const error = preferencesError + + return ( + <CenteredView + testID="moderationScreen" + style={[ + t.atoms.border_contrast_low, + t.atoms.bg, + {minHeight: height}, + ...(gtMobile ? [a.border_l, a.border_r] : []), + ]}> + <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> + + {isLoading ? ( + <View style={[a.w_full, a.align_center, a.pt_2xl]}> + <Loader size="xl" fill={t.atoms.text.color} /> + </View> + ) : error || !preferences ? ( + <ErrorState + error={ + preferencesError?.toString() || + _(msg`Something went wrong, please try again.`) + } + /> + ) : ( + <ModerationScreenInner preferences={preferences} /> + )} + </CenteredView> + ) +} + +function SubItem({ + title, + icon: Icon, + style, +}: ViewStyleProp & { + title: string + icon: React.ComponentType<SVGIconProps> +}) { + const t = useTheme() + return ( + <View + style={[ + a.w_full, + a.flex_row, + a.align_center, + a.justify_between, + a.p_lg, + a.gap_sm, + style, + ]}> + <View style={[a.flex_row, a.align_center, a.gap_md]}> + <Icon size="md" style={[t.atoms.text_contrast_medium]} /> + <Text style={[a.text_sm, a.font_bold]}>{title}</Text> + </View> + <ChevronRight + size="sm" + style={[t.atoms.text_contrast_low, a.self_end, {paddingBottom: 2}]} + /> + </View> + ) +} + +export function ModerationScreenInner({ + preferences, +}: { + preferences: UsePreferencesQueryResponse +}) { + const {_} = useLingui() + const t = useTheme() + const setMinimalShellMode = useSetMinimalShellMode() + const {screen} = useAnalytics() + const {gtMobile} = useBreakpoints() + const {mutedWordsDialogControl} = useGlobalDialogsControlContext() + const birthdateDialogControl = Dialog.useDialogControl() + const { + isLoading: isLabelersLoading, + data: labelers, + error: labelersError, + } = useMyLabelersQuery() + + useFocusEffect( + React.useCallback(() => { + screen('Moderation') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) + + const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} = + usePreferencesSetAdultContentMutation() + const adultContentEnabled = !!( + (optimisticAdultContent && optimisticAdultContent.enabled) || + (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled) + ) + const ageNotSet = !preferences.userAge + const isUnderage = (preferences.userAge || 0) < 18 + + const onToggleAdultContentEnabled = React.useCallback( + async (selected: boolean) => { + try { + await setAdultContentPref({ + enabled: selected, + }) + } catch (e: any) { + logger.error(`Failed to set adult content pref`, { + message: e.message, + }) + } + }, + [setAdultContentPref], + ) + + return ( + <View> + <ScrollView + contentContainerStyle={[ + a.border_0, + a.pt_2xl, + a.px_lg, + gtMobile && a.px_2xl, + ]}> + <Text + style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> + <Trans>Moderation tools</Trans> + </Text> + + <View + style={[ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + ]}> + <Button + testID="mutedWordsBtn" + label={_(msg`Open muted words and tags settings`)} + onPress={() => mutedWordsDialogControl.open()}> + {state => ( + <SubItem + title={_(msg`Muted words & tags`)} + icon={Filter} + style={[ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ]} + /> + )} + </Button> + <Divider /> + <Link testID="moderationlistsBtn" to="/moderation/modlists"> + {state => ( + <SubItem + title={_(msg`Moderation lists`)} + icon={Group} + style={[ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ]} + /> + )} + </Link> + <Divider /> + <Link testID="mutedAccountsBtn" to="/moderation/muted-accounts"> + {state => ( + <SubItem + title={_(msg`Muted accounts`)} + icon={Person} + style={[ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ]} + /> + )} + </Link> + <Divider /> + <Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts"> + {state => ( + <SubItem + title={_(msg`Blocked accounts`)} + icon={CircleBanSign} + style={[ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ]} + /> + )} + </Link> + </View> + + <Text + style={[ + a.pt_2xl, + a.pb_md, + a.text_md, + a.font_bold, + t.atoms.text_contrast_high, + ]}> + <Trans>Content filters</Trans> + </Text> + + <View style={[a.gap_md]}> + {ageNotSet && ( + <> + <Button + label={_(msg`Confirm your birthdate`)} + size="small" + variant="solid" + color="secondary" + onPress={() => { + birthdateDialogControl.open() + }} + style={[a.justify_between, a.rounded_md, a.px_lg, a.py_lg]}> + <ButtonText> + <Trans>Confirm your age:</Trans> + </ButtonText> + <ButtonText> + <Trans>Set birthdate</Trans> + </ButtonText> + </Button> + + <BirthDateSettingsDialog + control={birthdateDialogControl} + preferences={preferences} + /> + </> + )} + <View + style={[ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + ]}> + {!ageNotSet && !isUnderage && ( + <> + <View + style={[ + a.py_lg, + a.px_lg, + a.flex_row, + a.align_center, + a.justify_between, + ]}> + <Text style={[a.font_semibold, t.atoms.text_contrast_high]}> + <Trans>Enable adult content</Trans> + </Text> + <Toggle.Item + label={_(msg`Toggle to enable or disable adult content`)} + name="adultContent" + value={adultContentEnabled} + onChange={onToggleAdultContentEnabled}> + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <Text style={[t.atoms.text_contrast_medium]}> + {adultContentEnabled ? ( + <Trans>Enabled</Trans> + ) : ( + <Trans>Disabled</Trans> + )} + </Text> + <Toggle.Switch /> + </View> + </Toggle.Item> + </View> + <Divider /> + </> + )} + {!isUnderage && adultContentEnabled && ( + <> + <GlobalModerationLabelPref labelValueDefinition={LABELS.porn} /> + <Divider /> + <GlobalModerationLabelPref + labelValueDefinition={LABELS.sexual} + /> + <Divider /> + <GlobalModerationLabelPref + labelValueDefinition={LABELS['graphic-media']} + /> + <Divider /> + </> + )} + <GlobalModerationLabelPref labelValueDefinition={LABELS.nudity} /> + </View> + </View> + + <Text + style={[ + a.text_md, + a.font_bold, + a.pt_2xl, + a.pb_md, + t.atoms.text_contrast_high, + ]}> + <Trans>Advanced</Trans> + </Text> + + {isLabelersLoading ? ( + <Loader /> + ) : labelersError || !labelers ? ( + <View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}> + <Text> + <Trans> + We were unable to load your configured labelers at this time. + </Trans> + </Text> + </View> + ) : ( + <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> + {labelers.map((labeler, i) => { + return ( + <React.Fragment key={labeler.creator.did}> + {i !== 0 && <Divider />} + <LabelingService.Link labeler={labeler}> + {state => ( + <LabelingService.Outer + style={[ + i === 0 && { + borderTopLeftRadius: a.rounded_sm.borderRadius, + borderTopRightRadius: a.rounded_sm.borderRadius, + }, + i === labelers.length - 1 && { + borderBottomLeftRadius: a.rounded_sm.borderRadius, + borderBottomRightRadius: a.rounded_sm.borderRadius, + }, + (state.hovered || state.pressed) && [ + t.atoms.bg_contrast_50, + ], + ]}> + <LabelingService.Avatar /> + <LabelingService.Content> + <LabelingService.Title + value={getLabelingServiceTitle({ + displayName: labeler.creator.displayName, + handle: labeler.creator.handle, + })} + /> + <LabelingService.Description + value={labeler.creator.description} + handle={labeler.creator.handle} + /> + </LabelingService.Content> + </LabelingService.Outer> + )} + </LabelingService.Link> + </React.Fragment> + ) + })} + </View> + )} + + <Text + style={[ + a.text_md, + a.font_bold, + a.pt_2xl, + a.pb_md, + t.atoms.text_contrast_high, + ]}> + <Trans>Logged-out visibility</Trans> + </Text> + + <PwiOptOut /> + + <View style={{height: 200}} /> + </ScrollView> + </View> + ) +} + +function PwiOptOut() { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + const updateProfile = useProfileUpdateMutation() + + const isOptedOut = + profile?.labels?.some(l => l.val === '!no-unauthenticated') || false + const canToggle = profile && !updateProfile.isPending + + const onToggleOptOut = React.useCallback(() => { + if (!profile) { + return + } + let wasAdded = false + updateProfile.mutate({ + profile, + updates: existing => { + // create labels attr if needed + existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) + ? existing.labels + : { + $type: 'com.atproto.label.defs#selfLabels', + values: [], + } + + // toggle the label + const hasLabel = existing.labels.values.some( + l => l.val === '!no-unauthenticated', + ) + if (hasLabel) { + wasAdded = false + existing.labels.values = existing.labels.values.filter( + l => l.val !== '!no-unauthenticated', + ) + } else { + wasAdded = true + existing.labels.values.push({val: '!no-unauthenticated'}) + } + + // delete if no longer needed + if (existing.labels.values.length === 0) { + delete existing.labels + } + return existing + }, + checkCommitted: res => { + const exists = !!res.data.labels?.some( + l => l.val === '!no-unauthenticated', + ) + return exists === wasAdded + }, + }) + }, [updateProfile, profile]) + + return ( + <View style={[a.pt_sm]}> + <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> + <Toggle.Item + disabled={!canToggle} + value={isOptedOut} + onChange={onToggleOptOut} + name="logged_out_visibility" + label={_( + msg`Discourage apps from showing my account to logged-out users`, + )}> + <Toggle.Switch /> + <Toggle.Label style={[a.text_md]}> + <Trans> + Discourage apps from showing my account to logged-out users + </Trans> + </Toggle.Label> + </Toggle.Item> + + {updateProfile.isPending && <Loader />} + </View> + + <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}> + <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> + <Trans> + Bluesky will not show your profile and posts to logged-out users. + Other apps may not honor this request. This does not make your + account private. + </Trans> + </Text> + <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> + <Trans> + Note: Bluesky is an open and public network. This setting only + limits the visibility of your content on the Bluesky app and + website, and other apps may not respect this setting. Your content + may still be shown to logged-out users by other apps and websites. + </Trans> + </Text> + + <InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> + <Trans>Learn more about what is public on Bluesky.</Trans> + </InlineLink> + </View> + </View> + ) +} |