Simplify useRichText hook to use synchronous facet detection

Replace async detectFacets with synchronous detectFacetsWithoutResolution,
eliminating the need for loading states, agent dependency, and complex
effect-based resolution. This simplifies the hook from ~30 lines to ~6 lines
and removes the duplicate local implementation from Profile.tsx.
This commit is contained in:
Claude
2026-01-11 12:51:12 +00:00
committed by Samuel Newman
parent ac939dc84e
commit 0dd634bda8
5 changed files with 14 additions and 68 deletions
@@ -430,7 +430,7 @@ function Inner({
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
) )
const [descriptionRT] = useRichText(profile.description ?? '') const descriptionRT = useRichText(profile.description ?? '')
const profileShadow = useProfileShadow(profile) const profileShadow = useProfileShadow(profile)
const {follow, unfollow} = useFollowMethods({ const {follow, unfollow} = useFollowMethods({
profile: profileShadow, profile: profileShadow,
+7 -31
View File
@@ -1,34 +1,10 @@
import React from 'react' import {useMemo} from 'react'
import {RichText as RichTextAPI} from '@atproto/api' import {RichText as RichTextAPI} from '@atproto/api'
import {useAgent} from '#/state/session' export function useRichText(text: string): RichTextAPI {
return useMemo(() => {
export function useRichText(text: string): [RichTextAPI, boolean] { const rt = new RichTextAPI({text})
const [prevText, setPrevText] = React.useState(text) rt.detectFacetsWithoutResolution()
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text})) return rt
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null) }, [text])
const agent = useAgent()
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
setResolvedRT(null)
// This will queue an immediate re-render
}
React.useEffect(() => {
let ignore = false
async function resolveRTFacets() {
// new each time
const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(agent)
if (!ignore) {
setResolvedRT(resolvedRT)
}
}
resolveRTFacets()
return () => {
ignore = true
}
}, [text, agent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
} }
@@ -394,7 +394,7 @@ function DialogInner({
const playHaptic = useHaptics() const playHaptic = useHaptics()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const reportDialogControl = useReportDialogControl() const reportDialogControl = useReportDialogControl()
const [rt] = useRichText(info.description.text) const rt = useRichText(info.description.text)
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
useUnlikeMutation() useUnlikeMutation()
@@ -125,7 +125,7 @@ function LandingScreenLoaded({
const setActiveStarterPack = useSetActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const androidDialogControl = useDialogControl() const androidDialogControl = useDialogControl()
const [descriptionRt] = useRichText(record.description || '') const descriptionRt = useRichText(record.description || '')
const [appClipOverlayVisible, setAppClipOverlayVisible] = const [appClipOverlayVisible, setAppClipOverlayVisible] =
React.useState(false) React.useState(false)
+4 -34
View File
@@ -5,7 +5,6 @@ import {
type AppBskyActorDefs, type AppBskyActorDefs,
moderateProfile, moderateProfile,
type ModerationOpts, type ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -32,7 +31,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useAgent, useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
import {ProfileLists} from '#/view/com/lists/ProfileLists' import {ProfileLists} from '#/view/com/lists/ProfileLists'
@@ -44,6 +43,7 @@ import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {useRichText} from '#/components/hooks/useRichText'
import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as CircleAndSquareIcon} from '#/components/icons/CircleAndSquare' import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as CircleAndSquareIcon} from '#/components/icons/CircleAndSquare'
import {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' import {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2'
import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
@@ -205,8 +205,8 @@ function ProfileScreenLoaded({
const description = profile.description ?? '' const description = profile.description ?? ''
const hasDescription = description !== '' const hasDescription = description !== ''
const [descriptionRT, isResolvingDescriptionRT] = useRichText(description) const descriptionRT = useRichText(description)
const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT const showPlaceholder = isPlaceholderProfile
const moderation = useMemo( const moderation = useMemo(
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
@@ -599,36 +599,6 @@ function ProfileScreenLoaded({
) )
} }
function useRichText(text: string): [RichTextAPI, boolean] {
const agent = useAgent()
const [prevText, setPrevText] = useState(text)
const [rawRT, setRawRT] = useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null)
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
setResolvedRT(null)
// This will queue an immediate re-render
}
useEffect(() => {
let ignore = false
async function resolveRTFacets() {
// new each time
const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(agent)
if (!ignore) {
setResolvedRT(resolvedRT)
}
}
void resolveRTFacets()
return () => {
ignore = true
}
}, [text, agent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'column', flexDirection: 'column',