- Added real-time messaging with Socket.io - Created comprehensive database schema (8 tables, functions, triggers) - Implemented messaging service with full CRUD operations - Built Socket.io service for real-time communication - Created React messaging components (Chat, ConversationList, MessageList, MessageInput) - Added end-to-end encryption utilities (RSA + AES-256-GCM) - Implemented 16 RESTful API endpoints - Added typing indicators, presence tracking, reactions - Created modern, responsive UI with animations - Updated server with Socket.io integration - Fixed auth middleware imports - Added comprehensive documentation Features: - Direct and group conversations - Real-time message delivery - Message editing and deletion - Emoji reactions - Typing indicators - Online/offline presence - Read receipts - User search - File attachment support (endpoint ready) - Client-side encryption utilities Dependencies: - socket.io ^4.7.5 - socket.io-client ^4.7.5
199 lines
8 KiB
PL/PgSQL
199 lines
8 KiB
PL/PgSQL
-- Migration 002: Messaging System
|
|
-- Creates tables for conversations, messages, participants, reactions, calls, and files
|
|
|
|
-- ============================================================================
|
|
-- 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 users(id) ON DELETE SET NULL,
|
|
gameforge_project_id UUID, -- For GameForge integration (future)
|
|
is_archived BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_conversations_type ON conversations(type);
|
|
CREATE INDEX idx_conversations_creator ON conversations(created_by);
|
|
CREATE INDEX idx_conversations_project ON conversations(gameforge_project_id);
|
|
CREATE INDEX 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 users(id) ON DELETE CASCADE,
|
|
identity_id UUID REFERENCES identities(id) ON DELETE SET NULL,
|
|
role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')),
|
|
joined_at TIMESTAMP DEFAULT NOW(),
|
|
last_read_at TIMESTAMP,
|
|
notification_settings JSONB DEFAULT '{"enabled": true, "mentions_only": false}'::jsonb,
|
|
UNIQUE(conversation_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id);
|
|
CREATE INDEX idx_participants_user ON conversation_participants(user_id);
|
|
CREATE INDEX idx_participants_identity ON conversation_participants(identity_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 users(id) ON DELETE CASCADE,
|
|
sender_identity_id UUID REFERENCES identities(id) ON DELETE SET NULL,
|
|
content_encrypted TEXT NOT NULL, -- Encrypted message content
|
|
content_type VARCHAR(20) DEFAULT 'text' CHECK (content_type IN ('text', 'image', 'video', 'audio', 'file')),
|
|
metadata JSONB, -- Attachments, mentions, reactions, etc.
|
|
reply_to_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
|
edited_at TIMESTAMP,
|
|
deleted_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at DESC);
|
|
CREATE INDEX idx_messages_sender ON messages(sender_id);
|
|
CREATE INDEX idx_messages_reply_to ON messages(reply_to_id);
|
|
CREATE INDEX 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 users(id) ON DELETE CASCADE,
|
|
emoji VARCHAR(20) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(message_id, user_id, emoji)
|
|
);
|
|
|
|
CREATE INDEX idx_reactions_message ON message_reactions(message_id);
|
|
CREATE INDEX idx_reactions_user ON message_reactions(user_id);
|
|
|
|
-- ============================================================================
|
|
-- FILES
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
uploader_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
|
|
filename VARCHAR(255) NOT NULL,
|
|
original_filename VARCHAR(255) NOT NULL,
|
|
mime_type VARCHAR(100) NOT NULL,
|
|
size_bytes BIGINT NOT NULL,
|
|
storage_url VARCHAR(500) NOT NULL, -- GCP Cloud Storage URL or Supabase Storage
|
|
thumbnail_url VARCHAR(500), -- For images/videos
|
|
encryption_key TEXT, -- If file is encrypted
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
expires_at TIMESTAMP -- For temporary files
|
|
);
|
|
|
|
CREATE INDEX idx_files_uploader ON files(uploader_id);
|
|
CREATE INDEX idx_files_conversation ON files(conversation_id);
|
|
CREATE INDEX idx_files_created ON files(created_at DESC);
|
|
|
|
-- ============================================================================
|
|
-- CALLS
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS calls (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
|
|
type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')),
|
|
initiator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
status VARCHAR(20) DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'missed', 'declined')),
|
|
started_at TIMESTAMP,
|
|
ended_at TIMESTAMP,
|
|
duration_seconds INTEGER,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_calls_conversation ON calls(conversation_id);
|
|
CREATE INDEX idx_calls_initiator ON calls(initiator_id);
|
|
CREATE INDEX idx_calls_status ON calls(status);
|
|
CREATE INDEX idx_calls_created ON calls(created_at DESC);
|
|
|
|
-- ============================================================================
|
|
-- CALL PARTICIPANTS
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS call_participants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMP,
|
|
left_at TIMESTAMP,
|
|
media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb,
|
|
UNIQUE(call_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX idx_call_participants_call ON call_participants(call_id);
|
|
CREATE INDEX idx_call_participants_user ON call_participants(user_id);
|
|
|
|
-- ============================================================================
|
|
-- FUNCTIONS AND TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- 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
|
|
CREATE TRIGGER trigger_update_conversation_timestamp
|
|
AFTER INSERT ON messages
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_conversation_timestamp();
|
|
|
|
-- Function to automatically create direct conversation if it doesn't exist
|
|
CREATE OR REPLACE FUNCTION get_or_create_direct_conversation(user1_id UUID, user2_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
conv_id UUID;
|
|
BEGIN
|
|
-- Try to find existing direct conversation between these users
|
|
SELECT c.id INTO conv_id
|
|
FROM conversations c
|
|
WHERE c.type = 'direct'
|
|
AND EXISTS (
|
|
SELECT 1 FROM conversation_participants cp1
|
|
WHERE cp1.conversation_id = c.id AND cp1.user_id = user1_id
|
|
)
|
|
AND EXISTS (
|
|
SELECT 1 FROM conversation_participants cp2
|
|
WHERE cp2.conversation_id = c.id AND cp2.user_id = user2_id
|
|
);
|
|
|
|
-- If not found, create new direct 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)
|
|
VALUES (conv_id, user1_id), (conv_id, user2_id);
|
|
END IF;
|
|
|
|
RETURN conv_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|