Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions backend/src/routes/broadcasts.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function enqueueBroadcastRecipient({ broadcast, template, account, recipie
header_text: template.header_text || null,
// Stable pointer to the header image so the Chats bubble renders the
// real picture instead of a grey "Image header" placeholder.
header_media_library_id: broadcast.media_library_id || null,
header_media_library_id: broadcast.media_library_id || (template && template.header_media_library_id) || null,
footer: template.footer || null,
buttons: Array.isArray(template.buttons) ? template.buttons : (template.buttons || []),
},
Expand Down Expand Up @@ -425,7 +425,8 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re
try {
const { rows: bRows } = await pool.query(
`SELECT b.*, t.id AS t_id, t.name AS t_name, t.language AS t_language, t.body AS t_body,
t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples
t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples,
t.header_media_library_id AS t_header_media_library_id
FROM coexistence.broadcasts b
LEFT JOIN coexistence.message_templates t ON t.id = b.template_id
WHERE b.id = $1`,
Expand All @@ -435,7 +436,8 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re
const broadcast = bRows[0];
const template = broadcast.message_type === 'template'
? { id: broadcast.t_id, name: broadcast.t_name, language: broadcast.t_language, body: broadcast.t_body,
header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples }
header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples,
header_media_library_id: broadcast.t_header_media_library_id }
: null;

const { account, error } = await resolveAccount({ fromPhoneNumber: broadcast.from_number });
Expand All @@ -458,11 +460,17 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re
// broadcast.media_library_id.
const _tplHt = template ? String(template.header_type || '').toUpperCase() : '';
const _needsHeaderMedia = broadcast.message_type === 'template' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(_tplHt);
if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && broadcast.media_library_id) {
// A media-header template carries its own image in header_media_library_id.
// Fall back to it when the broadcast has no explicitly-attached media, so a
// template broadcast resolves the header exactly like a template test-send
// instead of failing at Meta with #131008 (missing required header param).
const mediaSourceId = broadcast.media_library_id
|| (_needsHeaderMedia && template ? template.header_media_library_id : null);
if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && mediaSourceId) {
const { syncMediaToAccount } = require('./mediaLibrary');
const { rows: mRows } = await pool.query(
`SELECT * FROM coexistence.media_library WHERE id = $1 AND deleted_at IS NULL`,
[broadcast.media_library_id]
[mediaSourceId]
);
if (mRows.length) {
const media = mRows[0];
Expand Down Expand Up @@ -538,7 +546,8 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re

const { rows: bRows } = await pool.query(
`SELECT b.*, t.id AS t_id, t.name AS t_name, t.language AS t_language, t.body AS t_body,
t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples
t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples,
t.header_media_library_id AS t_header_media_library_id
FROM coexistence.broadcasts b
LEFT JOIN coexistence.message_templates t ON t.id = b.template_id
WHERE b.id = $1`,
Expand All @@ -548,7 +557,8 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re
const broadcast = bRows[0];
const template = broadcast.message_type === 'template'
? { id: broadcast.t_id, name: broadcast.t_name, language: broadcast.t_language, body: broadcast.t_body,
header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples }
header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples,
header_media_library_id: broadcast.t_header_media_library_id }
: null;

const { account, error } = await resolveAccount({ fromPhoneNumber: broadcast.from_number });
Expand All @@ -561,11 +571,17 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re
// broadcast.media_library_id.
const _tplHt = template ? String(template.header_type || '').toUpperCase() : '';
const _needsHeaderMedia = broadcast.message_type === 'template' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(_tplHt);
if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && broadcast.media_library_id) {
// A media-header template carries its own image in header_media_library_id.
// Fall back to it when the broadcast has no explicitly-attached media, so a
// template broadcast resolves the header exactly like a template test-send
// instead of failing at Meta with #131008 (missing required header param).
const mediaSourceId = broadcast.media_library_id
|| (_needsHeaderMedia && template ? template.header_media_library_id : null);
if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && mediaSourceId) {
const { syncMediaToAccount } = require('./mediaLibrary');
const { rows: mRows } = await pool.query(
`SELECT * FROM coexistence.media_library WHERE id = $1 AND deleted_at IS NULL`,
[broadcast.media_library_id]
[mediaSourceId]
);
if (mRows.length) {
const media = mRows[0];
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/pages/BulkMessagePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,18 @@ export default function BulkMessagePage({ onNavigate }) {
.then(res => {
const filtered = (res.media || []).filter(m => m.mediaType === neededType);
setNewBroadcastMediaItems(filtered);
// For a media-header template, auto-prefill the picker with the image the
// template was built with (its header_media_library_id), if it still
// exists in the library — so the user doesn't have to re-select it and a
// template broadcast works just like a template test-send. They can still
// override the choice.
if (newBroadcastMessageType === 'template') {
const tpl = templates.find(t => t.id.toString() === newBroadcastTemplateId);
const savedId = tpl?.header_media_library_id;
if (savedId && filtered.some(m => String(m.id) === String(savedId))) {
setNewBroadcastMediaLibraryId(prev => prev || String(savedId));
}
}
})
.catch(() => setNewBroadcastMediaItems([]))
.finally(() => setNewBroadcastMediaLoading(false));
Expand Down