fix dm embeds being dropped when sending
embeds (quoted posts, invite links) were sometimes not attached to sent messages. the composer clears the embed state synchronously, but the actual send is deferred (a frame on native, a 20ms timeout on ios). by the time the deferred send ran, onSendMessage had been re-created with the now-cleared messageEmbed, so the embed was lost. thread the embed through as an argument captured at submit time, the same way replyTo already is, so an intermediate render can't drop it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user