Cleanup: delete old code
This commit is contained in:
@@ -211,7 +211,6 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/modservice", server.WebGeneric)
|
|
||||||
|
|
||||||
// profile RSS feed (DID not handle)
|
// profile RSS feed (DID not handle)
|
||||||
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
|
|||||||
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
||||||
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
|
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
|
||||||
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
|
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
|
||||||
import {ProfileModserviceScreen} from '#/screens/ProfileModerationService'
|
|
||||||
import {ProfileListScreen} from './view/screens/ProfileList'
|
import {ProfileListScreen} from './view/screens/ProfileList'
|
||||||
import {PostThreadScreen} from './view/screens/PostThread'
|
import {PostThreadScreen} from './view/screens/PostThread'
|
||||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||||
@@ -198,11 +197,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ProfileFeedLikedByScreen}
|
getComponent={() => ProfileFeedLikedByScreen}
|
||||||
options={{title: title(msg`Liked by`)}}
|
options={{title: title(msg`Liked by`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name="ProfileModservice"
|
|
||||||
getComponent={() => ProfileModserviceScreen}
|
|
||||||
options={{title: title(msg`Moderation service`)}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Debug"
|
name="Debug"
|
||||||
getComponent={() => Storybook}
|
getComponent={() => Storybook}
|
||||||
|
|||||||
+6
-30
@@ -3,52 +3,26 @@ import {View} from 'react-native'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {LABEL_GROUPS} from '@atproto/api'
|
import {LABEL_GROUPS} from '@atproto/api'
|
||||||
// TODO
|
|
||||||
import {ModPrefItem} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
|
|
||||||
|
|
||||||
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
||||||
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
|
|
||||||
export function PreferenceRow({
|
export function ModerationLabelPref({
|
||||||
labelGroup,
|
labelGroup,
|
||||||
disabled,
|
disabled,
|
||||||
modservicePreferences,
|
|
||||||
}: {
|
}: {
|
||||||
labelGroup: keyof typeof LABEL_GROUPS
|
labelGroup: keyof typeof LABEL_GROUPS
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
modservicePreferences?: ModPrefItem
|
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const labelGroupStrings = useLabelGroupStrings()
|
const labelGroupStrings = useLabelGroupStrings()
|
||||||
const groupInfoStrings = labelGroupStrings[labelGroup]
|
const groupInfoStrings = labelGroupStrings[labelGroup]
|
||||||
const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation()
|
|
||||||
const enabled =
|
|
||||||
variables?.enabled ??
|
|
||||||
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
|
|
||||||
|
|
||||||
const onToggleEnabled = React.useCallback(async () => {
|
// TODO add onChange behavior when mod prefs are updated
|
||||||
try {
|
|
||||||
if (!modservicePreferences)
|
|
||||||
throw new Error(`modservicePreferences not found`)
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
did: modservicePreferences.did,
|
|
||||||
group: labelGroup,
|
|
||||||
enabled: !enabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error(`Failed to toggle label group enabled`, {
|
|
||||||
message: e.message,
|
|
||||||
labelGroup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
|
|
||||||
|
|
||||||
const labelOptions = {
|
const labelOptions = {
|
||||||
hide: _(msg`Hide`),
|
hide: _(msg`Hide`),
|
||||||
@@ -62,7 +36,9 @@ export function PreferenceRow({
|
|||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.justify_between,
|
a.justify_between,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.py_xs,
|
a.py_md,
|
||||||
|
a.pl_lg,
|
||||||
|
a.pr_md,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.gap_xs, {width: '50%'}]}>
|
<View style={[a.gap_xs, {width: '50%'}]}>
|
||||||
@@ -72,7 +48,7 @@ export function PreferenceRow({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.justify_center, {minHeight: 35}]}>
|
<View style={[a.justify_center, {minHeight: 35}]}>
|
||||||
{modservicePreferences && (
|
{!disabled && (
|
||||||
<ToggleButton.Group
|
<ToggleButton.Group
|
||||||
label={_(
|
label={_(
|
||||||
msg`Configure content filtering setting for category: ${groupInfoStrings.name.toLowerCase()}`,
|
msg`Configure content filtering setting for category: ${groupInfoStrings.name.toLowerCase()}`,
|
||||||
@@ -21,7 +21,6 @@ export type CommonNavigatorParams = {
|
|||||||
PostRepostedBy: {name: string; rkey: string}
|
PostRepostedBy: {name: string; rkey: string}
|
||||||
ProfileFeed: {name: string; rkey: string}
|
ProfileFeed: {name: string; rkey: string}
|
||||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||||
ProfileModservice: {name: string}
|
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
DebugMod: undefined
|
DebugMod: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const router = new Router({
|
|||||||
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
||||||
ProfileFeed: '/profile/:name/feed/:rkey',
|
ProfileFeed: '/profile/:name/feed/:rkey',
|
||||||
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
||||||
ProfileModservice: '/profile/:name/modservice',
|
|
||||||
Debug: '/sys/debug',
|
Debug: '/sys/debug',
|
||||||
DebugMod: '/sys/debug-mod',
|
DebugMod: '/sys/debug-mod',
|
||||||
Log: '/sys/log',
|
Log: '/sys/log',
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {AppBskyModerationDefs} from '@atproto/api'
|
|
||||||
import {LabelGroupDefinition} from '@atproto/api'
|
|
||||||
import {Trans} from '@lingui/macro'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
|
||||||
import {getModerationServiceTitle} from '#/lib/moderation'
|
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
|
||||||
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import {InlineLink} from '#/components/Link'
|
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
function LabelerToggle({
|
|
||||||
labelGroup,
|
|
||||||
labeler,
|
|
||||||
preferences,
|
|
||||||
}: {
|
|
||||||
labelGroup: LabelGroupDefinition['id']
|
|
||||||
labeler: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {
|
|
||||||
mutateAsync: toggleGroupEnabled,
|
|
||||||
variables: optimisticToggleGroupEnabled,
|
|
||||||
} = useModServiceLabelGroupEnableMutation()
|
|
||||||
const {closeAllDialogs} = useDialogStateControlContext()
|
|
||||||
|
|
||||||
const labelerPrefs = React.useMemo(
|
|
||||||
() =>
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
({did}) => did === labeler.creator.did,
|
|
||||||
),
|
|
||||||
[preferences.moderationOpts.mods, labeler.creator.did],
|
|
||||||
)
|
|
||||||
const isLabelerEnabled = !!labelerPrefs?.enabled
|
|
||||||
const isEnabled = isLabelerEnabled
|
|
||||||
? optimisticToggleGroupEnabled?.enabled ??
|
|
||||||
!labelerPrefs?.disabledLabelGroups?.includes(labelGroup)
|
|
||||||
: false
|
|
||||||
const title = getModerationServiceTitle({
|
|
||||||
displayName: labeler.creator.displayName,
|
|
||||||
handle: labeler.creator.handle,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onToggleEnabled = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
if (!labelerPrefs) throw new Error(`labelerPrefs not found`)
|
|
||||||
|
|
||||||
await toggleGroupEnabled({
|
|
||||||
did: labelerPrefs.did,
|
|
||||||
group: labelGroup,
|
|
||||||
enabled: !isEnabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error(`Failed to toggle label group enabled`, {
|
|
||||||
message: e.message,
|
|
||||||
labelGroup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [toggleGroupEnabled, isEnabled, labelerPrefs, labelGroup])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.rounded_sm, a.border, t.atoms.border_contrast_low]}>
|
|
||||||
<View style={[a.flex_row, a.justify_between, a.p_md]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
|
||||||
<ModerationServiceCard.Card.Avatar avatar={labeler.creator.avatar} />
|
|
||||||
<View>
|
|
||||||
<ModerationServiceCard.Card.Title value={title} />
|
|
||||||
<ModerationServiceCard.Card.Description
|
|
||||||
value={labeler.description}
|
|
||||||
handle={labeler.creator.handle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
disabled={!isLabelerEnabled}
|
|
||||||
name={labeler.creator.did}
|
|
||||||
label={title}
|
|
||||||
key={labeler.creator.did}
|
|
||||||
value={isEnabled}
|
|
||||||
onChange={onToggleEnabled}>
|
|
||||||
<Toggle.Label>{isEnabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.p_md]}>
|
|
||||||
<Text style={[a.text_xs]}>
|
|
||||||
Configure more settings for this labeler{' '}
|
|
||||||
<InlineLink
|
|
||||||
to={{
|
|
||||||
screen: 'ProfileModservice',
|
|
||||||
params: {
|
|
||||||
name: labeler.creator.handle,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
style={[a.text_xs]}
|
|
||||||
onPress={closeAllDialogs}>
|
|
||||||
here.
|
|
||||||
</InlineLink>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SettingsDialogProps = {
|
|
||||||
labelGroup: LabelGroupDefinition['id']
|
|
||||||
modservices: AppBskyModerationDefs.ModServiceViewDetailed[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsDialog({
|
|
||||||
labelGroup,
|
|
||||||
modservices,
|
|
||||||
preferences,
|
|
||||||
}: SettingsDialogProps & {
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const labelGroupStrings = useLabelGroupStrings()
|
|
||||||
const groupInfoStrings = labelGroupStrings[labelGroup]
|
|
||||||
|
|
||||||
// this is mounted on native
|
|
||||||
if (!groupInfoStrings) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.ScrollableInner label="Configure moderation service settings">
|
|
||||||
<Dialog.Close />
|
|
||||||
|
|
||||||
<Text style={[a.text_xl, a.font_bold, a.pb_xl]}>
|
|
||||||
<Trans>Configure enabled labelers</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.gap_lg,
|
|
||||||
a.p_md,
|
|
||||||
a.rounded_md,
|
|
||||||
a.mb_xl,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
|
||||||
<Text style={[a.font_bold]}>{groupInfoStrings.name}</Text>
|
|
||||||
<Text style={[a.leading_tight, {maxWidth: 400}]}>
|
|
||||||
{groupInfoStrings.description}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.leading_snug,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.pb_lg,
|
|
||||||
]}>
|
|
||||||
<Trans>Select which labelers to use for this type of content:</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={[a.w_full, a.gap_md]}>
|
|
||||||
{modservices.map(modservice => {
|
|
||||||
return (
|
|
||||||
<LabelerToggle
|
|
||||||
key={modservice.creator.did}
|
|
||||||
labelGroup={labelGroup}
|
|
||||||
labeler={modservice}
|
|
||||||
preferences={preferences}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</Dialog.ScrollableInner>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {ComAtprotoLabelDefs, LabelPreference, LABELS} from '@atproto/api'
|
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {LabelGroupDefinition, AppBskyModerationDefs} from '@atproto/api'
|
import {AppBskyModerationDefs} from '@atproto/api'
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
@@ -18,43 +18,27 @@ import {
|
|||||||
useProfileUpdateMutation,
|
useProfileUpdateMutation,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
useSetContentLabelMutation,
|
|
||||||
usePreferencesSetAdultContentMutation,
|
usePreferencesSetAdultContentMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
|
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
|
||||||
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a, useBreakpoints, native} from '#/alf'
|
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
|
||||||
import {InlineLink, Link} from '#/components/Link'
|
import {InlineLink, Link} from '#/components/Link'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
import {getModerationServiceTitle} from '#/lib/moderation'
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
import {Button} from '#/components/Button'
|
|
||||||
import {
|
|
||||||
getModerationServiceTitle,
|
|
||||||
useConfigurableLabelGroups,
|
|
||||||
getLabelGroupToLabelerMap,
|
|
||||||
} from '#/lib/moderation'
|
|
||||||
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
||||||
|
import {ModerationLabelPref} from '#/components/ModerationLabelPref'
|
||||||
import {
|
|
||||||
SettingsDialog,
|
|
||||||
SettingsDialogProps,
|
|
||||||
} from '#/screens/Moderation/SettingsDialog'
|
|
||||||
|
|
||||||
function ErrorState({error}: {error: string}) {
|
function ErrorState({error}: {error: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -153,17 +137,6 @@ export function ModerationScreenInner({
|
|||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {screen} = useAnalytics()
|
const {screen} = useAnalytics()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const modSettingsDialogControl = Dialog.useDialogControl()
|
|
||||||
const [settingsDialogProps, setSettingsDialogProps] =
|
|
||||||
React.useState<SettingsDialogProps>({
|
|
||||||
// prefill with valid value to appease TS
|
|
||||||
labelGroup: 'intolerance',
|
|
||||||
modservices: [],
|
|
||||||
})
|
|
||||||
const groups = useConfigurableLabelGroups()
|
|
||||||
const labelGroupToLabelerMap = React.useMemo(() => {
|
|
||||||
return getLabelGroupToLabelerMap(modservices)
|
|
||||||
}, [modservices])
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
@@ -172,17 +145,6 @@ export function ModerationScreenInner({
|
|||||||
}, [screen, setMinimalShellMode]),
|
}, [screen, setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const openModSettingsDialog = React.useCallback(
|
|
||||||
({labelGroup, modservices}: Omit<SettingsDialogProps, 'onComplete'>) => {
|
|
||||||
setSettingsDialogProps({
|
|
||||||
labelGroup,
|
|
||||||
modservices,
|
|
||||||
})
|
|
||||||
modSettingsDialogControl.open()
|
|
||||||
},
|
|
||||||
[modSettingsDialogControl],
|
|
||||||
)
|
|
||||||
|
|
||||||
const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} =
|
const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} =
|
||||||
usePreferencesSetAdultContentMutation()
|
usePreferencesSetAdultContentMutation()
|
||||||
const adultContentEnabled = !!(
|
const adultContentEnabled = !!(
|
||||||
@@ -206,11 +168,6 @@ export function ModerationScreenInner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Dialog.Outer control={modSettingsDialogControl}>
|
|
||||||
<Dialog.Handle />
|
|
||||||
<SettingsDialog {...settingsDialogProps} preferences={preferences} />
|
|
||||||
</Dialog.Outer>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
a.border_0,
|
a.border_0,
|
||||||
@@ -340,65 +297,15 @@ export function ModerationScreenInner({
|
|||||||
{adultContentEnabled && (
|
{adultContentEnabled && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<LabelGroup
|
<ModerationLabelPref labelGroup="porn" />
|
||||||
labelGroup={groups.find(g => g.id === 'porn')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<LabelGroup
|
<ModerationLabelPref labelGroup="suggestive" />
|
||||||
labelGroup={groups.find(g => g.id === 'suggestive')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<LabelGroup
|
<ModerationLabelPref labelGroup="nudity" />
|
||||||
labelGroup={groups.find(g => g.id === 'nudity')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<LabelGroup
|
<ModerationLabelPref labelGroup="violence" />
|
||||||
labelGroup={groups.find(g => g.id === 'violence')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
|
||||||
<View
|
|
||||||
style={[a.pt_lg, a.pb_md, a.px_lg, a.flex_row, a.justify_between]}>
|
|
||||||
<View style={[a.flex_wrap, a.flex_1]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_semibold,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
a.pb_xs,
|
|
||||||
]}>
|
|
||||||
<Trans>Hide posts by bots</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Bots are automated users which follow the rules.</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Toggle.Item
|
|
||||||
label={'Toggle hiding posts by bots'}
|
|
||||||
name="hideBots"
|
|
||||||
value={false}
|
|
||||||
onChange={undefined}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
|
||||||
{false ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
@@ -436,184 +343,32 @@ export function ModerationScreenInner({
|
|||||||
/>
|
/>
|
||||||
</ModerationServiceCard.Card.Outer>
|
</ModerationServiceCard.Card.Outer>
|
||||||
</ModerationServiceCard.Link>
|
</ModerationServiceCard.Link>
|
||||||
<Divider />
|
|
||||||
{modservices.map(mod => {
|
{modservices.map(mod => {
|
||||||
return (
|
return (
|
||||||
<ModerationServiceCard.Link
|
<>
|
||||||
modservice={mod}
|
<Divider />
|
||||||
key={mod.creator.did}>
|
<ModerationServiceCard.Link
|
||||||
<ModerationServiceCard.Card.Outer>
|
modservice={mod}
|
||||||
<ModerationServiceCard.Card.Avatar
|
key={mod.creator.did}>
|
||||||
avatar={mod.creator.avatar}
|
<ModerationServiceCard.Card.Outer>
|
||||||
/>
|
<ModerationServiceCard.Card.Avatar
|
||||||
<ModerationServiceCard.Card.Content
|
avatar={mod.creator.avatar}
|
||||||
title={getModerationServiceTitle({
|
/>
|
||||||
displayName: mod.creator.displayName,
|
<ModerationServiceCard.Card.Content
|
||||||
handle: mod.creator.handle,
|
title={getModerationServiceTitle({
|
||||||
})}
|
displayName: mod.creator.displayName,
|
||||||
handle={mod.creator.handle}
|
handle: mod.creator.handle,
|
||||||
description={mod.description}
|
})}
|
||||||
/>
|
handle={mod.creator.handle}
|
||||||
</ModerationServiceCard.Card.Outer>
|
description={mod.description}
|
||||||
</ModerationServiceCard.Link>
|
/>
|
||||||
|
</ModerationServiceCard.Card.Outer>
|
||||||
|
</ModerationServiceCard.Link>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{
|
|
||||||
undefined /*<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
a.pt_2xl,
|
|
||||||
a.pb_md,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
]}>
|
|
||||||
<Trans>Bluesky Safety</Trans>{' '}
|
|
||||||
<Text style={[a.text_sm, a.pb_md, t.atoms.text_contrast_medium]}>
|
|
||||||
@safety.bsky.app
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={groups.find(g => g.id === 'intolerance')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={groups.find(g => g.id === 'misrepresentation')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={groups.find(g => g.id === 'security')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={groups.find(g => g.id === 'spam')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{modservices.map(mod => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
a.pt_2xl,
|
|
||||||
a.pb_md,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
]}>
|
|
||||||
{mod.creator.displayName}{' '}
|
|
||||||
<Text
|
|
||||||
style={[a.text_sm, a.pb_md, t.atoms.text_contrast_medium]}>
|
|
||||||
@{mod.creator.handle}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
{mod.policies.labelValues
|
|
||||||
.filter(label => !label.startsWith('!'))
|
|
||||||
.map((label, i) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={mod.did + '-' + label}>
|
|
||||||
{i !== 0 && <Divider />}
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={LABELS[label].groupId}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
a.pt_2xl,
|
|
||||||
a.pb_md,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
]}>
|
|
||||||
<Trans>Other</Trans>
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={groups.find(g => g.id === 'bot')!.id}
|
|
||||||
labelers={[]}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
a.pt_2xl,
|
|
||||||
a.pb_md,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
]}>
|
|
||||||
<Trans>Content filtering</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
{groups.map((def, i) => {
|
|
||||||
const labelers = labelGroupToLabelerMap[def.id] || []
|
|
||||||
return (
|
|
||||||
<React.Fragment key={def.id}>
|
|
||||||
{i !== 0 && <Divider />}
|
|
||||||
<LabelGroup
|
|
||||||
labelGroup={def.id}
|
|
||||||
labelers={labelers}
|
|
||||||
preferences={preferences}
|
|
||||||
openModSettingsDialog={openModSettingsDialog}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>*/
|
|
||||||
}
|
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
@@ -633,264 +388,6 @@ export function ModerationScreenInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LabelGroup({
|
|
||||||
labelGroup,
|
|
||||||
labelers: mods,
|
|
||||||
preferences,
|
|
||||||
}: // openModSettingsDialog,
|
|
||||||
{
|
|
||||||
labelGroup: LabelGroupDefinition['id']
|
|
||||||
labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
openModSettingsDialog: (props: SettingsDialogProps) => void
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const labelGroupStrings = useLabelGroupStrings()
|
|
||||||
const {mutateAsync: setContentLabelPref, variables: optimisticContentLabel} =
|
|
||||||
useSetContentLabelMutation()
|
|
||||||
const [settingsOpen, setSettingsOpen] = React.useState(false)
|
|
||||||
|
|
||||||
const onChangeVisibility = React.useCallback(
|
|
||||||
async (values: string[]) => {
|
|
||||||
try {
|
|
||||||
await setContentLabelPref({
|
|
||||||
labelGroup,
|
|
||||||
visibility: values[0] as LabelPreference,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
// TODO
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[labelGroup, setContentLabelPref],
|
|
||||||
)
|
|
||||||
|
|
||||||
const {name, description} = labelGroupStrings[labelGroup]
|
|
||||||
const value =
|
|
||||||
optimisticContentLabel?.visibility ??
|
|
||||||
preferences.moderationOpts.labelGroups[labelGroup]
|
|
||||||
const labelOptions = {
|
|
||||||
hide: _(msg`Hide`),
|
|
||||||
warn: _(msg`Warn`),
|
|
||||||
// show: _(msg`Show`),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.px_lg, a.pr_md, gtMobile && a.px_lg]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.py_md,
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.gap_sm,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<View style={[a.gap_xs, {width: '50%'}]}>
|
|
||||||
<Text style={[a.font_semibold, t.atoms.text_contrast_high]}>
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.leading_tight, {maxWidth: 400}]}>{description}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<ToggleButton.Group
|
|
||||||
label={_(
|
|
||||||
msg`Configure content filtering setting for category: ${name.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
values={[value]}
|
|
||||||
onChange={onChangeVisibility}>
|
|
||||||
<ToggleButton.Button name="warn" label={labelOptions.warn}>
|
|
||||||
{labelOptions.warn}
|
|
||||||
</ToggleButton.Button>
|
|
||||||
<ToggleButton.Button name="hide" label={labelOptions.hide}>
|
|
||||||
{labelOptions.hide}
|
|
||||||
</ToggleButton.Button>
|
|
||||||
{
|
|
||||||
undefined /*<ToggleButton.Button name="ignore" label={labelOptions.show}>
|
|
||||||
{labelOptions.show}
|
|
||||||
</ToggleButton.Button>*/
|
|
||||||
}
|
|
||||||
</ToggleButton.Group>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!!mods.length && (
|
|
||||||
<View style={[a.mb_md, a.rounded_sm, t.atoms.bg]}>
|
|
||||||
<Button
|
|
||||||
label="Expand to configure labelers"
|
|
||||||
onPress={() => setSettingsOpen(!settingsOpen)}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.p_md,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
|
||||||
{mods.map(mod => {
|
|
||||||
const modservicePreferences =
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
({did}) => did === mod.creator.did,
|
|
||||||
)
|
|
||||||
const labelGroupEnabled =
|
|
||||||
!modservicePreferences?.disabledLabelGroups?.includes(
|
|
||||||
labelGroup,
|
|
||||||
)
|
|
||||||
const isLabelerEnabled = modservicePreferences?.enabled
|
|
||||||
const enabled = labelGroupEnabled && isLabelerEnabled
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
key={mod.creator.did}
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_xs,
|
|
||||||
a.py_xs,
|
|
||||||
a.px_sm,
|
|
||||||
a.rounded_xs,
|
|
||||||
t.atoms.bg_contrast_600,
|
|
||||||
enabled && t.atoms.bg_contrast_800,
|
|
||||||
]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_300,
|
|
||||||
{
|
|
||||||
height: 6,
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
enabled && {
|
|
||||||
backgroundColor: t.palette.positive_500,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.font_bold,
|
|
||||||
native({
|
|
||||||
top: 1,
|
|
||||||
}),
|
|
||||||
t.atoms.text_inverted,
|
|
||||||
]}>
|
|
||||||
{getModerationServiceTitle({
|
|
||||||
displayName: mod.creator.displayName,
|
|
||||||
handle: mod.creator.handle,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ChevronDown size="sm" />
|
|
||||||
</View>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{settingsOpen && (
|
|
||||||
<View style={[a.px_md]}>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{mods.map((mod, i) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={mod.creator.did}>
|
|
||||||
{i !== 0 && <Divider />}
|
|
||||||
|
|
||||||
<LabelerToggle
|
|
||||||
labelGroup={labelGroup}
|
|
||||||
labeler={mod}
|
|
||||||
preferences={preferences}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LabelerToggle({
|
|
||||||
labelGroup,
|
|
||||||
labeler,
|
|
||||||
preferences,
|
|
||||||
}: {
|
|
||||||
labelGroup: LabelGroupDefinition['id']
|
|
||||||
labeler: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {
|
|
||||||
mutateAsync: toggleGroupEnabled,
|
|
||||||
variables: optimisticToggleGroupEnabled,
|
|
||||||
} = useModServiceLabelGroupEnableMutation()
|
|
||||||
|
|
||||||
const labelerPrefs = React.useMemo(
|
|
||||||
() =>
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
({did}) => did === labeler.creator.did,
|
|
||||||
),
|
|
||||||
[preferences.moderationOpts.mods, labeler.creator.did],
|
|
||||||
)
|
|
||||||
const isLabelerEnabled = !!labelerPrefs?.enabled
|
|
||||||
const isEnabled = isLabelerEnabled
|
|
||||||
? optimisticToggleGroupEnabled?.enabled ??
|
|
||||||
!labelerPrefs?.disabledLabelGroups?.includes(labelGroup)
|
|
||||||
: false
|
|
||||||
const title = getModerationServiceTitle({
|
|
||||||
displayName: labeler.creator.displayName,
|
|
||||||
handle: labeler.creator.handle,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onToggleEnabled = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
if (!labelerPrefs) throw new Error(`labelerPrefs not found`)
|
|
||||||
|
|
||||||
await toggleGroupEnabled({
|
|
||||||
did: labelerPrefs.did,
|
|
||||||
group: labelGroup,
|
|
||||||
enabled: !isEnabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error(`Failed to toggle label group enabled`, {
|
|
||||||
message: e.message,
|
|
||||||
labelGroup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [toggleGroupEnabled, isEnabled, labelerPrefs, labelGroup])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Toggle.Item
|
|
||||||
disabled={!isLabelerEnabled}
|
|
||||||
label={'Toggle label'}
|
|
||||||
name={labeler.creator.did}
|
|
||||||
value={isEnabled}
|
|
||||||
onChange={onToggleEnabled}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.py_md,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.font_bold]}>
|
|
||||||
{title}{' '}
|
|
||||||
<Text style={[t.atoms.text_contrast_low]}>
|
|
||||||
{isLabelerEnabled ? '' : '(labeler disabled)'}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PwiOptOut() {
|
function PwiOptOut() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|||||||
@@ -5,26 +5,16 @@ import {Trans, msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
import {Haptics} from '#/lib/haptics'
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useModServiceEnableMutation} from '#/state/queries/modservice'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
|
||||||
|
|
||||||
import {getLabelGroupsFromLabels} from '#/lib/moderation'
|
import {getLabelGroupsFromLabels} from '#/lib/moderation'
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {ErrorState} from '../ErrorState'
|
||||||
import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
|
import {ModerationLabelPref} from '#/components/ModerationLabelPref'
|
||||||
import {PreferenceRow} from '#/screens/ProfileModerationService/PreferenceRow'
|
|
||||||
|
|
||||||
export function ProfileContentFiltersSection({
|
export function ProfileContentFiltersSection({
|
||||||
modServiceQuery,
|
modServiceQuery,
|
||||||
@@ -83,77 +73,19 @@ export function ProfileContentFiltersSectionInner({
|
|||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
|
||||||
const {hasSession} = useSession()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
|
||||||
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
|
||||||
useUnlikeMutation()
|
|
||||||
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
|
|
||||||
useModServiceEnableMutation()
|
|
||||||
|
|
||||||
const [likeUri, setLikeUri] = React.useState<string>(
|
|
||||||
modservice.viewer?.like || '',
|
|
||||||
)
|
|
||||||
// TODO error state
|
|
||||||
const [_enablementError, setEnablementError] = React.useState<string>('')
|
|
||||||
|
|
||||||
const isLiked = !!likeUri
|
|
||||||
const groups = React.useMemo(() => {
|
const groups = React.useMemo(() => {
|
||||||
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
|
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
|
||||||
def => def.configurable,
|
def => def.configurable,
|
||||||
)
|
)
|
||||||
}, [modservice.policies.labelValues])
|
}, [modservice.policies.labelValues])
|
||||||
const modservicePreferences = React.useMemo(() => {
|
|
||||||
return moderationOpts.mods.find(p => p.did === modservice.creator.did)
|
|
||||||
}, [modservice.creator.did, moderationOpts.mods])
|
|
||||||
const isSubscribed = moderationOpts.mods.find(
|
|
||||||
mod => mod.did === modservice.creator.did,
|
|
||||||
)
|
|
||||||
const isEnabled = Boolean(
|
const isEnabled = Boolean(
|
||||||
enabledVariables?.enabled ??
|
moderationOpts.mods.find(
|
||||||
moderationOpts.mods.find(
|
mod => mod.did === modservice.creator.did && mod.enabled,
|
||||||
mod => mod.did === modservice.creator.did && mod.enabled,
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
|
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
|
||||||
|
|
||||||
const onToggleLiked = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
Haptics.default()
|
|
||||||
|
|
||||||
if (isLiked && likeUri) {
|
|
||||||
await unlikeMod({uri: likeUri})
|
|
||||||
track('CustomFeed:Unlike')
|
|
||||||
setLikeUri('')
|
|
||||||
} else {
|
|
||||||
const res = await likeMod({uri: modservice.uri, cid: modservice.cid})
|
|
||||||
track('CustomFeed:Like')
|
|
||||||
setLikeUri(res.uri)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
Toast.show(
|
|
||||||
_(
|
|
||||||
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
logger.error(`Failed to toggle labeler like`, {message: e.message})
|
|
||||||
}
|
|
||||||
}, [likeUri, isLiked, modservice, likeMod, unlikeMod, track, _])
|
|
||||||
|
|
||||||
const onToggleLabelerEnabled = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await toggleEnabled({
|
|
||||||
did: modservice.creator.did,
|
|
||||||
enabled: !isEnabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
setEnablementError(e.message)
|
|
||||||
logger.error(`Failed to toggle labeler enabled`, {message: e.message})
|
|
||||||
}
|
|
||||||
}, [toggleEnabled, isEnabled, modservice.creator.did])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
@@ -161,56 +93,31 @@ export function ProfileContentFiltersSectionInner({
|
|||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
paddingHorizontal: a.px_xl.paddingLeft,
|
paddingHorizontal: a.px_xl.paddingLeft,
|
||||||
}}>
|
}}>
|
||||||
{false /* TODO wanted? isSubscribed*/ ? (
|
<Text
|
||||||
<View style={[a.flex_row, a.pr_lg, a.pt_xl]}>
|
style={[
|
||||||
<View style={[a.gap_sm, a.flex_1]}>
|
a.pt_xl,
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
t.atoms.text_contrast_high,
|
||||||
Enable or disable labels from this service.
|
a.leading_snug,
|
||||||
</Text>
|
a.text_md,
|
||||||
</View>
|
]}>
|
||||||
|
<Trans>This service labels the following types of content.</Trans>
|
||||||
<Toggle.Item
|
</Text>
|
||||||
name="enable"
|
|
||||||
value={isEnabled}
|
|
||||||
onChange={onToggleLabelerEnabled}
|
|
||||||
label={
|
|
||||||
isEnabled
|
|
||||||
? _(msg`Disable this moderation service`)
|
|
||||||
: _(msg`Enable this moderation service`)
|
|
||||||
}>
|
|
||||||
<Toggle.Label>{isEnabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={[a.gap_sm, a.pt_xl]}>
|
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
|
||||||
This service labels the following types of content.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.gap_md,
|
|
||||||
a.mt_xl,
|
a.mt_xl,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.border,
|
a.border,
|
||||||
a.py_md,
|
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
{groups.map((def, i) => {
|
{groups.map((def, i) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={def.id}>
|
<React.Fragment key={def.id}>
|
||||||
{i !== 0 && <Divider />}
|
{i !== 0 && <Divider />}
|
||||||
<View style={[a.px_lg]}>
|
<ModerationLabelPref
|
||||||
<PreferenceRow
|
disabled={isEnabled ? undefined : true}
|
||||||
disabled={isEnabled ? undefined : true}
|
labelGroup={def.id}
|
||||||
labelGroup={def.id}
|
/>
|
||||||
modservicePreferences={modservicePreferences}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
import {AppBskyModerationDefs} from '@atproto/api'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
|
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
|
||||||
import {emitSoftReset} from '#/state/events'
|
|
||||||
import {shareUrl} from '#/lib/sharing'
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
||||||
import {
|
|
||||||
NativeDropdown,
|
|
||||||
DropdownItem,
|
|
||||||
} from '#/view/com/util/forms/NativeDropdown'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useModServiceSubscriptionMutation} from '#/state/queries/modservice'
|
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a, tokens, web, native, useBreakpoints} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
|
||||||
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
|
||||||
import {DotGrid1x3Horizontal_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
|
||||||
import {Bars3_Stroke2_Corner0_Rounded as Bars} from '#/components/icons/Bars'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
|
|
||||||
export function Header({
|
|
||||||
preferences,
|
|
||||||
modservice,
|
|
||||||
}: {
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const canGoBack = navigation.canGoBack()
|
|
||||||
const {mutateAsync: toggleSubscription, variables} =
|
|
||||||
useModServiceSubscriptionMutation()
|
|
||||||
const isSubscribed =
|
|
||||||
variables?.subscribe ??
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
mod => mod.did === modservice.creator.did,
|
|
||||||
)
|
|
||||||
const [subscriptionError, setSubscriptionError] = React.useState<string>('')
|
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
|
||||||
if (navigation.canGoBack()) {
|
|
||||||
navigation.goBack()
|
|
||||||
} else {
|
|
||||||
navigation.navigate('Home')
|
|
||||||
}
|
|
||||||
}, [navigation])
|
|
||||||
|
|
||||||
const onPressMenu = React.useCallback(() => {
|
|
||||||
setDrawerOpen(true)
|
|
||||||
}, [setDrawerOpen])
|
|
||||||
|
|
||||||
const onPressSubscribe = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await toggleSubscription({
|
|
||||||
did: modservice.creator.did,
|
|
||||||
subscribe: !isSubscribed,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
setSubscriptionError(e.message)
|
|
||||||
logger.error(`Failed to subscribe to labeler`, {message: e.message})
|
|
||||||
}
|
|
||||||
}, [toggleSubscription, isSubscribed, modservice.creator.did])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.pb_xl, web(a.pt_md), gtMobile && web(a.pt_2xl)]}>
|
|
||||||
{!gtMobile && (
|
|
||||||
<View style={[a.mb_xl]}>
|
|
||||||
<View
|
|
||||||
style={[a.flex_row, a.justify_between, a.align_center, a.pb_md]}>
|
|
||||||
<Button
|
|
||||||
testID="headerDrawerBtn"
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
variant="solid"
|
|
||||||
shape="square"
|
|
||||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
|
||||||
label={canGoBack ? _(msg`Go back`) : _(msg`Open main menu`)}>
|
|
||||||
<ButtonIcon icon={canGoBack ? ChevronLeft : Bars} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
label={_(msg`Subscribe to this moderation service`)}
|
|
||||||
onPress={onPressSubscribe}>
|
|
||||||
<ButtonText>
|
|
||||||
{isSubscribed ? (
|
|
||||||
<Trans>Unsubscribe</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Subscribe</Trans>
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<CommonControls modservice={modservice} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_lg, a.align_start]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.p_md,
|
|
||||||
t.atoms.bg_contrast_50,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
]}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={tokens.gradients.midnight.values.map(c => c[1])}
|
|
||||||
locations={tokens.gradients.midnight.values.map(c => c[0])}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
/>
|
|
||||||
<RaisingHand width={32} fill={t.palette.white} style={[a.z_10]} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_1, a.gap_xs, native(a.pt_2xs)]}>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Refresh page`)}
|
|
||||||
testID="headerTitle"
|
|
||||||
// TODO do we need this?
|
|
||||||
onPress={emitSoftReset}
|
|
||||||
style={[a.justify_start]}>
|
|
||||||
<Text style={[a.text_4xl, a.font_bold, t.atoms.text]}>
|
|
||||||
{modservice.creator.displayName
|
|
||||||
? sanitizeDisplayName(modservice.creator.displayName)
|
|
||||||
: sanitizeHandle(modservice.creator.handle, '@')}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Moderation service</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{gtMobile && (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
label={
|
|
||||||
isSubscribed
|
|
||||||
? _(msg`Unsubscribe from this moderation service`)
|
|
||||||
: _(msg`Subscribe to this moderation service`)
|
|
||||||
}
|
|
||||||
onPress={onPressSubscribe}>
|
|
||||||
<ButtonText>
|
|
||||||
{isSubscribed ? (
|
|
||||||
<Trans>Unsubscribe</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Subscribe</Trans>
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<CommonControls modservice={modservice} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{subscriptionError && (
|
|
||||||
<View style={[a.pt_lg]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.border,
|
|
||||||
a.gap_xs,
|
|
||||||
{
|
|
||||||
backgroundColor: t.palette.negative_50,
|
|
||||||
borderColor: t.palette.negative_100,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text style={[a.font_bold, {color: t.palette.negative_950}]}>
|
|
||||||
<Trans>An error occurred while editing your subscription.</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[{color: t.palette.negative_900}]}>
|
|
||||||
{subscriptionError || enablementError}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CommonControls({
|
|
||||||
modservice,
|
|
||||||
}: {
|
|
||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {hasSession} = useSession()
|
|
||||||
const {openModal} = useModalControls()
|
|
||||||
|
|
||||||
const onPressShare = React.useCallback(() => {
|
|
||||||
const url = makeProfileLink(modservice.creator, 'modservice')
|
|
||||||
shareUrl(url)
|
|
||||||
// track('CustomFeed:Share') TODO
|
|
||||||
}, [modservice /*, track*/])
|
|
||||||
|
|
||||||
const onPressReport = React.useCallback(() => {
|
|
||||||
if (!modservice) return
|
|
||||||
openModal({
|
|
||||||
name: 'report',
|
|
||||||
uri: modservice.uri,
|
|
||||||
cid: modservice.cid,
|
|
||||||
})
|
|
||||||
}, [openModal, modservice])
|
|
||||||
|
|
||||||
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={[a.rounded_full, a.p_sm, t.atoms.bg_contrast_50]}>
|
|
||||||
<Ellipsis width={20} fill={t.atoms.text_contrast_medium.color} />
|
|
||||||
</View>
|
|
||||||
</NativeDropdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
|
||||||
import {AppBskyModerationDefs} from '@atproto/api'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
|
||||||
|
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
|
||||||
import {Haptics} from '#/lib/haptics'
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {pluralize} from '#/lib/strings/helpers'
|
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {
|
|
||||||
useModServiceInfoQuery,
|
|
||||||
useModServiceEnableMutation,
|
|
||||||
} from '#/state/queries/modservice'
|
|
||||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
|
||||||
import {
|
|
||||||
UsePreferencesQueryResponse,
|
|
||||||
usePreferencesQuery,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
|
||||||
|
|
||||||
import {getLabelGroupsFromLabels} from '#/lib/moderation'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Button} from '#/components/Button'
|
|
||||||
import {
|
|
||||||
Heart2_Stroke2_Corner0_Rounded as Heart,
|
|
||||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
|
||||||
} from '#/components/icons/Heart2'
|
|
||||||
import {RichText} from '#/components/RichText'
|
|
||||||
import {InlineLink} from '#/components/Link'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
|
|
||||||
import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
|
|
||||||
import {Header} from '#/screens/ProfileModerationService/Header'
|
|
||||||
import {PreferenceRow} from '#/screens/ProfileModerationService/PreferenceRow'
|
|
||||||
|
|
||||||
export function ProfileModserviceScreen(
|
|
||||||
props: NativeStackScreenProps<CommonNavigatorParams, 'ProfileModservice'>,
|
|
||||||
) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {height: minHeight} = useSafeAreaFrame()
|
|
||||||
const {name: handleOrDid} = props.route.params
|
|
||||||
const {
|
|
||||||
isLoading: isDidResolutionLoading,
|
|
||||||
error: didResolutionError,
|
|
||||||
data: did,
|
|
||||||
} = useResolveDidQuery(handleOrDid)
|
|
||||||
const {
|
|
||||||
isLoading: isPreferencesLoading,
|
|
||||||
error: preferencesError,
|
|
||||||
data: preferences,
|
|
||||||
} = usePreferencesQuery()
|
|
||||||
const {
|
|
||||||
isLoading: isModServiceLoading,
|
|
||||||
error: modServiceError,
|
|
||||||
data: modservice,
|
|
||||||
} = useModServiceInfoQuery({did})
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
isDidResolutionLoading || isPreferencesLoading || isModServiceLoading
|
|
||||||
const error = didResolutionError || preferencesError || modServiceError
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CenteredView>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.border_l,
|
|
||||||
a.border_r,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
{
|
|
||||||
minHeight,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{isLoading ? (
|
|
||||||
<View style={[a.w_full, a.align_center]}>
|
|
||||||
<Loader size="xl" />
|
|
||||||
</View>
|
|
||||||
) : error || !(did && preferences && modservice) ? (
|
|
||||||
<ErrorState
|
|
||||||
error={
|
|
||||||
error?.toString() ||
|
|
||||||
_(msg`Something went wrong, please try again.`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ProfileModserviceScreenInner
|
|
||||||
preferences={preferences}
|
|
||||||
modservice={modservice}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</CenteredView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProfileModserviceScreenInner({
|
|
||||||
preferences,
|
|
||||||
modservice,
|
|
||||||
}: {
|
|
||||||
preferences: UsePreferencesQueryResponse
|
|
||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {height: minHeight} = useSafeAreaFrame()
|
|
||||||
const {hasSession} = useSession()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
|
||||||
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
|
||||||
useUnlikeMutation()
|
|
||||||
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
|
|
||||||
useModServiceEnableMutation()
|
|
||||||
|
|
||||||
const [likeUri, setLikeUri] = React.useState<string>(
|
|
||||||
modservice.viewer?.like || '',
|
|
||||||
)
|
|
||||||
// TODO error state
|
|
||||||
const [_enablementError, setEnablementError] = React.useState<string>('')
|
|
||||||
|
|
||||||
const isLiked = !!likeUri
|
|
||||||
const groups = React.useMemo(() => {
|
|
||||||
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
|
|
||||||
def => def.configurable,
|
|
||||||
)
|
|
||||||
}, [modservice.policies.labelValues])
|
|
||||||
const modservicePreferences = React.useMemo(() => {
|
|
||||||
return preferences.moderationOpts.mods.find(
|
|
||||||
p => p.did === modservice.creator.did,
|
|
||||||
)
|
|
||||||
}, [modservice.creator.did, preferences.moderationOpts.mods])
|
|
||||||
const isSubscribed = preferences.moderationOpts.mods.find(
|
|
||||||
mod => mod.did === modservice.creator.did,
|
|
||||||
)
|
|
||||||
const isEnabled = Boolean(
|
|
||||||
enabledVariables?.enabled ??
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
mod => mod.did === modservice.creator.did && mod.enabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
|
|
||||||
|
|
||||||
const onToggleLiked = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
Haptics.default()
|
|
||||||
|
|
||||||
if (isLiked && likeUri) {
|
|
||||||
await unlikeMod({uri: likeUri})
|
|
||||||
track('CustomFeed:Unlike')
|
|
||||||
setLikeUri('')
|
|
||||||
} else {
|
|
||||||
const res = await likeMod({uri: modservice.uri, cid: modservice.cid})
|
|
||||||
track('CustomFeed:Like')
|
|
||||||
setLikeUri(res.uri)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
Toast.show(
|
|
||||||
_(
|
|
||||||
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
logger.error(`Failed to toggle labeler like`, {message: e.message})
|
|
||||||
}
|
|
||||||
}, [likeUri, isLiked, modservice, likeMod, unlikeMod, track, _])
|
|
||||||
|
|
||||||
const onToggleLabelerEnabled = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await toggleEnabled({
|
|
||||||
did: modservice.creator.did,
|
|
||||||
enabled: !isEnabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
setEnablementError(e.message)
|
|
||||||
logger.error(`Failed to toggle labeler enabled`, {message: e.message})
|
|
||||||
}
|
|
||||||
}, [toggleEnabled, isEnabled, modservice.creator.did])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
scrollEventThrottle={1}
|
|
||||||
contentContainerStyle={{
|
|
||||||
minHeight,
|
|
||||||
borderWidth: 0,
|
|
||||||
paddingHorizontal: a.px_xl.paddingLeft,
|
|
||||||
}}>
|
|
||||||
<Header modservice={modservice} preferences={preferences} />
|
|
||||||
|
|
||||||
<View style={[a.gap_md, a.pb_xl]}>
|
|
||||||
{modservice.description ? (
|
|
||||||
<RichText
|
|
||||||
testID="modinfoDescription"
|
|
||||||
resolveFacets
|
|
||||||
value={modservice.description}
|
|
||||||
style={[a.text_md, a.leading_normal]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.leading_normal,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
{fontStyle: 'italic'},
|
|
||||||
]}>
|
|
||||||
<Trans>
|
|
||||||
Moderation service managed by @{modservice.creator.handle}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
|
||||||
<Button
|
|
||||||
testID="toggleLikeBtn"
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
variant="solid"
|
|
||||||
shape="round"
|
|
||||||
label={_(msg`Like this feed`)}
|
|
||||||
disabled={!hasSession || isLikePending || isUnlikePending}
|
|
||||||
onPress={onToggleLiked}>
|
|
||||||
{isLiked ? (
|
|
||||||
<HeartFilled fill={t.palette.negative_400} />
|
|
||||||
) : (
|
|
||||||
<Heart fill={t.atoms.text_contrast_medium.color} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{typeof modservice.likeCount === 'number' && (
|
|
||||||
<InlineLink
|
|
||||||
to={'#todo'}
|
|
||||||
style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
|
||||||
<Trans>
|
|
||||||
Liked by {modservice.likeCount}{' '}
|
|
||||||
{pluralize(modservice.likeCount, 'user')}
|
|
||||||
</Trans>
|
|
||||||
</InlineLink>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{isSubscribed ? (
|
|
||||||
<View style={[a.flex_row, a.pr_lg, a.pt_xl]}>
|
|
||||||
<View style={[a.gap_sm, a.flex_1]}>
|
|
||||||
<Text style={[a.text_md, a.font_bold]}>Settings</Text>
|
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
|
||||||
Enable or disable labels from this service.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
name="enable"
|
|
||||||
value={isEnabled}
|
|
||||||
onChange={onToggleLabelerEnabled}
|
|
||||||
label={
|
|
||||||
isEnabled
|
|
||||||
? _(msg`Disable this moderation service`)
|
|
||||||
: _(msg`Enable this moderation service`)
|
|
||||||
}>
|
|
||||||
<Toggle.Label>{isEnabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={[a.gap_sm, a.pt_xl]}>
|
|
||||||
<Text style={[a.text_md, a.font_bold]}>Labels</Text>
|
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
|
||||||
This labeler moderates the following types of content.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.gap_md,
|
|
||||||
a.mt_xl,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
a.rounded_md,
|
|
||||||
a.border,
|
|
||||||
a.py_md,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
{groups.map((def, i) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={def.id}>
|
|
||||||
{i !== 0 && <Divider />}
|
|
||||||
<View style={[a.px_lg]}>
|
|
||||||
<PreferenceRow
|
|
||||||
disabled={isEnabled ? undefined : true}
|
|
||||||
labelGroup={def.id}
|
|
||||||
modservicePreferences={modservicePreferences}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{height: 100}} />
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user