From abac959d03c5a35021744180a16ece1d4143311b Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Wed, 24 Jan 2024 01:39:13 +0700 Subject: [PATCH] Resolve facets on list descriptions (#2485) * feat: add strict/loose link mapping * feat: resolve facets on list description --- src/lib/strings/rich-text-helpers.ts | 4 +- src/state/queries/list.ts | 15 ++- src/view/com/modals/CreateOrEditList.tsx | 113 ++++++++++++++++---- src/view/com/util/forms/PostDropdownBtn.tsx | 2 +- 4 files changed, 110 insertions(+), 24 deletions(-) diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts index 08971ca0..66200459 100644 --- a/src/lib/strings/rich-text-helpers.ts +++ b/src/lib/strings/rich-text-helpers.ts @@ -1,7 +1,7 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {linkRequiresWarning} from './url-helpers' -export function richTextToString(rt: RichText): string { +export function richTextToString(rt: RichText, loose: boolean): string { const {text, facets} = rt if (!facets?.length) { @@ -19,7 +19,7 @@ export function richTextToString(rt: RichText): string { const requiresWarning = linkRequiresWarning(href, text) - result += !requiresWarning ? href : `[${text}](${href})` + result += !requiresWarning ? href : loose ? `[${text}](${href})` : text } else { result += segment.text } diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index 013a6907..845658a2 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -3,6 +3,7 @@ import { AppBskyGraphGetList, AppBskyGraphList, AppBskyGraphDefs, + Facet, } from '@atproto/api' import {Image as RNImage} from 'react-native-image-crop-picker' import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' @@ -38,6 +39,7 @@ export interface ListCreateMutateParams { purpose: string name: string description: string + descriptionFacets: Facet[] | undefined avatar: RNImage | null | undefined } export function useListCreateMutation() { @@ -45,7 +47,13 @@ export function useListCreateMutation() { const queryClient = useQueryClient() return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>( { - async mutationFn({purpose, name, description, avatar}) { + async mutationFn({ + purpose, + name, + description, + descriptionFacets, + avatar, + }) { if (!currentAccount) { throw new Error('Not logged in') } @@ -59,6 +67,7 @@ export function useListCreateMutation() { purpose, name, description, + descriptionFacets, avatar: undefined, createdAt: new Date().toISOString(), } @@ -93,6 +102,7 @@ export interface ListMetadataMutateParams { uri: string name: string description: string + descriptionFacets: Facet[] | undefined avatar: RNImage | null | undefined } export function useListMetadataMutation() { @@ -103,7 +113,7 @@ export function useListMetadataMutation() { Error, ListMetadataMutateParams >({ - async mutationFn({uri, name, description, avatar}) { + async mutationFn({uri, name, description, descriptionFacets, avatar}) { const {hostname, rkey} = new AtUri(uri) if (!currentAccount) { throw new Error('Not logged in') @@ -121,6 +131,7 @@ export function useListMetadataMutation() { // update the fields record.name = name record.description = description + record.descriptionFacets = descriptionFacets if (avatar) { const blobRes = await uploadBlob(getAgent(), avatar.path, avatar.mime) record.avatar = blobRes.data.blob diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index 77a1debe..0e11fcff 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -8,7 +8,11 @@ import { TouchableOpacity, View, } from 'react-native' -import {AppBskyGraphDefs} from '@atproto/api' +import { + AppBskyGraphDefs, + AppBskyRichtextFacet, + RichText as RichTextAPI, +} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' import {Image as RNImage} from 'react-native-image-crop-picker' import {Text} from '../util/text/Text' @@ -30,6 +34,9 @@ import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list' +import {richTextToString} from '#/lib/strings/rich-text-helpers' +import {shortenLinks} from '#/lib/strings/rich-text-manip' +import {getAgent} from '#/state/session' const MAX_NAME = 64 // todo const MAX_DESCRIPTION = 300 // todo @@ -68,12 +75,42 @@ export function Component({ const [isProcessing, setProcessing] = useState(false) const [name, setName] = useState(list?.name || '') - const [description, setDescription] = useState( - list?.description || '', - ) + + const [descriptionRt, setDescriptionRt] = useState(() => { + const text = list?.description + const facets = list?.descriptionFacets + + if (!text || !facets) { + return new RichTextAPI({text: text || ''}) + } + + // We want to be working with a blank state here, so let's get the + // serialized version and turn it back into a RichText + const serialized = richTextToString(new RichTextAPI({text, facets}), false) + + const richText = new RichTextAPI({text: serialized}) + richText.detectFacetsWithoutResolution() + + return richText + }) + const graphemeLength = useMemo(() => { + return shortenLinks(descriptionRt).graphemeLength + }, [descriptionRt]) + const isDescriptionOver = graphemeLength > MAX_DESCRIPTION + const [avatar, setAvatar] = useState(list?.avatar) const [newAvatar, setNewAvatar] = useState() + const onDescriptionChange = useCallback( + (newText: string) => { + const richText = new RichTextAPI({text: newText}) + richText.detectFacetsWithoutResolution() + + setDescriptionRt(richText) + }, + [setDescriptionRt], + ) + const onPressCancel = useCallback(() => { closeModal() }, [closeModal]) @@ -113,11 +150,31 @@ export function Component({ setError('') } try { + let richText = new RichTextAPI( + {text: descriptionRt.text.trimEnd()}, + {cleanNewlines: true}, + ) + + await richText.detectFacets(getAgent()) + richText = shortenLinks(richText) + + // filter out any mention facets that didn't map to a user + richText.facets = richText.facets?.filter(facet => { + const mention = facet.features.find(feature => + AppBskyRichtextFacet.isMention(feature), + ) + if (mention && !mention.did) { + return false + } + return true + }) + if (list) { await listMetadataMutation.mutateAsync({ uri: list.uri, name: nameTrimmed, - description: description.trim(), + description: richText.text, + descriptionFacets: richText.facets, avatar: newAvatar, }) Toast.show( @@ -130,7 +187,8 @@ export function Component({ const res = await listCreateMutation.mutateAsync({ purpose: activePurpose, name, - description, + description: richText.text, + descriptionFacets: richText.facets, avatar: newAvatar, }) Toast.show( @@ -163,7 +221,7 @@ export function Component({ activePurpose, isCurateList, name, - description, + descriptionRt, newAvatar, list, listMetadataMutation, @@ -212,9 +270,11 @@ export function Component({ - - List Name - + + + List Name + + - - Description - + + + Description + + + {graphemeLength}/{MAX_DESCRIPTION} + + setDescription(enforceLen(v, MAX_DESCRIPTION))} + value={descriptionRt.text} + onChangeText={onDescriptionChange} accessible={true} accessibilityLabel={_(msg`Description`)} accessibilityHint="" @@ -262,7 +330,8 @@ export function Component({ ) : ( + style={styles.btn}> Save @@ -305,12 +374,18 @@ const styles = StyleSheet.create({ fontSize: 24, marginBottom: 18, }, - label: { - fontWeight: 'bold', + labelWrapper: { + flexDirection: 'row', + gap: 8, + alignItems: 'center', + justifyContent: 'space-between', paddingHorizontal: 4, paddingBottom: 4, marginTop: 20, }, + label: { + fontWeight: 'bold', + }, form: { paddingHorizontal: 6, }, diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 8e31c9e6..b21caf2e 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -104,7 +104,7 @@ let PostDropdownBtn = ({ }, [rootUri, toggleThreadMute, _]) const onCopyPostText = React.useCallback(() => { - const str = richTextToString(richText) + const str = richTextToString(richText, true) Clipboard.setString(str) Toast.show(_(msg`Copied to clipboard`))