fix(carousel): fix carousel rendering on Android, iOS and Web
fix(carousel): fix carousel rendering on Android, iOS and Web
This commit is contained in:
@@ -29,7 +29,6 @@ import {
|
|||||||
generateMessageIDV2,
|
generateMessageIDV2,
|
||||||
generateParticipantHashV2,
|
generateParticipantHashV2,
|
||||||
generateWAMessage,
|
generateWAMessage,
|
||||||
generateWAMessageContent,
|
|
||||||
getStatusCodeForMediaRetry,
|
getStatusCodeForMediaRetry,
|
||||||
getUrlFromDirectPath,
|
getUrlFromDirectPath,
|
||||||
getWAUploadToServer,
|
getWAUploadToServer,
|
||||||
@@ -1146,9 +1145,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const { user: mePnUser } = jidDecode(meId)!
|
const { user: mePnUser } = jidDecode(meId)!
|
||||||
const { user: meLidUser } = meLid ? jidDecode(meLid)! : { user: null }
|
const { user: meLidUser } = meLid ? jidDecode(meLid)! : { user: null }
|
||||||
|
|
||||||
// Carousel in DSM wrapper causes error 479 on own devices
|
|
||||||
const isCarouselMsg = isCarouselMessage(message)
|
|
||||||
|
|
||||||
for (const { user, jid } of devices) {
|
for (const { user, jid } of devices) {
|
||||||
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
||||||
if (isExactSenderDevice) {
|
if (isExactSenderDevice) {
|
||||||
@@ -1160,11 +1156,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const isMe = user === mePnUser || user === meLidUser
|
const isMe = user === mePnUser || user === meLidUser
|
||||||
|
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
if (isCarouselMsg) {
|
// Send DSM to ALL own companion devices including carousel
|
||||||
logger.debug({ jid }, '[CAROUSEL] Skipping own device - DSM carousel causes error 479')
|
// Error 479 on companion devices is non-fatal
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
meRecipients.push(jid)
|
meRecipients.push(jid)
|
||||||
} else {
|
} else {
|
||||||
otherRecipients.push(jid)
|
otherRecipients.push(jid)
|
||||||
@@ -1200,8 +1193,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const isParticipantLid = isLidUser(participant.jid)
|
const isParticipantLid = isLidUser(participant.jid)
|
||||||
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
||||||
|
|
||||||
// Skip DSM for carousel - own devices reject it with error 479
|
// Send DSM for all message types including carousel
|
||||||
const usesDSM = isMe && !isCarouselMessage(message)
|
const usesDSM = isMe
|
||||||
const encodedMessageToSend = usesDSM
|
const encodedMessageToSend = usesDSM
|
||||||
? encodeWAMessage({
|
? encodeWAMessage({
|
||||||
deviceSentMessage: {
|
deviceSentMessage: {
|
||||||
@@ -1265,8 +1258,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
// The biz node MUST be last for WhatsApp Web carousel rendering
|
// The biz node MUST be last for WhatsApp Web carousel rendering
|
||||||
const deferredNodes: BinaryNode[] = []
|
const deferredNodes: BinaryNode[] = []
|
||||||
|
|
||||||
// EXPERIMENTAL: Try biz node injection for ALL interactive messages including catalog
|
// Inject biz node for interactive messages (including carousel)
|
||||||
// Previously we skipped catalog messages but they weren't rendering properly
|
|
||||||
if (buttonType && enableInteractiveMessages) {
|
if (buttonType && enableInteractiveMessages) {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
@@ -2188,59 +2180,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const userJid = authState.creds.me!.id
|
const userJid = authState.creds.me!.id
|
||||||
|
|
||||||
// Special path for carousel: call relayMessage directly with plain JS object
|
|
||||||
// This matches the known working approach:
|
|
||||||
// 1. Build message as plain JS object (not proto instance)
|
|
||||||
// 2. Pass directly to relayMessage (no WAProto.Message.fromObject/create)
|
|
||||||
// 3. protobuf encode() handles plain objects correctly during serialization
|
|
||||||
// Going through fromObject() can corrupt nested carousel InteractiveMessage oneOf fields
|
|
||||||
if (typeof content === 'object' && 'nativeCarousel' in content && (content as any).nativeCarousel) {
|
|
||||||
const messageId = generateMessageIDV2(sock.user?.id)
|
|
||||||
// generateWAMessageContent returns a plain JS object for carousel
|
|
||||||
// (not a proto instance) to preserve nested structure integrity
|
|
||||||
const msgContent = await generateWAMessageContent(content, {
|
|
||||||
logger,
|
|
||||||
upload: waUploadToServer,
|
|
||||||
mediaCache: config.mediaCache,
|
|
||||||
options: config.options,
|
|
||||||
jid
|
|
||||||
})
|
|
||||||
|
|
||||||
// Pass the plain JS object directly to relayMessage
|
|
||||||
// This avoids proto.WebMessageInfo.fromObject() which would call
|
|
||||||
// Message.fromObject() on the nested message, potentially corrupting it
|
|
||||||
await relayMessage(jid, msgContent, {
|
|
||||||
messageId,
|
|
||||||
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
||||||
additionalAttributes: {},
|
|
||||||
statusJidList: options.statusJidList
|
|
||||||
})
|
|
||||||
|
|
||||||
// Build WebMessageInfo only for event emission and return value
|
|
||||||
const timestamp = unixTimestampSeconds()
|
|
||||||
const fullMsg = proto.WebMessageInfo.fromObject({
|
|
||||||
key: {
|
|
||||||
remoteJid: jid,
|
|
||||||
fromMe: true,
|
|
||||||
id: messageId
|
|
||||||
},
|
|
||||||
message: msgContent,
|
|
||||||
messageTimestamp: timestamp,
|
|
||||||
messageStubParameters: [],
|
|
||||||
participant: isJidGroup(jid) ? userJid : undefined,
|
|
||||||
status: proto.WebMessageInfo.Status.PENDING
|
|
||||||
}) as WAMessage
|
|
||||||
|
|
||||||
if (config.emitOwnEvents) {
|
|
||||||
process.nextTick(async () => {
|
|
||||||
const mutexKey = fullMsg.key.remoteJid || fullMsg.key.id || 'unknown'
|
|
||||||
await messageMutex.mutex(mutexKey, () => upsertMessage(fullMsg, 'append'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof content === 'object' &&
|
typeof content === 'object' &&
|
||||||
'disappearingMessagesInChat' in content &&
|
'disappearingMessagesInChat' in content &&
|
||||||
|
|||||||
+19
-37
@@ -640,6 +640,7 @@ export const generateCarouselMessage = async (
|
|||||||
|
|
||||||
const header: any = {
|
const header: any = {
|
||||||
title: card.title || '',
|
title: card.title || '',
|
||||||
|
subtitle: card.footer || '',
|
||||||
hasMediaAttachment: hasMedia
|
hasMediaAttachment: hasMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,19 +684,15 @@ export const generateCarouselMessage = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Build the interactive message with carousel
|
// Build the interactive message with carousel
|
||||||
// Known working structure:
|
// carouselMessage first, then header/body/footer (matches working structure)
|
||||||
// - Direct interactiveMessage at root (NO viewOnceMessage wrapper)
|
|
||||||
// - Root header with title + hasMediaAttachment: false
|
|
||||||
// - messageVersion: 1 in carouselMessage
|
|
||||||
// - Body and footer at root level
|
|
||||||
const interactiveMessage: proto.Message.IInteractiveMessage = {
|
const interactiveMessage: proto.Message.IInteractiveMessage = {
|
||||||
header: { title: title || ' ', hasMediaAttachment: false },
|
|
||||||
body: { text: text || '' },
|
|
||||||
footer: footer ? { text: footer } : undefined,
|
|
||||||
carouselMessage: {
|
carouselMessage: {
|
||||||
cards: carouselCards,
|
cards: carouselCards,
|
||||||
messageVersion: 1
|
messageVersion: 1
|
||||||
}
|
},
|
||||||
|
header: { title: title || ' ', hasMediaAttachment: false },
|
||||||
|
body: { text: text || '' },
|
||||||
|
footer: footer ? { text: footer } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log carousel structure summary for debugging
|
// Log carousel structure summary for debugging
|
||||||
@@ -720,9 +717,7 @@ export const generateCarouselMessage = async (
|
|||||||
'[CAROUSEL] Structure summary'
|
'[CAROUSEL] Structure summary'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Direct interactiveMessage at root - NO viewOnceMessage, NO messageContextInfo
|
// Direct interactiveMessage at root (field 45) — no viewOnceMessage wrapper
|
||||||
// viewOnceMessage breaks iOS, messageContextInfo breaks iOS
|
|
||||||
// Web shows header-only fallback (carousel is mobile-only on WhatsApp)
|
|
||||||
return {
|
return {
|
||||||
interactiveMessage
|
interactiveMessage
|
||||||
}
|
}
|
||||||
@@ -1244,12 +1239,9 @@ export const generateWAMessageContent = async (
|
|||||||
}
|
}
|
||||||
// Pass options for media processing if cards have images/videos
|
// Pass options for media processing if cards have images/videos
|
||||||
const generated = await generateCarouselMessage(carouselOptions, options)
|
const generated = await generateCarouselMessage(carouselOptions, options)
|
||||||
// Send carousel as direct interactiveMessage (no viewOnceMessage wrapper)
|
// Direct interactiveMessage — no viewOnceMessage wrapper, no root header/body/footer
|
||||||
// viewOnceMessage breaks iOS; messageContextInfo breaks iOS delivery
|
|
||||||
m.interactiveMessage = generated.interactiveMessage
|
m.interactiveMessage = generated.interactiveMessage
|
||||||
options.logger?.info('Sending carousel as direct interactiveMessage')
|
return m as proto.IMessage
|
||||||
// Return plain JS object - no fromObject() to avoid corrupting nested carousel structures
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
// Check for nativeList
|
// Check for nativeList
|
||||||
else if (hasNonNullishProperty(message, 'nativeList')) {
|
else if (hasNonNullishProperty(message, 'nativeList')) {
|
||||||
@@ -1377,17 +1369,8 @@ export const generateWAMessageContent = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap in viewOnceMessage for better iOS/Android compatibility
|
// Direct interactiveMessage — no viewOnceMessage wrapper
|
||||||
m.viewOnceMessage = {
|
m.interactiveMessage = interactiveMessage
|
||||||
message: {
|
|
||||||
messageContextInfo: {
|
|
||||||
deviceListMetadata: {},
|
|
||||||
deviceListMetadataVersion: 2
|
|
||||||
},
|
|
||||||
interactiveMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options.logger?.warn('[EXPERIMENTAL] Sending carouselMessage with viewOnceMessage wrapper')
|
|
||||||
} else if (hasNonNullishProperty(message, 'album')) {
|
} else if (hasNonNullishProperty(message, 'album')) {
|
||||||
// Album message validation - actual sending is handled in messages-send.ts
|
// Album message validation - actual sending is handled in messages-send.ts
|
||||||
const { medias } = message.album
|
const { medias } = message.album
|
||||||
@@ -1827,10 +1810,9 @@ export const generateWAMessageFromContent = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip ephemeral contextInfo for carousel messages - carousel was already
|
// Skip ephemeral contextInfo for carousel messages
|
||||||
// processed with fromObject and adding contextInfo.expiration may cause
|
const isCarouselEphemeral = !!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||||
// error 479 on linked devices (Web/Desktop)
|
!!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
||||||
const isCarouselEphemeral = !!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
|
||||||
if (
|
if (
|
||||||
// if we want to send a disappearing message
|
// if we want to send a disappearing message
|
||||||
!!options?.ephemeralExpiration &&
|
!!options?.ephemeralExpiration &&
|
||||||
@@ -1851,11 +1833,11 @@ export const generateWAMessageFromContent = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For carousel messages already processed with fromObject, skip create() to
|
// Skip Message.create() for carousel — InteractiveMessage has oneOf (fields 4-7)
|
||||||
// preserve the deep protobuf conversion. create() does shallow assignment which
|
// and create() may corrupt the carouselMessage/nativeFlowMessage oneOf resolution
|
||||||
// may not properly handle deeply nested carousel structures (cards > headers > media)
|
const isCarouselMsg = !!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||||
const isCarouselVOM = !!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
!!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
||||||
if (!isCarouselVOM) {
|
if (!isCarouselMsg) {
|
||||||
message = WAProto.Message.create(message)
|
message = WAProto.Message.create(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user