mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Merge pull request #14 from ArnasDon/fix/media-messages-pipeline
fix: media messages (image/video/audio/document/sticker) now render
This commit is contained in:
commit
03b40e0e8e
@ -27,6 +27,7 @@ interface WhatsAppMessage {
|
||||
video?: { id: string; mime_type: string; caption?: string }
|
||||
document?: { id: string; mime_type: string; filename?: string; caption?: string }
|
||||
audio?: { id: string; mime_type: string }
|
||||
sticker?: { id: string; mime_type: string }
|
||||
location?: { latitude: number; longitude: number; name?: string; address?: string }
|
||||
reaction?: { message_id: string; emoji: string }
|
||||
}
|
||||
@ -235,10 +236,24 @@ async function processMessage(
|
||||
// column; the MIME type is only used to construct the proxy URL during
|
||||
// parseMessageContent. Silence the unused-var warning:
|
||||
void mediaType
|
||||
|
||||
// The messages.content_type CHECK constraint only allows:
|
||||
// text, image, document, audio, video, location, template
|
||||
// Map incoming WhatsApp types that aren't in that list to the closest
|
||||
// allowed value so the INSERT doesn't fail with a constraint error.
|
||||
const ALLOWED_CONTENT_TYPES = new Set([
|
||||
'text', 'image', 'document', 'audio', 'video', 'location', 'template',
|
||||
])
|
||||
const contentType = ALLOWED_CONTENT_TYPES.has(message.type)
|
||||
? message.type
|
||||
: message.type === 'sticker'
|
||||
? 'image' // stickers are images
|
||||
: 'text' // reaction, unknown → text fallback
|
||||
|
||||
const { error: msgError } = await supabaseAdmin().from('messages').insert({
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'customer',
|
||||
content_type: message.type,
|
||||
content_type: contentType,
|
||||
content_text: contentText,
|
||||
media_url: mediaUrl,
|
||||
message_id: message.id,
|
||||
@ -275,6 +290,25 @@ async function parseMessageContent(
|
||||
mediaUrl: string | null
|
||||
mediaType: string | null
|
||||
}> {
|
||||
// getMediaUrl signature is (mediaId, accessToken) — earlier code had
|
||||
// the args swapped, so every verification hit an invalid Meta URL and
|
||||
// fell through to the catch block, leaving mediaUrl as null. That's
|
||||
// why images showed up as empty bubbles in the inbox.
|
||||
const verifyAndBuildUrl = async (
|
||||
mediaId: string
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
await getMediaUrl(mediaId, accessToken)
|
||||
return `/api/whatsapp/media/${mediaId}`
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to verify media ${mediaId} with Meta:`,
|
||||
error instanceof Error ? error.message : error
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'text':
|
||||
return {
|
||||
@ -284,61 +318,55 @@ async function parseMessageContent(
|
||||
}
|
||||
|
||||
case 'image':
|
||||
if (message.image) {
|
||||
try {
|
||||
await getMediaUrl(accessToken, message.image.id)
|
||||
if (message.image?.id) {
|
||||
return {
|
||||
contentText: message.image.caption || null,
|
||||
mediaUrl: `/api/whatsapp/media/${message.image.id}`,
|
||||
mediaUrl: await verifyAndBuildUrl(message.image.id),
|
||||
mediaType: message.image.mime_type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying image media:', error)
|
||||
}
|
||||
}
|
||||
return { contentText: message.image?.caption || null, mediaUrl: null, mediaType: null }
|
||||
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||
|
||||
case 'video':
|
||||
if (message.video) {
|
||||
try {
|
||||
await getMediaUrl(accessToken, message.video.id)
|
||||
if (message.video?.id) {
|
||||
return {
|
||||
contentText: message.video.caption || null,
|
||||
mediaUrl: `/api/whatsapp/media/${message.video.id}`,
|
||||
mediaUrl: await verifyAndBuildUrl(message.video.id),
|
||||
mediaType: message.video.mime_type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying video media:', error)
|
||||
}
|
||||
}
|
||||
return { contentText: message.video?.caption || null, mediaUrl: null, mediaType: null }
|
||||
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||
|
||||
case 'document':
|
||||
if (message.document) {
|
||||
try {
|
||||
await getMediaUrl(accessToken, message.document.id)
|
||||
if (message.document?.id) {
|
||||
return {
|
||||
contentText: message.document.caption || message.document.filename || null,
|
||||
mediaUrl: `/api/whatsapp/media/${message.document.id}`,
|
||||
contentText:
|
||||
message.document.caption || message.document.filename || null,
|
||||
mediaUrl: await verifyAndBuildUrl(message.document.id),
|
||||
mediaType: message.document.mime_type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying document media:', error)
|
||||
}
|
||||
}
|
||||
return { contentText: message.document?.filename || null, mediaUrl: null, mediaType: null }
|
||||
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||
|
||||
case 'audio':
|
||||
if (message.audio) {
|
||||
try {
|
||||
await getMediaUrl(accessToken, message.audio.id)
|
||||
if (message.audio?.id) {
|
||||
return {
|
||||
contentText: null,
|
||||
mediaUrl: `/api/whatsapp/media/${message.audio.id}`,
|
||||
mediaUrl: await verifyAndBuildUrl(message.audio.id),
|
||||
mediaType: message.audio.mime_type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying audio media:', error)
|
||||
}
|
||||
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||
|
||||
case 'sticker':
|
||||
// Stickers are images under the hood. Treat them as such so the
|
||||
// MessageBubble renders the <img>. The caller maps the DB
|
||||
// content_type to 'image' for the CHECK constraint.
|
||||
if (message.sticker?.id) {
|
||||
return {
|
||||
contentText: null,
|
||||
mediaUrl: await verifyAndBuildUrl(message.sticker.id),
|
||||
mediaType: message.sticker.mime_type,
|
||||
}
|
||||
}
|
||||
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||
|
||||
@ -36,6 +36,15 @@ function StatusIcon({ status }: { status: Message["status"] }) {
|
||||
}
|
||||
}
|
||||
|
||||
function MediaUnavailable({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-slate-700/40 px-3 py-2 text-xs text-slate-300">
|
||||
<ImageOff className="h-4 w-4 shrink-0 text-slate-500" />
|
||||
<span>{label} unavailable</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaImage({ url, alt }: { url: string; alt: string }) {
|
||||
const [src, setSrc] = useState<string | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
@ -111,8 +120,10 @@ function MessageContent({ message }: { message: Message }) {
|
||||
case "image":
|
||||
return (
|
||||
<div>
|
||||
{message.media_url && (
|
||||
{message.media_url ? (
|
||||
<MediaImage url={message.media_url} alt="Shared image" />
|
||||
) : (
|
||||
<MediaUnavailable label="Image" />
|
||||
)}
|
||||
{message.content_text && (
|
||||
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
||||
@ -125,12 +136,14 @@ function MessageContent({ message }: { message: Message }) {
|
||||
case "video":
|
||||
return (
|
||||
<div>
|
||||
{message.media_url && (
|
||||
{message.media_url ? (
|
||||
<video
|
||||
src={message.media_url}
|
||||
controls
|
||||
className="max-h-64 max-w-60 rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<MediaUnavailable label="Video" />
|
||||
)}
|
||||
{message.content_text && (
|
||||
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
||||
@ -143,16 +156,21 @@ function MessageContent({ message }: { message: Message }) {
|
||||
case "audio":
|
||||
return (
|
||||
<div>
|
||||
{message.media_url && (
|
||||
{message.media_url ? (
|
||||
<audio src={message.media_url} controls className="max-w-60" />
|
||||
) : (
|
||||
<MediaUnavailable label="Audio" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "document":
|
||||
if (!message.media_url) {
|
||||
return <MediaUnavailable label={message.content_text || "Document"} />;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={message.media_url ?? "#"}
|
||||
href={message.media_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-700/50 px-3 py-2 text-sm hover:bg-slate-700"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user