Implement preference controls on labelers

This commit is contained in:
Paul Frazee
2024-03-04 18:14:02 -08:00
parent b26e280373
commit 19c0ae80eb
8 changed files with 440 additions and 97 deletions
@@ -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>
)
}
+31 -21
View File
@@ -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
+123 -60
View File
@@ -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>
) )
} }
+3 -3
View File
@@ -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,
+5 -6
View File
@@ -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}