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),
[profile, moderationOpts],
)
const [descriptionRT] = useRichText(profile.description ?? '')
const descriptionRT = useRichText(profile.description ?? '')
const profileShadow = useProfileShadow(profile)
const {follow, unfollow} = useFollowMethods({
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 {useAgent} from '#/state/session'
export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
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]
export function useRichText(text: string): RichTextAPI {
return useMemo(() => {
const rt = new RichTextAPI({text})
rt.detectFacetsWithoutResolution()
return rt
}, [text])
}
@@ -394,7 +394,7 @@ function DialogInner({
const playHaptic = useHaptics()
const control = Dialog.useDialogContext()
const reportDialogControl = useReportDialogControl()
const [rt] = useRichText(info.description.text)
const rt = useRichText(info.description.text)
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
useUnlikeMutation()
@@ -125,7 +125,7 @@ function LandingScreenLoaded({
const setActiveStarterPack = useSetActiveStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries()
const androidDialogControl = useDialogControl()
const [descriptionRt] = useRichText(record.description || '')
const descriptionRt = useRichText(record.description || '')
const [appClipOverlayVisible, setAppClipOverlayVisible] =
React.useState(false)
+4 -34
View File
@@ -5,7 +5,6 @@ import {
type AppBskyActorDefs,
moderateProfile,
type ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -32,7 +31,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useAgent, useSession} from '#/state/session'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
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 {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
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 {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2'
import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
@@ -205,8 +205,8 @@ function ProfileScreenLoaded({
const description = profile.description ?? ''
const hasDescription = description !== ''
const [descriptionRT, isResolvingDescriptionRT] = useRichText(description)
const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT
const descriptionRT = useRichText(description)
const showPlaceholder = isPlaceholderProfile
const moderation = useMemo(
() => moderateProfile(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({
container: {
flexDirection: 'column',