mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Adds three WhatsApp-standard affordances to the inbox: per-message emoji reactions, reply-with-quote, and copy-text. Bubbles stay pure presenters — actions layer on by composition (MessageActions wraps MessageBubble). Hover toolbar on desktop, long-press (onContextMenu) on touch. Data model (migration 009): - messages.reply_to_message_id (self-FK, ON DELETE SET NULL) so replies survive a deleted parent - message_reactions table — one row per (message, actor) with a unique constraint, conversation_id denormalised so Supabase Realtime can filter without a join - RLS scoped via conversation ownership; service-role bypass for the webhook API: - /api/whatsapp/send accepts reply_to_message_id, resolves the parent's Meta message_id and forwards it as context.message_id - /api/whatsapp/react (new) sends Meta reaction payloads and mirrors the upsert/delete into message_reactions - Webhook stops storing inbound reactions as fake text messages and instead upserts/deletes message_reactions; inbound replies (context.id) resolve to internal UUID and populate reply_to_message_id UI: - MessageActions hover toolbar with inline 6-emoji Popover (static set; no emoji-picker library) - ReplyQuote shared between the in-bubble quote and the composer chip - MessageReactions pill row with click-to-toggle - Thread subscribes to message_reactions filtered by conversation_id; optimistic upsert with rollback on failure Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.4 KiB
SQL
115 lines
4.4 KiB
SQL
-- ============================================================
|
|
-- Chat actions: reply linkage + reactions
|
|
--
|
|
-- Adds two things the chat UI now needs:
|
|
--
|
|
-- 1. `messages.reply_to_message_id` — a self-FK so a message can
|
|
-- point at the message it replies to. We use the internal UUID
|
|
-- (not Meta's message_id text), because Meta IDs aren't unique
|
|
-- across phone numbers and can't be FK-constrained. The webhook
|
|
-- resolves `context.id` from Meta into our internal UUID before
|
|
-- writing. ON DELETE SET NULL — a deleted parent must not nuke
|
|
-- its replies (which today never happens, but the constraint
|
|
-- should match intent).
|
|
--
|
|
-- 2. `message_reactions` table — one row per (message, actor).
|
|
-- Reactions arrive concurrently from agents (UI) and customers
|
|
-- (webhook). A row-level uniqueness constraint enforces "one
|
|
-- reaction per actor per message" without read-modify-write
|
|
-- games on a JSONB column.
|
|
--
|
|
-- `conversation_id` is denormalised purely so Supabase Realtime
|
|
-- can filter on it with a plain `eq`. Realtime can't join.
|
|
--
|
|
-- Idempotent — safe to run multiple times.
|
|
-- ============================================================
|
|
|
|
-- ============================================================
|
|
-- 1. Reply linkage on messages
|
|
-- ============================================================
|
|
ALTER TABLE messages
|
|
ADD COLUMN IF NOT EXISTS reply_to_message_id UUID
|
|
REFERENCES messages(id) ON DELETE SET NULL;
|
|
|
|
-- Partial index — most messages aren't replies, so skip nulls.
|
|
CREATE INDEX IF NOT EXISTS idx_messages_reply_to
|
|
ON messages(reply_to_message_id)
|
|
WHERE reply_to_message_id IS NOT NULL;
|
|
|
|
-- ============================================================
|
|
-- 2. message_reactions
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
actor_type TEXT NOT NULL CHECK (actor_type IN ('customer', 'agent')),
|
|
actor_id UUID,
|
|
emoji TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (message_id, actor_type, actor_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_message_reactions_conversation
|
|
ON message_reactions(conversation_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_message_reactions_message
|
|
ON message_reactions(message_id);
|
|
|
|
ALTER TABLE message_reactions ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "Users see reactions on their conversations" ON message_reactions;
|
|
CREATE POLICY "Users see reactions on their conversations" ON message_reactions FOR SELECT
|
|
USING (EXISTS (
|
|
SELECT 1 FROM conversations c
|
|
WHERE c.id = message_reactions.conversation_id
|
|
AND c.user_id = auth.uid()
|
|
));
|
|
|
|
DROP POLICY IF EXISTS "Users insert reactions on their conversations" ON message_reactions;
|
|
CREATE POLICY "Users insert reactions on their conversations" ON message_reactions FOR INSERT
|
|
WITH CHECK (EXISTS (
|
|
SELECT 1 FROM conversations c
|
|
WHERE c.id = message_reactions.conversation_id
|
|
AND c.user_id = auth.uid()
|
|
));
|
|
|
|
-- Agents may remove their own reactions. Customer reactions are managed
|
|
-- by the webhook (service-role bypass), not the UI.
|
|
DROP POLICY IF EXISTS "Users delete their own agent reactions" ON message_reactions;
|
|
CREATE POLICY "Users delete their own agent reactions" ON message_reactions FOR DELETE
|
|
USING (
|
|
actor_type = 'agent'
|
|
AND actor_id = auth.uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM conversations c
|
|
WHERE c.id = message_reactions.conversation_id
|
|
AND c.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Agents may swap their own reaction emoji (UPDATE path is also used by
|
|
-- the upsert in /api/whatsapp/react).
|
|
DROP POLICY IF EXISTS "Users update their own agent reactions" ON message_reactions;
|
|
CREATE POLICY "Users update their own agent reactions" ON message_reactions FOR UPDATE
|
|
USING (
|
|
actor_type = 'agent'
|
|
AND actor_id = auth.uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM conversations c
|
|
WHERE c.id = message_reactions.conversation_id
|
|
AND c.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Realtime — let the thread subscribe filtered by conversation_id.
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_publication_tables
|
|
WHERE pubname = 'supabase_realtime' AND tablename = 'message_reactions'
|
|
) THEN
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE message_reactions;
|
|
END IF;
|
|
END $$;
|