diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 872f68640..aa8b9571e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -217,7 +217,11 @@ export default { isReplyButtonDisabled() { if (this.isEditorDisabled) return true; if (this.isATwitterInbox) return true; - if (this.hasAttachments || this.hasRecordedAudio) return false; + // Note: only `hasAttachments` (attachedFiles populated) enables send. + // `hasRecordedAudio` becomes true the moment recording stops, but the + // file upload that follows is async — sending in that window produced + // empty messages that Meta rejected with "text.body is required". + if (this.hasAttachments) return false; return ( this.isMessageEmpty || @@ -764,6 +768,13 @@ export default { if (this.isReplyButtonDisabled) { return; } + // Defensive: never send a payload that has neither content nor a file. + // The send button's disabled state already covers this, but guard here + // too so any other entry point (hotkey, programmatic) cannot create the + // empty Message rows that Meta rejects with "text.body is required". + if (this.isMessageEmpty && !this.hasAttachments) { + return; + } if (!this.showMentions) { const copilotAcceptedMessage = this.getCopilotAcceptedMessage(); const isOnWhatsApp =