Implement preference controls on labelers
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Pressable} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useLabelBehaviorDescription,
|
||||||
|
useLabelLongBehaviorDescription,
|
||||||
|
} from '#/lib/moderation/useLabelBehaviorDescription'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||||
|
import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '../icons/Check'
|
||||||
|
|
||||||
|
export function PreferenceButton({
|
||||||
|
name,
|
||||||
|
pref,
|
||||||
|
labelValueDefinition,
|
||||||
|
onSelectPref,
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
pref: LabelPreference
|
||||||
|
labelValueDefinition: InterprettedLabelValueDefinition
|
||||||
|
onSelectPref: (pref: LabelPreference) => void
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref)
|
||||||
|
const hideLabel = useLabelLongBehaviorDescription(
|
||||||
|
labelValueDefinition,
|
||||||
|
'hide',
|
||||||
|
)
|
||||||
|
const warnLabel = useLabelLongBehaviorDescription(
|
||||||
|
labelValueDefinition,
|
||||||
|
'warn',
|
||||||
|
)
|
||||||
|
const ignoreLabel = useLabelLongBehaviorDescription(
|
||||||
|
labelValueDefinition,
|
||||||
|
'ignore',
|
||||||
|
)
|
||||||
|
const canWarn = !(
|
||||||
|
labelValueDefinition.blurs === 'none' &&
|
||||||
|
labelValueDefinition.severity === 'none'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => control.open()}
|
||||||
|
accessibilityLabel={settingDesc}
|
||||||
|
accessibilityHint=""
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_end,
|
||||||
|
a.gap_xs,
|
||||||
|
a.py_xs,
|
||||||
|
a.rounded_2xs,
|
||||||
|
]}>
|
||||||
|
<Text style={[{color: t.palette.primary_500}, a.font_semibold]}>
|
||||||
|
{settingDesc}
|
||||||
|
</Text>
|
||||||
|
<ArrowTriangleBottom width={8} fill={t.palette.primary_500} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Dialog.Outer control={control}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
|
||||||
|
<Dialog.Inner
|
||||||
|
label={_(msg`Settings: ${labelValueDefinition.identifier}`)}
|
||||||
|
style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, t.atoms.text]}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, a.pb_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Choose how this label should be handled.</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={hideLabel}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color={pref === 'hide' ? 'primary' : 'secondary'}
|
||||||
|
onPress={() => {
|
||||||
|
onSelectPref('hide')
|
||||||
|
control.close()
|
||||||
|
}}>
|
||||||
|
<ButtonText style={[a.flex_1, a.text_left]}>{hideLabel}</ButtonText>
|
||||||
|
{pref === 'hide' && <ButtonIcon icon={Check} position="right" />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{canWarn && (
|
||||||
|
<Button
|
||||||
|
label={warnLabel}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color={pref === 'warn' ? 'primary' : 'secondary'}
|
||||||
|
onPress={() => {
|
||||||
|
onSelectPref('warn')
|
||||||
|
control.close()
|
||||||
|
}}>
|
||||||
|
<ButtonText style={[a.flex_1, a.text_left]}>
|
||||||
|
{warnLabel}
|
||||||
|
</ButtonText>
|
||||||
|
{pref === 'warn' && <ButtonIcon icon={Check} position="right" />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={ignoreLabel}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color={pref === 'ignore' ? 'primary' : 'secondary'}
|
||||||
|
onPress={() => {
|
||||||
|
onSelectPref('ignore')
|
||||||
|
control.close()
|
||||||
|
}}>
|
||||||
|
<ButtonText style={[a.flex_1, a.text_left]}>
|
||||||
|
{ignoreLabel}
|
||||||
|
</ButtonText>
|
||||||
|
{pref === 'ignore' && <ButtonIcon icon={Check} position="right" />}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Inner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||||
|
|
||||||
|
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
|
||||||
|
import {
|
||||||
|
NativeDropdown,
|
||||||
|
DropdownItem,
|
||||||
|
} from '#/view/com/util/forms/NativeDropdown'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle'
|
||||||
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
|
|
||||||
|
const CHECK_ICON: DropdownItem['icon'] = {
|
||||||
|
web: ['fas', 'check'],
|
||||||
|
ios: {name: 'trash'}, //doesnt matter
|
||||||
|
android: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreferenceButton({
|
||||||
|
pref,
|
||||||
|
labelValueDefinition,
|
||||||
|
onSelectPref,
|
||||||
|
}: {
|
||||||
|
pref: LabelPreference
|
||||||
|
labelValueDefinition: InterprettedLabelValueDefinition
|
||||||
|
onSelectPref: (pref: LabelPreference) => void
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: hovered,
|
||||||
|
onIn: onHoverIn,
|
||||||
|
onOut: onHoverOut,
|
||||||
|
} = useInteractionState()
|
||||||
|
|
||||||
|
const settingDesc = useLabelBehaviorDescription(labelValueDefinition, pref)
|
||||||
|
const hideLabel = useLabelBehaviorDescription(labelValueDefinition, 'hide')
|
||||||
|
const warnLabel = useLabelBehaviorDescription(labelValueDefinition, 'warn')
|
||||||
|
const ignoreLabel = useLabelBehaviorDescription(
|
||||||
|
labelValueDefinition,
|
||||||
|
'ignore',
|
||||||
|
)
|
||||||
|
const canWarn = !(
|
||||||
|
labelValueDefinition.blurs === 'none' &&
|
||||||
|
labelValueDefinition.severity === 'none'
|
||||||
|
)
|
||||||
|
|
||||||
|
const dropdownItems: DropdownItem[] = []
|
||||||
|
dropdownItems.push({
|
||||||
|
icon: pref === 'hide' ? CHECK_ICON : undefined,
|
||||||
|
label: hideLabel,
|
||||||
|
onPress: () => onSelectPref('hide'),
|
||||||
|
})
|
||||||
|
if (canWarn) {
|
||||||
|
dropdownItems.push({
|
||||||
|
icon: pref === 'warn' ? CHECK_ICON : undefined,
|
||||||
|
label: warnLabel,
|
||||||
|
onPress: () => onSelectPref('warn'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dropdownItems.push({
|
||||||
|
icon: pref === 'ignore' ? CHECK_ICON : undefined,
|
||||||
|
label: ignoreLabel,
|
||||||
|
onPress: () => onSelectPref('ignore'),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NativeDropdown
|
||||||
|
items={dropdownItems}
|
||||||
|
accessibilityLabel={_(msg`More post options`)}
|
||||||
|
accessibilityHint="">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_end,
|
||||||
|
a.gap_xs,
|
||||||
|
a.py_xs,
|
||||||
|
a.rounded_2xs,
|
||||||
|
hovered && {
|
||||||
|
// @ts-ignore
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
textDecorationColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
// @ts-ignore
|
||||||
|
onMouseEnter={onHoverIn}
|
||||||
|
onMouseLeave={onHoverOut}>
|
||||||
|
<Text style={[{color: t.palette.primary_500}, a.font_semibold]}>
|
||||||
|
{settingDesc}
|
||||||
|
</Text>
|
||||||
|
<ArrowTriangleBottom width={8} fill={t.palette.primary_500} />
|
||||||
|
</View>
|
||||||
|
</NativeDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {InterprettedLabelValueDefinition} from '@atproto/api'
|
import {InterprettedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||||
|
|
||||||
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
|
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
|
||||||
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
usePreferencesSetContentLabelMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
import {PreferenceButton} from './PreferenceButton'
|
||||||
import {ArrowTriangleBottom_Stroke2_Corner1_Rounded as ArrowTriangleBottom} from '../icons/ArrowTriangle'
|
|
||||||
|
|
||||||
export function ModerationLabelPref({
|
export function ModerationLabelPref({
|
||||||
labelValueDefinition,
|
labelValueDefinition,
|
||||||
|
labelerDid,
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
labelValueDefinition: InterprettedLabelValueDefinition
|
labelValueDefinition: InterprettedLabelValueDefinition
|
||||||
|
labelerDid: string | undefined
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const allLabelStrings = useLabelStrings()
|
const allLabelStrings = useLabelStrings()
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate, variables} = usePreferencesSetContentLabelMutation()
|
||||||
|
|
||||||
|
const {identifier} = labelValueDefinition
|
||||||
const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
|
const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
|
||||||
? labelValueDefinition.locales[0]
|
? labelValueDefinition.locales[0]
|
||||||
: labelValueDefinition.identifier in allLabelStrings
|
: labelValueDefinition.identifier in allLabelStrings
|
||||||
@@ -27,9 +35,16 @@ export function ModerationLabelPref({
|
|||||||
name: labelValueDefinition.identifier,
|
name: labelValueDefinition.identifier,
|
||||||
description: `Labeled "${labelValueDefinition.identifier}"`,
|
description: `Labeled "${labelValueDefinition.identifier}"`,
|
||||||
}
|
}
|
||||||
const settingDesc = useLabelBehaviorDescription(labelValueDefinition, 'hide')
|
|
||||||
|
|
||||||
// TODO add onChange behavior when mod prefs are updated
|
const savedPref = labelerDid
|
||||||
|
? preferences?.moderationPrefs.mods.find(m => m.did === labelerDid)?.labels[
|
||||||
|
identifier
|
||||||
|
]
|
||||||
|
: preferences?.moderationPrefs.labels[identifier]
|
||||||
|
const pref = variables?.visibility ?? savedPref ?? 'warn'
|
||||||
|
|
||||||
|
const onSelectPref = (newPref: LabelPreference) =>
|
||||||
|
mutate({label: identifier, visibility: newPref, labelerDid})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.align_center]}>
|
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.align_center]}>
|
||||||
@@ -39,21 +54,16 @@ export function ModerationLabelPref({
|
|||||||
{labelStrings.description}
|
{labelStrings.description}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{!disabled && (
|
<View style={[{width: 110}]}>
|
||||||
<View
|
{!disabled && (
|
||||||
style={[
|
<PreferenceButton
|
||||||
a.flex_row,
|
name={labelStrings.name}
|
||||||
a.align_center,
|
pref={pref}
|
||||||
a.justify_end,
|
labelValueDefinition={labelValueDefinition}
|
||||||
a.gap_xs,
|
onSelectPref={onSelectPref}
|
||||||
{width: 125},
|
/>
|
||||||
]}>
|
)}
|
||||||
<Text style={[{color: t.palette.primary_500}, a.font_semibold]}>
|
</View>
|
||||||
{settingDesc}
|
|
||||||
</Text>
|
|
||||||
<ArrowTriangleBottom width={8} fill={t.palette.primary_500} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,58 @@ export function useLabelBehaviorDescription(
|
|||||||
}
|
}
|
||||||
if (labelValueDef.blurs === 'content') {
|
if (labelValueDef.blurs === 'content') {
|
||||||
if (pref === 'hide') {
|
if (pref === 'hide') {
|
||||||
return _(msg`Hide content`)
|
return _(msg`Hide`)
|
||||||
}
|
}
|
||||||
return _(msg`Warn content`)
|
return _(msg`Warn`)
|
||||||
} else if (labelValueDef.blurs === 'media') {
|
} else if (labelValueDef.blurs === 'media') {
|
||||||
if (pref === 'hide') {
|
if (pref === 'hide') {
|
||||||
return _(msg`Hide images`)
|
return _(msg`Hide`)
|
||||||
}
|
}
|
||||||
return _(msg`Warn images`)
|
return _(msg`Blur images`)
|
||||||
} else if (labelValueDef.severity === 'alert') {
|
} else if (labelValueDef.severity === 'alert') {
|
||||||
if (pref === 'hide') {
|
if (pref === 'hide') {
|
||||||
return _(msg`Filter from feeds`)
|
return _(msg`Hide`)
|
||||||
}
|
}
|
||||||
return _(msg`Show warning`)
|
return _(msg`Show warning`)
|
||||||
} else if (labelValueDef.severity === 'inform') {
|
} else if (labelValueDef.severity === 'inform') {
|
||||||
if (pref === 'hide') {
|
if (pref === 'hide') {
|
||||||
return _(msg`Filter from feeds`)
|
return _(msg`Hide`)
|
||||||
|
}
|
||||||
|
return _(msg`Show badge`)
|
||||||
|
} else {
|
||||||
|
if (pref === 'hide') {
|
||||||
|
return _(msg`Hide`)
|
||||||
|
}
|
||||||
|
return _(msg`Disabled`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLabelLongBehaviorDescription(
|
||||||
|
labelValueDef: InterprettedLabelValueDefinition,
|
||||||
|
pref: LabelPreference,
|
||||||
|
) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
if (pref === 'ignore') {
|
||||||
|
return _(msg`Disabled`)
|
||||||
|
}
|
||||||
|
if (labelValueDef.blurs === 'content') {
|
||||||
|
if (pref === 'hide') {
|
||||||
|
return _(msg`Warn content and filter from feeds`)
|
||||||
|
}
|
||||||
|
return _(msg`Warn content`)
|
||||||
|
} else if (labelValueDef.blurs === 'media') {
|
||||||
|
if (pref === 'hide') {
|
||||||
|
return _(msg`Blur images and filter from feeds`)
|
||||||
|
}
|
||||||
|
return _(msg`Blur images`)
|
||||||
|
} else if (labelValueDef.severity === 'alert') {
|
||||||
|
if (pref === 'hide') {
|
||||||
|
return _(msg`Show warning and filter from feeds`)
|
||||||
|
}
|
||||||
|
return _(msg`Show warning`)
|
||||||
|
} else if (labelValueDef.severity === 'inform') {
|
||||||
|
if (pref === 'hide') {
|
||||||
|
return _(msg`Show badge and filter from feeds`)
|
||||||
}
|
}
|
||||||
return _(msg`Show badge`)
|
return _(msg`Show badge`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ let ProfileHeaderLabeler = ({
|
|||||||
isPlaceholderProfile={isPlaceholderProfile}>
|
isPlaceholderProfile={isPlaceholderProfile}>
|
||||||
<View style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents="box-none">
|
<View style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents="box-none">
|
||||||
<View
|
<View
|
||||||
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
|
style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_lg]}
|
||||||
pointerEvents="box-none">
|
pointerEvents="box-none">
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ 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 {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {logger} from '#/logger'
|
import {runOnJS} from 'react-native-reanimated'
|
||||||
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
import {lookupLabelValueDefinition} from '#/lib/moderation'
|
import {lookupLabelValueDefinition} from '#/lib/moderation'
|
||||||
|
import {ListRef} from '#/view/com/util/List'
|
||||||
|
import {SectionRef} from './types'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -23,20 +27,44 @@ import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
|||||||
import {ErrorState} from '../ErrorState'
|
import {ErrorState} from '../ErrorState'
|
||||||
import {ModerationLabelPref} from '#/components/ModerationLabelPref'
|
import {ModerationLabelPref} from '#/components/ModerationLabelPref'
|
||||||
|
|
||||||
export function ProfileLabelsSection({
|
interface LabelsSectionProps {
|
||||||
isLabelerLoading,
|
|
||||||
labelerInfo,
|
|
||||||
labelerError,
|
|
||||||
moderationOpts,
|
|
||||||
}: {
|
|
||||||
isLabelerLoading: boolean
|
isLabelerLoading: boolean
|
||||||
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined
|
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined
|
||||||
labelerError: Error | null
|
labelerError: Error | null
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
scrollElRef: ListRef
|
||||||
|
headerHeight: number
|
||||||
|
}
|
||||||
|
export const ProfileLabelsSection = React.forwardRef<
|
||||||
|
SectionRef,
|
||||||
|
LabelsSectionProps
|
||||||
|
>(function LabelsSectionImpl(
|
||||||
|
{
|
||||||
|
isLabelerLoading,
|
||||||
|
labelerInfo,
|
||||||
|
labelerError,
|
||||||
|
moderationOpts,
|
||||||
|
scrollElRef,
|
||||||
|
headerHeight,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {height: minHeight} = useSafeAreaFrame()
|
const {height: minHeight} = useSafeAreaFrame()
|
||||||
|
|
||||||
|
const onScrollToTop = React.useCallback(() => {
|
||||||
|
scrollElRef.current?.scrollTo({
|
||||||
|
animated: isNative,
|
||||||
|
x: 0,
|
||||||
|
y: -headerHeight,
|
||||||
|
})
|
||||||
|
}, [scrollElRef, headerHeight])
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
scrollToTop: onScrollToTop,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<View
|
<View
|
||||||
@@ -64,22 +92,42 @@ export function ProfileLabelsSection({
|
|||||||
<ProfileLabelsSectionInner
|
<ProfileLabelsSectionInner
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
labelerInfo={labelerInfo}
|
labelerInfo={labelerInfo}
|
||||||
|
scrollElRef={scrollElRef}
|
||||||
|
headerHeight={headerHeight}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export function ProfileLabelsSectionInner({
|
export function ProfileLabelsSectionInner({
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
labelerInfo,
|
labelerInfo,
|
||||||
|
scrollElRef,
|
||||||
|
headerHeight,
|
||||||
}: {
|
}: {
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed
|
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed
|
||||||
|
scrollElRef: ListRef
|
||||||
|
headerHeight: number
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const contextScrollHandlers = useScrollHandlers()
|
||||||
|
|
||||||
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
|
onBeginDrag(e, ctx) {
|
||||||
|
contextScrollHandlers.onBeginDrag?.(e, ctx)
|
||||||
|
},
|
||||||
|
onEndDrag(e, ctx) {
|
||||||
|
contextScrollHandlers.onEndDrag?.(e, ctx)
|
||||||
|
},
|
||||||
|
onScroll(e, ctx) {
|
||||||
|
contextScrollHandlers.onScroll?.(e, ctx)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const {labelValues} = labelerInfo.policies
|
const {labelValues} = labelerInfo.policies
|
||||||
const isSubscribed = moderationOpts.prefs.mods.find(
|
const isSubscribed = moderationOpts.prefs.mods.find(
|
||||||
mod => mod.did === labelerInfo.creator.did,
|
mod => mod.did === labelerInfo.creator.did,
|
||||||
@@ -93,66 +141,81 @@ export function ProfileLabelsSectionInner({
|
|||||||
) as InterprettedLabelValueDefinition[]
|
) as InterprettedLabelValueDefinition[]
|
||||||
}, [labelerInfo, labelValues])
|
}, [labelerInfo, labelValues])
|
||||||
|
|
||||||
|
console.log(headerHeight, scrollElRef.current)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
ref={scrollElRef}
|
||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
|
paddingTop: headerHeight,
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
paddingHorizontal: a.px_xl.paddingLeft,
|
}}
|
||||||
}}>
|
contentOffset={{x: 0, y: headerHeight * -1}}
|
||||||
<View style={[a.pt_xl]}>
|
onScroll={scrollHandler}>
|
||||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
|
<View
|
||||||
<Trans>
|
style={[
|
||||||
Labels are annotations on users and content. They can be used to
|
a.pt_xl,
|
||||||
hide, warn, and categorize the network.
|
a.px_xl,
|
||||||
</Trans>
|
isNative && a.border_t,
|
||||||
</Text>
|
t.atoms.border_contrast_low,
|
||||||
{labelValues.length === 0 ? (
|
]}>
|
||||||
<Text
|
<View>
|
||||||
style={[
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
|
||||||
a.pt_xl,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
a.leading_snug,
|
|
||||||
a.text_sm,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
<Trans>
|
||||||
This labeler hasn't declared what labels it publishes, and may not
|
Labels are annotations on users and content. They can be used to
|
||||||
be active.
|
hide, warn, and categorize the network.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : !isSubscribed ? (
|
{labelValues.length === 0 ? (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.pt_xl,
|
a.pt_xl,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
a.text_sm,
|
a.text_sm,
|
||||||
]}>
|
]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
Subscribe to @{labelerInfo.creator.handle} to use these labels:
|
This labeler hasn't declared what labels it publishes, and may
|
||||||
</Trans>
|
not be active.
|
||||||
</Text>
|
</Trans>
|
||||||
) : null}
|
</Text>
|
||||||
</View>
|
) : !isSubscribed ? (
|
||||||
{labelDefs.length > 0 && (
|
<Text
|
||||||
<View style={[a.mt_xl, t.atoms.bg_contrast_25, a.rounded_md, a.py_xs]}>
|
style={[
|
||||||
{labelDefs.map((labelDef, i) => {
|
a.pt_xl,
|
||||||
return (
|
t.atoms.text_contrast_high,
|
||||||
<React.Fragment key={labelDef.identifier}>
|
a.leading_snug,
|
||||||
{i !== 0 && <Divider />}
|
a.text_sm,
|
||||||
<View style={[a.py_md, a.px_md]}>
|
]}>
|
||||||
<ModerationLabelPref
|
<Trans>
|
||||||
disabled={isSubscribed ? undefined : true}
|
Subscribe to @{labelerInfo.creator.handle} to use these labels:
|
||||||
labelValueDefinition={labelDef}
|
</Trans>
|
||||||
/>
|
</Text>
|
||||||
</View>
|
) : null}
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
{labelDefs.length > 0 && (
|
||||||
|
<View
|
||||||
|
style={[a.mt_xl, t.atoms.bg_contrast_25, a.rounded_md, a.py_xs]}>
|
||||||
|
{labelDefs.map((labelDef, i) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={labelDef.identifier}>
|
||||||
|
{i !== 0 && <Divider />}
|
||||||
|
<View style={[a.py_md, a.px_md]}>
|
||||||
|
<ModerationLabelPref
|
||||||
|
disabled={isSubscribed ? undefined : true}
|
||||||
|
labelValueDefinition={labelDef}
|
||||||
|
labelerDid={labelerInfo.creator.did}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{height: 100}} />
|
<View style={{height: 400}} />
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ export function usePreferencesSetContentLabelMutation() {
|
|||||||
return useMutation<
|
return useMutation<
|
||||||
void,
|
void,
|
||||||
unknown,
|
unknown,
|
||||||
{labelGroup: ConfigurableLabelGroup; visibility: LabelPreference}
|
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
|
||||||
>({
|
>({
|
||||||
mutationFn: async ({labelGroup, visibility}) => {
|
mutationFn: async ({label, visibility, labelerDid}) => {
|
||||||
await getAgent().setContentLabelPref(labelGroup, visibility)
|
await getAgent().setContentLabelPref(label, visibility, labelerDid)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: preferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function ProfileScreenLoaded({
|
|||||||
const likesSectionRef = React.useRef<SectionRef>(null)
|
const likesSectionRef = React.useRef<SectionRef>(null)
|
||||||
const feedsSectionRef = React.useRef<SectionRef>(null)
|
const feedsSectionRef = React.useRef<SectionRef>(null)
|
||||||
const listsSectionRef = React.useRef<SectionRef>(null)
|
const listsSectionRef = React.useRef<SectionRef>(null)
|
||||||
const filtersSectionRef = React.useRef<SectionRef>(null)
|
const labelsSectionRef = React.useRef<SectionRef>(null)
|
||||||
|
|
||||||
useSetTitle(combinedDisplayName(profile))
|
useSetTitle(combinedDisplayName(profile))
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ function ProfileScreenLoaded({
|
|||||||
const scrollSectionToTop = React.useCallback(
|
const scrollSectionToTop = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
if (index === filtersIndex) {
|
if (index === filtersIndex) {
|
||||||
filtersSectionRef.current?.scrollToTop()
|
labelsSectionRef.current?.scrollToTop()
|
||||||
} else if (index === postsIndex) {
|
} else if (index === postsIndex) {
|
||||||
postsSectionRef.current?.scrollToTop()
|
postsSectionRef.current?.scrollToTop()
|
||||||
} else if (index === repliesIndex) {
|
} else if (index === repliesIndex) {
|
||||||
@@ -347,16 +347,15 @@ function ProfileScreenLoaded({
|
|||||||
onCurrentPageSelected={onCurrentPageSelected}
|
onCurrentPageSelected={onCurrentPageSelected}
|
||||||
renderHeader={renderHeader}>
|
renderHeader={renderHeader}>
|
||||||
{showFiltersTab
|
{showFiltersTab
|
||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, scrollElRef}) => (
|
||||||
<ProfileLabelsSection
|
<ProfileLabelsSection
|
||||||
// ref={moderationSectionRef}
|
ref={labelsSectionRef}
|
||||||
labelerInfo={labelerInfo}
|
labelerInfo={labelerInfo}
|
||||||
labelerError={labelerError}
|
labelerError={labelerError}
|
||||||
isLabelerLoading={isLabelerLoading}
|
isLabelerLoading={isLabelerLoading}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerHeight={headerHeight}
|
||||||
enabled={isFocused}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|||||||
Reference in New Issue
Block a user