-- ============================================================================= -- Table: communication.messages -- Description: Messages and chat system for teacher-student communication -- Priority: P1 - Important for communication features -- User Stories: US-PM-005 (Teacher Communication), US-AE-006 (Communication Management) -- Created: 2025-11-19 -- ============================================================================= -- Drop table if exists DROP TABLE IF EXISTS communication.messages CASCADE; -- Create messages table CREATE TABLE communication.messages ( -- Primary key id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Message relationships sender_id UUID NOT NULL REFERENCES auth_management.profiles(id) ON DELETE CASCADE, recipient_id UUID REFERENCES auth_management.profiles(id) ON DELETE CASCADE, -- Context (where message belongs) classroom_id UUID REFERENCES social_features.classrooms(id) ON DELETE CASCADE, thread_id UUID REFERENCES communication.messages(id) ON DELETE CASCADE, -- For threaded conversations parent_message_id UUID REFERENCES communication.messages(id) ON DELETE SET NULL, -- For replies -- Message content subject VARCHAR(255), -- For direct messages content TEXT NOT NULL, message_type VARCHAR(50) NOT NULL DEFAULT 'direct', -- Types: 'direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment' -- Attachments and media attachments JSONB DEFAULT '[]'::jsonb, -- Example: [{"type": "file", "url": "...", "name": "...", "size": 1024}] -- Message status is_read BOOLEAN DEFAULT FALSE, read_at TIMESTAMPTZ, is_deleted BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL, -- Priority and flags priority VARCHAR(20) DEFAULT 'normal', -- 'low', 'normal', 'high', 'urgent' is_pinned BOOLEAN DEFAULT FALSE, is_archived BOOLEAN DEFAULT FALSE, requires_response BOOLEAN DEFAULT FALSE, response_deadline TIMESTAMPTZ, -- Reactions and engagement reactions JSONB DEFAULT '{}'::jsonb, -- Example: {"thumbs_up": ["user-uuid-1", "user-uuid-2"], "heart": ["user-uuid-3"]} -- Moderation is_flagged BOOLEAN DEFAULT FALSE, flagged_reason TEXT, flagged_by UUID REFERENCES auth_management.profiles(id) ON DELETE SET NULL, flagged_at TIMESTAMPTZ, moderation_status VARCHAR(50) DEFAULT 'approved', -- 'approved', 'pending', 'flagged', 'removed' -- Metadata metadata JSONB DEFAULT '{}'::jsonb, -- Additional context: assignment_id, exercise_id, etc. -- Audit fields created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), edited_at TIMESTAMPTZ, edit_count INTEGER DEFAULT 0, -- Constraints CONSTRAINT messages_type_valid CHECK (message_type IN ('direct', 'classroom_announcement', 'classroom_chat', 'private_feedback', 'assignment_comment', 'system')), CONSTRAINT messages_priority_valid CHECK (priority IN ('low', 'normal', 'high', 'urgent')), CONSTRAINT messages_moderation_status_valid CHECK (moderation_status IN ('approved', 'pending', 'flagged', 'removed')), CONSTRAINT messages_recipient_or_classroom CHECK (recipient_id IS NOT NULL OR classroom_id IS NOT NULL) -- Must have either recipient (direct) or classroom (broadcast) ); -- ============================================================================= -- Indexes for performance -- ============================================================================= -- Index for sender's sent messages CREATE INDEX idx_messages_sender ON communication.messages(sender_id, created_at DESC) WHERE is_deleted = FALSE; -- Index for recipient's inbox CREATE INDEX idx_messages_recipient ON communication.messages(recipient_id, created_at DESC) WHERE is_deleted = FALSE; -- Index for classroom messages/announcements CREATE INDEX idx_messages_classroom ON communication.messages(classroom_id, created_at DESC) WHERE is_deleted = FALSE; -- Index for unread messages (important for notifications) CREATE INDEX idx_messages_unread ON communication.messages(recipient_id, created_at DESC) WHERE is_read = FALSE AND is_deleted = FALSE; -- Index for threaded conversations CREATE INDEX idx_messages_thread ON communication.messages(thread_id, created_at ASC) WHERE thread_id IS NOT NULL AND is_deleted = FALSE; -- Index for parent-child reply structure CREATE INDEX idx_messages_parent ON communication.messages(parent_message_id, created_at ASC) WHERE parent_message_id IS NOT NULL AND is_deleted = FALSE; -- Index for flagged messages (moderation) CREATE INDEX idx_messages_flagged ON communication.messages(flagged_at DESC) WHERE is_flagged = TRUE; -- Index for messages requiring response CREATE INDEX idx_messages_requiring_response ON communication.messages(recipient_id, response_deadline) WHERE requires_response = TRUE AND is_deleted = FALSE; -- Composite index for classroom + type (efficient filtering) CREATE INDEX idx_messages_classroom_type ON communication.messages(classroom_id, message_type, created_at DESC) WHERE is_deleted = FALSE; -- GIN index for searching attachments CREATE INDEX idx_messages_attachments ON communication.messages USING GIN(attachments); -- GIN index for metadata search CREATE INDEX idx_messages_metadata ON communication.messages USING GIN(metadata); -- ============================================================================= -- Triggers -- ============================================================================= -- Trigger for updated_at timestamp CREATE OR REPLACE FUNCTION communication.update_messages_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); -- Track edits IF OLD.content IS DISTINCT FROM NEW.content THEN NEW.edited_at = NOW(); NEW.edit_count = COALESCE(OLD.edit_count, 0) + 1; END IF; -- Auto-set read_at when is_read changes to true IF NEW.is_read = TRUE AND OLD.is_read = FALSE THEN NEW.read_at = NOW(); END IF; -- Auto-set deleted_at when is_deleted changes to true IF NEW.is_deleted = TRUE AND OLD.is_deleted = FALSE THEN NEW.deleted_at = NOW(); END IF; -- Auto-set flagged_at when is_flagged changes to true IF NEW.is_flagged = TRUE AND OLD.is_flagged = FALSE THEN NEW.flagged_at = NOW(); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_messages_timestamp BEFORE UPDATE ON communication.messages FOR EACH ROW EXECUTE FUNCTION communication.update_messages_timestamp(); -- ============================================================================= -- Helper function to get unread count -- ============================================================================= CREATE OR REPLACE FUNCTION communication.get_unread_count( p_user_id UUID, p_classroom_id UUID DEFAULT NULL ) RETURNS INTEGER AS $$ DECLARE v_count INTEGER; BEGIN SELECT COUNT(*) INTO v_count FROM communication.messages WHERE recipient_id = p_user_id AND is_read = FALSE AND is_deleted = FALSE AND (p_classroom_id IS NULL OR classroom_id = p_classroom_id); RETURN v_count; END; $$ LANGUAGE plpgsql STABLE; -- ============================================================================= -- Helper function to mark conversation as read -- ============================================================================= CREATE OR REPLACE FUNCTION communication.mark_conversation_read( p_user_id UUID, p_thread_id UUID ) RETURNS INTEGER AS $$ DECLARE v_updated INTEGER; BEGIN UPDATE communication.messages SET is_read = TRUE, read_at = NOW() WHERE recipient_id = p_user_id AND thread_id = p_thread_id AND is_read = FALSE AND is_deleted = FALSE; GET DIAGNOSTICS v_updated = ROW_COUNT; RETURN v_updated; END; $$ LANGUAGE plpgsql; -- ============================================================================= -- View: Recent classroom messages (for chat widget) -- ============================================================================= CREATE OR REPLACE VIEW communication.recent_classroom_messages AS SELECT m.id, m.classroom_id, m.sender_id, p.display_name as sender_name, p.avatar_url as sender_avatar, m.content, m.message_type, m.attachments, m.reactions, m.is_pinned, m.created_at, m.edited_at, m.edit_count, -- Count replies (SELECT COUNT(*) FROM communication.messages replies WHERE replies.parent_message_id = m.id AND replies.is_deleted = FALSE) as reply_count FROM communication.messages m JOIN auth_management.profiles p ON p.id = m.sender_id WHERE m.classroom_id IS NOT NULL AND m.is_deleted = FALSE AND m.parent_message_id IS NULL -- Only top-level messages ORDER BY CASE WHEN m.is_pinned THEN 0 ELSE 1 END, m.created_at DESC; -- ============================================================================= -- Comments for documentation -- ============================================================================= COMMENT ON TABLE communication.messages IS 'Messages and chat system for teacher-student communication. Supports direct messages, classroom announcements, threaded conversations, and assignment feedback.'; COMMENT ON COLUMN communication.messages.message_type IS 'Type of message: direct (1-to-1), classroom_announcement (teacher to all), classroom_chat (group), private_feedback (assignment/exercise feedback), assignment_comment'; COMMENT ON COLUMN communication.messages.attachments IS 'JSON array of file attachments with structure: [{"type": "file|image|video", "url": "...", "name": "...", "size": 1024, "mime_type": "..."}]'; COMMENT ON COLUMN communication.messages.reactions IS 'JSON object tracking emoji reactions: {"thumbs_up": ["user-uuid-1"], "heart": ["user-uuid-2", "user-uuid-3"]}'; COMMENT ON COLUMN communication.messages.thread_id IS 'For grouping messages in a conversation thread. Set to the first message ID in the thread.'; COMMENT ON COLUMN communication.messages.parent_message_id IS 'For direct replies to a specific message. Creates a tree structure within threads.'; COMMENT ON FUNCTION communication.get_unread_count IS 'Get count of unread messages for a user, optionally filtered by classroom. Usage: SELECT communication.get_unread_count(user_uuid, classroom_uuid);'; COMMENT ON FUNCTION communication.mark_conversation_read IS 'Mark all messages in a thread as read for a user. Usage: SELECT communication.mark_conversation_read(user_uuid, thread_uuid);'; -- ============================================================================= -- Grant permissions -- ============================================================================= GRANT SELECT, INSERT, UPDATE, DELETE ON communication.messages TO gamilit_user; GRANT SELECT ON communication.recent_classroom_messages TO gamilit_user; GRANT EXECUTE ON FUNCTION communication.get_unread_count TO gamilit_user; GRANT EXECUTE ON FUNCTION communication.mark_conversation_read TO gamilit_user;