diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index c04c047c4a..669a5dbcc4 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -1,7 +1,5 @@ -import React from 'react' -import {findNodeHandle, View} from 'react-native' -import type Animated from 'react-native-reanimated' -import {useSafeAreaFrame} from 'react-native-safe-area-context' +import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react' +import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native' import { type AppBskyLabelerDefs, type InterpretedLabelValueDefinition, @@ -11,15 +9,13 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' -import {useScrollHandlers} from '#/lib/ScrollContext' import {isIOS, isNative} from '#/platform/detection' -import {type ListRef} from '#/view/com/util/List' -import {atoms as a, useTheme} from '#/alf' +import {List, type ListRef} from '#/view/com/util/List' +import {atoms as a, ios, tokens, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import * as Layout from '#/components/Layout' +import {ListFooter} from '#/components/Lists' import {Loader} from '#/components/Loader' import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' import {Text} from '#/components/Typography' @@ -27,6 +23,7 @@ import {ErrorState} from '../ErrorState' import {type SectionRef} from './types' interface LabelsSectionProps { + ref: React.Ref isLabelerLoading: boolean labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined labelerError: Error | null @@ -36,50 +33,21 @@ interface LabelsSectionProps { isFocused: boolean setScrollViewTag: (tag: number | null) => void } -export const ProfileLabelsSection = React.forwardRef< - SectionRef, - LabelsSectionProps ->(function LabelsSectionImpl( - { - isLabelerLoading, - labelerInfo, - labelerError, - moderationOpts, - scrollElRef, - headerHeight, - isFocused, - setScrollViewTag, - }, +export function ProfileLabelsSection({ ref, -) { - const {_} = useLingui() - const {height: minHeight} = useSafeAreaFrame() + isLabelerLoading, + labelerInfo, + labelerError, + moderationOpts, + scrollElRef, + headerHeight, + isFocused, + setScrollViewTag, +}: LabelsSectionProps) { + const t = useTheme() - // Intentionally destructured outside the main thread closure. - // See https://github.com/bluesky-social/social-app/pull/4108. - const { - onBeginDrag: onBeginDragFromContext, - onEndDrag: onEndDragFromContext, - onScroll: onScrollFromContext, - onMomentumEnd: onMomentumEndFromContext, - } = useScrollHandlers() - const scrollHandler = useAnimatedScrollHandler({ - onBeginDrag(e, ctx) { - onBeginDragFromContext?.(e, ctx) - }, - onEndDrag(e, ctx) { - onEndDragFromContext?.(e, ctx) - }, - onScroll(e, ctx) { - onScrollFromContext?.(e, ctx) - }, - onMomentumEnd(e, ctx) { - onMomentumEndFromContext?.(e, ctx) - }, - }) - - const onScrollToTop = React.useCallback(() => { - // @ts-ignore TODO fix this + const onScrollToTop = useCallback(() => { + // @ts-expect-error TODO fix this scrollElRef.current?.scrollTo({ animated: isNative, x: 0, @@ -87,143 +55,184 @@ export const ProfileLabelsSection = React.forwardRef< }) }, [scrollElRef, headerHeight]) - React.useImperativeHandle(ref, () => ({ + useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, })) - React.useEffect(() => { + useEffect(() => { if (isIOS && isFocused && scrollElRef.current) { const nativeTag = findNodeHandle(scrollElRef.current) setScrollViewTag(nativeTag) } }, [isFocused, scrollElRef, setScrollViewTag]) - return ( - - } - scrollEventThrottle={1} - contentContainerStyle={{ - paddingTop: headerHeight, - borderWidth: 0, - }} - contentOffset={{x: 0, y: headerHeight * -1}} - onScroll={scrollHandler}> - {isLabelerLoading ? ( - - - - ) : labelerError || !labelerInfo ? ( - - - - ) : ( - - )} - - - ) -}) + const isSubscribed = labelerInfo + ? !!isLabelerSubscribed(labelerInfo, moderationOpts) + : false -export function ProfileLabelsSectionInner({ - moderationOpts, - labelerInfo, -}: { - moderationOpts: ModerationOpts - labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed -}) { - const t = useTheme() - - const {labelValues} = labelerInfo.policies - const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts) - const labelDefs = React.useMemo(() => { + const labelValues = useMemo(() => { + if (isLabelerLoading || !labelerInfo || labelerError) return [] const customDefs = interpretLabelValueDefinitions(labelerInfo) - return labelValues + return labelerInfo.policies.labelValues + .filter((val, i, arr) => arr.indexOf(val) === i) // dedupe .map(val => lookupLabelValueDefinition(val, customDefs)) .filter( def => def && def?.configurable, ) as InterpretedLabelValueDefinition[] - }, [labelerInfo, labelValues]) + }, [labelerInfo, labelerError, isLabelerLoading]) - return ( - - - - - Labels are annotations on users and content. They can be used to - hide, warn, and categorize the network. - - - {labelerInfo.creator.viewer?.blocking ? ( - - - - - Blocking does not prevent this labeler from placing labels on - your account. - - - - ) : null} - {labelValues.length === 0 ? ( - - - This labeler hasn't declared what labels it publishes, and may not - be active. - - - ) : !isSubscribed ? ( - - - Subscribe to @{labelerInfo.creator.handle} to use these labels: - - - ) : null} - - {labelDefs.length > 0 && ( + const numItems = labelValues.length + + const renderItem = useCallback( + ({item, index}: ListRenderItemInfo) => { + if (!labelerInfo) return null + return ( - {labelDefs.map((labelDef, i) => { - return ( - - {i !== 0 && } - - - ) - })} + {index !== 0 && } + - )} + ) + }, + [labelerInfo, isSubscribed, numItems, t], + ) + + return ( + + + } + ListFooterComponent={ + + } + /> + + ) +} + +function keyExtractor(item: InterpretedLabelValueDefinition) { + return item.identifier +} + +export function LabelerListHeader({ + isLabelerLoading, + labelerError, + labelerInfo, + hasValues, + isSubscribed, +}: { + isLabelerLoading: boolean + labelerError?: Error | null + labelerInfo?: AppBskyLabelerDefs.LabelerViewDetailed + hasValues: boolean + isSubscribed: boolean +}) { + const t = useTheme() + const {_} = useLingui() + + if (isLabelerLoading) { + return ( + + + + ) + } + + if (labelerError || !labelerInfo) { + return ( + + + + ) + } + + return ( + + + + Labels are annotations on users and content. They can be used to hide, + warn, and categorize the network. + + + {labelerInfo?.creator.viewer?.blocking ? ( + + + + + Blocking does not prevent this labeler from placing labels on your + account. + + + + ) : null} + {!hasValues ? ( + + + This labeler hasn't declared what labels it publishes, and may not + be active. + + + ) : !isSubscribed ? ( + + + Subscribe to @{labelerInfo.creator.handle} to use these labels: + + + ) : null} ) } diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index 53e923a857..4eddb27f4e 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -1,4 +1,4 @@ -import {AppBskyLabelerDefs} from '@atproto/api' +import {type AppBskyLabelerDefs} from '@atproto/api' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {z} from 'zod' @@ -41,7 +41,7 @@ export function useLabelerInfoQuery({ queryKey: labelerInfoQueryKey(did as string), queryFn: async () => { const res = await agent.app.bsky.labeler.getServices({ - dids: [did as string], + dids: [did!], detailed: true, }) return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed