about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/strings/rich-text-helpers.ts4
-rw-r--r--src/state/queries/list.ts15
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx113
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx2
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`))