Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators

This commit is contained in:
Paul Frazee
2024-02-15 13:13:32 -08:00
4 changed files with 134 additions and 139 deletions
+6 -2
View File
@@ -7,11 +7,12 @@ import Animated, {
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common' import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader' import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
export function Loader(props: Props) { export function Loader(props: Props) {
const t = useTheme()
const common = useCommonSVGProps(props) const common = useCommonSVGProps(props)
const rotation = useSharedValue(0) const rotation = useSharedValue(0)
@@ -35,7 +36,10 @@ export function Loader(props: Props) {
{width: common.size, height: common.size}, {width: common.size, height: common.size},
animatedStyles, animatedStyles,
]}> ]}>
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} /> <Icon
{...props}
style={[a.absolute, a.inset_0, props.style, t.atoms.text_contrast_high]}
/>
</Animated.View> </Animated.View>
) )
} }
+89 -90
View File
@@ -5,6 +5,7 @@ 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 {LabelGroupDefinition, AppBskyModerationDefs} from '@atproto/api' import {LabelGroupDefinition, AppBskyModerationDefs} from '@atproto/api'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
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'
@@ -39,9 +40,9 @@ 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 { import {
getLabelGroupsFromLabels,
getModerationServiceTitle, getModerationServiceTitle,
useConfigurableLabelGroups, useConfigurableLabelGroups,
getLabelGroupToLabelerMap,
} from '#/lib/moderation' } from '#/lib/moderation'
import { import {
@@ -49,51 +50,91 @@ import {
SettingsDialogProps, SettingsDialogProps,
} from '#/screens/Moderation/SettingsDialog' } from '#/screens/Moderation/SettingsDialog'
function ErrorState({error}: {error: string}) {
const t = useTheme()
return (
<View style={[a.p_xl]}>
<Text
style={[
a.text_md,
a.leading_normal,
a.pb_md,
t.atoms.text_contrast_medium,
]}>
<Trans>
Hmmmm, it seems we're having trouble loading this data. See below for
more details. If this issue persists, please contact us.
</Trans>
</Text>
<View
style={[
a.relative,
a.py_md,
a.px_lg,
a.rounded_md,
a.mb_2xl,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.text_md, a.leading_normal]}>{error}</Text>
</View>
</View>
)
}
export function ModerationScreen( export function ModerationScreen(
_props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
) { ) {
const t = useTheme() const t = useTheme()
const {_} = useLingui()
const { const {
isLoading: isPreferencesLoading, isLoading: isPreferencesLoading,
// error: preferencesError, error: preferencesError,
data: preferences, data: preferences,
} = usePreferencesQuery() } = usePreferencesQuery()
return isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
) : preferences ? (
<ModerationScreenIntermediate preferences={preferences} />
) : // TODO
null
}
function ModerationScreenIntermediate({
preferences,
}: {
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const { const {
isLoading: isModServicesLoading, isLoading: isModServicesLoading,
data: modservices, data: modservices,
// error: modservicesError, error: modservicesError,
} = useModServicesDetailedInfoQuery({ } = useModServicesDetailedInfoQuery({
dids: preferences.moderationOpts.mods.map(m => m.did), dids: preferences ? preferences.moderationOpts.mods.map(m => m.did) : [],
}) })
const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame()
return isModServicesLoading ? ( const isLoading = isPreferencesLoading || isModServicesLoading
<View style={[a.w_full, a.align_center]}> const error = preferencesError || modservicesError
<Loader size="xl" fill={t.atoms.text.color} />
</View> return (
) : modservices ? ( <CenteredView
<ModerationScreenInner testID="moderationScreen"
preferences={preferences} style={[
modservices={modservices} a.border,
/> t.atoms.border_contrast_low,
) : // TODO t.atoms.bg,
null {minHeight: height},
...(gtMobile ? [a.border_l, a.border_r] : []),
]}>
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
{isLoading ? (
<View style={[a.w_full, a.align_center, a.pt_2xl]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
) : error || !(preferences && modservices) ? (
<ErrorState
error={
preferencesError?.toString() ||
_(msg`Something went wrong, please try again.`)
}
/>
) : (
<ModerationScreenInner
preferences={preferences}
modservices={modservices}
/>
)}
</CenteredView>
)
} }
export function ModerationScreenInner({ export function ModerationScreenInner({
@@ -104,19 +145,20 @@ export function ModerationScreenInner({
modservices: AppBskyModerationDefs.ModServiceViewDetailed[] modservices: AppBskyModerationDefs.ModServiceViewDetailed[]
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {gtMobile, gtTablet} = useBreakpoints() const {gtTablet} = useBreakpoints()
const labelGroupStrings = useLabelGroupStrings()
const modSettingsDialogControl = Dialog.useDialogControl() const modSettingsDialogControl = Dialog.useDialogControl()
const [settingsDialogProps, setSettingsDialogProps] = const [settingsDialogProps, setSettingsDialogProps] =
React.useState<SettingsDialogProps>({ React.useState<SettingsDialogProps>({
// @ts-ignore // prefill with valid value to appease TS
labelGroup: '', labelGroup: 'intolerance',
modservices: [], modservices: [],
}) })
const groups = useConfigurableLabelGroups()
const labelGroupToLabelerMap = React.useMemo(() => {
return getLabelGroupToLabelerMap(modservices)
}, [modservices])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -125,34 +167,6 @@ export function ModerationScreenInner({
}, [screen, setMinimalShellMode]), }, [screen, setMinimalShellMode]),
) )
const groups = useConfigurableLabelGroups()
const didToModServiceMap = React.useMemo<
Record<string, AppBskyModerationDefs.ModServiceViewDetailed>
>(() => {
return modservices.reduce((acc, modservice) => {
return {
...acc,
[modservice.creator.did]: modservice,
}
}, {})
}, [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(
({labelGroup, modservices}: Omit<SettingsDialogProps, 'onComplete'>) => { ({labelGroup, modservices}: Omit<SettingsDialogProps, 'onComplete'>) => {
setSettingsDialogProps({ setSettingsDialogProps({
@@ -165,21 +179,12 @@ export function ModerationScreenInner({
) )
return ( return (
<CenteredView <View>
style={[
a.border,
t.atoms.border_contrast_low,
t.atoms.bg,
...(gtMobile ? [a.border_l, a.border_r] : []),
]}
testID="moderationScreen">
<Dialog.Outer control={modSettingsDialogControl}> <Dialog.Outer control={modSettingsDialogControl}>
<Dialog.Handle /> <Dialog.Handle />
<SettingsDialog {...settingsDialogProps} preferences={preferences} /> <SettingsDialog {...settingsDialogProps} preferences={preferences} />
</Dialog.Outer> </Dialog.Outer>
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
<ScrollView contentContainerStyle={[a.border_0]}> <ScrollView contentContainerStyle={[a.border_0]}>
{!gtTablet && <Divider />} {!gtTablet && <Divider />}
@@ -252,19 +257,15 @@ export function ModerationScreenInner({
</Text> </Text>
{groups.map((def, i) => { {groups.map((def, i) => {
const groupStrings = labelGroupStrings[def.id] const labelers = labelGroupToLabelerMap[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 <LabelGroup
name={groupStrings.name}
description={groupStrings.description}
labelers={mods}
openModSettingsDialog={openModSettingsDialog}
preferences={preferences}
labelGroup={def.id} labelGroup={def.id}
labelers={labelers}
preferences={preferences}
openModSettingsDialog={openModSettingsDialog}
/> />
</React.Fragment> </React.Fragment>
) )
@@ -280,27 +281,24 @@ export function ModerationScreenInner({
<View style={{height: 200}} /> <View style={{height: 200}} />
</ScrollView> </ScrollView>
</CenteredView> </View>
) )
} }
function LabelGroup({ function LabelGroup({
labelGroup, labelGroup,
name,
description,
labelers: mods, labelers: mods,
preferences, preferences,
openModSettingsDialog, openModSettingsDialog,
}: { }: {
labelGroup: LabelGroupDefinition['id'] labelGroup: LabelGroupDefinition['id']
name: string
description: string
labelers: AppBskyModerationDefs.ModServiceViewDetailed[] labelers: AppBskyModerationDefs.ModServiceViewDetailed[]
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
openModSettingsDialog: (props: SettingsDialogProps) => void openModSettingsDialog: (props: SettingsDialogProps) => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const labelGroupStrings = useLabelGroupStrings()
const {mutateAsync: setContentLabelPref, variables: optimisticContentLabel} = const {mutateAsync: setContentLabelPref, variables: optimisticContentLabel} =
useSetContentLabelMutation() useSetContentLabelMutation()
@@ -312,16 +310,17 @@ function LabelGroup({
visibility: values[0] as LabelPreference, visibility: values[0] as LabelPreference,
}) })
} catch (e) { } catch (e) {
// TODO
console.error(e) console.error(e)
} }
}, },
[labelGroup, setContentLabelPref], [labelGroup, setContentLabelPref],
) )
const {name, description} = labelGroupStrings[labelGroup]
const value = const value =
optimisticContentLabel?.visibility ?? optimisticContentLabel?.visibility ??
preferences.moderationOpts.labelGroups[labelGroup] preferences.moderationOpts.labelGroups[labelGroup]
const labelOptions = { const labelOptions = {
hide: _(msg`Hide`), hide: _(msg`Hide`),
warn: _(msg`Warn`), warn: _(msg`Warn`),
+32 -44
View File
@@ -1,9 +1,10 @@
import React from 'react' import React from 'react'
import {Dimensions, View} from 'react-native' import {View} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {AppBskyModerationDefs} from '@atproto/api' import {AppBskyModerationDefs} 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 {useSafeAreaFrame} from 'react-native-safe-area-context'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {CommonNavigatorParams} from '#/lib/routes/types' import {CommonNavigatorParams} from '#/lib/routes/types'
@@ -45,17 +46,32 @@ import {ErrorState} from '#/screens/ProfileModerationService/ErrorState'
import {Header} from '#/screens/ProfileModerationService/Header' import {Header} from '#/screens/ProfileModerationService/Header'
import {PreferenceRow} from '#/screens/ProfileModerationService/PreferenceRow' import {PreferenceRow} from '#/screens/ProfileModerationService/PreferenceRow'
// TODO
const MIN_HEIGHT = Dimensions.get('window').height * 1.5
// TODO loader default color
export function ProfileModserviceScreen( export function ProfileModserviceScreen(
props: NativeStackScreenProps<CommonNavigatorParams, 'ProfileModservice'>, props: NativeStackScreenProps<CommonNavigatorParams, 'ProfileModservice'>,
) { ) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame()
const {name: handleOrDid} = props.route.params const {name: handleOrDid} = props.route.params
const {isLoading, error, data: resolvedDid} = useResolveDidQuery(handleOrDid) 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 ( return (
<CenteredView> <CenteredView>
@@ -65,60 +81,31 @@ export function ProfileModserviceScreen(
a.border_r, a.border_r,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
{ {
minHeight: MIN_HEIGHT, minHeight,
}, },
]}> ]}>
{isLoading ? ( {isLoading ? (
<View style={[a.w_full, a.align_center]}> <View style={[a.w_full, a.align_center]}>
<Loader size="xl" fill={t.atoms.text.color} /> <Loader size="xl" />
</View> </View>
) : resolvedDid ? ( ) : error || !(did && preferences && modservice) ? (
<ProfileModservicecreenIntermediate modDid={resolvedDid} />
) : (
<ErrorState <ErrorState
error={ error={
error?.toString() || error?.toString() ||
_(msg`Something went wrong, please try again.`) _(msg`Something went wrong, please try again.`)
} }
/> />
) : (
<ProfileModserviceScreenInner
preferences={preferences}
modservice={modservice}
/>
)} )}
</View> </View>
</CenteredView> </CenteredView>
) )
} }
function ProfileModservicecreenIntermediate({modDid}: {modDid: string}) {
const t = useTheme()
const {_} = useLingui()
const {
isLoading: isPreferencesLoading,
error: preferencesError,
data: preferences,
} = usePreferencesQuery()
const {
isLoading: isModServiceLoading,
error: modServiceError,
data: info,
} = useModServiceInfoQuery({did: modDid})
const isLoading = isPreferencesLoading || isModServiceLoading
const error = preferencesError || modServiceError
return isLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
) : preferences && info ? (
<ProfileModserviceScreenInner preferences={preferences} modservice={info} />
) : (
<ErrorState
error={
error?.toString() || _(msg`Something went wrong, please try again.`)
}
/>
)
}
export function ProfileModserviceScreenInner({ export function ProfileModserviceScreenInner({
preferences, preferences,
modservice, modservice,
@@ -128,6 +115,7 @@ export function ProfileModserviceScreenInner({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame()
const pal = usePalette('default') const pal = usePalette('default')
const {hasSession} = useSession() const {hasSession} = useSession()
const {track} = useAnalytics() const {track} = useAnalytics()
@@ -178,7 +166,7 @@ export function ProfileModserviceScreenInner({
<ScrollView <ScrollView
scrollEventThrottle={1} scrollEventThrottle={1}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight,
borderWidth: 0, borderWidth: 0,
paddingHorizontal: a.px_xl.paddingLeft, paddingHorizontal: a.px_xl.paddingLeft,
}}> }}>
+7 -3
View File
@@ -15,11 +15,14 @@ export const modServicesDetailedInfoQueryKey = (dids: string[]) => [
dids, dids,
] ]
export function useModServiceInfoQuery({did}: {did: string}) { export function useModServiceInfoQuery({did}: {did?: string}) {
return useQuery({ return useQuery({
queryKey: modServiceInfoQueryKey(did), enabled: !!did,
queryKey: modServiceInfoQueryKey(did as string),
queryFn: async () => { queryFn: async () => {
const res = await getAgent().app.bsky.moderation.getService({did}) const res = await getAgent().app.bsky.moderation.getService({
did: did as string,
})
return res.data return res.data
}, },
}) })
@@ -38,6 +41,7 @@ export function useModServicesInfoQuery({dids}: {dids: string[]}) {
export function useModServicesDetailedInfoQuery({dids}: {dids: string[]}) { export function useModServicesDetailedInfoQuery({dids}: {dids: string[]}) {
return useQuery({ return useQuery({
enabled: !!dids.length,
queryKey: modServicesDetailedInfoQueryKey(dids), queryKey: modServicesDetailedInfoQueryKey(dids),
queryFn: async () => { queryFn: async () => {
const views: AppBskyModerationDefs.ModServiceViewDetailed[] = [] const views: AppBskyModerationDefs.ModServiceViewDetailed[] = []