Resolve facets on list descriptions (#2485)
* feat: add strict/loose link mapping * feat: resolve facets on list descriptionzio/stable
parent
1828bc9755
commit
abac959d03
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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`))
|
||||
|
|
Loading…
Reference in New Issue