mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Resolve facets on list descriptions (#2485)

* feat: add strict/loose link mapping

* feat: resolve facets on list description

authored by

Mary and committed by
GitHub
abac959d 1828bc97

+110 -24
+2 -2
src/lib/strings/rich-text-helpers.ts
··· 1 1 import {AppBskyRichtextFacet, RichText} from '@atproto/api' 2 2 import {linkRequiresWarning} from './url-helpers' 3 3 4 - export function richTextToString(rt: RichText): string { 4 + export function richTextToString(rt: RichText, loose: boolean): string { 5 5 const {text, facets} = rt 6 6 7 7 if (!facets?.length) { ··· 19 19 20 20 const requiresWarning = linkRequiresWarning(href, text) 21 21 22 - result += !requiresWarning ? href : `[${text}](${href})` 22 + result += !requiresWarning ? href : loose ? `[${text}](${href})` : text 23 23 } else { 24 24 result += segment.text 25 25 }
+13 -2
src/state/queries/list.ts
··· 3 3 AppBskyGraphGetList, 4 4 AppBskyGraphList, 5 5 AppBskyGraphDefs, 6 + Facet, 6 7 } from '@atproto/api' 7 8 import {Image as RNImage} from 'react-native-image-crop-picker' 8 9 import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' ··· 38 39 purpose: string 39 40 name: string 40 41 description: string 42 + descriptionFacets: Facet[] | undefined 41 43 avatar: RNImage | null | undefined 42 44 } 43 45 export function useListCreateMutation() { ··· 45 47 const queryClient = useQueryClient() 46 48 return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>( 47 49 { 48 - async mutationFn({purpose, name, description, avatar}) { 50 + async mutationFn({ 51 + purpose, 52 + name, 53 + description, 54 + descriptionFacets, 55 + avatar, 56 + }) { 49 57 if (!currentAccount) { 50 58 throw new Error('Not logged in') 51 59 } ··· 59 67 purpose, 60 68 name, 61 69 description, 70 + descriptionFacets, 62 71 avatar: undefined, 63 72 createdAt: new Date().toISOString(), 64 73 } ··· 93 102 uri: string 94 103 name: string 95 104 description: string 105 + descriptionFacets: Facet[] | undefined 96 106 avatar: RNImage | null | undefined 97 107 } 98 108 export function useListMetadataMutation() { ··· 103 113 Error, 104 114 ListMetadataMutateParams 105 115 >({ 106 - async mutationFn({uri, name, description, avatar}) { 116 + async mutationFn({uri, name, description, descriptionFacets, avatar}) { 107 117 const {hostname, rkey} = new AtUri(uri) 108 118 if (!currentAccount) { 109 119 throw new Error('Not logged in') ··· 121 131 // update the fields 122 132 record.name = name 123 133 record.description = description 134 + record.descriptionFacets = descriptionFacets 124 135 if (avatar) { 125 136 const blobRes = await uploadBlob(getAgent(), avatar.path, avatar.mime) 126 137 record.avatar = blobRes.data.blob
+94 -19
src/view/com/modals/CreateOrEditList.tsx
··· 8 8 TouchableOpacity, 9 9 View, 10 10 } from 'react-native' 11 - import {AppBskyGraphDefs} from '@atproto/api' 11 + import { 12 + AppBskyGraphDefs, 13 + AppBskyRichtextFacet, 14 + RichText as RichTextAPI, 15 + } from '@atproto/api' 12 16 import LinearGradient from 'react-native-linear-gradient' 13 17 import {Image as RNImage} from 'react-native-image-crop-picker' 14 18 import {Text} from '../util/text/Text' ··· 30 34 useListCreateMutation, 31 35 useListMetadataMutation, 32 36 } from '#/state/queries/list' 37 + import {richTextToString} from '#/lib/strings/rich-text-helpers' 38 + import {shortenLinks} from '#/lib/strings/rich-text-manip' 39 + import {getAgent} from '#/state/session' 33 40 34 41 const MAX_NAME = 64 // todo 35 42 const MAX_DESCRIPTION = 300 // todo ··· 68 75 69 76 const [isProcessing, setProcessing] = useState<boolean>(false) 70 77 const [name, setName] = useState<string>(list?.name || '') 71 - const [description, setDescription] = useState<string>( 72 - list?.description || '', 73 - ) 78 + 79 + const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => { 80 + const text = list?.description 81 + const facets = list?.descriptionFacets 82 + 83 + if (!text || !facets) { 84 + return new RichTextAPI({text: text || ''}) 85 + } 86 + 87 + // We want to be working with a blank state here, so let's get the 88 + // serialized version and turn it back into a RichText 89 + const serialized = richTextToString(new RichTextAPI({text, facets}), false) 90 + 91 + const richText = new RichTextAPI({text: serialized}) 92 + richText.detectFacetsWithoutResolution() 93 + 94 + return richText 95 + }) 96 + const graphemeLength = useMemo(() => { 97 + return shortenLinks(descriptionRt).graphemeLength 98 + }, [descriptionRt]) 99 + const isDescriptionOver = graphemeLength > MAX_DESCRIPTION 100 + 74 101 const [avatar, setAvatar] = useState<string | undefined>(list?.avatar) 75 102 const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() 103 + 104 + const onDescriptionChange = useCallback( 105 + (newText: string) => { 106 + const richText = new RichTextAPI({text: newText}) 107 + richText.detectFacetsWithoutResolution() 108 + 109 + setDescriptionRt(richText) 110 + }, 111 + [setDescriptionRt], 112 + ) 76 113 77 114 const onPressCancel = useCallback(() => { 78 115 closeModal() ··· 113 150 setError('') 114 151 } 115 152 try { 153 + let richText = new RichTextAPI( 154 + {text: descriptionRt.text.trimEnd()}, 155 + {cleanNewlines: true}, 156 + ) 157 + 158 + await richText.detectFacets(getAgent()) 159 + richText = shortenLinks(richText) 160 + 161 + // filter out any mention facets that didn't map to a user 162 + richText.facets = richText.facets?.filter(facet => { 163 + const mention = facet.features.find(feature => 164 + AppBskyRichtextFacet.isMention(feature), 165 + ) 166 + if (mention && !mention.did) { 167 + return false 168 + } 169 + return true 170 + }) 171 + 116 172 if (list) { 117 173 await listMetadataMutation.mutateAsync({ 118 174 uri: list.uri, 119 175 name: nameTrimmed, 120 - description: description.trim(), 176 + description: richText.text, 177 + descriptionFacets: richText.facets, 121 178 avatar: newAvatar, 122 179 }) 123 180 Toast.show( ··· 130 187 const res = await listCreateMutation.mutateAsync({ 131 188 purpose: activePurpose, 132 189 name, 133 - description, 190 + description: richText.text, 191 + descriptionFacets: richText.facets, 134 192 avatar: newAvatar, 135 193 }) 136 194 Toast.show( ··· 163 221 activePurpose, 164 222 isCurateList, 165 223 name, 166 - description, 224 + descriptionRt, 167 225 newAvatar, 168 226 list, 169 227 listMetadataMutation, ··· 212 270 </View> 213 271 <View style={styles.form}> 214 272 <View> 215 - <Text style={[styles.label, pal.text]} nativeID="list-name"> 216 - <Trans>List Name</Trans> 217 - </Text> 273 + <View style={styles.labelWrapper}> 274 + <Text style={[styles.label, pal.text]} nativeID="list-name"> 275 + <Trans>List Name</Trans> 276 + </Text> 277 + </View> 218 278 <TextInput 219 279 testID="editNameInput" 220 280 style={[styles.textInput, pal.border, pal.text]} ··· 233 293 /> 234 294 </View> 235 295 <View style={s.pb10}> 236 - <Text style={[styles.label, pal.text]} nativeID="list-description"> 237 - <Trans>Description</Trans> 238 - </Text> 296 + <View style={styles.labelWrapper}> 297 + <Text 298 + style={[styles.label, pal.text]} 299 + nativeID="list-description"> 300 + <Trans>Description</Trans> 301 + </Text> 302 + <Text 303 + style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}> 304 + {graphemeLength}/{MAX_DESCRIPTION} 305 + </Text> 306 + </View> 239 307 <TextInput 240 308 testID="editDescriptionInput" 241 309 style={[styles.textArea, pal.border, pal.text]} ··· 247 315 placeholderTextColor={colors.gray4} 248 316 keyboardAppearance={theme.colorScheme} 249 317 multiline 250 - value={description} 251 - onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} 318 + value={descriptionRt.text} 319 + onChangeText={onDescriptionChange} 252 320 accessible={true} 253 321 accessibilityLabel={_(msg`Description`)} 254 322 accessibilityHint="" ··· 262 330 ) : ( 263 331 <TouchableOpacity 264 332 testID="saveBtn" 265 - style={s.mt10} 333 + style={[s.mt10, isDescriptionOver && s.dimmed]} 334 + disabled={isDescriptionOver} 266 335 onPress={onPressSave} 267 336 accessibilityRole="button" 268 337 accessibilityLabel={_(msg`Save`)} ··· 271 340 colors={[gradients.blueLight.start, gradients.blueLight.end]} 272 341 start={{x: 0, y: 0}} 273 342 end={{x: 1, y: 1}} 274 - style={[styles.btn]}> 343 + style={styles.btn}> 275 344 <Text style={[s.white, s.bold]}> 276 345 <Trans context="action">Save</Trans> 277 346 </Text> ··· 305 374 fontSize: 24, 306 375 marginBottom: 18, 307 376 }, 308 - label: { 309 - fontWeight: 'bold', 377 + labelWrapper: { 378 + flexDirection: 'row', 379 + gap: 8, 380 + alignItems: 'center', 381 + justifyContent: 'space-between', 310 382 paddingHorizontal: 4, 311 383 paddingBottom: 4, 312 384 marginTop: 20, 385 + }, 386 + label: { 387 + fontWeight: 'bold', 313 388 }, 314 389 form: { 315 390 paddingHorizontal: 6,
+1 -1
src/view/com/util/forms/PostDropdownBtn.tsx
··· 104 104 }, [rootUri, toggleThreadMute, _]) 105 105 106 106 const onCopyPostText = React.useCallback(() => { 107 - const str = richTextToString(richText) 107 + const str = richTextToString(richText, true) 108 108 109 109 Clipboard.setString(str) 110 110 Toast.show(_(msg`Copied to clipboard`))