Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
+10
-1
@@ -171,11 +171,18 @@ export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
|||||||
* Intended to behave as a web anchor tag. For more complex routing, use a
|
* Intended to behave as a web anchor tag. For more complex routing, use a
|
||||||
* `Button`.
|
* `Button`.
|
||||||
*/
|
*/
|
||||||
export function Link({children, to, action = 'push', ...rest}: LinkProps) {
|
export function Link({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
action = 'push',
|
||||||
|
onPress: outerOnPress,
|
||||||
|
...rest
|
||||||
|
}: LinkProps) {
|
||||||
const {href, isExternal, onPress} = useLink({
|
const {href, isExternal, onPress} = useLink({
|
||||||
to,
|
to,
|
||||||
displayText: typeof children === 'string' ? children : '',
|
displayText: typeof children === 'string' ? children : '',
|
||||||
action,
|
action,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -218,6 +225,7 @@ export function InlineLink({
|
|||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
style,
|
style,
|
||||||
|
onPress: outerOnPress,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -227,6 +235,7 @@ export function InlineLink({
|
|||||||
displayText: stringChildren ? children : '',
|
displayText: stringChildren ? children : '',
|
||||||
action,
|
action,
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
children,
|
children,
|
||||||
@@ -43,6 +44,22 @@ export function Avatar({avatar}: {avatar?: string}) {
|
|||||||
return <UserAvatar type="list" size={40} avatar={avatar} />
|
return <UserAvatar type="list" size={40} avatar={avatar} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Title({value}: {value: string}) {
|
||||||
|
return <Text style={[a.text_md, a.font_bold, a.pb_2xs]}>{value}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Description({value, handle}: {value?: string; handle: string}) {
|
||||||
|
return value ? (
|
||||||
|
<RichText value={value} style={[]} />
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
<Trans>
|
||||||
|
Moderation service managed by @{sanitizeHandle(handle, '@')}
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Content({
|
export function Content({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -63,7 +80,9 @@ export function Content({
|
|||||||
<RichText value={description} style={[]} />
|
<RichText value={description} style={[]} />
|
||||||
) : (
|
) : (
|
||||||
<Text>
|
<Text>
|
||||||
<Trans>Moderation service managed by @{handle}</Trans>
|
<Trans>
|
||||||
|
Moderation service managed by @{sanitizeHandle(handle, '@')}
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const SquareBehindSquare4_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z',
|
||||||
|
})
|
||||||
@@ -13,6 +13,10 @@ import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
|||||||
import {getModerationServiceTitle} from '#/lib/moderation'
|
import {getModerationServiceTitle} from '#/lib/moderation'
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
||||||
import {useModServiceLabelGroupEnableMutation} from '#/state/queries/modservice'
|
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({
|
function LabelerToggle({
|
||||||
labelGroup,
|
labelGroup,
|
||||||
@@ -24,14 +28,24 @@ function LabelerToggle({
|
|||||||
preferences: UsePreferencesQueryResponse
|
preferences: UsePreferencesQueryResponse
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {mutateAsync, variables} = useModServiceLabelGroupEnableMutation()
|
const {
|
||||||
|
mutateAsync: toggleGroupEnabled,
|
||||||
|
variables: optimisticToggleGroupEnabled,
|
||||||
|
} = useModServiceLabelGroupEnableMutation()
|
||||||
|
const {closeAllDialogs} = useDialogStateControlContext()
|
||||||
|
|
||||||
const modservicePreferences = preferences.moderationOpts.mods.find(
|
const labelerPrefs = React.useMemo(
|
||||||
|
() =>
|
||||||
|
preferences.moderationOpts.mods.find(
|
||||||
({did}) => did === labeler.creator.did,
|
({did}) => did === labeler.creator.did,
|
||||||
|
),
|
||||||
|
[preferences.moderationOpts.mods, labeler.creator.did],
|
||||||
)
|
)
|
||||||
const enabled =
|
const isLabelerEnabled = !!labelerPrefs?.enabled
|
||||||
variables?.enabled ??
|
const isEnabled = isLabelerEnabled
|
||||||
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
|
? optimisticToggleGroupEnabled?.enabled ??
|
||||||
|
!labelerPrefs?.disabledLabelGroups?.includes(labelGroup)
|
||||||
|
: false
|
||||||
const title = getModerationServiceTitle({
|
const title = getModerationServiceTitle({
|
||||||
displayName: labeler.creator.displayName,
|
displayName: labeler.creator.displayName,
|
||||||
handle: labeler.creator.handle,
|
handle: labeler.creator.handle,
|
||||||
@@ -39,42 +53,66 @@ function LabelerToggle({
|
|||||||
|
|
||||||
const onToggleEnabled = React.useCallback(async () => {
|
const onToggleEnabled = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await mutateAsync({
|
if (!labelerPrefs) throw new Error(`labelerPrefs not found`)
|
||||||
// @ts-ignore TODO
|
|
||||||
did: modservicePreferences?.did,
|
await toggleGroupEnabled({
|
||||||
|
did: labelerPrefs.did,
|
||||||
group: labelGroup,
|
group: labelGroup,
|
||||||
enabled: !enabled,
|
enabled: !isEnabled,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// TODO
|
logger.error(`Failed to toggle label group enabled`, {
|
||||||
console.error(e)
|
message: e.message,
|
||||||
|
labelGroup,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
|
}, [toggleGroupEnabled, isEnabled, labelerPrefs, labelGroup])
|
||||||
|
|
||||||
return (
|
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
|
<Toggle.Item
|
||||||
|
disabled={!isLabelerEnabled}
|
||||||
name={labeler.creator.did}
|
name={labeler.creator.did}
|
||||||
label={title}
|
label={title}
|
||||||
key={labeler.creator.did}
|
key={labeler.creator.did}
|
||||||
value={enabled}
|
value={isEnabled}
|
||||||
onChange={onToggleEnabled}>
|
onChange={onToggleEnabled}>
|
||||||
{ctx => (
|
<Toggle.Label>{isEnabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
||||||
<ModerationServiceCard.Card.Outer
|
<Toggle.Switch />
|
||||||
style={[
|
|
||||||
...(ctx.focused || ctx.hovered ? [t.atoms.bg_contrast_50] : []),
|
|
||||||
ctx.selected && {
|
|
||||||
backgroundColor: t.palette.primary_50,
|
|
||||||
borderColor: t.palette.primary_100,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<ModerationServiceCard.Card.Avatar avatar={labeler.creator.avatar} />
|
|
||||||
<ModerationServiceCard.Card.Content
|
|
||||||
title={title}
|
|
||||||
description={labeler.description}
|
|
||||||
handle={labeler.creator.handle}
|
|
||||||
/>
|
|
||||||
</ModerationServiceCard.Card.Outer>
|
|
||||||
)}
|
|
||||||
</Toggle.Item>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +153,10 @@ export function SettingsDialog({
|
|||||||
a.p_md,
|
a.p_md,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.mb_xl,
|
a.mb_xl,
|
||||||
a.border,
|
t.atoms.bg_contrast_25,
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
<Text style={[a.text_md, a.font_bold]}>{groupInfoStrings.name}</Text>
|
<Text style={[a.font_bold]}>{groupInfoStrings.name}</Text>
|
||||||
<Text style={[a.leading_tight, {maxWidth: 400}]}>
|
<Text style={[a.leading_tight, {maxWidth: 400}]}>
|
||||||
{groupInfoStrings.description}
|
{groupInfoStrings.description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ import {
|
|||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
|
import {useModServicesDetailedInfoQuery} from '#/state/queries/modservice'
|
||||||
|
|
||||||
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
import {useTheme, atoms as a, useBreakpoints, native} 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 {SquareBehindSquare4_Stroke2_Corner0_Rounded as SquareBehindSquare} from '#/components/icons/SquareBehindSquare4'
|
||||||
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 * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
@@ -38,7 +39,7 @@ import {InlineLink, Link} from '#/components/Link'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
import {useLabelGroupStrings} from '#/lib/moderation/useLabelGroupStrings'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||||
import {
|
import {
|
||||||
getModerationServiceTitle,
|
getModerationServiceTitle,
|
||||||
useConfigurableLabelGroups,
|
useConfigurableLabelGroups,
|
||||||
@@ -365,57 +366,65 @@ function LabelGroup({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!!mods.length && (
|
{!!mods.length && (
|
||||||
<View style={[a.flex_row, a.align_start, a.pb_md]}>
|
|
||||||
<Button
|
|
||||||
label={_(
|
|
||||||
msg`Configure moderation services for category: ${name.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
onPress={() =>
|
|
||||||
openModSettingsDialog({
|
|
||||||
labelGroup,
|
|
||||||
modservices: mods,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
{ctx => (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_start,
|
||||||
a.gap_xs,
|
a.justify_between,
|
||||||
|
a.mb_md,
|
||||||
a.p_sm,
|
a.p_sm,
|
||||||
a.border,
|
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.bg_contrast_25,
|
||||||
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
||||||
{mods.map(mod => {
|
{mods.map(mod => {
|
||||||
const modservicePreferences =
|
const modservicePreferences =
|
||||||
preferences.moderationOpts.mods.find(
|
preferences.moderationOpts.mods.find(
|
||||||
({did}) => did === mod.creator.did,
|
({did}) => did === mod.creator.did,
|
||||||
)
|
)
|
||||||
const enabled =
|
const labelGroupEnabled =
|
||||||
!modservicePreferences?.disabledLabelGroups?.includes(
|
!modservicePreferences?.disabledLabelGroups?.includes(
|
||||||
labelGroup,
|
labelGroup,
|
||||||
)
|
)
|
||||||
|
const isLabelerEnabled = modservicePreferences?.enabled
|
||||||
|
const enabled = labelGroupEnabled && isLabelerEnabled
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={mod.creator.did}
|
key={mod.creator.did}
|
||||||
style={[
|
style={[
|
||||||
a.py_xs,
|
a.flex_row,
|
||||||
a.px_sm,
|
a.align_center,
|
||||||
a.rounded_sm,
|
a.gap_sm,
|
||||||
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.rounded_full,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
enabled && t.atoms.bg_contrast_800,
|
enabled && t.atoms.border_contrast_high,
|
||||||
|
enabled && t.atoms.bg,
|
||||||
]}>
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.bg_contrast_300,
|
||||||
|
{
|
||||||
|
height: 6,
|
||||||
|
width: 6,
|
||||||
|
},
|
||||||
|
enabled && {
|
||||||
|
backgroundColor: t.palette.positive_500,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_xs,
|
a.text_xs,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
t.atoms.text_contrast_high,
|
native({
|
||||||
enabled && t.atoms.text_inverted,
|
top: 1,
|
||||||
|
}),
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
enabled && t.atoms.text_contrast_high,
|
||||||
]}>
|
]}>
|
||||||
{getModerationServiceTitle({
|
{getModerationServiceTitle({
|
||||||
displayName: mod.creator.displayName,
|
displayName: mod.creator.displayName,
|
||||||
@@ -425,19 +434,22 @@ function LabelGroup({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.font_bold,
|
|
||||||
a.pl_sm,
|
|
||||||
a.italic,
|
|
||||||
t.atoms.text_contrast_low,
|
|
||||||
]}>
|
|
||||||
Configure
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
<Button
|
||||||
|
size="tiny"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
label={_(msg`Configure enabled labelers for this content type`)}
|
||||||
|
style={[a.mt_xs]}
|
||||||
|
onPress={() => {
|
||||||
|
openModSettingsDialog({
|
||||||
|
labelGroup,
|
||||||
|
modservices: mods,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<ButtonText>Configure</ButtonText>
|
||||||
|
<ButtonIcon icon={SquareBehindSquare} position="right" />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,11 +19,9 @@ import {
|
|||||||
} from '#/view/com/util/forms/NativeDropdown'
|
} from '#/view/com/util/forms/NativeDropdown'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {useModServiceSubscriptionMutation} from '#/state/queries/modservice'
|
||||||
useModServiceSubscriptionMutation,
|
|
||||||
useModServiceEnableMutation,
|
|
||||||
} from '#/state/queries/modservice'
|
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
import {useTheme, atoms as a, tokens, web, native, useBreakpoints} from '#/alf'
|
import {useTheme, atoms as a, tokens, web, native, useBreakpoints} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -54,16 +52,7 @@ export function Header({
|
|||||||
preferences.moderationOpts.mods.find(
|
preferences.moderationOpts.mods.find(
|
||||||
mod => mod.did === modservice.creator.did,
|
mod => mod.did === modservice.creator.did,
|
||||||
)
|
)
|
||||||
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
|
|
||||||
useModServiceEnableMutation()
|
|
||||||
const isEnabled =
|
|
||||||
enabledVariables?.enabled ??
|
|
||||||
preferences.moderationOpts.mods.find(
|
|
||||||
mod => mod.did === modservice.creator.did && mod.enabled,
|
|
||||||
) ??
|
|
||||||
variables?.subscribe
|
|
||||||
const [subscriptionError, setSubscriptionError] = React.useState<string>('')
|
const [subscriptionError, setSubscriptionError] = React.useState<string>('')
|
||||||
const [enablementError, setEnablementError] = React.useState<string>('')
|
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
@@ -85,20 +74,10 @@ export function Header({
|
|||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setSubscriptionError(e.message)
|
setSubscriptionError(e.message)
|
||||||
|
logger.error(`Failed to subscribe to labeler`, {message: e.message})
|
||||||
}
|
}
|
||||||
}, [toggleSubscription, isSubscribed, modservice.creator.did])
|
}, [toggleSubscription, isSubscribed, modservice.creator.did])
|
||||||
|
|
||||||
const onPressEnable = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await toggleEnabled({
|
|
||||||
did: modservice.creator.did,
|
|
||||||
enabled: !isEnabled,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
setEnablementError(e.message)
|
|
||||||
}
|
|
||||||
}, [toggleEnabled, isEnabled, modservice.creator.did])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.pb_xl, web(a.pt_md), gtMobile && web(a.pt_2xl)]}>
|
<View style={[a.pb_xl, web(a.pt_md), gtMobile && web(a.pt_2xl)]}>
|
||||||
{!gtMobile && (
|
{!gtMobile && (
|
||||||
@@ -179,23 +158,6 @@ export function Header({
|
|||||||
|
|
||||||
{gtMobile && (
|
{gtMobile && (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||||
{isSubscribed && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
|
||||||
label={
|
|
||||||
isEnabled
|
|
||||||
? _(msg`Disable this moderation service`)
|
|
||||||
: _(msg`Enable this moderation service`)
|
|
||||||
}
|
|
||||||
onPress={onPressEnable}>
|
|
||||||
<ButtonText>
|
|
||||||
{isEnabled ? <Trans>Disable</Trans> : <Trans>Enable</Trans>}
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -220,8 +182,7 @@ export function Header({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{subscriptionError ||
|
{subscriptionError && (
|
||||||
(enablementError && (
|
|
||||||
<View style={[a.pt_lg]}>
|
<View style={[a.pt_lg]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -236,16 +197,14 @@ export function Header({
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.font_bold, {color: t.palette.negative_950}]}>
|
<Text style={[a.font_bold, {color: t.palette.negative_950}]}>
|
||||||
<Trans>
|
<Trans>An error occurred while editing your subscription.</Trans>
|
||||||
An error occurred while editing your subscription.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[{color: t.palette.negative_900}]}>
|
<Text style={[{color: t.palette.negative_900}]}>
|
||||||
{subscriptionError || enablementError}
|
{subscriptionError || enablementError}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import React from 'react'
|
|||||||
import {View} from 'react-native'
|
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 {msg} from '@lingui/macro'
|
|
||||||
import {LABEL_GROUPS} from '@atproto/api'
|
import {LABEL_GROUPS} from '@atproto/api'
|
||||||
// TODO
|
// TODO
|
||||||
import {ModPrefItem} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
|
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 {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'
|
||||||
@@ -16,9 +16,11 @@ import * as Toggle from '#/components/forms/Toggle'
|
|||||||
|
|
||||||
export function PreferenceRow({
|
export function PreferenceRow({
|
||||||
labelGroup,
|
labelGroup,
|
||||||
|
disabled,
|
||||||
modservicePreferences,
|
modservicePreferences,
|
||||||
}: {
|
}: {
|
||||||
labelGroup: keyof typeof LABEL_GROUPS
|
labelGroup: keyof typeof LABEL_GROUPS
|
||||||
|
disabled?: boolean
|
||||||
modservicePreferences?: ModPrefItem
|
modservicePreferences?: ModPrefItem
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -32,15 +34,19 @@ export function PreferenceRow({
|
|||||||
|
|
||||||
const onToggleEnabled = React.useCallback(async () => {
|
const onToggleEnabled = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!modservicePreferences)
|
||||||
|
throw new Error(`modservicePreferences not found`)
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
// @ts-ignore TODO
|
did: modservicePreferences.did,
|
||||||
did: modservicePreferences?.did,
|
|
||||||
group: labelGroup,
|
group: labelGroup,
|
||||||
enabled: !enabled,
|
enabled: !enabled,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// TODO
|
logger.error(`Failed to toggle label group enabled`, {
|
||||||
console.error(e)
|
message: e.message,
|
||||||
|
labelGroup,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
|
}, [mutateAsync, enabled, modservicePreferences, labelGroup])
|
||||||
|
|
||||||
@@ -62,8 +68,9 @@ export function PreferenceRow({
|
|||||||
<View style={[a.justify_center, {minHeight: 35}]}>
|
<View style={[a.justify_center, {minHeight: 35}]}>
|
||||||
{modservicePreferences && (
|
{modservicePreferences && (
|
||||||
<Toggle.Item
|
<Toggle.Item
|
||||||
|
disabled={disabled}
|
||||||
name="enable"
|
name="enable"
|
||||||
value={enabled}
|
value={disabled ? false : enabled}
|
||||||
onChange={onToggleEnabled}
|
onChange={onToggleEnabled}
|
||||||
label={_(msg`Enable`)}>
|
label={_(msg`Enable`)}>
|
||||||
<Toggle.Label>{enabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
<Toggle.Label>{enabled ? 'Enabled' : 'Disabled'}</Toggle.Label>
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ 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 {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {TextLink} from '#/view/com/util/Link'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
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 {Haptics} from '#/lib/haptics'
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {pluralize} from '#/lib/strings/helpers'
|
import {pluralize} from '#/lib/strings/helpers'
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModServiceInfoQuery} from '#/state/queries/modservice'
|
import {
|
||||||
|
useModServiceInfoQuery,
|
||||||
|
useModServiceEnableMutation,
|
||||||
|
} from '#/state/queries/modservice'
|
||||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {
|
import {
|
||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
@@ -26,7 +25,6 @@ import {
|
|||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
|
|
||||||
import {getLabelGroupsFromLabels} from '#/lib/moderation'
|
import {getLabelGroupsFromLabels} from '#/lib/moderation'
|
||||||
|
|
||||||
@@ -41,6 +39,7 @@ import {
|
|||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {InlineLink} from '#/components/Link'
|
import {InlineLink} from '#/components/Link'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
|
||||||
import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
|
import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
|
||||||
import {Header} from '#/screens/ProfileModerationService/Header'
|
import {Header} from '#/screens/ProfileModerationService/Header'
|
||||||
@@ -116,15 +115,20 @@ export function ProfileModserviceScreenInner({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {height: minHeight} = useSafeAreaFrame()
|
const {height: minHeight} = useSafeAreaFrame()
|
||||||
const pal = usePalette('default')
|
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
||||||
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
||||||
useUnlikeMutation()
|
useUnlikeMutation()
|
||||||
|
const {mutateAsync: toggleEnabled, variables: enabledVariables} =
|
||||||
|
useModServiceEnableMutation()
|
||||||
|
|
||||||
const [likeUri, setLikeUri] = React.useState<string>(
|
const [likeUri, setLikeUri] = React.useState<string>(
|
||||||
modservice.viewer?.like || '',
|
modservice.viewer?.like || '',
|
||||||
)
|
)
|
||||||
|
// TODO error state
|
||||||
|
const [_enablementError, setEnablementError] = React.useState<string>('')
|
||||||
|
|
||||||
const isLiked = !!likeUri
|
const isLiked = !!likeUri
|
||||||
const groups = React.useMemo(() => {
|
const groups = React.useMemo(() => {
|
||||||
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
|
return getLabelGroupsFromLabels(modservice.policies.labelValues).filter(
|
||||||
@@ -136,6 +140,12 @@ export function ProfileModserviceScreenInner({
|
|||||||
p => p.did === modservice.creator.did,
|
p => p.did === modservice.creator.did,
|
||||||
)
|
)
|
||||||
}, [modservice.creator.did, preferences.moderationOpts.mods])
|
}, [modservice.creator.did, preferences.moderationOpts.mods])
|
||||||
|
const isEnabled = Boolean(
|
||||||
|
enabledVariables?.enabled ??
|
||||||
|
preferences.moderationOpts.mods.find(
|
||||||
|
mod => mod.did === modservice.creator.did && mod.enabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
|
useSetTitle(modservice.creator.displayName || modservice.creator.handle)
|
||||||
|
|
||||||
@@ -152,16 +162,28 @@ export function ProfileModserviceScreenInner({
|
|||||||
track('CustomFeed:Like')
|
track('CustomFeed:Like')
|
||||||
setLikeUri(res.uri)
|
setLikeUri(res.uri)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e: any) {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
_(
|
_(
|
||||||
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
logger.error('Failed up toggle like', {error: err})
|
logger.error(`Failed to toggle labeler like`, {message: e.message})
|
||||||
}
|
}
|
||||||
}, [likeUri, isLiked, modservice, likeMod, unlikeMod, track, _])
|
}, [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}
|
||||||
@@ -194,25 +216,6 @@ export function ProfileModserviceScreenInner({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.leading_normal,
|
|
||||||
a.italic,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
|
||||||
Operated by{' '}
|
|
||||||
<TextLink
|
|
||||||
href={makeProfileLink(modservice.creator)}
|
|
||||||
text={sanitizeHandle(modservice.creator.handle, '@')}
|
|
||||||
style={pal.link}
|
|
||||||
/>
|
|
||||||
. Handles reports of anti-social behavior, illegal content, unwanted
|
|
||||||
sexual content, and misinformation.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
<View style={[a.flex_row, a.gap_md, a.align_center]}>
|
||||||
<Button
|
<Button
|
||||||
testID="toggleLikeBtn"
|
testID="toggleLikeBtn"
|
||||||
@@ -231,7 +234,9 @@ export function ProfileModserviceScreenInner({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{typeof modservice.likeCount === 'number' && (
|
{typeof modservice.likeCount === 'number' && (
|
||||||
<InlineLink to={'#todo'} style={[pal.textLight, s.semiBold]}>
|
<InlineLink
|
||||||
|
to={'#todo'}
|
||||||
|
style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
Liked by {modservice.likeCount}{' '}
|
Liked by {modservice.likeCount}{' '}
|
||||||
{pluralize(modservice.likeCount, 'user')}
|
{pluralize(modservice.likeCount, 'user')}
|
||||||
@@ -248,18 +253,53 @@ export function ProfileModserviceScreenInner({
|
|||||||
Configure moderation service
|
Configure moderation service
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||||
This moderation service supports the following types of content.
|
This labeler moderates the following types of content. You can
|
||||||
Configure which types of content you'd like to respect from this
|
optionally enable or disable the labeler's recommendations for each
|
||||||
service.
|
type of content below.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.mt_xl]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="enable"
|
||||||
|
value={isEnabled}
|
||||||
|
onChange={onToggleLabelerEnabled}
|
||||||
|
label={
|
||||||
|
isEnabled
|
||||||
|
? _(msg`Disable this moderation service`)
|
||||||
|
: _(msg`Enable this moderation service`)
|
||||||
|
}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_lg,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<Text style={[a.font_bold, a.pb_xs]}>
|
||||||
|
This service is {isEnabled ? 'enabled' : 'disabled'}
|
||||||
|
</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||||
|
Optionally disable this service without affecting your
|
||||||
|
subscription.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</View>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={[a.gap_md, a.mt_xl]}>
|
<View style={[a.gap_md, a.mt_xl]}>
|
||||||
{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 />}
|
||||||
<PreferenceRow
|
<PreferenceRow
|
||||||
|
disabled={isEnabled ? undefined : true}
|
||||||
labelGroup={def.id}
|
labelGroup={def.id}
|
||||||
modservicePreferences={modservicePreferences}
|
modservicePreferences={modservicePreferences}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user