-- ============================================================ -- 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 $$;