From 72c9821270bf288f8df7ed17c0ba02e5dfe5fb77 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Thu, 30 Apr 2026 21:09:12 -0300 Subject: [PATCH] feat(whatsapp): add emoji reactions UI (#276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(whatsapp): add emoji reactions UI Adds end-to-end agent UI for emoji reactions on WhatsApp inboxes (Cloud API, Baileys, Z-API). Reactions arrive as Messages with is_reaction=true; this PR exposes them in the bubble UI and lets agents react with toggle/replace/remove semantics. - Add POST /reactions endpoint with toggle/replace logic that handles multi-device echoes from the same connected number - Add Channel::Whatsapp#supports_reactions? capability - Add Message.hide_removed_reactions scope and use it in conversation card preview / last_non_activity_message - Enrich last_non_activity_message with in_reply_to_snippet for reaction previews in chat list - Frontend: hover EmojiReactionPicker (8 quick + full picker) with alignment-aware positioning, single ReactionDisplay chip aggregating emojis with total count, conversation card preview shows "Você reagiu" for own/multi-device echoes * fix: address CodeRabbit review feedback - MessagePreview: render "Você" for outgoing reaction echoes that have no sender (multi-device echoes from the connected number) - MessagesView#findCurrentUserReaction: prefer active reactions over deleted rows so a stale deleted echo cannot hijack the toggle target - conversationHelper: drop removed reactions up-front so the activity fallback never returns null when older non-removed messages exist - imap_import rake: wrap IMAP work in begin/ensure so the session is closed even when uid_search/scan_new_email_uids raises - ReactionDisplay: include reaction.id in the user row so v-for keys stay stable across re-renders * fix: address CodeRabbit round 2 feedback - enterprise Message override of mark_pending_conversation_as_open_for_human_response now early-returns on reaction? so reactions can no longer auto-open Captain-pending conversations (matches the OSS guard) - whatsapp incoming reaction-removal handlers (Cloud/Baileys/Z-API) look up the reaction Message globally by sender instead of through the inbound conversation scope, then operate on existing.conversation; otherwise an old/resolved thread could be silently no-op'd while the inbound flow created a stray empty thread - EmojiReactionPicker: localize quick-emoji tooltip labels via i18n keys - Message.vue: track pendingTimeouts and clear them on unmount so the cooldown setTimeout no longer touches state after teardown - toggleMessageReaction action returns the API promise so callers can reconcile if the cable echo is delayed * fix: address CodeRabbit round 3 feedback - MessageFinder#page_window: pluck the 20-row window IDs before taking .min so the latest page honors PAGE_LIMIT (ActiveRecord's .minimum(:id) ignores .limit and aggregates over the whole relation) - ReactionsController#current_user_reaction: rank active reactions ahead of deleted rows (same invariant as the frontend lookup) so a stale deleted echo can no longer hijack the toggle target and resurrect itself - Whatsapp incoming handlers (Cloud, Baileys individual & group, Z-API) now branch on reaction_removal? BEFORE set_conversation / find_or_create_group_ conversation, so a blank reaction-removal webhook can never open or create a stray thread just to no-op - Message#reaction?: strict-boolean cast (via ActiveModel::Type::Boolean) so a stored string "false" no longer leaks through .present? as truthy * fix: address CodeRabbit round 4 feedback - MessageList: anchor unread divider on the filtered visibleMessages (firstUnreadId can land on a reaction that's filtered out, otherwise the separator silently disappears) - ReactionDisplay: render the removable user row as a real