feat(06-01): document complete chat API surface
- Map REST endpoints (create, index) with request/response formats - Document WebSocket Protocol Buffer messages - Capture database schema and ActiveRecord model - Identify security, validation, and access control - Document data flow and routing patterns - Note missing functionality (read/unread tracking) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
940009c22d
commit
2cb263542e
|
|
@ -0,0 +1,798 @@
|
|||
# Chat API Surface Documentation
|
||||
|
||||
**Purpose:** Document all chat-related API endpoints, WebSocket messages, database models, and data structures.
|
||||
|
||||
**Date:** 2026-01-26
|
||||
**Phase:** 06-session-chat-research-design
|
||||
**Plan:** 01
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The chat system uses a **hybrid architecture**:
|
||||
- **REST API** for sending messages and fetching history
|
||||
- **WebSocket (Protocol Buffers)** for real-time message delivery
|
||||
- **No jamClient methods** - chat is purely web-based (no native client integration)
|
||||
|
||||
**Supported Channels:**
|
||||
- `global` - All online users (disabled for school users)
|
||||
- `session` - Users in a specific music session
|
||||
- `lesson` - Direct messages between teacher and student in a lesson
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Base Path
|
||||
All endpoints are under `/api/chat` (configured in `web/config/routes.rb`)
|
||||
|
||||
### Authentication
|
||||
All endpoints require:
|
||||
- `before_filter :api_signed_in_user` - valid session/token
|
||||
- `before_filter :check_session` - validates session/lesson access
|
||||
|
||||
---
|
||||
|
||||
### 1. Create Chat Message
|
||||
|
||||
**Endpoint:** `POST /api/chat`
|
||||
|
||||
**Controller:** `ApiChatsController#create`
|
||||
**File:** `/web/app/controllers/api_chats_controller.rb`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"message": "Hello world",
|
||||
"channel": "session",
|
||||
"music_session": "session-uuid",
|
||||
"lesson_session": "lesson-uuid",
|
||||
"client_id": "client-uuid",
|
||||
"target_user": "user-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `message` (string, required) - Message text (1-255 chars, profanity filtered)
|
||||
- `channel` (string, required) - Channel type: `"global"`, `"session"`, `"lesson"`
|
||||
- `music_session` (string, conditional) - Required if `channel="session"`
|
||||
- `lesson_session` (string, conditional) - Required if `channel="lesson"`
|
||||
- `client_id` (string, optional) - Client UUID for routing (excludes sender from broadcast)
|
||||
- `target_user` (string, conditional) - Required if `channel="lesson"` (recipient user ID)
|
||||
|
||||
**Response (Success 200):**
|
||||
```json
|
||||
{
|
||||
"id": "msg-uuid",
|
||||
"message": "Hello world",
|
||||
"user_id": "user-uuid",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T12:00:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "John Doe"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error 404):**
|
||||
```json
|
||||
{
|
||||
"error": "specified session not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error 403):**
|
||||
```json
|
||||
{
|
||||
"error": "not allowed to join the specified session"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error 422):**
|
||||
```json
|
||||
{
|
||||
"errors": {
|
||||
"message": ["is too short (minimum is 1 character)"],
|
||||
"user": ["Global chat is disabled for school users"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Business Logic (from `ChatMessage.create`):**
|
||||
1. Validates message length (1-255 chars) and profanity
|
||||
2. HTML sanitizes message content (strict mode)
|
||||
3. For lesson channel:
|
||||
- Auto-determines source_user and target_user from lesson
|
||||
- Sets unread flags: `teacher_unread_messages` or `student_unread_messages`
|
||||
- Sends email notification if target user offline
|
||||
4. Saves message to database
|
||||
5. Broadcasts via WebSocket (see WebSocket section)
|
||||
|
||||
**Access Control:**
|
||||
- Global channel: Blocked for school users (`user.school_id` present)
|
||||
- Session channel: Must be participant in session (`music_session.access?(user)`)
|
||||
- Lesson channel: Must be teacher or student in lesson (`lesson_session.access?(user)`)
|
||||
- School protection: Can only chat with users from same school (unless platform instructor)
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Chat History
|
||||
|
||||
**Endpoint:** `GET /api/chat`
|
||||
|
||||
**Controller:** `ApiChatsController#index`
|
||||
**File:** `/web/app/controllers/api_chats_controller.rb`
|
||||
|
||||
**Query Parameters:**
|
||||
```
|
||||
GET /api/chat?channel=session&music_session=session-uuid&limit=20&page=0
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `channel` (string, required) - Channel type: `"global"`, `"session"`, `"lesson"`
|
||||
- `music_session` (string, conditional) - Required if `channel="session"`
|
||||
- `lesson_session` (string, conditional) - Required if `channel="lesson"`
|
||||
- `limit` (integer, optional) - Number of messages to fetch (default: 20)
|
||||
- `page` (integer, optional) - Page number (default: 0)
|
||||
- `start` (integer, optional) - Offset cursor for pagination
|
||||
|
||||
**Response (Success 200):**
|
||||
```json
|
||||
{
|
||||
"next": 20,
|
||||
"chats": [
|
||||
{
|
||||
"id": "msg-uuid",
|
||||
"message": "Hello world",
|
||||
"user_id": "user-uuid",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T12:00:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "John Doe"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `next` (integer|null) - Cursor for next page, or `null` if no more messages
|
||||
- `chats` (array) - Array of chat messages (newest first, due to `default_scope` order)
|
||||
|
||||
**Business Logic (from `ChatMessage.index`):**
|
||||
1. Queries by channel
|
||||
2. If `music_session` param: filters by `music_session_id`
|
||||
3. If `lesson_session` param: filters by `lesson_session_id`
|
||||
4. Orders by `created_at DESC` (newest first)
|
||||
5. Includes associated `user` records (eager loading)
|
||||
6. Returns pagination cursor if more messages available
|
||||
|
||||
**Access Control:**
|
||||
- Same as create endpoint (validates session/lesson access)
|
||||
- Global channel: Returns empty array for school users
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Messages
|
||||
|
||||
### Protocol
|
||||
- Uses **Protocol Buffers** (defined in `pb/src/client_container.proto`)
|
||||
- Routed through `websocket-gateway` service
|
||||
- Published via RabbitMQ to appropriate targets
|
||||
|
||||
### Message Type: CHAT_MESSAGE
|
||||
|
||||
**Protocol Buffer Definition:**
|
||||
```protobuf
|
||||
message ChatMessage {
|
||||
optional string sender_name = 1;
|
||||
optional string sender_id = 2;
|
||||
optional string msg = 3;
|
||||
optional string msg_id = 4;
|
||||
optional string created_at = 5;
|
||||
optional string channel = 6;
|
||||
optional string lesson_session_id = 7;
|
||||
optional string purpose = 8;
|
||||
optional string attachment_id = 9;
|
||||
optional string attachment_type = 10;
|
||||
optional string attachment_name = 11;
|
||||
}
|
||||
```
|
||||
|
||||
**JSON Equivalent (for jam-ui WebSocket handling):**
|
||||
```json
|
||||
{
|
||||
"sender_name": "John Doe",
|
||||
"sender_id": "user-uuid",
|
||||
"msg": "Hello world",
|
||||
"msg_id": "msg-uuid",
|
||||
"created_at": "2026-01-26T12:00:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"attachment_id": null,
|
||||
"attachment_type": null,
|
||||
"attachment_name": null
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions:**
|
||||
- `sender_name` - Display name of sender
|
||||
- `sender_id` - User UUID of sender
|
||||
- `msg` - Message text content
|
||||
- `msg_id` - Unique message ID (for deduplication)
|
||||
- `created_at` - ISO 8601 timestamp
|
||||
- `channel` - Channel type: `"global"`, `"session"`, `"lesson"`
|
||||
- `lesson_session_id` - Lesson UUID (if lesson channel)
|
||||
- `purpose` - Optional purpose: `"Notation File"`, `"Audio File"`, `"JamKazam Recording"`, `"Video Uploaded"`, etc.
|
||||
- `attachment_id` - ID of attached resource (notation, recording, video)
|
||||
- `attachment_type` - Type: `"notation"`, `"audio"`, `"recording"`, `"video"`
|
||||
- `attachment_name` - Display name of attachment
|
||||
|
||||
---
|
||||
|
||||
### Message Routing (from `ChatMessage.send_chat_msg`)
|
||||
|
||||
**Session Channel:**
|
||||
```ruby
|
||||
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
|
||||
```
|
||||
- Broadcasts to all users in the session
|
||||
- Excludes sender (by client_id)
|
||||
- Route target: `session:{session_id}`
|
||||
|
||||
**Global Channel:**
|
||||
```ruby
|
||||
@@mq_router.publish_to_active_clients(msg)
|
||||
```
|
||||
- Broadcasts to all active (online) clients
|
||||
- Route target: `__ALL_ACTIVE_CLIENTS__`
|
||||
|
||||
**Lobby Channel:**
|
||||
```ruby
|
||||
@@mq_router.publish_to_active_clients(msg)
|
||||
```
|
||||
- Same as global (TODO: should filter to lobby users only)
|
||||
- Route target: `__ALL_ACTIVE_CLIENTS__`
|
||||
|
||||
**Lesson Channel:**
|
||||
```ruby
|
||||
@@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id})
|
||||
@@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id})
|
||||
```
|
||||
- Sends to both teacher and student
|
||||
- Route target: `user:{user_id}`
|
||||
|
||||
---
|
||||
|
||||
### Client-Side WebSocket Handling
|
||||
|
||||
**Registration (legacy pattern):**
|
||||
```javascript
|
||||
context.JK.JamServer.registerMessageCallback(
|
||||
context.JK.MessageType.CHAT_MESSAGE,
|
||||
function (header, payload) {
|
||||
// Handle incoming chat message
|
||||
ChatActions.msgReceived(payload);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**jam-ui Integration:**
|
||||
- Uses `JamServer.js` helper for WebSocket management
|
||||
- Messages routed through Redux middleware (similar to MIXER_CHANGES, JAM_TRACK_CHANGES)
|
||||
- Should subscribe/unsubscribe based on session state
|
||||
|
||||
---
|
||||
|
||||
## Database Model
|
||||
|
||||
### Table: chat_messages
|
||||
|
||||
**Schema File:** `/ruby/db/schema.rb`
|
||||
|
||||
**Table Definition:**
|
||||
```sql
|
||||
CREATE TABLE chat_messages (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id VARCHAR(64) REFERENCES users(id),
|
||||
music_session_id VARCHAR(64) REFERENCES music_sessions(id),
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
channel VARCHAR(128) NOT NULL DEFAULT 'session',
|
||||
target_user_id VARCHAR(64) REFERENCES users(id),
|
||||
lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id) ON DELETE CASCADE,
|
||||
purpose VARCHAR(200),
|
||||
music_notation_id VARCHAR(64) REFERENCES music_notations(id),
|
||||
claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id)
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes:**
|
||||
```sql
|
||||
CREATE INDEX chat_messages_idx_channels ON chat_messages(channel);
|
||||
CREATE INDEX chat_messages_idx_created_at ON chat_messages(created_at);
|
||||
CREATE INDEX chat_messages_idx_music_session_id ON chat_messages(music_session_id);
|
||||
```
|
||||
|
||||
**Columns:**
|
||||
- `id` - UUID primary key
|
||||
- `user_id` - Sender user ID (FK to users)
|
||||
- `music_session_id` - Session ID (FK to music_sessions, nullable)
|
||||
- `message` - Message text (TEXT, NOT NULL)
|
||||
- `created_at` - Timestamp (default: current_timestamp)
|
||||
- `channel` - Channel type (default: "session")
|
||||
- `target_user_id` - Recipient user ID for direct messages (FK to users, nullable)
|
||||
- `lesson_session_id` - Lesson ID (FK to lesson_sessions, nullable, CASCADE delete)
|
||||
- `purpose` - Optional purpose/type (VARCHAR 200)
|
||||
- `music_notation_id` - Attached notation file (FK to music_notations, nullable)
|
||||
- `claimed_recording_id` - Attached recording (FK to claimed_recordings, nullable)
|
||||
|
||||
---
|
||||
|
||||
### ActiveRecord Model
|
||||
|
||||
**File:** `/ruby/lib/jam_ruby/models/chat_message.rb`
|
||||
|
||||
**Class:** `JamRuby::ChatMessage`
|
||||
|
||||
**Associations:**
|
||||
```ruby
|
||||
belongs_to :user
|
||||
belongs_to :music_session
|
||||
belongs_to :target_user, class_name: "JamRuby::User"
|
||||
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
|
||||
belongs_to :music_notation, class_name: "JamRuby::MusicNotation"
|
||||
belongs_to :claimed_recording, class_name: "JamRuby::ClaimedRecording"
|
||||
```
|
||||
|
||||
**Validations:**
|
||||
```ruby
|
||||
validates :user, presence: true
|
||||
validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true
|
||||
validate :same_school_protection
|
||||
validate :no_global_for_schools
|
||||
```
|
||||
|
||||
**Default Scope:**
|
||||
```ruby
|
||||
default_scope { order('created_at DESC') }
|
||||
```
|
||||
Note: Messages are ordered newest-first by default!
|
||||
|
||||
**HTML Sanitization:**
|
||||
```ruby
|
||||
html_sanitize strict: [:message]
|
||||
```
|
||||
Strips all HTML tags from message content.
|
||||
|
||||
---
|
||||
|
||||
### Constants
|
||||
|
||||
**Channels:**
|
||||
```ruby
|
||||
CHANNEL_SESSION = 'session'
|
||||
CHANNEL_LESSON = 'lesson'
|
||||
CHANNEL_LOBBY = 'lobby'
|
||||
```
|
||||
Note: `global` channel is used but not defined as constant.
|
||||
|
||||
---
|
||||
|
||||
## Related Models
|
||||
|
||||
### lesson_sessions Table
|
||||
|
||||
**Unread Message Tracking (for lesson chat):**
|
||||
```sql
|
||||
ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
|
||||
ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
|
||||
```
|
||||
|
||||
**Business Logic:**
|
||||
- When student sends message: sets `teacher_unread_messages = true`
|
||||
- When teacher sends message: sets `student_unread_messages = true`
|
||||
- Flags should be cleared when user opens lesson chat (not implemented in code yet)
|
||||
|
||||
**Email Notifications:**
|
||||
- If target user is offline (`!target.online?`) and message is present
|
||||
- Sends email via `UserMailer.lesson_chat(chat_msg).deliver_now`
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Diagrams
|
||||
|
||||
### Create Message Flow
|
||||
|
||||
```
|
||||
Client (jam-ui)
|
||||
↓ POST /api/chat
|
||||
ApiChatsController#create
|
||||
↓
|
||||
ChatMessage.create(user, session, message, channel, client_id, target_user, lesson)
|
||||
↓ Validates + Sanitizes
|
||||
↓ Saves to DB
|
||||
ChatMessage.send_chat_msg(...)
|
||||
↓
|
||||
MessageFactory.chat_message(...) → Protocol Buffer
|
||||
↓
|
||||
MQRouter.publish_to_*
|
||||
↓ RabbitMQ
|
||||
WebSocket Gateway
|
||||
↓ WebSocket
|
||||
All Clients (in channel)
|
||||
↓
|
||||
JamServer.handleMessage(CHAT_MESSAGE)
|
||||
↓
|
||||
ChatActions.msgReceived(payload)
|
||||
↓
|
||||
Redux Store Update
|
||||
↓
|
||||
UI Re-render
|
||||
```
|
||||
|
||||
### Get History Flow
|
||||
|
||||
```
|
||||
Client (jam-ui)
|
||||
↓ GET /api/chat?channel=session&music_session=123
|
||||
ApiChatsController#index
|
||||
↓
|
||||
ChatMessage.index(user, params)
|
||||
↓ Queries DB (with pagination)
|
||||
↓ Includes user records
|
||||
↓ Returns {chats: [...], next: cursor}
|
||||
Rabl Template (api_chats/index.rabl)
|
||||
↓ JSON serialization
|
||||
Client receives chat history
|
||||
↓
|
||||
Merges with Redux store
|
||||
↓
|
||||
UI displays messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example API Calls
|
||||
|
||||
### Create Session Message
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
POST /api/chat
|
||||
Content-Type: application/json
|
||||
Cookie: remember_token=...
|
||||
|
||||
{
|
||||
"message": "Great jam everyone!",
|
||||
"channel": "session",
|
||||
"music_session": "abc123",
|
||||
"client_id": "client-uuid-456"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "msg-789",
|
||||
"message": "Great jam everyone!",
|
||||
"user_id": "user-123",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T15:30:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "John Doe"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
}
|
||||
```
|
||||
|
||||
**WebSocket Broadcast (to all session participants):**
|
||||
```json
|
||||
{
|
||||
"type": "CHAT_MESSAGE",
|
||||
"chat_message": {
|
||||
"sender_name": "John Doe",
|
||||
"sender_id": "user-123",
|
||||
"msg": "Great jam everyone!",
|
||||
"msg_id": "msg-789",
|
||||
"created_at": "2026-01-26T15:30:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Session History
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
GET /api/chat?channel=session&music_session=abc123&limit=20&page=0
|
||||
Cookie: remember_token=...
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"next": 20,
|
||||
"chats": [
|
||||
{
|
||||
"id": "msg-789",
|
||||
"message": "Great jam everyone!",
|
||||
"user_id": "user-123",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T15:30:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "John Doe"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
},
|
||||
{
|
||||
"id": "msg-788",
|
||||
"message": "Let's start!",
|
||||
"user_id": "user-456",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T15:29:00.000Z",
|
||||
"channel": "session",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "Jane Smith"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Pagination:**
|
||||
- If `next` is not null: More messages available
|
||||
- Next request: `GET /api/chat?...&start=20`
|
||||
|
||||
---
|
||||
|
||||
### Create Global Message
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
POST /api/chat
|
||||
Content-Type: application/json
|
||||
Cookie: remember_token=...
|
||||
|
||||
{
|
||||
"message": "Anyone want to jam?",
|
||||
"channel": "global",
|
||||
"client_id": "client-uuid-456"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "msg-999",
|
||||
"message": "Anyone want to jam?",
|
||||
"user_id": "user-123",
|
||||
"session_id": null,
|
||||
"created_at": "2026-01-26T15:31:00.000Z",
|
||||
"channel": "global",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null,
|
||||
"user": {
|
||||
"name": "John Doe"
|
||||
},
|
||||
"music_notation": null,
|
||||
"claimed_recording": null
|
||||
}
|
||||
```
|
||||
|
||||
**WebSocket Broadcast (to all active clients):**
|
||||
```json
|
||||
{
|
||||
"type": "CHAT_MESSAGE",
|
||||
"chat_message": {
|
||||
"sender_name": "John Doe",
|
||||
"sender_id": "user-123",
|
||||
"msg": "Anyone want to jam?",
|
||||
"msg_id": "msg-999",
|
||||
"created_at": "2026-01-26T15:31:00.000Z",
|
||||
"channel": "global",
|
||||
"lesson_session_id": null,
|
||||
"purpose": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Missing Functionality
|
||||
|
||||
### Read/Unread Tracking
|
||||
|
||||
**Current State:**
|
||||
- ✅ Exists for **lesson chat**: `teacher_unread_messages`, `student_unread_messages` boolean flags
|
||||
- ❌ Does NOT exist for **session chat** or **global chat**
|
||||
- ❌ No per-message read receipts
|
||||
- ❌ No persistent unread counts
|
||||
|
||||
**What Needs to Be Added (Phase 6):**
|
||||
|
||||
1. **Database Schema Changes:**
|
||||
- Add `last_read_at` timestamp to track when user last viewed channel
|
||||
- OR: Add `unread_count` cache column per user/channel
|
||||
- OR: Add `message_reads` join table for per-message tracking (overkill for MVP)
|
||||
|
||||
2. **API Endpoints:**
|
||||
- `PUT /api/chat/mark_read` - Mark messages as read for current user/channel
|
||||
- `GET /api/chat/unread_count` - Get unread count per channel
|
||||
|
||||
3. **WebSocket Messages:**
|
||||
- Broadcast read receipts to other participants (optional, Phase 11)
|
||||
|
||||
4. **Business Logic:**
|
||||
- Increment unread count when message received (if user not viewing chat)
|
||||
- Reset unread count when user opens chat window
|
||||
- Show badge on chat button with unread count
|
||||
|
||||
**Implementation Strategy (to be designed in Phase 6 Plan 2):**
|
||||
- Use localStorage for client-side unread tracking (simple, no schema changes)
|
||||
- OR: Use server-side tracking with new `chat_message_reads` table (more robust)
|
||||
- Preference: Server-side for cross-device consistency
|
||||
|
||||
---
|
||||
|
||||
### File Attachments
|
||||
|
||||
**Current State:**
|
||||
- ✅ Lesson chat supports attachments (`music_notation`, `claimed_recording`)
|
||||
- ❌ Session chat does NOT support attachments
|
||||
- ❌ No UI in jam-ui for attaching files
|
||||
|
||||
**Out of Scope for Phase 6:**
|
||||
- File attachment functionality deferred to next milestone
|
||||
- Can be added later without breaking changes (fields already exist)
|
||||
|
||||
---
|
||||
|
||||
## jamClient Methods
|
||||
|
||||
**Conclusion:** Chat is purely web-based. No native client (C++) methods exist.
|
||||
|
||||
**Relevant jamClient Calls:**
|
||||
- `jamClient.UserAttention(true)` - Triggers system notification when message received (if chat hidden)
|
||||
|
||||
**Note:** In jam-ui, we won't use legacy jamClient directly. Instead, use `JamServer.js` for WebSocket and browser Notification API for desktop notifications.
|
||||
|
||||
---
|
||||
|
||||
## Security & Validation
|
||||
|
||||
### Message Validation
|
||||
- Length: 1-255 characters
|
||||
- Profanity filter: `no_profanity: true` validation
|
||||
- HTML sanitization: Strips all tags (`html_sanitize strict: [:message]`)
|
||||
|
||||
### Access Control
|
||||
- **Global Channel:**
|
||||
- Blocked for school users (`user.school_id` present)
|
||||
- All non-school users can send/receive
|
||||
- **Session Channel:**
|
||||
- Must be participant: `music_session.access?(current_user)`
|
||||
- Validated via `check_session` before_filter
|
||||
- **Lesson Channel:**
|
||||
- Must be teacher or student: `lesson_session.access?(current_user)`
|
||||
- Auto-routes messages between teacher/student
|
||||
- School protection: Can only chat with same school (unless platform instructor)
|
||||
|
||||
### Rate Limiting
|
||||
- **NOT IMPLEMENTED** in current API
|
||||
- Consider adding in Phase 11 (error handling)
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Database Indexes
|
||||
- ✅ Indexed: `channel`, `created_at`, `music_session_id`
|
||||
- ❌ NOT indexed: `lesson_session_id` (should add if lesson chat heavily used)
|
||||
- ❌ NOT indexed: `user_id` (consider adding for user history queries)
|
||||
|
||||
### Query Patterns
|
||||
- Default scope orders by `created_at DESC` - always scans newest messages first
|
||||
- Pagination uses offset (not cursor) - can be slow for deep pages
|
||||
- Eager loads `user` records - prevents N+1 queries
|
||||
|
||||
### Message Retention
|
||||
- Global channel: Client-side limit of 100 messages (`max_global_msgs`)
|
||||
- Session/lesson: No limit in database (messages persist forever)
|
||||
- **Consider:** Add TTL or archiving for old messages (future optimization)
|
||||
|
||||
---
|
||||
|
||||
## Integration Checklist for jam-ui
|
||||
|
||||
### Phase 7 (Data Layer):
|
||||
- [ ] Create Redux slice for chat messages (`chatSlice.js`)
|
||||
- [ ] Add API client methods: `fetchChatHistory()`, `sendChatMessage()`
|
||||
- [ ] Add WebSocket message handler for `CHAT_MESSAGE` type
|
||||
- [ ] Implement message deduplication (by `msg_id`)
|
||||
- [ ] Add read/unread tracking logic
|
||||
|
||||
### Phase 8 (UI Components):
|
||||
- [ ] Create `JKSessionChatWindow` component
|
||||
- [ ] Render message list with timestamps
|
||||
- [ ] Add message composition area (textarea + send button)
|
||||
- [ ] Implement auto-scroll on new messages
|
||||
- [ ] Add loading states (fetching history)
|
||||
|
||||
### Phase 9 (State Management):
|
||||
- [ ] Wire up Redux actions (send, receive, fetch history)
|
||||
- [ ] Integrate WebSocket subscription
|
||||
- [ ] Handle session join/leave (activate channel)
|
||||
- [ ] Sync unread counts with Redux state
|
||||
|
||||
### Phase 10 (Testing):
|
||||
- [ ] Unit tests: API client, Redux reducers
|
||||
- [ ] Integration tests: Message send/receive flow
|
||||
- [ ] E2E tests: Complete chat workflow
|
||||
|
||||
### Phase 11 (Error Handling):
|
||||
- [ ] Handle API errors (404, 403, 422)
|
||||
- [ ] Handle WebSocket disconnection
|
||||
- [ ] Retry failed message sends
|
||||
- [ ] Show error messages to user
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**REST API:**
|
||||
- ✅ Simple 2-endpoint design (create, index)
|
||||
- ✅ Channel-based routing (global, session, lesson)
|
||||
- ✅ Access control enforced
|
||||
- ❌ No read/unread tracking for session chat
|
||||
- ❌ No rate limiting
|
||||
|
||||
**WebSocket:**
|
||||
- ✅ Real-time delivery via Protocol Buffers
|
||||
- ✅ Efficient routing (session, user, broadcast)
|
||||
- ✅ Attachment metadata included
|
||||
|
||||
**Database:**
|
||||
- ✅ Single table for all channels
|
||||
- ✅ Foreign keys for associations
|
||||
- ✅ Indexes for common queries
|
||||
- ❌ No read tracking columns
|
||||
|
||||
**Missing Features:**
|
||||
- Read/unread tracking (session/global)
|
||||
- Rate limiting
|
||||
- Message deletion/editing
|
||||
- File attachments for session chat
|
||||
|
||||
**Next Steps:**
|
||||
- Analyze React patterns in jam-ui (CHAT_REACT_PATTERNS.md)
|
||||
- Design Redux state structure (Phase 6 Plan 2)
|
||||
- Design read/unread tracking strategy (Phase 6 Plan 2)
|
||||
Loading…
Reference in New Issue