diff options
-rw-r--r-- | src/lib/strings/rich-text-helpers.ts | 4 | ||||
-rw-r--r-- | src/state/queries/list.ts | 15 | ||||
-rw-r--r-- | src/view/com/modals/CreateOrEditList.tsx | 113 | ||||
-rw-r--r-- | 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 08971ca03..662004599 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 013a69076..845658a27 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 77a1debec..0e11fcffd 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<boolean>(false) const [name, setName] = useState<string>(list?.name || '') - const [description, setDescription] = useState<string>( - list?.description || '', - ) + + const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => { + 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<string | undefined>(list?.avatar) const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() + 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({ </View> <View style={styles.form}> <View> - <Text style={[styles.label, pal.text]} nativeID="list-name"> - <Trans>List Name</Trans> - </Text> + <View style={styles.labelWrapper}> + <Text style={[styles.label, pal.text]} nativeID="list-name"> + <Trans>List Name</Trans> + </Text> + </View> <TextInput testID="editNameInput" style={[styles.textInput, pal.border, pal.text]} @@ -233,9 +293,17 @@ export function Component({ /> </View> <View style={s.pb10}> - <Text style={[styles.label, pal.text]} nativeID="list-description"> - <Trans>Description</Trans> - </Text> + <View style={styles.labelWrapper}> + <Text + style={[styles.label, pal.text]} + nativeID="list-description"> + <Trans>Description</Trans> + </Text> + <Text + style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}> + {graphemeLength}/{MAX_DESCRIPTION} + </Text> + </View> <TextInput testID="editDescriptionInput" style={[styles.textArea, pal.border, pal.text]} @@ -247,8 +315,8 @@ export function Component({ placeholderTextColor={colors.gray4} keyboardAppearance={theme.colorScheme} multiline - value={description} - onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} + value={descriptionRt.text} + onChangeText={onDescriptionChange} accessible={true} accessibilityLabel={_(msg`Description`)} accessibilityHint="" @@ -262,7 +330,8 @@ export function Component({ ) : ( <TouchableOpacity testID="saveBtn" - style={s.mt10} + style={[s.mt10, isDescriptionOver && s.dimmed]} + disabled={isDescriptionOver} onPress={onPressSave} accessibilityRole="button" accessibilityLabel={_(msg`Save`)} @@ -271,7 +340,7 @@ export function Component({ colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} end={{x: 1, y: 1}} - style={[styles.btn]}> + style={styles.btn}> <Text style={[s.white, s.bold]}> <Trans context="action">Save</Trans> </Text> @@ -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 8e31c9e63..b21caf2e7 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`)) |