[🐴] send record via link in text (Record DMs - base PR) (#4227)

* send record via link in text

* re-trim text after removing link
zio/stable
Samuel Newman 2024-05-31 18:41:06 +03:00 committed by GitHub
parent 455937dd0f
commit 8eb3cebb36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 51 deletions

View File

@ -4,7 +4,6 @@ import {
AppBskyEmbedRecord, AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AppBskyRichtextFacet,
BskyAgent, BskyAgent,
ComAtprotoLabelDefs, ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob, ComAtprotoRepoUploadBlob,
@ -15,7 +14,7 @@ import {AtUri} from '@atproto/api'
import {logger} from '#/logger' import {logger} from '#/logger'
import {ThreadgateSetting} from '#/state/queries/threadgate' import {ThreadgateSetting} from '#/state/queries/threadgate'
import {isNetworkError} from 'lib/strings/errors' import {isNetworkError} from 'lib/strings/errors'
import {shortenLinks} from 'lib/strings/rich-text-manip' import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
import {isNative, isWeb} from 'platform/detection' import {isNative, isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image' import {ImageModel} from 'state/models/media/image'
import {LinkMeta} from '../link-meta/link-meta' import {LinkMeta} from '../link-meta/link-meta'
@ -81,17 +80,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
opts.onStateChange?.('Processing...') opts.onStateChange?.('Processing...')
await rt.detectFacets(agent) await rt.detectFacets(agent)
rt = shortenLinks(rt) rt = shortenLinks(rt)
rt = stripInvalidMentions(rt)
// filter out any mention facets that didn't map to a user
rt.facets = rt.facets?.filter(facet => {
const mention = facet.features.find(feature =>
AppBskyRichtextFacet.isMention(feature),
)
if (mention && !mention.did) {
return false
}
return true
})
// add quote embed if present // add quote embed if present
if (opts.quote) { if (opts.quote) {

View File

@ -1,4 +1,4 @@
import {RichText, UnicodeString} from '@atproto/api' import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
import {toShortUrl} from './url-helpers' import {toShortUrl} from './url-helpers'
@ -10,9 +10,7 @@ export function shortenLinks(rt: RichText): RichText {
// enumerate the link facets // enumerate the link facets
if (rt.facets) { if (rt.facets) {
for (const facet of rt.facets) { for (const facet of rt.facets) {
const isLink = !!facet.features.find( const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink)
f => f.$type === 'app.bsky.richtext.facet#link',
)
if (!isLink) { if (!isLink) {
continue continue
} }
@ -33,3 +31,21 @@ export function shortenLinks(rt: RichText): RichText {
} }
return rt return rt
} }
// filter out any mention facets that didn't map to a user
export function stripInvalidMentions(rt: RichText): RichText {
if (!rt.facets?.length) {
return rt
}
rt = rt.clone()
if (rt.facets) {
rt.facets = rt.facets?.filter(facet => {
const mention = facet.features.find(AppBskyRichtextFacet.isMention)
if (mention && !mention.did) {
return false
}
return true
})
}
return rt
}

View File

@ -63,7 +63,7 @@ export function MessageInput({
return return
} }
clearDraft() clearDraft()
onSendMessage(message.trimEnd()) onSendMessage(message)
playHaptic() playHaptic()
setMessage('') setMessage('')

View File

@ -43,7 +43,7 @@ export function MessageInput({
return return
} }
clearDraft() clearDraft()
onSendMessage(message.trimEnd()) onSendMessage(message)
setMessage('') setMessage('')
}, [message, onSendMessage, _, clearDraft]) }, [message, onSendMessage, _, clearDraft])

View File

@ -13,12 +13,16 @@ import {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
import {shortenLinks} from '#/lib/strings/rich-text-manip' import {getPostAsQuote} from '#/lib/link-meta/bsky'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {isConvoActive, useConvoActive} from '#/state/messages/convo' import {isConvoActive, useConvoActive} from '#/state/messages/convo'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {clamp} from 'lib/numbers' import {clamp} from 'lib/numbers'
import {ScrollProvider} from 'lib/ScrollContext' import {ScrollProvider} from 'lib/ScrollContext'
@ -80,6 +84,7 @@ export function MessagesList({
}) { }) {
const convoState = useConvoActive() const convoState = useConvoActive()
const agent = useAgent() const agent = useAgent()
const getPost = useGetPost()
const flatListRef = useAnimatedRef<FlatList>() const flatListRef = useAnimatedRef<FlatList>()
@ -264,21 +269,72 @@ export function MessagesList({
// -- Message sending // -- Message sending
const onSendMessage = useCallback( const onSendMessage = useCallback(
async (text: string) => { async (text: string) => {
let rt = new RichText({text}, {cleanNewlines: true}) let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
await rt.detectFacets(agent)
rt = shortenLinks(rt)
// filter out any mention facets that didn't map to a user // detect facets without resolution first - this is used to see if there's
rt.facets = rt.facets?.filter(facet => { // any post links in the text that we can embed. We do this first because
const mention = facet.features.find(feature => // we want to remove the post link from the text, re-trim, then detect facets
AppBskyRichtextFacet.isMention(feature), rt.detectFacetsWithoutResolution()
)
if (mention && !mention.did) { let embed: AppBskyEmbedRecord.Main | undefined
// find the first link facet that is a link to a post
const postLinkFacet = rt.facets?.find(facet => {
return facet.features.find(feature => {
if (AppBskyRichtextFacet.isLink(feature)) {
return isBskyPostUrl(feature.uri)
}
return false return false
} })
return true
}) })
// if we found a post link, get the post and embed it
if (postLinkFacet) {
const postLink = postLinkFacet.features.find(
AppBskyRichtextFacet.isLink,
)
if (!postLink) return
try {
const post = await getPostAsQuote(getPost, postLink.uri)
if (post) {
embed = {
$type: 'app.bsky.embed.record',
record: {
uri: post.uri,
cid: post.cid,
},
}
// remove the post link from the text
rt.delete(
postLinkFacet.index.byteStart,
postLinkFacet.index.byteEnd,
)
// re-trim the text, now that we've removed the post link
//
// if the post link is at the start of the text, we don't want to leave a leading space
// so trim on both sides
if (postLinkFacet.index.byteStart === 0) {
rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
} else {
// otherwise just trim the end
rt = new RichText(
{text: rt.text.trimEnd()},
{cleanNewlines: true},
)
}
}
} catch (error) {
logger.error('Failed to get post as quote for DM', {error})
}
}
await rt.detectFacets(agent)
rt = shortenLinks(rt)
rt = stripInvalidMentions(rt)
if (!hasScrolled) { if (!hasScrolled) {
setHasScrolled(true) setHasScrolled(true)
} }
@ -286,9 +342,10 @@ export function MessagesList({
convoState.sendMessage({ convoState.sendMessage({
text: rt.text, text: rt.text,
facets: rt.facets, facets: rt.facets,
embed,
}) })
}, },
[convoState, agent, hasScrolled, setHasScrolled], [agent, convoState, getPost, hasScrolled, setHasScrolled],
) )
// -- List layout changes (opening emoji keyboard, etc.) // -- List layout changes (opening emoji keyboard, etc.)

View File

@ -753,7 +753,7 @@ export class Convo {
sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
// Ignore empty messages for now since they have no other purpose atm // Ignore empty messages for now since they have no other purpose atm
if (!message.text.trim()) return if (!message.text.trim() && !message.embed) return
logger.debug('Convo: send message', {}, logger.DebugContext.convo) logger.debug('Convo: send message', {}, logger.DebugContext.convo)

View File

@ -10,16 +10,12 @@ import {
} from 'react-native' } from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import { import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
AppBskyGraphDefs,
AppBskyRichtextFacet,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks} from '#/lib/strings/rich-text-manip' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import { import {
useListCreateMutation, useListCreateMutation,
@ -159,17 +155,7 @@ export function Component({
await richText.detectFacets(agent) await richText.detectFacets(agent)
richText = shortenLinks(richText) richText = shortenLinks(richText)
richText = stripInvalidMentions(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) { if (list) {
await listMetadataMutation.mutateAsync({ await listMetadataMutation.mutateAsync({