[🐴] send record via link in text (Record DMs - base PR) (#4227)
* send record via link in text * re-trim text after removing linkzio/stable
parent
455937dd0f
commit
8eb3cebb36
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ export function MessageInput({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearDraft()
|
clearDraft()
|
||||||
onSendMessage(message.trimEnd())
|
onSendMessage(message)
|
||||||
playHaptic()
|
playHaptic()
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function MessageInput({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearDraft()
|
clearDraft()
|
||||||
onSendMessage(message.trimEnd())
|
onSendMessage(message)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
}, [message, onSendMessage, _, clearDraft])
|
}, [message, onSendMessage, _, clearDraft])
|
||||||
|
|
||||||
|
|
|
@ -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.)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in New Issue