Fix DM embeds being dropped when sending (#10924)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-16 21:43:49 +03:00
committed by GitHub
parent 721965e67c
commit 8d4f59a146
4 changed files with 54 additions and 28 deletions
@@ -37,13 +37,14 @@ import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} fro
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
import {type MessageEmbedState} from './MessageInputEmbed'
const MIN_HEIGHT = 40
export function MessageComposer({
textInputId,
onSendMessage,
hasEmbed,
messageEmbed,
setEmbed,
children,
loading = false,
@@ -51,9 +52,10 @@ export function MessageComposer({
textInputId?: string
onSendMessage: (
message: string,
embed?: MessageEmbedState,
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
) => void
hasEmbed: boolean
messageEmbed: MessageEmbedState | undefined
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
@@ -89,14 +91,16 @@ export function MessageComposer({
},
})
const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0)
const submitDisabled =
!editable || (!messageEmbed && text.trim().length === 0)
const onSubmit = (
message: string,
embed: MessageEmbedState | undefined,
replyTo: ChatBskyConvoDefs.MessageView | null,
) => {
if (!editable) return
if (!hasEmbed && message.trim() === '') return
if (!embed && message.trim() === '') return
const graphemeCount = countGraphemes(message)
if (graphemeCount > MAX_DM_GRAPHEME_LENGTH) {
Toast.show(
@@ -120,6 +124,7 @@ export function MessageComposer({
requestAnimationFrame(() => {
onSendMessage(
message,
embed,
replyTo
? {
...replyTo,
@@ -152,18 +157,18 @@ export function MessageComposer({
setTimeout(() => {
if (isFlushingAutocorrectSuggestion.current) {
isFlushingAutocorrectSuggestion.current = false
onSubmit(text, replyTo)
onSubmit(text, messageEmbed, replyTo)
}
}, 20)
} else {
onSubmit(text, replyTo)
onSubmit(text, messageEmbed, replyTo)
}
}
const handleChange = (nextText: string) => {
if (IS_IOS && isFlushingAutocorrectSuggestion.current) {
isFlushingAutocorrectSuggestion.current = false
onSubmit(nextText, replyTo)
onSubmit(nextText, messageEmbed, replyTo)
} else {
setText(nextText)
}
@@ -34,7 +34,10 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
import {ComposerContainer} from './MessageComposer'
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
import {
type MessageEmbedState,
useExtractEmbedFromFacets,
} from './MessageInputEmbed'
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
@@ -43,7 +46,7 @@ const MIN_HEIGHT = 40
export function MessageInput({
textInputId,
onSendMessage,
hasEmbed,
messageEmbed,
setEmbed,
children,
loading = false,
@@ -51,9 +54,10 @@ export function MessageInput({
textInputId?: string
onSendMessage: (
message: string,
embed?: MessageEmbedState,
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
) => Promise<void> | void
hasEmbed: boolean
messageEmbed: MessageEmbedState | undefined
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
@@ -85,7 +89,7 @@ export function MessageInput({
if (!editable) {
return
}
if (!hasEmbed && message.trim() === '') {
if (!messageEmbed && message.trim() === '') {
return
}
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
@@ -96,6 +100,8 @@ export function MessageInput({
}
clearDraft()
playHaptic()
// Capture the embed before clearing - the deferred send below reads it.
const embed = messageEmbed
setEmbed(undefined)
setMessage('')
// Capture the reply before clearing - the deferred send below reads it.
@@ -115,6 +121,7 @@ export function MessageInput({
requestAnimationFrame(() => {
void onSendMessage(
message,
embed,
reply
? {...reply, $type: 'chat.bsky.convo.defs#messageView'}
: undefined,
@@ -122,7 +129,7 @@ export function MessageInput({
})
}, [
editable,
hasEmbed,
messageEmbed,
message,
clearDraft,
onSendMessage,
@@ -159,7 +166,8 @@ export function MessageInput({
scrollEnabled: isInputScrollable.get(),
}))
const submitDisabled = !editable || (!hasEmbed && message.trim().length === 0)
const submitDisabled =
!editable || (!messageEmbed && message.trim().length === 0)
const blur = useCallback(() => {
inputRef.current?.blur()
@@ -21,20 +21,24 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import * as Toast from '#/components/Toast'
import {IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE} from '#/env'
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
import {
type MessageEmbedState,
useExtractEmbedFromFacets,
} from './MessageInputEmbed'
export function MessageInput({
onSendMessage,
hasEmbed,
messageEmbed,
setEmbed,
children,
loading = false,
}: {
onSendMessage: (
message: string,
embed?: MessageEmbedState,
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
) => void
hasEmbed: boolean
messageEmbed: MessageEmbedState | undefined
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
@@ -54,7 +58,7 @@ export function MessageInput({
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const onSubmit = useCallback(() => {
if (!hasEmbed && message.trim() === '') {
if (!messageEmbed && message.trim() === '') {
return
}
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
@@ -66,6 +70,7 @@ export function MessageInput({
clearDraft()
onSendMessage(
message,
messageEmbed,
replyTo
? {...replyTo, $type: 'chat.bsky.convo.defs#messageView'}
: undefined,
@@ -78,7 +83,7 @@ export function MessageInput({
onSendMessage,
l,
clearDraft,
hasEmbed,
messageEmbed,
setEmbed,
replyTo,
clearReply,
@@ -378,7 +378,11 @@ export function MessagesList({
// -- Message sending
const onSendMessage = useCallback(
async (text: string, reply?: $Typed<ChatBskyConvoDefs.MessageView>) => {
async (
text: string,
embedState?: MessageEmbedState,
reply?: $Typed<ChatBskyConvoDefs.MessageView>,
) => {
let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
// detect facets without resolution first - this is used to see if there's
@@ -416,9 +420,9 @@ export function MessagesList({
}
}
if (messageEmbed?.type === 'post') {
if (embedState?.type === 'post') {
try {
const post = await getPost({uri: messageEmbed.uri})
const post = await getPost({uri: embedState.uri})
if (post) {
embed = {
$type: 'app.bsky.embed.record',
@@ -445,8 +449,8 @@ export function MessagesList({
} catch (error) {
logger.error('Failed to get post as quote for DM', {error})
}
} else if (messageEmbed?.type === 'invite') {
const code = messageEmbed.code
} else if (embedState?.type === 'invite') {
const code = embedState.code
embed = {
$type: 'chat.bsky.embed.joinLink',
code,
@@ -512,7 +516,6 @@ export function MessagesList({
[
agent,
convoState,
messageEmbed,
getPost,
getJoinLinkPreview,
hasSession,
@@ -737,6 +740,7 @@ function Composer({
textInputId: string
onSendMessage: (
message: string,
embed?: MessageEmbedState,
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
) => Promise<void>
messageEmbed: MessageEmbedState | undefined
@@ -745,8 +749,12 @@ function Composer({
useNewComposer: boolean
}) {
const handleSendMessage = useNonReactiveCallback(
(message: string, replyTo?: $Typed<ChatBskyConvoDefs.MessageView>) => {
void onSendMessage(message, replyTo)
(
message: string,
embed?: MessageEmbedState,
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
) => {
void onSendMessage(message, embed, replyTo)
},
)
@@ -761,7 +769,7 @@ function Composer({
<MessageComposer
textInputId={textInputId}
onSendMessage={handleSendMessage}
hasEmbed={!!messageEmbed}
messageEmbed={messageEmbed}
setEmbed={setEmbed}
loading={loading}>
{previews}
@@ -770,7 +778,7 @@ function Composer({
<MessageInput
textInputId={textInputId}
onSendMessage={handleSendMessage}
hasEmbed={!!messageEmbed}
messageEmbed={messageEmbed}
setEmbed={setEmbed}
loading={loading}>
{previews}