-- Migration 001: Messaging System -- Creates tables for conversations, messages, participants, reactions -- ============================================================================ -- CONVERSATIONS -- ============================================================================ CREATE TABLE IF NOT EXISTS conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')), title VARCHAR(200), description TEXT, avatar_url VARCHAR(500), created_by UUID REFERENCES auth.users(id), is_archived BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_conversations_type ON conversations(type); CREATE INDEX IF NOT EXISTS idx_conversations_creator ON conversations(created_by); CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC); -- ============================================================================ -- CONVERSATION PARTICIPANTS -- ============================================================================ CREATE TABLE IF NOT EXISTS conversation_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')), joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_read_at TIMESTAMP WITH TIME ZONE, notification_settings JSONB DEFAULT '{"enabled": true, "mentions_only": false}'::jsonb, UNIQUE(conversation_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_participants_conversation ON conversation_participants(conversation_id); CREATE INDEX IF NOT EXISTS idx_participants_user ON conversation_participants(user_id); -- ============================================================================ -- MESSAGES -- ============================================================================ CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_id UUID NOT NULL REFERENCES auth.users(id), content TEXT NOT NULL, content_type VARCHAR(20) DEFAULT 'text' CHECK (content_type IN ('text', 'image', 'video', 'audio', 'file', 'code')), metadata JSONB, reply_to_id UUID REFERENCES messages(id) ON DELETE SET NULL, edited_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id); CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to_id); CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at DESC); -- ============================================================================ -- MESSAGE REACTIONS -- ============================================================================ CREATE TABLE IF NOT EXISTS message_reactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, emoji VARCHAR(20) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(message_id, user_id, emoji) ); CREATE INDEX IF NOT EXISTS idx_reactions_message ON message_reactions(message_id); CREATE INDEX IF NOT EXISTS idx_reactions_user ON message_reactions(user_id); -- ============================================================================ -- MESSAGE ATTACHMENTS -- ============================================================================ CREATE TABLE IF NOT EXISTS message_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, file_name VARCHAR(500) NOT NULL, file_url VARCHAR(1000) NOT NULL, file_size INTEGER, file_type VARCHAR(100), uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_attachments_message ON message_attachments(message_id); -- ============================================================================ -- FUNCTIONS -- ============================================================================ -- Function to update conversation updated_at timestamp when messages are added CREATE OR REPLACE FUNCTION update_conversation_timestamp() RETURNS TRIGGER AS $$ BEGIN UPDATE conversations SET updated_at = NOW() WHERE id = NEW.conversation_id; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to update conversation timestamp on new message DROP TRIGGER IF EXISTS trigger_update_conversation_timestamp ON messages; CREATE TRIGGER trigger_update_conversation_timestamp AFTER INSERT ON messages FOR EACH ROW EXECUTE FUNCTION update_conversation_timestamp(); -- Function to get or create a direct conversation between two users CREATE OR REPLACE FUNCTION get_or_create_direct_conversation(user1_id UUID, user2_id UUID) RETURNS UUID AS $$ DECLARE conv_id UUID; BEGIN -- Check if direct conversation exists between these two users SELECT c.id INTO conv_id FROM conversations c WHERE c.type = 'direct' AND EXISTS (SELECT 1 FROM conversation_participants WHERE conversation_id = c.id AND user_id = user1_id) AND EXISTS (SELECT 1 FROM conversation_participants WHERE conversation_id = c.id AND user_id = user2_id) AND (SELECT COUNT(*) FROM conversation_participants WHERE conversation_id = c.id) = 2 LIMIT 1; -- If not found, create new conversation IF conv_id IS NULL THEN INSERT INTO conversations (type, created_by) VALUES ('direct', user1_id) RETURNING id INTO conv_id; -- Add both participants INSERT INTO conversation_participants (conversation_id, user_id, role) VALUES (conv_id, user1_id, 'member'), (conv_id, user2_id, 'member'); END IF; RETURN conv_id; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- ROW LEVEL SECURITY -- ============================================================================ ALTER TABLE conversations ENABLE ROW LEVEL SECURITY; ALTER TABLE conversation_participants ENABLE ROW LEVEL SECURITY; ALTER TABLE messages ENABLE ROW LEVEL SECURITY; ALTER TABLE message_reactions ENABLE ROW LEVEL SECURITY; ALTER TABLE message_attachments ENABLE ROW LEVEL SECURITY; -- Conversations: Users can see conversations they're participants in CREATE POLICY "Users can view their conversations" ON conversations FOR SELECT USING ( EXISTS ( SELECT 1 FROM conversation_participants WHERE conversation_id = id AND user_id = auth.uid() ) ); CREATE POLICY "Users can create conversations" ON conversations FOR INSERT WITH CHECK (created_by = auth.uid()); -- Participants: Users can see participants in their conversations CREATE POLICY "Users can view participants in their conversations" ON conversation_participants FOR SELECT USING ( EXISTS ( SELECT 1 FROM conversation_participants cp WHERE cp.conversation_id = conversation_participants.conversation_id AND cp.user_id = auth.uid() ) ); -- Messages: Users can see messages in conversations they're in CREATE POLICY "Users can view messages in their conversations" ON messages FOR SELECT USING ( EXISTS ( SELECT 1 FROM conversation_participants WHERE conversation_id = messages.conversation_id AND user_id = auth.uid() ) ); CREATE POLICY "Users can send messages to their conversations" ON messages FOR INSERT WITH CHECK ( sender_id = auth.uid() AND EXISTS ( SELECT 1 FROM conversation_participants WHERE conversation_id = messages.conversation_id AND user_id = auth.uid() ) ); CREATE POLICY "Users can edit their own messages" ON messages FOR UPDATE USING (sender_id = auth.uid()); CREATE POLICY "Users can delete their own messages" ON messages FOR DELETE USING (sender_id = auth.uid());