Remove dead code and start moving toward latest modsdk
This commit is contained in:
@@ -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={() => {}}>
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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}} />
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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 />
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -302,7 +302,6 @@ let FeedItemInner = ({
|
||||
moderation={moderation}
|
||||
richText={richText}
|
||||
postEmbed={post.embed}
|
||||
postAuthor={post.author}
|
||||
/>
|
||||
<PostCtrls
|
||||
post={post}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user