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
|
|
@ -1,139 +0,0 @@
|
|||
import React, {useState} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {ComAtprotoModerationDefs} from '@atproto/api'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {CharProgress} from '../composer/char-progress/CharProgress'
|
||||
import {getAgent} from '#/state/session'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
|
||||
export const snapPoints = ['40%']
|
||||
|
||||
type ReportComponentProps =
|
||||
| {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
| {
|
||||
did: string
|
||||
}
|
||||
|
||||
export function Component(props: ReportComponentProps) {
|
||||
const pal = usePalette('default')
|
||||
const [details, setDetails] = useState<string>('')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const isAccountReport = 'did' in props
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
const $type = !isAccountReport
|
||||
? 'com.atproto.repo.strongRef'
|
||||
: 'com.atproto.admin.defs#repoRef'
|
||||
await getAgent().createModerationReport({
|
||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type,
|
||||
...props,
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
Toast.show(_(msg`We'll look into your appeal promptly.`))
|
||||
} finally {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
s.flex1,
|
||||
isMobile ? {paddingHorizontal: 12} : undefined,
|
||||
]}
|
||||
testID="appealLabelModal">
|
||||
<Text
|
||||
type="2xl-bold"
|
||||
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
|
||||
<Trans>Appeal Content Warning</Trans>
|
||||
</Text>
|
||||
<ScrollView>
|
||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
||||
<TextInput
|
||||
accessibilityLabel={_(msg`Text input field`)}
|
||||
accessibilityHint={_(
|
||||
msg`Please tell us why you think this content warning was incorrectly applied!`,
|
||||
)}
|
||||
placeholder={_(
|
||||
msg`Please tell us why you think this content warning was incorrectly applied!`,
|
||||
)}
|
||||
placeholderTextColor={pal.textLight.color}
|
||||
value={details}
|
||||
onChangeText={setDetails}
|
||||
autoFocus={true}
|
||||
numberOfLines={3}
|
||||
multiline={true}
|
||||
textAlignVertical="top"
|
||||
maxLength={300}
|
||||
style={[styles.detailsInput, pal.text]}
|
||||
/>
|
||||
<View style={styles.detailsInputBottomBar}>
|
||||
<View style={styles.charCounter}>
|
||||
<CharProgress count={details?.length || 0} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
testID="confirmBtn"
|
||||
onPress={submit}
|
||||
style={styles.btn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Submit</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
detailsInputContainer: {
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
detailsInput: {
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
borderRadius: 8,
|
||||
minHeight: 100,
|
||||
fontSize: 16,
|
||||
},
|
||||
detailsInputBottomBar: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
charCounter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
import React from 'react'
|
||||
import {LabelPreference} from '@atproto/api'
|
||||
import {StyleSheet, Pressable, View, Linking} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {ScrollView} from './util'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {ToggleButton} from '../util/forms/ToggleButton'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
usePreferencesSetContentLabelMutation,
|
||||
usePreferencesSetAdultContentMutation,
|
||||
ConfigurableLabelGroup,
|
||||
CONFIGURABLE_LABEL_GROUPS,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
export function Component({}: {}) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
const onPressDone = React.useCallback(() => {
|
||||
closeModal()
|
||||
}, [closeModal])
|
||||
|
||||
return (
|
||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Content Filtering</Trans>
|
||||
</Text>
|
||||
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<AdultContentEnabledPref />
|
||||
<ContentLabelPref
|
||||
preferences={preferences}
|
||||
labelGroup="nsfw"
|
||||
disabled={!preferences?.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
preferences={preferences}
|
||||
labelGroup="nudity"
|
||||
disabled={!preferences?.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
preferences={preferences}
|
||||
labelGroup="suggestive"
|
||||
disabled={!preferences?.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
preferences={preferences}
|
||||
labelGroup="gore"
|
||||
disabled={!preferences?.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref preferences={preferences} labelGroup="hate" />
|
||||
<ContentLabelPref preferences={preferences} labelGroup="spam" />
|
||||
<ContentLabelPref
|
||||
preferences={preferences}
|
||||
labelGroup="impersonation"
|
||||
/>
|
||||
<View style={{height: isMobile ? 60 : 0}} />
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.btnContainer,
|
||||
isMobile && styles.btnContainerMobile,
|
||||
pal.borderDark,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="sendReportBtn"
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Done`)}
|
||||
accessibilityHint="">
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function AdultContentEnabledPref() {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {mutate, variables} = usePreferencesSetAdultContentMutation()
|
||||
const bithdayDialogControl = useDialogControl()
|
||||
|
||||
const onSetAge = React.useCallback(
|
||||
() => bithdayDialogControl.open(),
|
||||
[bithdayDialogControl],
|
||||
)
|
||||
|
||||
const onToggleAdultContent = React.useCallback(async () => {
|
||||
if (isIOS) return
|
||||
|
||||
try {
|
||||
mutate({
|
||||
enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
|
||||
})
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
_(msg`There was an issue syncing your preferences with the server`),
|
||||
)
|
||||
logger.error('Failed to update preferences with server', {message: e})
|
||||
}
|
||||
}, [variables, preferences, mutate, _])
|
||||
|
||||
const onAdultContentLinkPress = React.useCallback(() => {
|
||||
Linking.openURL('https://bsky.app/')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={s.mb10}>
|
||||
<BirthDateSettingsDialog
|
||||
control={bithdayDialogControl}
|
||||
preferences={preferences}
|
||||
/>
|
||||
{isIOS ? (
|
||||
preferences?.adultContentEnabled ? null : (
|
||||
<Text type="md" style={pal.textLight}>
|
||||
<Trans>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href=""
|
||||
text="bsky.app"
|
||||
onPress={onAdultContentLinkPress}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
) : typeof preferences?.birthDate === 'undefined' ? (
|
||||
<View style={[pal.viewLight, styles.agePrompt]}>
|
||||
<Text type="md" style={[pal.text, {flex: 1}]}>
|
||||
<Trans>Confirm your age to enable adult content.</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
label={_(msg({message: 'Set Age', context: 'action'}))}
|
||||
onPress={onSetAge}
|
||||
/>
|
||||
</View>
|
||||
) : (preferences.userAge || 0) >= 18 ? (
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={_(msg`Enable Adult Content`)}
|
||||
isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
|
||||
onPress={onToggleAdultContent}
|
||||
style={styles.toggleBtn}
|
||||
/>
|
||||
) : (
|
||||
<View style={[pal.viewLight, styles.agePrompt]}>
|
||||
<Text type="md" style={[pal.text, {flex: 1}]}>
|
||||
<Trans>You must be 18 or older to enable adult content.</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
label={_(msg({message: 'Set Age', context: 'action'}))}
|
||||
onPress={onSetAge}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Refactor this component to pass labels down to each tab
|
||||
function ContentLabelPref({
|
||||
preferences,
|
||||
labelGroup,
|
||||
disabled,
|
||||
}: {
|
||||
preferences?: UsePreferencesQueryResponse
|
||||
labelGroup: ConfigurableLabelGroup
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const visibility = preferences?.contentLabels?.[labelGroup]
|
||||
const {mutate, variables} = usePreferencesSetContentLabelMutation()
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(vis: LabelPreference) => {
|
||||
mutate({labelGroup, visibility: vis})
|
||||
},
|
||||
[mutate, labelGroup],
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[styles.contentLabelPref, pal.border]}>
|
||||
<View style={s.flex1}>
|
||||
<Text type="md-medium" style={[pal.text]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
|
||||
</Text>
|
||||
{typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{disabled || !visibility ? (
|
||||
<Text type="sm-bold" style={pal.textLight}>
|
||||
<Trans context="action">Hide</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<SelectGroup
|
||||
current={variables?.visibility || visibility}
|
||||
onChange={onChange}
|
||||
labelGroup={labelGroup}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
interface SelectGroupProps {
|
||||
current: LabelPreference
|
||||
onChange: (v: LabelPreference) => void
|
||||
labelGroup: ConfigurableLabelGroup
|
||||
}
|
||||
|
||||
function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<View style={styles.selectableBtns}>
|
||||
<SelectableBtn
|
||||
current={current}
|
||||
value="hide"
|
||||
label={_(msg`Hide`)}
|
||||
left
|
||||
onChange={onChange}
|
||||
labelGroup={labelGroup}
|
||||
/>
|
||||
<SelectableBtn
|
||||
current={current}
|
||||
value="warn"
|
||||
label={_(msg`Warn`)}
|
||||
onChange={onChange}
|
||||
labelGroup={labelGroup}
|
||||
/>
|
||||
<SelectableBtn
|
||||
current={current}
|
||||
value="ignore"
|
||||
label={_(msg`Show`)}
|
||||
right
|
||||
onChange={onChange}
|
||||
labelGroup={labelGroup}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
interface SelectableBtnProps {
|
||||
current: string
|
||||
value: LabelPreference
|
||||
label: string
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
onChange: (v: LabelPreference) => void
|
||||
labelGroup: ConfigurableLabelGroup
|
||||
}
|
||||
|
||||
function SelectableBtn({
|
||||
current,
|
||||
value,
|
||||
label,
|
||||
left,
|
||||
right,
|
||||
onChange,
|
||||
labelGroup,
|
||||
}: SelectableBtnProps) {
|
||||
const pal = usePalette('default')
|
||||
const palPrimary = usePalette('inverted')
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
styles.selectableBtn,
|
||||
left && styles.selectableBtnLeft,
|
||||
right && styles.selectableBtnRight,
|
||||
pal.border,
|
||||
current === value ? palPrimary.view : pal.view,
|
||||
]}
|
||||
onPress={() => onChange(value)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={value}
|
||||
accessibilityHint={_(
|
||||
msg`Set ${value} for ${labelGroup} content moderation policy`,
|
||||
)}>
|
||||
<Text style={current === value ? palPrimary.text : pal.text}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
paddingHorizontal: 2,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
btnContainer: {
|
||||
paddingTop: 10,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
btnContainerMobile: {
|
||||
paddingBottom: 40,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
|
||||
agePrompt: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 14,
|
||||
paddingRight: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
},
|
||||
|
||||
contentLabelPref: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 14,
|
||||
paddingLeft: 4,
|
||||
marginBottom: 14,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
|
||||
selectableBtns: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 10,
|
||||
},
|
||||
selectableBtn: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderLeftWidth: 0,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
selectableBtnLeft: {
|
||||
borderTopLeftRadius: 8,
|
||||
borderBottomLeftRadius: 8,
|
||||
borderLeftWidth: 1,
|
||||
},
|
||||
selectableBtnRight: {
|
||||
borderTopRightRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.gray1,
|
||||
},
|
||||
toggleBtn: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
})
|
||||
|
|
@ -15,16 +15,12 @@ import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
|||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||
import * as AltImageModal from './AltImage'
|
||||
import * as EditImageModal from './AltImage'
|
||||
import * as ReportModal from './report/Modal'
|
||||
import * as AppealLabelModal from './AppealLabel'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as AddAppPassword from './AddAppPasswords'
|
||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
import * as VerifyEmailModal from './VerifyEmail'
|
||||
import * as ChangeEmailModal from './ChangeEmail'
|
||||
import * as ChangePasswordModal from './ChangePassword'
|
||||
|
|
@ -67,12 +63,6 @@ export function ModalsContainer() {
|
|||
if (activeModal?.name === 'edit-profile') {
|
||||
snapPoints = EditProfileModal.snapPoints
|
||||
element = <EditProfileModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'report') {
|
||||
snapPoints = ReportModal.snapPoints
|
||||
element = <ReportModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'appeal-label') {
|
||||
snapPoints = AppealLabelModal.snapPoints
|
||||
element = <AppealLabelModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-or-edit-list') {
|
||||
snapPoints = CreateOrEditListModal.snapPoints
|
||||
element = <CreateOrEditListModal.Component {...activeModal} />
|
||||
|
|
@ -109,18 +99,12 @@ export function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'add-app-password') {
|
||||
snapPoints = AddAppPassword.snapPoints
|
||||
element = <AddAppPassword.Component />
|
||||
} else if (activeModal?.name === 'content-filtering-settings') {
|
||||
snapPoints = ContentFilteringSettingsModal.snapPoints
|
||||
element = <ContentFilteringSettingsModal.Component />
|
||||
} else if (activeModal?.name === 'content-languages-settings') {
|
||||
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
||||
element = <ContentLanguagesSettingsModal.Component />
|
||||
} else if (activeModal?.name === 'post-languages-settings') {
|
||||
snapPoints = PostLanguagesSettingsModal.snapPoints
|
||||
element = <PostLanguagesSettingsModal.Component />
|
||||
} else if (activeModal?.name === 'moderation-details') {
|
||||
snapPoints = ModerationDetailsModal.snapPoints
|
||||
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'verify-email') {
|
||||
snapPoints = VerifyEmailModal.snapPoints
|
||||
element = <VerifyEmailModal.Component {...activeModal} />
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
|||
import {useModals, useModalControls} from '#/state/modals'
|
||||
import type {Modal as ModalIface} from '#/state/modals'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as ReportModal from './report/Modal'
|
||||
import * as AppealLabelModal from './AppealLabel'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||
|
|
@ -23,10 +21,8 @@ import * as EditImageModal from './EditImage'
|
|||
import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as AddAppPassword from './AddAppPasswords'
|
||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
import * as VerifyEmailModal from './VerifyEmail'
|
||||
import * as ChangeEmailModal from './ChangeEmail'
|
||||
import * as ChangePasswordModal from './ChangePassword'
|
||||
|
|
@ -78,10 +74,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
let element
|
||||
if (modal.name === 'edit-profile') {
|
||||
element = <EditProfileModal.Component {...modal} />
|
||||
} else if (modal.name === 'report') {
|
||||
element = <ReportModal.Component {...modal} />
|
||||
} else if (modal.name === 'appeal-label') {
|
||||
element = <AppealLabelModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-or-edit-list') {
|
||||
element = <CreateOrEditListModal.Component {...modal} />
|
||||
} else if (modal.name === 'user-add-remove-lists') {
|
||||
|
|
@ -104,8 +96,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <InviteCodesModal.Component />
|
||||
} else if (modal.name === 'add-app-password') {
|
||||
element = <AddAppPassword.Component />
|
||||
} else if (modal.name === 'content-filtering-settings') {
|
||||
element = <ContentFilteringSettingsModal.Component />
|
||||
} else if (modal.name === 'content-languages-settings') {
|
||||
element = <ContentLanguagesSettingsModal.Component />
|
||||
} else if (modal.name === 'post-languages-settings') {
|
||||
|
|
@ -114,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <AltTextImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'edit-image') {
|
||||
element = <EditImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'moderation-details') {
|
||||
element = <ModerationDetailsModal.Component {...modal} />
|
||||
} else if (modal.name === 'verify-email') {
|
||||
element = <VerifyEmailModal.Component {...modal} />
|
||||
} else if (modal.name === 'change-email') {
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {listUriToHref} from 'lib/strings/url-helpers'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
|
||||
export const snapPoints = [300]
|
||||
|
||||
export function Component({
|
||||
context,
|
||||
moderation,
|
||||
}: {
|
||||
context: 'account' | 'content'
|
||||
moderation: ModerationUI
|
||||
}) {
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
let name
|
||||
let description
|
||||
if (!moderation.cause) {
|
||||
name = _(msg`Content Warning`)
|
||||
description = _(
|
||||
msg`Moderator has chosen to set a general warning on the content.`,
|
||||
)
|
||||
} else if (moderation.cause.type === 'blocking') {
|
||||
if (moderation.cause.source.type === 'list') {
|
||||
const list = moderation.cause.source.list
|
||||
name = _(msg`User Blocked by List`)
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<TextLink
|
||||
type="2xl"
|
||||
href={listUriToHref(list.uri)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
/>{' '}
|
||||
list which you have blocked.
|
||||
</Trans>
|
||||
)
|
||||
} else {
|
||||
name = _(msg`User Blocked`)
|
||||
description = _(
|
||||
msg`You have blocked this user. You cannot view their content.`,
|
||||
)
|
||||
}
|
||||
} else if (moderation.cause.type === 'blocked-by') {
|
||||
name = _(msg`User Blocks You`)
|
||||
description = _(
|
||||
msg`This user has blocked you. You cannot view their content.`,
|
||||
)
|
||||
} else if (moderation.cause.type === 'block-other') {
|
||||
name = _(msg`Content Not Available`)
|
||||
description = _(
|
||||
msg`This content is not available because one of the users involved has blocked the other.`,
|
||||
)
|
||||
} else if (moderation.cause.type === 'muted') {
|
||||
if (moderation.cause.source.type === 'list') {
|
||||
const list = moderation.cause.source.list
|
||||
name = _(msg`Account Muted by List`)
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<TextLink
|
||||
type="2xl"
|
||||
href={listUriToHref(list.uri)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
/>{' '}
|
||||
list which you have muted.
|
||||
</Trans>
|
||||
)
|
||||
} else {
|
||||
name = _(msg`Account Muted`)
|
||||
description = _(msg`You have muted this user.`)
|
||||
}
|
||||
} else {
|
||||
name = moderation.cause.labelDef.strings[context].en.name
|
||||
description = moderation.cause.labelDef.strings[context].en.description
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="moderationDetailsModal"
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingHorizontal: isMobile ? 14 : 0,
|
||||
},
|
||||
pal.view,
|
||||
]}>
|
||||
<Text type="title-xl" style={[pal.text, styles.title]}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text type="2xl" style={[pal.text, styles.description]}>
|
||||
{description}
|
||||
</Text>
|
||||
<View style={s.flex1} />
|
||||
<Button
|
||||
type="primary"
|
||||
style={styles.btn}
|
||||
onPress={() => {
|
||||
closeModal()
|
||||
}}>
|
||||
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
|
||||
Okay
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
btn: {
|
||||
paddingVertical: 14,
|
||||
marginTop: isWeb ? 40 : 0,
|
||||
marginBottom: isWeb ? 0 : 40,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import React from 'react'
|
||||
import {View, TouchableOpacity, StyleSheet} from 'react-native'
|
||||
import {TextInput} from '../util'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {CharProgress} from '../../composer/char-progress/CharProgress'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {SendReportButton} from './SendReportButton'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function InputIssueDetails({
|
||||
details,
|
||||
setDetails,
|
||||
goBack,
|
||||
submitReport,
|
||||
isProcessing,
|
||||
}: {
|
||||
details: string | undefined
|
||||
setDetails: (v: string) => void
|
||||
goBack: () => void
|
||||
submitReport: () => void
|
||||
isProcessing: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginTop: isMobile ? 12 : 0,
|
||||
}}>
|
||||
<TouchableOpacity
|
||||
testID="addDetailsBtn"
|
||||
style={[s.mb10, styles.backBtn]}
|
||||
onPress={goBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Add details`)}
|
||||
accessibilityHint="Add more details to your report">
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
||||
<Text style={[pal.text, s.f18, pal.link]}>
|
||||
{' '}
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
||||
<TextInput
|
||||
accessibilityLabel={_(msg`Text input field`)}
|
||||
accessibilityHint="Enter a reason for reporting this post."
|
||||
placeholder="Enter a reason or any other details here."
|
||||
placeholderTextColor={pal.textLight.color}
|
||||
value={details}
|
||||
onChangeText={setDetails}
|
||||
autoFocus={true}
|
||||
numberOfLines={3}
|
||||
multiline={true}
|
||||
textAlignVertical="top"
|
||||
maxLength={300}
|
||||
style={[styles.detailsInput, pal.text]}
|
||||
/>
|
||||
<View style={styles.detailsInputBottomBar}>
|
||||
<View style={styles.charCounter}>
|
||||
<CharProgress count={details?.length || 0} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<SendReportButton onPress={submitReport} isProcessing={isProcessing} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
detailsInputContainer: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
detailsInput: {
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
borderRadius: 8,
|
||||
minHeight: 100,
|
||||
fontSize: 16,
|
||||
},
|
||||
detailsInputBottomBar: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
charCounter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
import React, {useState, useMemo} from 'react'
|
||||
import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import * as Toast from '../../util/Toast'
|
||||
import {ErrorMessage} from '../../util/error/ErrorMessage'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {SendReportButton} from './SendReportButton'
|
||||
import {InputIssueDetails} from './InputIssueDetails'
|
||||
import {ReportReasonOptions} from './ReasonOptions'
|
||||
import {CollectionId} from './types'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {getAgent} from '#/state/session'
|
||||
|
||||
const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
||||
|
||||
export const snapPoints = [575]
|
||||
|
||||
const CollectionNames = {
|
||||
[CollectionId.FeedGenerator]: 'Feed',
|
||||
[CollectionId.Profile]: 'Profile',
|
||||
[CollectionId.List]: 'List',
|
||||
[CollectionId.Post]: 'Post',
|
||||
}
|
||||
|
||||
type ReportComponentProps =
|
||||
| {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
| {
|
||||
did: string
|
||||
}
|
||||
|
||||
export function Component(content: ReportComponentProps) {
|
||||
const {closeModal} = useModalControls()
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [showDetailsInput, setShowDetailsInput] = useState(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [issue, setIssue] = useState<string>('')
|
||||
const [details, setDetails] = useState<string>('')
|
||||
const isAccountReport = 'did' in content
|
||||
const subjectKey = isAccountReport ? content.did : content.uri
|
||||
const atUri = useMemo(
|
||||
() => (!isAccountReport ? new AtUri(subjectKey) : null),
|
||||
[isAccountReport, subjectKey],
|
||||
)
|
||||
|
||||
const submitReport = async () => {
|
||||
setError('')
|
||||
if (!issue) {
|
||||
return
|
||||
}
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
if (issue === '__copyright__') {
|
||||
Linking.openURL(DMCA_LINK)
|
||||
closeModal()
|
||||
return
|
||||
}
|
||||
const $type = !isAccountReport
|
||||
? 'com.atproto.repo.strongRef'
|
||||
: 'com.atproto.admin.defs#repoRef'
|
||||
await getAgent().createModerationReport({
|
||||
reasonType: issue,
|
||||
subject: {
|
||||
$type,
|
||||
...content,
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
Toast.show("Thank you for your report! We'll look into it promptly.")
|
||||
|
||||
closeModal()
|
||||
return
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
setShowDetailsInput(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView testID="reportModal" style={[s.flex1, pal.view]}>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
isMobile && {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
]}>
|
||||
{showDetailsInput ? (
|
||||
<InputIssueDetails
|
||||
details={details}
|
||||
setDetails={setDetails}
|
||||
goBack={goBack}
|
||||
submitReport={submitReport}
|
||||
isProcessing={isProcessing}
|
||||
/>
|
||||
) : (
|
||||
<SelectIssue
|
||||
setShowDetailsInput={setShowDetailsInput}
|
||||
error={error}
|
||||
issue={issue}
|
||||
setIssue={setIssue}
|
||||
submitReport={submitReport}
|
||||
isProcessing={isProcessing}
|
||||
atUri={atUri}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
// If no atUri is passed, that means the reporting collection is account
|
||||
const getCollectionNameForReport = (atUri: AtUri | null) => {
|
||||
if (!atUri) return 'Account'
|
||||
// Generic fallback for any collection being reported
|
||||
return CollectionNames[atUri.collection as CollectionId] || 'Content'
|
||||
}
|
||||
|
||||
const SelectIssue = ({
|
||||
error,
|
||||
setShowDetailsInput,
|
||||
issue,
|
||||
setIssue,
|
||||
submitReport,
|
||||
isProcessing,
|
||||
atUri,
|
||||
}: {
|
||||
error: string | undefined
|
||||
setShowDetailsInput: (v: boolean) => void
|
||||
issue: string | undefined
|
||||
setIssue: (v: string) => void
|
||||
submitReport: () => void
|
||||
isProcessing: boolean
|
||||
atUri: AtUri | null
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const collectionName = getCollectionNameForReport(atUri)
|
||||
const onSelectIssue = (v: string) => setIssue(v)
|
||||
const goToDetails = () => {
|
||||
if (issue === '__copyright__') {
|
||||
Linking.openURL(DMCA_LINK)
|
||||
return
|
||||
}
|
||||
setShowDetailsInput(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Report {collectionName}</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.textLight, styles.description]}>
|
||||
<Trans>What is the issue with this {collectionName}?</Trans>
|
||||
</Text>
|
||||
<View style={{marginBottom: 10}}>
|
||||
<ReportReasonOptions
|
||||
atUri={atUri}
|
||||
selectedIssue={issue}
|
||||
onSelectIssue={onSelectIssue}
|
||||
/>
|
||||
</View>
|
||||
{error ? <ErrorMessage message={error} /> : undefined}
|
||||
{/* If no atUri is present, the report would be for account in which case, we allow sending without specifying a reason */}
|
||||
{issue || !atUri ? (
|
||||
<>
|
||||
<SendReportButton
|
||||
onPress={submitReport}
|
||||
isProcessing={isProcessing}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="addDetailsBtn"
|
||||
style={styles.addDetailsBtn}
|
||||
onPress={goToDetails}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Add details`)}
|
||||
accessibilityHint="Add more details to your report">
|
||||
<Text style={[s.f18, pal.link]}>
|
||||
<Trans>Add details to report</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : undefined}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
fontSize: 17,
|
||||
paddingHorizontal: 22,
|
||||
marginBottom: 10,
|
||||
},
|
||||
addDetailsBtn: {
|
||||
padding: 14,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
})
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import {View} from 'react-native'
|
||||
import React, {useMemo} from 'react'
|
||||
import {AtUri, ComAtprotoModerationDefs} from '@atproto/api'
|
||||
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {UsePaletteValue, usePalette} from 'lib/hooks/usePalette'
|
||||
import {RadioGroup, RadioGroupItem} from 'view/com/util/forms/RadioGroup'
|
||||
import {CollectionId} from './types'
|
||||
|
||||
type ReasonMap = Record<string, {title: string; description: string}>
|
||||
const CommonReasons = {
|
||||
[ComAtprotoModerationDefs.REASONRUDE]: {
|
||||
title: 'Anti-Social Behavior',
|
||||
description: 'Harassment, trolling, or intolerance',
|
||||
},
|
||||
[ComAtprotoModerationDefs.REASONVIOLATION]: {
|
||||
title: 'Illegal and Urgent',
|
||||
description: 'Glaring violations of law or terms of service',
|
||||
},
|
||||
[ComAtprotoModerationDefs.REASONOTHER]: {
|
||||
title: 'Other',
|
||||
description: 'An issue not included in these options',
|
||||
},
|
||||
}
|
||||
const CollectionToReasonsMap: Record<string, ReasonMap> = {
|
||||
[CollectionId.Post]: {
|
||||
[ComAtprotoModerationDefs.REASONSPAM]: {
|
||||
title: 'Spam',
|
||||
description: 'Excessive mentions or replies',
|
||||
},
|
||||
[ComAtprotoModerationDefs.REASONSEXUAL]: {
|
||||
title: 'Unwanted Sexual Content',
|
||||
description: 'Nudity or pornography not labeled as such',
|
||||
},
|
||||
__copyright__: {
|
||||
title: 'Copyright Violation',
|
||||
description: 'Contains copyrighted material',
|
||||
},
|
||||
...CommonReasons,
|
||||
},
|
||||
[CollectionId.List]: {
|
||||
...CommonReasons,
|
||||
[ComAtprotoModerationDefs.REASONVIOLATION]: {
|
||||
title: 'Name or Description Violates Community Standards',
|
||||
description: 'Terms used violate community standards',
|
||||
},
|
||||
},
|
||||
}
|
||||
const AccountReportReasons = {
|
||||
[ComAtprotoModerationDefs.REASONMISLEADING]: {
|
||||
title: 'Misleading Account',
|
||||
description: 'Impersonation or false claims about identity or affiliation',
|
||||
},
|
||||
[ComAtprotoModerationDefs.REASONSPAM]: {
|
||||
title: 'Frequently Posts Unwanted Content',
|
||||
description: 'Spam; excessive mentions or replies',
|
||||
},
|
||||
[ComAtprotoModerationDefs.REASONVIOLATION]: {
|
||||
title: 'Name or Description Violates Community Standards',
|
||||
description: 'Terms used violate community standards',
|
||||
},
|
||||
}
|
||||
|
||||
const Option = ({
|
||||
pal,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
pal: UsePaletteValue
|
||||
description: string
|
||||
title: string
|
||||
}) => {
|
||||
return (
|
||||
<View>
|
||||
<Text style={pal.text} type="md-bold">
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={pal.textLight}>{description}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// This is mostly just content copy without almost any logic
|
||||
// so this may grow over time and it makes sense to split it up into its own file
|
||||
// to keep it separate from the actual reporting modal logic
|
||||
const useReportRadioOptions = (pal: UsePaletteValue, atUri: AtUri | null) =>
|
||||
useMemo(() => {
|
||||
let items: ReasonMap = {...CommonReasons}
|
||||
// If no atUri is passed, that means the reporting collection is account
|
||||
if (!atUri) {
|
||||
items = {...AccountReportReasons}
|
||||
}
|
||||
|
||||
if (atUri?.collection && CollectionToReasonsMap[atUri.collection]) {
|
||||
items = {...CollectionToReasonsMap[atUri.collection]}
|
||||
}
|
||||
|
||||
return Object.entries(items).map(([key, {title, description}]) => ({
|
||||
key,
|
||||
label: <Option pal={pal} title={title} description={description} />,
|
||||
}))
|
||||
}, [pal, atUri])
|
||||
|
||||
export const ReportReasonOptions = ({
|
||||
atUri,
|
||||
selectedIssue,
|
||||
onSelectIssue,
|
||||
}: {
|
||||
atUri: AtUri | null
|
||||
selectedIssue?: string
|
||||
onSelectIssue: (key: string) => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const ITEMS: RadioGroupItem[] = useReportRadioOptions(pal, atUri)
|
||||
return (
|
||||
<RadioGroup
|
||||
items={ITEMS}
|
||||
onSelect={onSelectIssue}
|
||||
testID="reportReasonRadios"
|
||||
initialSelection={selectedIssue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import React from 'react'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s, gradients, colors} from 'lib/styles'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function SendReportButton({
|
||||
onPress,
|
||||
isProcessing,
|
||||
}: {
|
||||
onPress: () => void
|
||||
isProcessing: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
// loading state
|
||||
// =
|
||||
if (isProcessing) {
|
||||
return (
|
||||
<View style={[styles.btn, s.mt10]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID="sendReportBtn"
|
||||
style={s.mt10}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Report post`)}
|
||||
accessibilityHint={`Reports post with reason and details`}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Send Report</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.gray1,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// TODO: ATM, @atproto/api does not export ids but it does have these listed at @atproto/api/client/lexicons
|
||||
// once we start exporting the ids from the @atproto/ap package, replace these hardcoded ones
|
||||
export enum CollectionId {
|
||||
FeedGenerator = 'app.bsky.feed.generator',
|
||||
Profile = 'app.bsky.actor.profile',
|
||||
List = 'app.bsky.graph.list',
|
||||
Post = 'app.bsky.feed.post',
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue