Remove dead code and start moving toward latest modsdk

This commit is contained in:
Paul Frazee
2024-02-29 16:51:37 -08:00
parent 05863426c5
commit ad135fa705
24 changed files with 87 additions and 1179 deletions
+13 -9
View File
@@ -2,25 +2,29 @@ import React from 'react'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {LABEL_GROUPS} from '@atproto/api'
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import * as ToggleButton from '#/components/forms/ToggleButton'
export function ModerationLabelPref({
labelGroup,
label,
disabled,
}: {
labelGroup: keyof typeof LABEL_GROUPS
label: string
disabled?: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const labelGroupStrings = useLabelGroupStrings()
const groupInfoStrings = labelGroupStrings[labelGroup]
const allLabelStrings = useLabelStrings()
const labelStrings = allLabelStrings[label] || {
general: {
name: label,
description: `Labeled "${label}"`,
},
}
// TODO add onChange behavior when mod prefs are updated
@@ -42,16 +46,16 @@ export function ModerationLabelPref({
a.align_center,
]}>
<View style={[a.gap_xs, {width: '50%'}]}>
<Text style={[a.font_bold]}>{groupInfoStrings.name}</Text>
<Text style={[a.font_bold]}>{labelStrings.general.name}</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{groupInfoStrings.description}
{labelStrings.general.description}
</Text>
</View>
<View style={[a.justify_center, {minHeight: 35}]}>
{!disabled && (
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${groupInfoStrings.name.toLowerCase()}`,
msg`Configure content filtering setting for category: ${labelStrings.general.name.toLowerCase()}`,
)}
values={['hide']}
onChange={() => {}}>
+1 -4
View File
@@ -32,7 +32,6 @@ import {
getModerationServiceTitle,
useConfigurableContentLabelGroups,
useConfigurableProfileLabelGroups,
getLabelGroupToLabelerMap,
} from '#/lib/moderation'
import {DMCA_LINK} from '#/components/ReportDialog/const'
import {Link} from '#/components/Link'
@@ -165,12 +164,10 @@ function SubmitView({
selectedLabelGroup,
goBack,
onSubmitComplete,
labelGroupToLabelerMap,
}: ReportDialogProps & {
selectedLabelGroup: ReportDialogLabelIds
goBack: () => void
onSubmitComplete: () => void
labelGroupToLabelerMap: ReturnType<typeof getLabelGroupToLabelerMap>
}) {
const t = useTheme()
const {_} = useLingui()
@@ -178,7 +175,7 @@ function SubmitView({
const groupInfoStrings = labelGroupStrings[selectedLabelGroup]
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const supportedLabelers = labelGroupToLabelerMap[selectedLabelGroup]
const supportedLabelers = [] //labelGroupToLabelerMap[selectedLabelGroup]
const [selectedServices, setSelectedServices] = React.useState<string[]>(
supportedLabelers?.map(labeler => labeler.creator.did) || [],
)
+6 -71
View File
@@ -2,8 +2,6 @@ import React from 'react'
import {
ModerationCause,
ModerationUI,
LABEL_GROUPS,
LabelGroupDefinition,
AppBskyModerationDefs,
} from '@atproto/api'
@@ -27,54 +25,19 @@ export function isJustAMute(modui: ModerationUI): boolean {
return modui.filters.length === 1 && modui.filters[0].type === 'muted'
}
export function getLabelGroupsFromLabels(labels: string[]) {
const groups: LabelGroupDefinition[] = []
for (const label of labels) {
for (const group in LABEL_GROUPS) {
const def = LABEL_GROUPS[group as LabelGroupDefinition['id']]
if (def.labels.find(l => l.id === label)) {
groups.push(def)
}
}
}
return Array.from(groups)
}
export function getConfigurableLabelGroups() {
return Object.values(LABEL_GROUPS).filter(group => group.configurable)
}
export function useConfigurableLabelGroups() {
return React.useMemo(() => getConfigurableLabelGroups(), [])
}
export function useConfigurableContentLabelGroups() {
return React.useMemo(() => {
const groups = getConfigurableLabelGroups()
return groups.filter(group => {
return group.labels.every(l => l.targets.includes('content'))
})
}, [])
// TODO removeme
return []
}
export function useConfigurableProfileLabelGroups() {
return React.useMemo(() => {
const groups = getConfigurableLabelGroups()
return groups.filter(group => {
return group.labels.every(l => l.targets.includes('profile'))
})
}, [])
// TODO removeme
return []
}
export function useConfigurableAccountLabelGroups() {
return React.useMemo(() => {
const groups = getConfigurableLabelGroups()
return groups.filter(group => {
return group.labels.every(l => l.targets.includes('account'))
})
}, [])
// TODO removeme
return []
}
export function getModerationServiceTitle({
@@ -88,31 +51,3 @@ export function getModerationServiceTitle({
? sanitizeDisplayName(displayName)
: sanitizeHandle(handle, '@')
}
export function getLabelGroupToLabelerMap(
labelers: AppBskyModerationDefs.ModServiceViewDetailed[],
) {
if (!labelers) return {}
const groups: Partial<
Record<
LabelGroupDefinition['id'] | 'other',
AppBskyModerationDefs.ModServiceViewDetailed[]
>
> = {
// `other` reports go to all labelers TODO confirm this
other: labelers,
}
for (const modservice of labelers) {
const labelGroups = getLabelGroupsFromLabels(
modservice.policies.labelValues,
)
for (const group of labelGroups) {
const g = (groups[group.id] = groups[group.id] || [])
g.push(modservice)
}
}
return groups
}
+1 -2
View File
@@ -1,10 +1,9 @@
import {LABELS} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
export type LabelStrings = Record<
keyof typeof LABELS,
string,
{
general: {name: string; description: string}
account: {name: string; description: string}
@@ -1,4 +1,4 @@
import {ModerationCause} from '@atproto/api'
import {ModerationCause, LABELS} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useLabelStrings} from './useLabelStrings'
@@ -76,8 +76,9 @@ export function useModerationCauseDescription(
}
}
if (cause.type === 'label') {
if (cause.labelDef.id in labelStrings) {
const strings = labelStrings[cause.labelDef.id]
if (cause.labelDef.identifier in labelStrings) {
const strings =
labelStrings[cause.labelDef.identifier as keyof typeof LABELS]
return {
name:
context === 'account' ? strings.account.name : strings.content.name,
@@ -88,8 +89,8 @@ export function useModerationCauseDescription(
}
}
return {
name: cause.labelDef.id,
description: _(msg`Labeled ${cause.labelDef.id}`),
name: cause.labelDef.identifier,
description: _(msg`Labeled ${cause.labelDef.identifier}`),
}
}
// should never happen
+6 -4
View File
@@ -86,7 +86,7 @@ export function ModerationScreen(
data: modservices,
error: modservicesError,
} = useModServicesDetailedInfoQuery({
dids: preferences ? preferences.moderationOpts.mods.map(m => m.did) : [],
dids: preferences ? preferences.moderationPrefs.mods.map(m => m.did) : [],
})
const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame()
@@ -149,7 +149,7 @@ export function ModerationScreenInner({
usePreferencesSetAdultContentMutation()
const adultContentEnabled = !!(
(optimisticAdultContent && optimisticAdultContent.enabled) ||
(!optimisticAdultContent && preferences.moderationOpts.adultContentEnabled)
(!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
)
const onToggleAdultContentEnabled = React.useCallback(
@@ -323,7 +323,9 @@ export function ModerationScreenInner({
modservice={{
uri: '',
cid: '',
policies: {},
policies: {
labelValues: [],
},
creator: {
did: '',
handle: 'safety.bsky.app',
@@ -360,7 +362,7 @@ export function ModerationScreenInner({
handle: mod.creator.handle,
})}
handle={mod.creator.handle}
description={mod.description}
description={mod.creator.description || ''}
/>
</ModerationServiceCard.Card.Outer>
</ModerationServiceCard.Link>
@@ -69,7 +69,7 @@ let ProfileHeaderModerator = ({
useModServiceSubscriptionMutation()
const isSubscribed =
variables?.subscribe ??
preferences?.moderationOpts.mods.find(mod => mod.did === profile.did)
preferences?.moderationPrefs.mods.find(mod => mod.did === profile.did)
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation()
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {useModServiceSubscriptionMutation} from '#/state/queries/modservice'
import {getLabelGroupsFromLabels} from '#/lib/moderation'
import {logger} from '#/logger'
import {useTheme, atoms as a} from '#/alf'
@@ -76,15 +75,8 @@ export function ProfileContentFiltersSectionInner({
}) {
const {_} = useLingui()
const t = useTheme()
const groups = React.useMemo(() => {
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
def => def.configurable,
)
}, [modservice.policies.labelValues])
const isEnabled = Boolean(
moderationOpts.mods.find(
mod => mod.did === modservice.creator.did && mod.enabled,
),
moderationOpts.prefs.mods.find(mod => mod.did === modservice.creator.did),
)
const hasSession = true // TODO
@@ -92,7 +84,7 @@ export function ProfileContentFiltersSectionInner({
useModServiceSubscriptionMutation()
const isSubscribed =
variables?.subscribe ??
moderationOpts.mods.find(mod => mod.did === modservice.creator.did)
moderationOpts.prefs.mods.find(mod => mod.did === modservice.creator.did)
const onPressSubscribe = React.useCallback(async () => {
try {
@@ -142,7 +134,8 @@ export function ProfileContentFiltersSectionInner({
a.border,
t.atoms.border_contrast_low,
]}>
{groups.map((def, i) => {
{
undefined /* TODO modservice.policies.labelValues.map((def, i) => {
return (
<React.Fragment key={def.id}>
{i !== 0 && <Divider />}
@@ -152,7 +145,8 @@ export function ProfileContentFiltersSectionInner({
/>
</React.Fragment>
)
})}
})*/
}
</View>
<View style={{height: 100}} />
-5
View File
@@ -119,10 +119,6 @@ export interface AddAppPasswordModal {
name: 'add-app-password'
}
export interface ContentFilteringSettingsModal {
name: 'content-filtering-settings'
}
export interface ContentLanguagesSettingsModal {
name: 'content-languages-settings'
}
@@ -182,7 +178,6 @@ export type Modal =
| SwitchAccountModal
// Curation
| ContentFilteringSettingsModal
| ContentLanguagesSettingsModal
| PostLanguagesSettingsModal
+4 -1
View File
@@ -10,7 +10,10 @@ import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
import {isInvalidHandle} from '#/lib/strings/handles'
import {isJustAMute} from '#/lib/moderation'
const DEFAULT_MOD_OPTS = DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts
const DEFAULT_MOD_OPTS = {
userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
}
export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix]
+10 -68
View File
@@ -26,10 +26,11 @@ export function useModServiceInfoQuery({
enabled: !!did && enabled !== false,
queryKey: modServiceInfoQueryKey(did as string),
queryFn: async () => {
const res = await getAgent().app.bsky.moderation.getService({
did: did as string,
const res = await getAgent().app.bsky.moderation.getServices({
dids: [did as string],
detailed: true,
})
return res.data
return res.data.views[0] as AppBskyModerationDefs.ModServiceViewDetailed
},
})
}
@@ -40,7 +41,7 @@ export function useModServicesInfoQuery({dids}: {dids: string[]}) {
queryKey: modServicesInfoQueryKey(dids),
queryFn: async () => {
const res = await getAgent().app.bsky.moderation.getServices({dids})
return res.data.views
return res.data.views as AppBskyModerationDefs.ModServiceView[]
},
})
}
@@ -50,23 +51,11 @@ export function useModServicesDetailedInfoQuery({dids}: {dids: string[]}) {
enabled: !!dids.length,
queryKey: modServicesDetailedInfoQueryKey(dids),
queryFn: async () => {
const views: AppBskyModerationDefs.ModServiceViewDetailed[] = []
await Promise.all(
dids.map(did => {
return getAgent()
.app.bsky.moderation.getService({did})
.then(res => {
views.push(res.data)
})
.catch(e => {
console.error(e)
return null
})
}),
)
return views
const res = await getAgent().app.bsky.moderation.getServices({
dids,
detailed: true,
})
return res.data.views as AppBskyModerationDefs.ModServiceViewDetailed[]
},
})
}
@@ -95,50 +84,3 @@ export function useModServiceSubscriptionMutation() {
},
})
}
export function useModServiceEnableMutation() {
const queryClient = useQueryClient()
return useMutation({
async mutationFn({did, enabled}: {did: string; enabled: boolean}) {
// TODO
z.object({
did: z.string(),
enabled: z.boolean(),
}).parse({did, enabled})
await getAgent().setModServiceEnabled(did, enabled)
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
export function useModServiceLabelGroupEnableMutation() {
const queryClient = useQueryClient()
return useMutation({
async mutationFn({
did,
group,
enabled,
}: {
did: string
group: string
enabled: boolean
}) {
// TODO
z.object({
did: z.string(),
group: z.string(),
enabled: z.boolean(),
}).parse({did, group, enabled})
await getAgent().setModServiceLabelGroupEnabled(did, group, enabled)
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
+4 -4
View File
@@ -423,10 +423,10 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
let somePostsPassModeration = false
for (const item of feed) {
const moderation = moderatePost(
item.post,
DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts,
)
const moderation = moderatePost(item.post, {
userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
})
if (!moderation.ui('contentList').filter) {
// we have a sfw post
+4 -3
View File
@@ -34,14 +34,15 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
pinned: [],
unpinned: [],
},
moderationOpts: {
userDid: '',
moderationPrefs: {
adultContentEnabled: false,
labelGroups: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
mods: [],
},
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
userAge: 13, // TODO(pwi)
interests: {tags: []},
mutedWords: [],
hiddenPosts: [],
}
+12 -8
View File
@@ -4,7 +4,6 @@ import {
LabelPreference,
BskyFeedViewPreference,
ModerationOpts,
LabelGroupDefinition,
} from '@atproto/api'
import {track} from '#/lib/analytics/analytics'
@@ -77,17 +76,20 @@ export function useModerationOpts() {
const override = useContext(moderationOptsOverrideContext)
const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => {
const opts = useMemo<ModerationOpts | undefined>(() => {
if (override) {
return override
}
if (!prefs.data) {
return
}
const moderationOpts = prefs.data.moderationOpts
const moderationPrefs = prefs.data.moderationPrefs
return {
...moderationOpts,
hiddenPosts,
userDid: '', // TODO
prefs: {
...moderationPrefs,
hiddenPosts,
},
}
}, [override, prefs.data, hiddenPosts])
return opts
@@ -130,13 +132,15 @@ export function useSetContentLabelMutation() {
return useMutation({
mutationFn: async ({
labelGroup,
label,
visibility,
labelerDid,
}: {
labelGroup: LabelGroupDefinition['id']
label: string
visibility: LabelPreference
labelerDid?: string
}) => {
await getAgent().setContentLabelPref(labelGroup, visibility)
await getAgent().setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
+3 -6
View File
@@ -1,4 +1,4 @@
import {ComAtprotoLabelDefs, DEFAULT_LABEL_GROUP_SETTINGS} from '@atproto/api'
import {ComAtprotoLabelDefs, DEFAULT_LABEL_SETTINGS} from '@atproto/api'
import {
LabelGroup,
@@ -21,12 +21,9 @@ export type LabelGroupConfig = {
*
* TODO(pwi)
*/
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_GROUP_SETTINGS =
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS =
Object.fromEntries(
Object.entries(DEFAULT_LABEL_GROUP_SETTINGS).map(([key, _pref]) => [
key,
'hide',
]),
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
)
export const CONFIGURABLE_LABEL_GROUPS: Record<
@@ -1,406 +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'
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?.moderationOpts.adultContentEnabled}
/>
<ContentLabelPref
preferences={preferences}
labelGroup="nudity"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<ContentLabelPref
preferences={preferences}
labelGroup="suggestive"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<ContentLabelPref
preferences={preferences}
labelGroup="gore"
disabled={!preferences?.moderationOpts.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 {openModal} = useModalControls()
const onSetAge = React.useCallback(
() => openModal({name: 'birth-date-settings'}),
[openModal],
)
const onToggleAdultContent = React.useCallback(async () => {
if (isIOS) return
try {
mutate({
enabled: !(
variables?.enabled ?? preferences?.moderationOpts.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}>
{isIOS ? (
preferences?.moderationOpts.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?.moderationOpts.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?.moderationOpts.labelGroups?.[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,
},
})
-4
View File
@@ -22,7 +22,6 @@ import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
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 BirthDateSettingsModal from './BirthDateSettings'
@@ -113,9 +112,6 @@ 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 />
-3
View File
@@ -24,7 +24,6 @@ import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
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 BirthDateSettingsModal from './BirthDateSettings'
@@ -107,8 +106,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') {
@@ -1,103 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {s} 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 * 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,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
export function AdultContentEnabledPref() {
const pal = usePalette('default')
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetAdultContentMutation()
const {openModal} = useModalControls()
const onSetAge = React.useCallback(
() => openModal({name: 'birth-date-settings'}),
[openModal],
)
const onToggleAdultContent = React.useCallback(async () => {
if (isIOS) return
try {
mutate({
enabled: !(
variables?.enabled ?? preferences?.moderationOpts.adultContentEnabled
),
})
} catch (e) {
Toast.show(
_(msg`There was an issue syncing your preferences with the server`),
)
logger.error('Failed to update preferences with server', {error: e})
}
}, [variables, preferences, mutate, _])
return (
<View style={[pal.border, {borderTopWidth: 1, paddingHorizontal: 12}]}>
{isIOS ? (
preferences?.moderationOpts.adultContentEnabled ? null : (
<View style={{paddingVertical: 12}}>
<Text type="md" style={pal.textLight}>
<Trans>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Trans>
</Text>
</View>
)
) : (preferences?.userAge || 0) >= 18 ? (
<View style={{paddingVertical: 4}}>
<ToggleButton
type="default-light"
label={_(msg`Enable Adult Content`)}
isSelected={
variables?.enabled ??
preferences?.moderationOpts.adultContentEnabled ??
false
}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
/>
</View>
) : (
<View style={styles.agePrompt}>
<Text type="md" style={[pal.text, {flex: 1}]}>
<Trans>You must be 18 or older to enable adult content.</Trans>
</Text>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
agePrompt: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
toggleBtn: {
paddingHorizontal: 0,
backgroundColor: 'transparent',
},
})
-178
View File
@@ -1,178 +0,0 @@
import React from 'react'
import {LabelPreference} from '@atproto/api'
import {StyleSheet, Pressable, View} from 'react-native'
import {s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
usePreferencesSetContentLabelMutation,
ConfigurableLabelGroup,
CONFIGURABLE_LABEL_GROUPS,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
export function LabelGroupPref({
preferences,
labelGroup,
disabled,
}: {
preferences?: UsePreferencesQueryResponse
labelGroup: ConfigurableLabelGroup
disabled?: boolean
}) {
const pal = usePalette('default')
const visibility = preferences?.moderationOpts.labelGroups?.[labelGroup]
const {mutate, variables} = usePreferencesSetContentLabelMutation()
const onChange = React.useCallback(
(vis: LabelPreference) => {
mutate({labelGroup, visibility: vis})
},
[mutate, labelGroup],
)
return (
<View style={[styles.labelGroupPref, 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({
labelGroupPref: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 14,
paddingLeft: 14,
paddingRight: 10,
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,
},
})
@@ -1,226 +0,0 @@
import React from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {CenteredView} from '../util/Views'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types'
import {BACK_HITSLOP} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useSetDrawerOpen} from '#/state/shell'
import {emitSoftReset} from '#/state/events'
import {AppBskyModerationDefs} from '@atproto/api'
import {HandIcon} from '#/lib/icons'
import {shareUrl} from 'lib/sharing'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {Button} from '../util/forms/Button'
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {useSession} from '#/state/session'
import {useModalControls} from '#/state/modals'
export function ModServiceHeader({
info,
}: {
info: AppBskyModerationDefs.ModServiceViewDetailed
}) {
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const canGoBack = navigation.canGoBack()
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
const onPressMenu = React.useCallback(() => {
setDrawerOpen(true)
}, [setDrawerOpen])
return (
<CenteredView style={pal.view}>
{isMobile && (
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
paddingTop: isNative ? 0 : 8,
paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14,
},
pal.border,
]}>
<Pressable
testID="headerDrawerBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}
accessibilityRole="button"
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
accessibilityHint="">
{canGoBack ? (
<FontAwesomeIcon
size={18}
icon="angle-left"
style={[styles.backIcon, pal.text]}
/>
) : (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
)}
</Pressable>
<View style={{flex: 1}} />
<CommonControls info={info} />
</View>
)}
<View
style={{
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
paddingTop: 14,
paddingBottom: 14,
paddingHorizontal: isMobile ? 12 : 14,
}}>
<View style={{alignSelf: 'center'}}>
<HandIcon style={pal.text} size={32} strokeWidth={5.5} />
</View>
<View style={{flex: 1}}>
<TextLink
testID="headerTitle"
type="title-xl"
href={makeProfileLink(info.creator, 'modservice')}
style={[pal.text, {fontWeight: 'bold'}]}
text={
info.creator.displayName
? sanitizeDisplayName(info.creator.displayName)
: sanitizeHandle(info.creator.handle, '@')
}
onPress={emitSoftReset}
numberOfLines={4}
/>
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
<Trans>Moderation service</Trans>
</Text>
</View>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
}}>
<Button type="primary" label={_(msg`Subscribe`)} />
{!isMobile && <CommonControls info={info} />}
</View>
</View>
</CenteredView>
)
}
function CommonControls({
info,
}: {
info: AppBskyModerationDefs.ModServiceViewDetailed
}) {
const pal = usePalette('default')
const {hasSession} = useSession()
const {openModal} = useModalControls()
const {_} = useLingui()
const onPressShare = React.useCallback(() => {
const url = makeProfileLink(info.creator, 'modservice')
shareUrl(url)
// track('CustomFeed:Share') TODO
}, [info /*, track*/])
const onPressReport = React.useCallback(() => {
if (!info) return
openModal({
name: 'report',
uri: info.uri,
cid: info.cid,
})
}, [openModal, info])
const dropdownItems: DropdownItem[] = React.useMemo(() => {
return [
hasSession && {
testID: 'modHeaderDropdownReportBtn',
label: _(msg`Report mod service`),
onPress: onPressReport,
icon: {
ios: {
name: 'exclamationmark.triangle',
},
android: 'ic_menu_report_image',
web: 'circle-exclamation',
},
},
{
testID: 'modHeaderDropdownShareBtn',
label: _(msg`Share mod service`),
onPress: onPressShare,
icon: {
ios: {
name: 'square.and.arrow.up',
},
android: 'ic_menu_share',
web: 'share',
},
},
].filter(Boolean) as DropdownItem[]
}, [hasSession, onPressReport, onPressShare, _])
return (
<>
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[pal.viewLight, styles.btn, {marginLeft: 6}]}>
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
</View>
</NativeDropdown>
</>
)
}
const styles = StyleSheet.create({
backBtn: {
width: 20,
height: 30,
},
backBtnWide: {
width: 20,
height: 30,
paddingHorizontal: 6,
},
backIcon: {
marginTop: 6,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingVertical: 7,
paddingHorizontal: 14,
borderRadius: 50,
},
})
@@ -1,42 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {LabelGroupPref} from './LabelGroupPref'
import {Text} from '../util/text/Text'
import {usePalette} from '#/lib/hooks/usePalette'
export function ModServicePrefs({}: {}) {
const pal = usePalette('default')
const {data: preferences} = usePreferencesQuery()
return (
<View testID="modServicePrefs" style={[pal.border, {borderBottomWidth: 1}]}>
<View style={{paddingHorizontal: 14, paddingBottom: 8}}>
<Text type="2xl-bold">Settings</Text>
</View>
<LabelGroupPref
preferences={preferences}
labelGroup="nsfw"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<LabelGroupPref
preferences={preferences}
labelGroup="nudity"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<LabelGroupPref
preferences={preferences}
labelGroup="suggestive"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<LabelGroupPref
preferences={preferences}
labelGroup="gore"
disabled={!preferences?.moderationOpts.adultContentEnabled}
/>
<LabelGroupPref preferences={preferences} labelGroup="hate" />
<LabelGroupPref preferences={preferences} labelGroup="spam" />
<LabelGroupPref preferences={preferences} labelGroup="impersonation" />
</View>
)
}
-1
View File
@@ -302,7 +302,6 @@ let FeedItemInner = ({
moderation={moderation}
richText={richText}
postEmbed={post.embed}
postAuthor={post.author}
/>
<PostCtrls
post={post}
+10 -13
View File
@@ -47,14 +47,13 @@ const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS,
) as (keyof typeof LABELS)[]
const MOCK_MOD_OPTS = {
userDid: '',
const MOCK_MOD_PREFS = {
adultContentEnabled: true,
labelGroups: {},
mods: [
{
did: 'did:plc:fake-labeler',
enabled: true,
labels: {},
},
],
}
@@ -225,12 +224,13 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
const modOpts = React.useMemo(() => {
return {
...MOCK_MOD_OPTS,
userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
adultContentEnabled: !noAdult,
labelGroups: {
[LABELS[label[0] as keyof typeof LABELS].groupId]:
visibility[0] as LabelPreference,
prefs: {
...MOCK_MOD_PREFS,
adultContentEnabled: !noAdult,
labels: {
[label[0]]: visibility[0] as LabelPreference,
},
},
}
}, [label, visibility, noAdult, isLoggedOut, isTargetMe, did])
@@ -353,11 +353,8 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
targetFixed = 'content'
}
const disabled =
!LABELS[labelValue].targets.includes(
targetFixed as LabelTarget,
) ||
(isSelfLabel &&
LABELS[labelValue].flags.includes('no-self'))
isSelfLabel &&
LABELS[labelValue].flags.includes('no-self')
return (
<Toggle.Item
key={labelValue}