mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +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 }
|
video?: { id: string; mime_type: string; caption?: string }
|
||||||
document?: { id: string; mime_type: string; filename?: string; caption?: string }
|
document?: { id: string; mime_type: string; filename?: string; caption?: string }
|
||||||
audio?: { id: string; mime_type: string }
|
audio?: { id: string; mime_type: string }
|
||||||
|
sticker?: { id: string; mime_type: string }
|
||||||
location?: { latitude: number; longitude: number; name?: string; address?: string }
|
location?: { latitude: number; longitude: number; name?: string; address?: string }
|
||||||
reaction?: { message_id: string; emoji: 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
|
// column; the MIME type is only used to construct the proxy URL during
|
||||||
// parseMessageContent. Silence the unused-var warning:
|
// parseMessageContent. Silence the unused-var warning:
|
||||||
void mediaType
|
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({
|
const { error: msgError } = await supabaseAdmin().from('messages').insert({
|
||||||
conversation_id: conversation.id,
|
conversation_id: conversation.id,
|
||||||
sender_type: 'customer',
|
sender_type: 'customer',
|
||||||
content_type: message.type,
|
content_type: contentType,
|
||||||
content_text: contentText,
|
content_text: contentText,
|
||||||
media_url: mediaUrl,
|
media_url: mediaUrl,
|
||||||
message_id: message.id,
|
message_id: message.id,
|
||||||
@ -275,6 +290,25 @@ async function parseMessageContent(
|
|||||||
mediaUrl: string | null
|
mediaUrl: string | null
|
||||||
mediaType: 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) {
|
switch (message.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return {
|
return {
|
||||||
@ -284,61 +318,55 @@ async function parseMessageContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'image':
|
case 'image':
|
||||||
if (message.image) {
|
if (message.image?.id) {
|
||||||
try {
|
|
||||||
await getMediaUrl(accessToken, message.image.id)
|
|
||||||
return {
|
return {
|
||||||
contentText: message.image.caption || null,
|
contentText: message.image.caption || null,
|
||||||
mediaUrl: `/api/whatsapp/media/${message.image.id}`,
|
mediaUrl: await verifyAndBuildUrl(message.image.id),
|
||||||
mediaType: message.image.mime_type,
|
mediaType: message.image.mime_type,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying image media:', error)
|
|
||||||
}
|
}
|
||||||
}
|
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||||
return { contentText: message.image?.caption || null, mediaUrl: null, mediaType: null }
|
|
||||||
|
|
||||||
case 'video':
|
case 'video':
|
||||||
if (message.video) {
|
if (message.video?.id) {
|
||||||
try {
|
|
||||||
await getMediaUrl(accessToken, message.video.id)
|
|
||||||
return {
|
return {
|
||||||
contentText: message.video.caption || null,
|
contentText: message.video.caption || null,
|
||||||
mediaUrl: `/api/whatsapp/media/${message.video.id}`,
|
mediaUrl: await verifyAndBuildUrl(message.video.id),
|
||||||
mediaType: message.video.mime_type,
|
mediaType: message.video.mime_type,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying video media:', error)
|
|
||||||
}
|
}
|
||||||
}
|
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||||
return { contentText: message.video?.caption || null, mediaUrl: null, mediaType: null }
|
|
||||||
|
|
||||||
case 'document':
|
case 'document':
|
||||||
if (message.document) {
|
if (message.document?.id) {
|
||||||
try {
|
|
||||||
await getMediaUrl(accessToken, message.document.id)
|
|
||||||
return {
|
return {
|
||||||
contentText: message.document.caption || message.document.filename || null,
|
contentText:
|
||||||
mediaUrl: `/api/whatsapp/media/${message.document.id}`,
|
message.document.caption || message.document.filename || null,
|
||||||
|
mediaUrl: await verifyAndBuildUrl(message.document.id),
|
||||||
mediaType: message.document.mime_type,
|
mediaType: message.document.mime_type,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying document media:', error)
|
|
||||||
}
|
}
|
||||||
}
|
return { contentText: null, mediaUrl: null, mediaType: null }
|
||||||
return { contentText: message.document?.filename || null, mediaUrl: null, mediaType: null }
|
|
||||||
|
|
||||||
case 'audio':
|
case 'audio':
|
||||||
if (message.audio) {
|
if (message.audio?.id) {
|
||||||
try {
|
|
||||||
await getMediaUrl(accessToken, message.audio.id)
|
|
||||||
return {
|
return {
|
||||||
contentText: null,
|
contentText: null,
|
||||||
mediaUrl: `/api/whatsapp/media/${message.audio.id}`,
|
mediaUrl: await verifyAndBuildUrl(message.audio.id),
|
||||||
mediaType: message.audio.mime_type,
|
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 }
|
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 }) {
|
function MediaImage({ url, alt }: { url: string; alt: string }) {
|
||||||
const [src, setSrc] = useState<string | null>(null);
|
const [src, setSrc] = useState<string | null>(null);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@ -111,8 +120,10 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
case "image":
|
case "image":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.media_url && (
|
{message.media_url ? (
|
||||||
<MediaImage url={message.media_url} alt="Shared image" />
|
<MediaImage url={message.media_url} alt="Shared image" />
|
||||||
|
) : (
|
||||||
|
<MediaUnavailable label="Image" />
|
||||||
)}
|
)}
|
||||||
{message.content_text && (
|
{message.content_text && (
|
||||||
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
||||||
@ -125,12 +136,14 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
case "video":
|
case "video":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.media_url && (
|
{message.media_url ? (
|
||||||
<video
|
<video
|
||||||
src={message.media_url}
|
src={message.media_url}
|
||||||
controls
|
controls
|
||||||
className="max-h-64 max-w-60 rounded-lg"
|
className="max-h-64 max-w-60 rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<MediaUnavailable label="Video" />
|
||||||
)}
|
)}
|
||||||
{message.content_text && (
|
{message.content_text && (
|
||||||
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
<p className="mt-1 whitespace-pre-wrap break-words text-sm">
|
||||||
@ -143,16 +156,21 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
case "audio":
|
case "audio":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.media_url && (
|
{message.media_url ? (
|
||||||
<audio src={message.media_url} controls className="max-w-60" />
|
<audio src={message.media_url} controls className="max-w-60" />
|
||||||
|
) : (
|
||||||
|
<MediaUnavailable label="Audio" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "document":
|
case "document":
|
||||||
|
if (!message.media_url) {
|
||||||
|
return <MediaUnavailable label={message.content_text || "Document"} />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={message.media_url ?? "#"}
|
href={message.media_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
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