Update profile to pull from labeler definition

This commit is contained in:
Paul Frazee
2024-03-04 14:02:54 -08:00
parent 473ca0deff
commit 4d0dc7438b
6 changed files with 138 additions and 80 deletions
+17 -11
View File
@@ -2,6 +2,7 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {InterprettedLabelValueDefinition} from '@atproto/api'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings' import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
@@ -10,21 +11,26 @@ import {Text} from '#/components/Typography'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
export function ModerationLabelPref({ export function ModerationLabelPref({
label, labelValueDefinition,
disabled, disabled,
}: { }: {
label: string labelValueDefinition: InterprettedLabelValueDefinition
disabled?: boolean disabled?: boolean
}) { }) {
console.log({labelValueDefinition})
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const allLabelStrings = useLabelStrings() const allLabelStrings = useLabelStrings()
const labelStrings = allLabelStrings[label] || { const labelStrings = labelValueDefinition.locales[0] // TODO look up locale
general: { ? labelValueDefinition.locales[0]
name: label, : labelValueDefinition.identifier in allLabelStrings
description: `Labeled "${label}"`, ? allLabelStrings[labelValueDefinition.identifier].general
}, : {
} general: {
name: labelValueDefinition.identifier,
description: `Labeled "${labelValueDefinition.identifier}"`,
},
}
// TODO add onChange behavior when mod prefs are updated // TODO add onChange behavior when mod prefs are updated
@@ -46,16 +52,16 @@ export function ModerationLabelPref({
a.align_center, a.align_center,
]}> ]}>
<View style={[a.gap_xs, {width: '50%'}]}> <View style={[a.gap_xs, {width: '50%'}]}>
<Text style={[a.font_bold]}>{labelStrings.general.name}</Text> <Text style={[a.font_bold]}>{labelStrings.name}</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{labelStrings.general.description} {labelStrings.description}
</Text> </Text>
</View> </View>
<View style={[a.justify_center, {minHeight: 35}]}> <View style={[a.justify_center, {minHeight: 35}]}>
{!disabled && ( {!disabled && (
<ToggleButton.Group <ToggleButton.Group
label={_( label={_(
msg`Configure content filtering setting for category: ${labelStrings.general.name.toLowerCase()}`, msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`,
)} )}
values={['hide']} values={['hide']}
onChange={() => {}}> onChange={() => {}}>
+20 -1
View File
@@ -1,5 +1,10 @@
import React from 'react' import React from 'react'
import {ModerationCause, ModerationUI, AppBskyLabelerDefs} from '@atproto/api' import {
ModerationCause,
ModerationUI,
InterprettedLabelValueDefinition,
LABELS,
} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
@@ -47,3 +52,17 @@ export function getModerationServiceTitle({
? sanitizeDisplayName(displayName) ? sanitizeDisplayName(displayName)
: sanitizeHandle(handle, '@') : sanitizeHandle(handle, '@')
} }
export function lookupLabelValueDefinition(
labelValue: string,
customDefs: InterprettedLabelValueDefinition[] | undefined,
): InterprettedLabelValueDefinition | undefined {
let def
if (!labelValue.startsWith('!') && customDefs) {
def = customDefs.find(d => d.identifier === labelValue)
}
if (!def) {
def = LABELS[labelValue as keyof typeof LABELS]
}
return def
}
@@ -38,16 +38,16 @@ import {
interface Props { interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
modservice: AppBskyLabelerDefs.LabelerViewDetailed labeler: AppBskyLabelerDefs.LabelerViewDetailed
descriptionRT: RichTextAPI | null descriptionRT: RichTextAPI | null
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
hideBackButton?: boolean hideBackButton?: boolean
isPlaceholderProfile?: boolean isPlaceholderProfile?: boolean
} }
let ProfileHeaderModerator = ({ let ProfileHeaderLabeler = ({
profile: profileUnshadowed, profile: profileUnshadowed,
modservice, labeler,
descriptionRT, descriptionRT,
moderationOpts, moderationOpts,
hideBackButton = false, hideBackButton = false,
@@ -74,12 +74,12 @@ let ProfileHeaderModerator = ({
const {mutateAsync: unlikeMod, isPending: isUnlikePending} = const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation() useUnlikeMutation()
const [likeUri, setLikeUri] = React.useState<string>( const [likeUri, setLikeUri] = React.useState<string>(
modservice.viewer?.like || '', labeler.viewer?.like || '',
) )
const isLiked = !!likeUri const isLiked = !!likeUri
const onToggleLiked = React.useCallback(async () => { const onToggleLiked = React.useCallback(async () => {
if (!modservice) { if (!labeler) {
return return
} }
try { try {
@@ -90,7 +90,7 @@ let ProfileHeaderModerator = ({
track('CustomFeed:Unlike') track('CustomFeed:Unlike')
setLikeUri('') setLikeUri('')
} else { } else {
const res = await likeMod({uri: modservice.uri, cid: modservice.cid}) const res = await likeMod({uri: labeler.uri, cid: labeler.cid})
track('CustomFeed:Like') track('CustomFeed:Like')
setLikeUri(res.uri) setLikeUri(res.uri)
} }
@@ -102,7 +102,7 @@ let ProfileHeaderModerator = ({
) )
logger.error(`Failed to toggle labeler like`, {message: e.message}) logger.error(`Failed to toggle labeler like`, {message: e.message})
} }
}, [modservice, likeUri, isLiked, likeMod, unlikeMod, track, _]) }, [labeler, likeUri, isLiked, likeMod, unlikeMod, track, _])
const onPressEditProfile = React.useCallback(() => { const onPressEditProfile = React.useCallback(() => {
track('ProfileHeader:EditProfileButtonClicked') track('ProfileHeader:EditProfileButtonClicked')
@@ -213,13 +213,13 @@ let ProfileHeaderModerator = ({
)} )}
</Button> </Button>
{typeof modservice.likeCount === 'number' && ( {typeof labeler.likeCount === 'number' && (
<InlineLink <InlineLink
to={'#todo'} to={'#todo'}
style={[t.atoms.text_contrast_medium, a.font_bold]}> style={[t.atoms.text_contrast_medium, a.font_bold]}>
<Trans> <Trans>
Liked by {modservice.likeCount}{' '} Liked by {labeler.likeCount}{' '}
{pluralize(modservice.likeCount, 'user')} {pluralize(labeler.likeCount, 'user')}
</Trans> </Trans>
</InlineLink> </InlineLink>
)} )}
@@ -230,5 +230,5 @@ let ProfileHeaderModerator = ({
</ProfileHeaderShell> </ProfileHeaderShell>
) )
} }
ProfileHeaderModerator = memo(ProfileHeaderModerator) ProfileHeaderLabeler = memo(ProfileHeaderLabeler)
export {ProfileHeaderModerator} export {ProfileHeaderLabeler}
+4 -4
View File
@@ -10,7 +10,7 @@ import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {ProfileHeaderStandard} from './ProfileHeaderStandard' import {ProfileHeaderStandard} from './ProfileHeaderStandard'
import {ProfileHeaderModerator} from './ProfileHeaderModerator' import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => { let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
@@ -34,7 +34,7 @@ export {ProfileHeaderLoading}
interface Props { interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
modservice: AppBskyLabelerDefs.LabelerViewDetailed | undefined labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined
descriptionRT: RichTextAPI | null descriptionRT: RichTextAPI | null
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
hideBackButton?: boolean hideBackButton?: boolean
@@ -43,10 +43,10 @@ interface Props {
let ProfileHeader = (props: Props): React.ReactNode => { let ProfileHeader = (props: Props): React.ReactNode => {
if (props.profile.associated?.labeler) { if (props.profile.associated?.labeler) {
if (!props.modservice) { if (!props.labeler) {
return <ProfileHeaderLoading /> return <ProfileHeaderLoading />
} }
return <ProfileHeaderModerator {...props} modservice={props.modservice} /> return <ProfileHeaderLabeler {...props} labeler={props.labeler} />
} }
return <ProfileHeaderStandard {...props} /> return <ProfileHeaderStandard {...props} />
} }
@@ -1,12 +1,18 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyLabelerDefs, ModerationOpts} from '@atproto/api' import {
AppBskyLabelerDefs,
ModerationOpts,
interpretLabelValueDefinitions,
InterprettedLabelValueDefinition,
} 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 {useSafeAreaFrame} from 'react-native-safe-area-context'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
import {logger} from '#/logger' import {logger} from '#/logger'
import {lookupLabelValueDefinition} from '#/lib/moderation'
import {useTheme, atoms as a} from '#/alf' import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -17,21 +23,20 @@ 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 ProfileContentFiltersSection({ export function ProfileLabelsSection({
modServiceQuery, isLabelerLoading,
labelerInfo,
labelerError,
moderationOpts, moderationOpts,
}: { }: {
modServiceQuery: { isLabelerLoading: boolean
data: AppBskyLabelerDefs.LabelerViewDetailed | undefined labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined
isLoading: boolean labelerError: Error | null
error: Error | null
}
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame() const {height: minHeight} = useSafeAreaFrame()
const {isLoading, error, data: modservice} = modServiceQuery
return ( return (
<CenteredView> <CenteredView>
<View <View
@@ -44,21 +49,21 @@ export function ProfileContentFiltersSection({
minHeight, minHeight,
}, },
]}> ]}>
{isLoading ? ( {isLabelerLoading ? (
<View style={[a.w_full, a.align_center]}> <View style={[a.w_full, a.align_center]}>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>
) : error || !modservice ? ( ) : labelerError || !labelerInfo ? (
<ErrorState <ErrorState
error={ error={
error?.toString() || labelerError?.toString() ||
_(msg`Something went wrong, please try again.`) _(msg`Something went wrong, please try again.`)
} }
/> />
) : ( ) : (
<ProfileContentFiltersSectionInner <ProfileLabelsSectionInner
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
modservice={modservice} labelerInfo={labelerInfo}
/> />
)} )}
</View> </View>
@@ -66,37 +71,46 @@ export function ProfileContentFiltersSection({
) )
} }
export function ProfileContentFiltersSectionInner({ export function ProfileLabelsSectionInner({
moderationOpts, moderationOpts,
modservice, labelerInfo,
}: { }: {
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
modservice: AppBskyLabelerDefs.LabelerViewDetailed labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const isEnabled = Boolean( const isEnabled = Boolean(
moderationOpts.prefs.mods.find(mod => mod.did === modservice.creator.did), moderationOpts.prefs.mods.find(mod => mod.did === labelerInfo.creator.did),
) )
const hasSession = true // TODO const hasSession = true // TODO
const {labelValues} = labelerInfo.policies
const labelDefs = React.useMemo(() => {
const customDefs = interpretLabelValueDefinitions(labelerInfo)
return labelValues
.map(val => lookupLabelValueDefinition(val, customDefs))
.filter(
def => def && def?.configurable,
) as InterprettedLabelValueDefinition[]
}, [labelerInfo, labelValues])
const {mutateAsync: toggleSubscription, variables} = const {mutateAsync: toggleSubscription, variables} =
useLabelerSubscriptionMutation() useLabelerSubscriptionMutation()
const isSubscribed = const isSubscribed =
variables?.subscribe ?? variables?.subscribe ??
moderationOpts.prefs.mods.find(mod => mod.did === modservice.creator.did) moderationOpts.prefs.mods.find(mod => mod.did === labelerInfo.creator.did)
const onPressSubscribe = React.useCallback(async () => { const onPressSubscribe = React.useCallback(async () => {
try { try {
await toggleSubscription({ await toggleSubscription({
did: modservice.creator.did, did: labelerInfo.creator.did,
subscribe: !isSubscribed, subscribe: !isSubscribed,
}) })
} catch (e: any) { } catch (e: any) {
// setSubscriptionError(e.message) // setSubscriptionError(e.message)
logger.error(`Failed to subscribe to labeler`, {message: e.message}) logger.error(`Failed to subscribe to labeler`, {message: e.message})
} }
}, [toggleSubscription, isSubscribed, modservice]) }, [toggleSubscription, isSubscribed, labelerInfo])
return ( return (
<ScrollView <ScrollView
@@ -112,7 +126,7 @@ export function ProfileContentFiltersSectionInner({
hide, warn, and categorize the network. hide, warn, and categorize the network.
</Trans> </Trans>
</Text> </Text>
{!isSubscribed && ( {labelValues.length === 0 ? (
<Text <Text
style={[ style={[
a.pt_xl, a.pt_xl,
@@ -121,33 +135,46 @@ export function ProfileContentFiltersSectionInner({
a.text_sm, a.text_sm,
]}> ]}>
<Trans> <Trans>
Subscribe to @{modservice.creator.handle} to use these labels: This labeler hasn't declared what labels it publishes, and may not
be active.
</Trans> </Trans>
</Text> </Text>
)} ) : !isSubscribed ? (
</View> <Text
<View style={[
style={[ a.pt_xl,
a.mt_xl, t.atoms.text_contrast_high,
t.atoms.bg_contrast_25, a.leading_snug,
a.rounded_md, a.text_sm,
a.border, ]}>
t.atoms.border_contrast_low, <Trans>
]}> Subscribe to @{labelerInfo.creator.handle} to use these labels:
{ </Trans>
undefined /* TODO modservice.policies.labelValues.map((def, i) => { </Text>
return ( ) : null}
<React.Fragment key={def.id}>
{i !== 0 && <Divider />}
<ModerationLabelPref
disabled={isEnabled ? undefined : true}
labelGroup={def.id}
/>
</React.Fragment>
)
})*/
}
</View> </View>
{labelDefs.length > 0 && (
<View
style={[
a.mt_xl,
t.atoms.bg_contrast_25,
a.rounded_md,
a.border,
t.atoms.border_contrast_low,
]}>
{labelDefs.map((labelDef, i) => {
return (
<React.Fragment key={labelDef?.identifier}>
{i !== 0 && <Divider />}
<ModerationLabelPref
disabled={isEnabled ? undefined : true}
labelValueDefinition={labelDef}
/>
</React.Fragment>
)
})}
</View>
)}
<View style={{height: 100}} /> <View style={{height: 100}} />
</ScrollView> </ScrollView>
+12 -6
View File
@@ -37,7 +37,7 @@ import {listenSoftReset} from '#/state/events'
import {isInvalidHandle} from '#/lib/strings/handles' import {isInvalidHandle} from '#/lib/strings/handles'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileContentFiltersSection} from '#/screens/Profile/Sections/ContentFilters' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
interface SectionRef { interface SectionRef {
@@ -139,7 +139,11 @@ function ProfileScreenLoaded({
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const modServiceQuery = useLabelerInfoQuery({ const {
data: labelerInfo,
error: labelerError,
isLoading: isLabelerLoading,
} = useLabelerInfoQuery({
did: profile.did, did: profile.did,
enabled: !!profile.associated?.labeler, enabled: !!profile.associated?.labeler,
}) })
@@ -312,7 +316,7 @@ function ProfileScreenLoaded({
return ( return (
<ProfileHeader <ProfileHeader
profile={profile} profile={profile}
modservice={modServiceQuery.data} labeler={labelerInfo}
descriptionRT={hasDescription ? descriptionRT : null} descriptionRT={hasDescription ? descriptionRT : null}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
hideBackButton={hideBackButton} hideBackButton={hideBackButton}
@@ -321,7 +325,7 @@ function ProfileScreenLoaded({
) )
}, [ }, [
profile, profile,
modServiceQuery, labelerInfo,
descriptionRT, descriptionRT,
hasDescription, hasDescription,
moderationOpts, moderationOpts,
@@ -344,9 +348,11 @@ function ProfileScreenLoaded({
renderHeader={renderHeader}> renderHeader={renderHeader}>
{showFiltersTab {showFiltersTab
? ({headerHeight, isFocused, scrollElRef}) => ( ? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileContentFiltersSection <ProfileLabelsSection
// ref={moderationSectionRef} // ref={moderationSectionRef}
modServiceQuery={modServiceQuery} labelerInfo={labelerInfo}
labelerError={labelerError}
isLabelerLoading={isLabelerLoading}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
scrollElRef={scrollElRef as ListRef} scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight} headerOffset={headerHeight}