Hook up data
This commit is contained in:
+27
-18
@@ -48,25 +48,30 @@ export type VariantProps = {
|
|||||||
shape?: ButtonShape
|
shape?: ButtonShape
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonProps = React.PropsWithChildren<
|
export type ButtonState = {
|
||||||
Pick<PressableProps, 'disabled' | 'onPress' | 'testID'> &
|
hovered: boolean
|
||||||
AccessibilityProps &
|
focused: boolean
|
||||||
VariantProps & {
|
pressed: boolean
|
||||||
testID?: string
|
disabled: boolean
|
||||||
label: string
|
}
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
}
|
export type ButtonProps = Pick<
|
||||||
>
|
PressableProps,
|
||||||
|
'disabled' | 'onPress' | 'testID'
|
||||||
|
> &
|
||||||
|
AccessibilityProps &
|
||||||
|
VariantProps & {
|
||||||
|
testID?: string
|
||||||
|
label: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| string
|
||||||
|
| ((state: VariantProps & ButtonState) => React.ReactNode | string)
|
||||||
|
}
|
||||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||||
|
|
||||||
const Context = React.createContext<
|
const Context = React.createContext<VariantProps & ButtonState>({
|
||||||
VariantProps & {
|
|
||||||
hovered: boolean
|
|
||||||
focused: boolean
|
|
||||||
pressed: boolean
|
|
||||||
disabled: boolean
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
hovered: false,
|
hovered: false,
|
||||||
focused: false,
|
focused: false,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
@@ -394,6 +399,8 @@ export function Button({
|
|||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
{typeof children === 'string' ? (
|
{typeof children === 'string' ? (
|
||||||
<ButtonText>{children}</ButtonText>
|
<ButtonText>{children}</ButtonText>
|
||||||
|
) : typeof children === 'function' ? (
|
||||||
|
children(context)
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
@@ -514,9 +521,11 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
|||||||
export function ButtonIcon({
|
export function ButtonIcon({
|
||||||
icon: Comp,
|
icon: Comp,
|
||||||
position,
|
position,
|
||||||
|
size: iconSize,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
position?: 'left' | 'right'
|
position?: 'left' | 'right'
|
||||||
|
size?: SVGIconProps['size']
|
||||||
}) {
|
}) {
|
||||||
const {size, disabled} = useButtonContext()
|
const {size, disabled} = useButtonContext()
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
@@ -532,7 +541,7 @@ export function ButtonIcon({
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Comp
|
<Comp
|
||||||
size={size === 'large' ? 'md' : 'sm'}
|
size={iconSize ?? (size === 'large' ? 'md' : 'sm')}
|
||||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {Portal} from '#/components/Portal'
|
|||||||
|
|
||||||
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
||||||
import {Context} from '#/components/Dialog/context'
|
import {Context} from '#/components/Dialog/context'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
|
||||||
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
@@ -171,25 +173,28 @@ export function Handle() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function Close() {
|
||||||
* TODO(eric) unused rn
|
const {_} = useLingui()
|
||||||
*/
|
const {close} = React.useContext(Context)
|
||||||
// export function Close() {
|
return (
|
||||||
// const {_} = useLingui()
|
<View
|
||||||
// const t = useTheme()
|
style={[
|
||||||
// const {close} = useDialogContext()
|
a.absolute,
|
||||||
// return (
|
a.z_10,
|
||||||
// <View
|
{
|
||||||
// style={[
|
top: a.pt_md.paddingTop,
|
||||||
// a.absolute,
|
right: a.pr_md.paddingRight,
|
||||||
// a.z_10,
|
},
|
||||||
// {
|
]}>
|
||||||
// top: a.pt_lg.paddingTop,
|
<Button
|
||||||
// right: a.pr_lg.paddingRight,
|
size="small"
|
||||||
// },
|
variant="ghost"
|
||||||
// ]}>
|
color="primary"
|
||||||
// <Button onPress={close} label={_(msg`Close active dialog`)}>
|
shape="round"
|
||||||
// </Button>
|
onPress={close}
|
||||||
// </View>
|
label={_(msg`Close active dialog`)}>
|
||||||
// )
|
<ButtonIcon icon={X} size="md" />
|
||||||
// }
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {AppBskyModerationDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme, tokens, web} from '#/alf'
|
|
||||||
import {Link, useLinkContext} from '#/components/Link'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {RichText} from '#/components/RichText'
|
|
||||||
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
|
||||||
import {useModServiceInfoQuery} from '#/state/queries/modservice'
|
|
||||||
|
|
||||||
type ModerationServiceCardProps = {
|
|
||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModerationServiceCard({
|
|
||||||
modservice,
|
|
||||||
}: ModerationServiceCardProps) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={{
|
|
||||||
screen: 'ProfileModservice',
|
|
||||||
params: {
|
|
||||||
name: modservice.creator.handle,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
label={_(
|
|
||||||
msg`View the moderation service provided by @${modservice.creator.handle}`,
|
|
||||||
)}>
|
|
||||||
<Inner modservice={modservice} />
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Inner({modservice}: ModerationServiceCardProps) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {hovered, pressed, focused} = useLinkContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.gap_sm,
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.pt_md,
|
|
||||||
a.pb_sm,
|
|
||||||
a.pl_lg,
|
|
||||||
a.pr_sm,
|
|
||||||
a.overflow_hidden,
|
|
||||||
web({
|
|
||||||
transition: 'transform 0.2s cubic-bezier(.02,.73,.27,.99)',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
transform: [{scale: pressed || hovered || focused ? 0.992 : 1}],
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<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]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={[a.z_10]}>
|
|
||||||
<Text
|
|
||||||
style={[a.text_md, a.font_bold, a.pb_2xs, {color: t.palette.white}]}>
|
|
||||||
{/* TODO */}
|
|
||||||
{modservice.displayName || 'Mod service'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{modservice.description ? (
|
|
||||||
<RichText
|
|
||||||
value={modservice.description}
|
|
||||||
style={[{color: t.palette.white}]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text>
|
|
||||||
<Trans>
|
|
||||||
Moderation service managed by @{modservice.creator.handle}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<RaisingHand size="xl" style={[a.z_10]} fill={t.palette.white} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModerationServiceCardSkeleton() {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text>Loading</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Loader({
|
|
||||||
did,
|
|
||||||
loading: LoadingComponent = ModerationServiceCardSkeleton,
|
|
||||||
error: ErrorComponent,
|
|
||||||
component: Component,
|
|
||||||
}: {
|
|
||||||
did: string
|
|
||||||
loading?: React.ComponentType<{}>
|
|
||||||
error?: React.ComponentType<{error: string}>
|
|
||||||
component: React.ComponentType<{
|
|
||||||
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
|
||||||
}>
|
|
||||||
}) {
|
|
||||||
const {isLoading, data, error} = useModServiceInfoQuery({did})
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
LoadingComponent ? (
|
|
||||||
<LoadingComponent />
|
|
||||||
) : null
|
|
||||||
) : error || !data ? (
|
|
||||||
ErrorComponent ? (
|
|
||||||
<ErrorComponent error={error?.message || 'Unknown error'} />
|
|
||||||
) : null
|
|
||||||
) : (
|
|
||||||
<Component modservice={data} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, ViewStyleProp, flatten} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
|
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
|
||||||
|
export function Outer({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<ViewStyleProp>) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.w_full,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.pt_md,
|
||||||
|
a.pb_md,
|
||||||
|
a.pl_md,
|
||||||
|
a.pr_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
flatten(style),
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_md]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Avatar({avatar}: {avatar?: string}) {
|
||||||
|
return <UserAvatar type="list" size={40} avatar={avatar} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Content({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
handle,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
handle: string
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_1, a.flex_row, a.align_center, a.justify_between]}>
|
||||||
|
<View>
|
||||||
|
<Text style={[a.text_md, a.font_bold, a.pb_2xs]}>{title}</Text>
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<RichText value={description} style={[]} />
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
<Trans>Moderation service managed by @{handle}</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<RaisingHand size="xl" style={[a.z_10]} fill={t.palette.primary_500} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {AppBskyModerationDefs} from '@atproto/api'
|
||||||
|
|
||||||
|
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useModServiceInfoQuery} from '#/state/queries/modservice'
|
||||||
|
|
||||||
|
export * as Card from '#/components/ModerationServiceCard/Card'
|
||||||
|
|
||||||
|
type ModerationServiceProps = {
|
||||||
|
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link({
|
||||||
|
children,
|
||||||
|
modservice,
|
||||||
|
}: ModerationServiceProps & Pick<LinkProps, 'children'>) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InternalLink
|
||||||
|
to={{
|
||||||
|
screen: 'ProfileModservice',
|
||||||
|
params: {
|
||||||
|
name: modservice.creator.handle,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
label={_(
|
||||||
|
msg`View the moderation service provided by @${modservice.creator.handle}`,
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</InternalLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModerationServiceCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Loading</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Loader({
|
||||||
|
did,
|
||||||
|
loading: LoadingComponent = ModerationServiceCardSkeleton,
|
||||||
|
error: ErrorComponent,
|
||||||
|
component: Component,
|
||||||
|
}: {
|
||||||
|
did: string
|
||||||
|
loading?: React.ComponentType<{}>
|
||||||
|
error?: React.ComponentType<{error: string}>
|
||||||
|
component: React.ComponentType<{
|
||||||
|
modservice: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
}>
|
||||||
|
}) {
|
||||||
|
const {isLoading, data, error} = useModServiceInfoQuery({did})
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
LoadingComponent ? (
|
||||||
|
<LoadingComponent />
|
||||||
|
) : null
|
||||||
|
) : error || !data ? (
|
||||||
|
ErrorComponent ? (
|
||||||
|
<ErrorComponent error={error?.message || 'Unknown error'} />
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<Component modservice={data} />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ModerationCause,
|
ModerationCause,
|
||||||
ProfileModeration,
|
ProfileModeration,
|
||||||
@@ -6,6 +7,9 @@ import {
|
|||||||
LabelGroupDefinition,
|
LabelGroupDefinition,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
|
||||||
export function getProfileModerationCauses(
|
export function getProfileModerationCauses(
|
||||||
moderation: ProfileModeration,
|
moderation: ProfileModeration,
|
||||||
): ModerationCause[] {
|
): ModerationCause[] {
|
||||||
@@ -91,3 +95,23 @@ export function getLabelGroupsFromLabels(labels: string[]) {
|
|||||||
|
|
||||||
return Array.from(groups)
|
return Array.from(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getConfigurableLabelGroups() {
|
||||||
|
return Object.values(LABEL_GROUPS).filter(group => group.configurable)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigurableLabelGroups() {
|
||||||
|
return React.useMemo(() => getConfigurableLabelGroups(), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModerationServiceTitle({
|
||||||
|
displayName,
|
||||||
|
handle,
|
||||||
|
}: {
|
||||||
|
displayName?: string
|
||||||
|
handle: string
|
||||||
|
}) {
|
||||||
|
return displayName
|
||||||
|
? sanitizeDisplayName(displayName)
|
||||||
|
: sanitizeHandle(handle, '@')
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
function LabelerToggle({
|
||||||
|
labelGroup,
|
||||||
|
labeler,
|
||||||
|
preferences,
|
||||||
|
}: {
|
||||||
|
labelGroup: LabelGroupDefinition['id']
|
||||||
|
labeler: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
preferences: UsePreferencesQueryResponse
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {mutateAsync, variables, reset} =
|
||||||
|
useModServiceLabelGroupEnableMutation()
|
||||||
|
|
||||||
|
const modservicePreferences = preferences.moderationOpts.mods.find(
|
||||||
|
({did}) => did === labeler.creator.did,
|
||||||
|
)
|
||||||
|
const enabled =
|
||||||
|
variables?.enabled ??
|
||||||
|
!modservicePreferences?.disabledLabelGroups?.includes(labelGroup)
|
||||||
|
const title = getModerationServiceTitle({
|
||||||
|
displayName: labeler.creator.displayName,
|
||||||
|
handle: labeler.creator.handle,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onToggleEnabled = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
// @ts-ignore TODO
|
||||||
|
did: modservicePreferences?.did,
|
||||||
|
group: labelGroup,
|
||||||
|
enabled: !enabled,
|
||||||
|
})
|
||||||
|
reset() // Important: clears query `variables`
|
||||||
|
} catch (e: any) {
|
||||||
|
// TODO
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}, [mutateAsync, enabled, modservicePreferences, labelGroup, reset])
|
||||||
|
return (
|
||||||
|
<Toggle.Item
|
||||||
|
name={labeler.creator.did}
|
||||||
|
label={title}
|
||||||
|
key={labeler.creator.did}
|
||||||
|
value={enabled}
|
||||||
|
onChange={onToggleEnabled}>
|
||||||
|
{ctx => (
|
||||||
|
<ModerationServiceCard.Card.Outer
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
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,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
<Text style={[a.text_md, 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+276
-211
@@ -1,14 +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} from '@atproto/api'
|
import {ComAtprotoLabelDefs, LabelPreference} 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 {
|
import {LabelGroupDefinition, AppBskyModerationDefs} from '@atproto/api'
|
||||||
LABEL_GROUPS,
|
|
||||||
LabelGroupDefinition,
|
|
||||||
DEFAULT_LABEL_GROUP_SETTINGS,
|
|
||||||
} from '@atproto/api'
|
|
||||||
|
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
@@ -22,12 +18,18 @@ import {
|
|||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
|
||||||
|
import {
|
||||||
|
UsePreferencesQueryResponse,
|
||||||
|
usePreferencesQuery,
|
||||||
|
useSetContentLabelMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {useModServicesInfoQuery} from '#/state/queries/modservice'
|
||||||
|
|
||||||
import {useTheme, atoms as a, useBreakpoints} 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 {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
|
||||||
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'
|
||||||
@@ -36,98 +38,71 @@ 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} from '#/components/Button'
|
||||||
|
import {
|
||||||
|
getLabelGroupsFromLabels,
|
||||||
|
getModerationServiceTitle,
|
||||||
|
useConfigurableLabelGroups,
|
||||||
|
} from '#/lib/moderation'
|
||||||
|
|
||||||
function ModSettingsToggleItem({name}: {name: string}) {
|
import {
|
||||||
|
SettingsDialog,
|
||||||
|
SettingsDialogProps,
|
||||||
|
} from '#/screens/Moderation/SettingsDialog'
|
||||||
|
|
||||||
|
export function ModerationScreen(
|
||||||
|
_props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
|
||||||
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ctx = Toggle.useItemContext()
|
const {
|
||||||
return (
|
isLoading: isPreferencesLoading,
|
||||||
<View
|
// error: preferencesError,
|
||||||
style={[
|
data: preferences,
|
||||||
a.w_full,
|
} = usePreferencesQuery()
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
return isPreferencesLoading ? (
|
||||||
a.align_center,
|
<View style={[a.w_full, a.align_center]}>
|
||||||
a.p_md,
|
<Loader size="xl" fill={t.atoms.text.color} />
|
||||||
a.rounded_sm,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{
|
|
||||||
backgroundColor: ctx.selected ? t.palette.primary_25 : undefined,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text>{name}</Text>
|
|
||||||
{ctx.selected && <Check />}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
) : preferences ? (
|
||||||
|
<ModerationScreenIntermediate preferences={preferences} />
|
||||||
|
) : // TODO
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModSettingsDialog({
|
function ModerationScreenIntermediate({
|
||||||
name,
|
preferences,
|
||||||
onComplete,
|
|
||||||
}: {
|
}: {
|
||||||
name: string
|
preferences: UsePreferencesQueryResponse
|
||||||
onComplete: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const labelGroupStrings = useLabelGroupStrings()
|
const {
|
||||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
isLoading: isModServicesLoading,
|
||||||
'bluesky',
|
data: modservices,
|
||||||
])
|
// error: modservicesError,
|
||||||
|
} = useModServicesInfoQuery({
|
||||||
|
dids: preferences.moderationOpts.mods.map(m => m.did),
|
||||||
|
})
|
||||||
|
|
||||||
const save = React.useCallback(() => {
|
return isModServicesLoading ? (
|
||||||
onComplete()
|
<View style={[a.w_full, a.align_center]}>
|
||||||
}, [onComplete])
|
<Loader size="xl" fill={t.atoms.text.color} />
|
||||||
|
</View>
|
||||||
return (
|
) : modservices ? (
|
||||||
<Dialog.Inner label="Configure moderation service settings">
|
<ModerationScreenInner
|
||||||
<Text style={[a.text_xl, a.font_bold, a.pb_sm]}>
|
preferences={preferences}
|
||||||
Configure moderation services
|
modservices={modservices}
|
||||||
</Text>
|
/>
|
||||||
<Text
|
) : // TODO
|
||||||
style={[
|
null
|
||||||
a.text_md,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
a.pb_lg,
|
|
||||||
]}>
|
|
||||||
Select which moderation services' labels you'd like to use to filter
|
|
||||||
content matching {labelGroupStrings[name].name}.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Toggle.Group
|
|
||||||
values={selectedServices}
|
|
||||||
onChange={setSelectedServices}
|
|
||||||
label="Select one or more">
|
|
||||||
<View style={[a.w_full, a.gap_md]}>
|
|
||||||
<Toggle.Item name="bluesky" label="Bluesky">
|
|
||||||
<ModSettingsToggleItem name="Bluesky" />
|
|
||||||
</Toggle.Item>
|
|
||||||
<Toggle.Item name="contraption" label="Contraption">
|
|
||||||
<ModSettingsToggleItem name="Contraption" />
|
|
||||||
</Toggle.Item>
|
|
||||||
<Toggle.Item name="safety" label="Safety Corp">
|
|
||||||
<ModSettingsToggleItem name="Safety Corp" />
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.justify_end, a.pt_lg]}>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
label="Save"
|
|
||||||
onPress={save}>
|
|
||||||
{selectedServices.length ? 'Save' : 'Ignore all'}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</Dialog.Inner>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModerationScreen({}: NativeStackScreenProps<
|
export function ModerationScreenInner({
|
||||||
CommonNavigatorParams,
|
preferences,
|
||||||
'Moderation'
|
modservices,
|
||||||
>) {
|
}: {
|
||||||
|
preferences: UsePreferencesQueryResponse
|
||||||
|
modservices: AppBskyModerationDefs.ModServiceViewDetailed[]
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
@@ -135,8 +110,13 @@ export function ModerationScreen({}: NativeStackScreenProps<
|
|||||||
const {gtMobile, gtTablet} = useBreakpoints()
|
const {gtMobile, gtTablet} = useBreakpoints()
|
||||||
const labelGroupStrings = useLabelGroupStrings()
|
const labelGroupStrings = useLabelGroupStrings()
|
||||||
const modSettingsDialogControl = Dialog.useDialogControl()
|
const modSettingsDialogControl = Dialog.useDialogControl()
|
||||||
const [modSettingsDialogLabelGroup, setModSettingsDialogLabelGroup] =
|
|
||||||
React.useState<string>(() => Object.keys(LABEL_GROUPS)[0])
|
const [settingsDialogProps, setSettingsDialogProps] =
|
||||||
|
React.useState<SettingsDialogProps>({
|
||||||
|
// @ts-ignore
|
||||||
|
labelGroup: '',
|
||||||
|
modservices: [],
|
||||||
|
})
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
@@ -145,20 +125,40 @@ export function ModerationScreen({}: NativeStackScreenProps<
|
|||||||
}, [screen, setMinimalShellMode]),
|
}, [screen, setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const groups = React.useMemo<
|
const groups = useConfigurableLabelGroups()
|
||||||
[keyof typeof LABEL_GROUPS, LabelGroupDefinition][]
|
|
||||||
|
const didToModServiceMap = React.useMemo<
|
||||||
|
Record<string, AppBskyModerationDefs.ModServiceViewDetailed>
|
||||||
>(() => {
|
>(() => {
|
||||||
return Object.entries(LABEL_GROUPS).filter(([, def]) => def.configurable)
|
return modservices.reduce((acc, modservice) => {
|
||||||
}, [])
|
return {
|
||||||
const labelOptions = {
|
...acc,
|
||||||
hide: _(msg`Hide`),
|
[modservice.creator.did]: modservice,
|
||||||
warn: _(msg`Warn`),
|
}
|
||||||
show: _(msg`Show`),
|
}, {})
|
||||||
}
|
}, [modservices])
|
||||||
|
const labelGroupToModServiceMap = React.useMemo(() => {
|
||||||
|
const groups: Partial<Record<LabelGroupDefinition['id'], string[]>> = {}
|
||||||
|
|
||||||
|
for (const modservice of modservices) {
|
||||||
|
const labelGroups = getLabelGroupsFromLabels(
|
||||||
|
modservice.policies.labelValues,
|
||||||
|
)
|
||||||
|
for (const group of labelGroups) {
|
||||||
|
const g = (groups[group.id] = groups[group.id] || [])
|
||||||
|
g.push(modservice.creator.did)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}, [modservices])
|
||||||
|
|
||||||
const openModSettingsDialog = React.useCallback(
|
const openModSettingsDialog = React.useCallback(
|
||||||
({name}: {name: string}) => {
|
({labelGroup, modservices}: Omit<SettingsDialogProps, 'onComplete'>) => {
|
||||||
setModSettingsDialogLabelGroup(name)
|
setSettingsDialogProps({
|
||||||
|
labelGroup,
|
||||||
|
modservices,
|
||||||
|
})
|
||||||
modSettingsDialogControl.open()
|
modSettingsDialogControl.open()
|
||||||
},
|
},
|
||||||
[modSettingsDialogControl],
|
[modSettingsDialogControl],
|
||||||
@@ -175,11 +175,7 @@ export function ModerationScreen({}: NativeStackScreenProps<
|
|||||||
testID="moderationScreen">
|
testID="moderationScreen">
|
||||||
<Dialog.Outer control={modSettingsDialogControl}>
|
<Dialog.Outer control={modSettingsDialogControl}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
<SettingsDialog {...settingsDialogProps} preferences={preferences} />
|
||||||
<ModSettingsDialog
|
|
||||||
name={modSettingsDialogLabelGroup}
|
|
||||||
onComplete={() => modSettingsDialogControl.close()}
|
|
||||||
/>
|
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|
||||||
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
|
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
|
||||||
@@ -255,114 +251,21 @@ export function ModerationScreen({}: NativeStackScreenProps<
|
|||||||
<Trans>Content filtering settings</Trans>
|
<Trans>Content filtering settings</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{groups.map(([name, def], i) => {
|
{groups.map((def, i) => {
|
||||||
const groupStrings = labelGroupStrings[name]
|
const groupStrings = labelGroupStrings[def.id]
|
||||||
|
const modDids = labelGroupToModServiceMap[def.id] || []
|
||||||
|
const mods = modDids.map(did => didToModServiceMap[did])
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={def.id}>
|
<React.Fragment key={def.id}>
|
||||||
{i !== 0 && <Divider />}
|
{i !== 0 && <Divider />}
|
||||||
|
<LabelGroup
|
||||||
<View style={[a.pb_md]}>
|
name={groupStrings.name}
|
||||||
<View
|
description={groupStrings.description}
|
||||||
style={[
|
labelers={mods}
|
||||||
a.py_md,
|
openModSettingsDialog={openModSettingsDialog}
|
||||||
a.flex_row,
|
preferences={preferences}
|
||||||
a.justify_between,
|
labelGroup={def.id}
|
||||||
a.gap_sm,
|
/>
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<View style={[a.gap_xs, {width: '50%'}]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
{groupStrings.name}
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.leading_tight, {maxWidth: 400}]}>
|
|
||||||
{groupStrings.description}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<ToggleButton.Group
|
|
||||||
label={_(
|
|
||||||
msg`Configure content filtering setting for category: ${groupStrings.name.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
values={[DEFAULT_LABEL_GROUP_SETTINGS[name]]}
|
|
||||||
onChange={() => {}}>
|
|
||||||
<ToggleButton.Button
|
|
||||||
name="hide"
|
|
||||||
label={labelOptions.hide}>
|
|
||||||
{labelOptions.hide}
|
|
||||||
</ToggleButton.Button>
|
|
||||||
<ToggleButton.Button
|
|
||||||
name="warn"
|
|
||||||
label={labelOptions.warn}>
|
|
||||||
{labelOptions.warn}
|
|
||||||
</ToggleButton.Button>
|
|
||||||
<ToggleButton.Button
|
|
||||||
name="ignore"
|
|
||||||
label={labelOptions.show}>
|
|
||||||
{labelOptions.show}
|
|
||||||
</ToggleButton.Button>
|
|
||||||
</ToggleButton.Group>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_start]}>
|
|
||||||
<Button
|
|
||||||
label={_(
|
|
||||||
msg`Configure moderation services for category: ${groupStrings.name.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
onPress={() =>
|
|
||||||
openModSettingsDialog({
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<View style={[a.flex_row, a.align_start, a.gap_xs]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.py_sm,
|
|
||||||
a.px_md,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_800,
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_inverted,
|
|
||||||
]}>
|
|
||||||
Bluesky
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.py_sm,
|
|
||||||
a.px_md,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_xs, a.font_bold]}>
|
|
||||||
Contraption
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.py_sm,
|
|
||||||
a.px_md,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_xs, a.font_bold]}>
|
|
||||||
Safety Corp
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -381,6 +284,168 @@ export function ModerationScreen({}: NativeStackScreenProps<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LabelGroup({
|
||||||
|
labelGroup,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
labelers: mods,
|
||||||
|
preferences,
|
||||||
|
openModSettingsDialog,
|
||||||
|
}: {
|
||||||
|
labelGroup: LabelGroupDefinition['id']
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
|
||||||
|
preferences: UsePreferencesQueryResponse
|
||||||
|
openModSettingsDialog: (props: SettingsDialogProps) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {mutateAsync: setContentLabelPref, variables: optimisticContentLabel} =
|
||||||
|
useSetContentLabelMutation()
|
||||||
|
|
||||||
|
const onChangeVisibility = React.useCallback(
|
||||||
|
async (values: string[]) => {
|
||||||
|
try {
|
||||||
|
await setContentLabelPref({
|
||||||
|
labelGroup,
|
||||||
|
visibility: values[0] as LabelPreference,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[labelGroup, setContentLabelPref],
|
||||||
|
)
|
||||||
|
|
||||||
|
const value =
|
||||||
|
optimisticContentLabel?.visibility ??
|
||||||
|
preferences.moderationOpts.labelGroups[labelGroup]
|
||||||
|
|
||||||
|
const labelOptions = {
|
||||||
|
hide: _(msg`Hide`),
|
||||||
|
warn: _(msg`Warn`),
|
||||||
|
show: _(msg`Show`),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[]}>
|
||||||
|
<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.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
|
||||||
|
{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="hide" label={labelOptions.hide}>
|
||||||
|
{labelOptions.hide}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="warn" label={labelOptions.warn}>
|
||||||
|
{labelOptions.warn}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="ignore" label={labelOptions.show}>
|
||||||
|
{labelOptions.show}
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!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
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
a.p_sm,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
{mods.map(mod => {
|
||||||
|
const modservicePreferences =
|
||||||
|
preferences.moderationOpts.mods.find(
|
||||||
|
({did}) => did === mod.creator.did,
|
||||||
|
)
|
||||||
|
const enabled =
|
||||||
|
!modservicePreferences?.disabledLabelGroups?.includes(
|
||||||
|
labelGroup,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={mod.creator.did}
|
||||||
|
style={[
|
||||||
|
a.py_xs,
|
||||||
|
a.px_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
enabled && t.atoms.bg_contrast_800,
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.font_bold,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
enabled && t.atoms.text_inverted,
|
||||||
|
]}>
|
||||||
|
{getModerationServiceTitle({
|
||||||
|
displayName: mod.creator.displayName,
|
||||||
|
handle: mod.creator.handle,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.font_bold,
|
||||||
|
a.pl_sm,
|
||||||
|
a.italic,
|
||||||
|
t.atoms.text_contrast_low,
|
||||||
|
]}>
|
||||||
|
Configure
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function PwiOptOut() {
|
function PwiOptOut() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {AppBskyModerationDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {getAgent} from '#/state/session'
|
import {getAgent} from '#/state/session'
|
||||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
|
|
||||||
export const modServiceInfoQueryKey = (did: string) => ['mod-service-info', did]
|
export const modServiceInfoQueryKey = (did: string) => ['mod-service-info', did]
|
||||||
|
export const modServicesInfoQueryKey = (dids: string[]) => [
|
||||||
|
'mod-service-info',
|
||||||
|
dids,
|
||||||
|
]
|
||||||
|
|
||||||
export function useModServiceInfoQuery({did}: {did: string}) {
|
export function useModServiceInfoQuery({did}: {did: string}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -16,6 +21,31 @@ export function useModServiceInfoQuery({did}: {did: string}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useModServicesInfoQuery({dids}: {dids: string[]}) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: modServicesInfoQueryKey(dids),
|
||||||
|
queryFn: async () => {
|
||||||
|
const views: AppBskyModerationDefs.ModServiceViewDetailed[] = []
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
dids.map(did => {
|
||||||
|
return getAgent()
|
||||||
|
.app.bsky.moderation.getService({did})
|
||||||
|
.then(res => {
|
||||||
|
views.push(res.data)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return views
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useModServiceSubscriptionMutation() {
|
export function useModServiceSubscriptionMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@@ -65,7 +95,15 @@ export function useModServiceLabelGroupEnableMutation() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
async mutationFn({did, group, enabled}: {did: string; group: string, enabled: boolean}) {
|
async mutationFn({
|
||||||
|
did,
|
||||||
|
group,
|
||||||
|
enabled,
|
||||||
|
}: {
|
||||||
|
did: string
|
||||||
|
group: string
|
||||||
|
enabled: boolean
|
||||||
|
}) {
|
||||||
// TODO
|
// TODO
|
||||||
z.object({
|
z.object({
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
import {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
|
import {
|
||||||
|
LabelPreference,
|
||||||
|
BskyFeedViewPreference,
|
||||||
|
LabelGroupDefinition,
|
||||||
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
import {getAge} from '#/lib/strings/time'
|
import {getAge} from '#/lib/strings/time'
|
||||||
@@ -111,6 +115,26 @@ export function usePreferencesSetContentLabelMutation() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSetContentLabelMutation() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
labelGroup,
|
||||||
|
visibility,
|
||||||
|
}: {
|
||||||
|
labelGroup: LabelGroupDefinition['id']
|
||||||
|
visibility: LabelPreference
|
||||||
|
}) => {
|
||||||
|
await getAgent().setContentLabelPref(labelGroup, visibility)
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function usePreferencesSetAdultContentMutation() {
|
export function usePreferencesSetAdultContentMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ import {Shadow} from '#/state/cache/types'
|
|||||||
import {useRequireAuth} from '#/state/session'
|
import {useRequireAuth} from '#/state/session'
|
||||||
import {LabelInfo} from '../util/moderation/LabelInfo'
|
import {LabelInfo} from '../util/moderation/LabelInfo'
|
||||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
import {
|
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
||||||
Loader as ModServiceLoader,
|
import {getModerationServiceTitle} from '#/lib/moderation'
|
||||||
ModerationServiceCard,
|
|
||||||
} from '#/components/ModerationServiceCard'
|
import {useTheme} from '#/alf'
|
||||||
|
|
||||||
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -96,6 +96,7 @@ let ProfileHeader = ({
|
|||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
isPlaceholderProfile,
|
isPlaceholderProfile,
|
||||||
}: Props): React.ReactNode => {
|
}: Props): React.ReactNode => {
|
||||||
|
const t = useTheme()
|
||||||
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
||||||
useProfileShadow(profileUnshadowed)
|
useProfileShadow(profileUnshadowed)
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -628,7 +629,30 @@ let ProfileHeader = ({
|
|||||||
<LabelInfo details={{did: profile.did}} labels={profile.labels} />
|
<LabelInfo details={{did: profile.did}} labels={profile.labels} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModServiceLoader did={profile.did} component={ModerationServiceCard} />
|
<ModerationServiceCard.Loader
|
||||||
|
did={profile.did}
|
||||||
|
component={({modservice}) => (
|
||||||
|
<ModerationServiceCard.Link modservice={modservice}>
|
||||||
|
{ctx => (
|
||||||
|
<ModerationServiceCard.Card.Outer
|
||||||
|
style={[
|
||||||
|
...(ctx.focused || ctx.hovered
|
||||||
|
? [t.atoms.bg_contrast_50]
|
||||||
|
: []),
|
||||||
|
]}>
|
||||||
|
<ModerationServiceCard.Card.Content
|
||||||
|
title={getModerationServiceTitle({
|
||||||
|
displayName: modservice.creator.displayName,
|
||||||
|
handle: modservice.creator.handle,
|
||||||
|
})}
|
||||||
|
description={modservice.description}
|
||||||
|
handle={modservice.creator.handle}
|
||||||
|
/>
|
||||||
|
</ModerationServiceCard.Card.Outer>
|
||||||
|
)}
|
||||||
|
</ModerationServiceCard.Link>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showSuggestedFollows && (
|
{showSuggestedFollows && (
|
||||||
|
|||||||
Reference in New Issue
Block a user