diff --git a/.planning/codebase/CHAT_LEGACY.md b/.planning/codebase/CHAT_LEGACY.md new file mode 100644 index 000000000..2be470220 --- /dev/null +++ b/.planning/codebase/CHAT_LEGACY.md @@ -0,0 +1,679 @@ +# Legacy Chat Implementation Analysis + +**Purpose:** Document how session chat works in the legacy jQuery/CoffeeScript codebase to inform React redesign. + +**Date:** 2026-01-26 +**Phase:** 06-session-chat-research-design +**Plan:** 01 + +--- + +## Overview + +The legacy chat implementation consists of: +- **React CoffeeScript components** for UI rendering (`ChatDialog`, `ChatWindow`) +- **Reflux stores/actions** for state management (`ChatStore`, `ChatActions`) +- **jQuery panel integration** for sidebar chat (`chatPanel.js`) +- **REST API + WebSocket** for message delivery +- **Multiple channels**: global, session, lesson + +The chat system supports modeless dialogs (can be repositioned) and multi-channel messaging. + +--- + +## File Locations + +### React Components (CoffeeScript) +- `/web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee` - Modal dialog wrapper +- `/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee` - Main chat UI component + +### State Management (Reflux) +- `/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee` - Central chat state store +- `/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee` - Chat actions + +### jQuery Integration +- `/web/app/assets/javascripts/chatPanel.js` - Sidebar panel integration +- `/web/app/views/clients/_sidebar.html.erb` - Sidebar HTML template + +### Styling +- `/web/app/assets/stylesheets/dialogs/chatDialog.scss` +- `/web/app/assets/stylesheets/client/react-components/ChatWindow.scss` + +### Views +- `/web/app/views/dialogs/_chatDialog.html.slim` - Dialog container template + +--- + +## Component Architecture + +### 1. ChatDialog (Modal Wrapper) + +**File:** `web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee` + +**Purpose:** Wraps ChatWindow in a modeless dialog for expanded view. + +**Key Features:** +- Uses React.createClass pattern +- Mixins: PostProcessorMixin, Reflux listeners (AppStore, SessionStore) +- Manages dialog lifecycle (`beforeShow`, `afterHide`) +- Parses channel IDs (`session_123`, `lesson_456`, `global`) +- Activates appropriate chat channel on open + +**Channel Parsing Logic:** +```coffeescript +parseId:(id) -> + if !id? + {id: null, type: null} + else + bits = id.split('_') + if bits.length == 2 + {id: bits[1], type: bits[0]} + else + {id: null, type: null} +``` + +**Dialog Binding:** +- Binds to `chat-dialog` layout ID +- Controlled by `@app.layout.showDialog('chat-dialog', {d1: 'global'})` +- Auto-switches back to global channel on close (if not in session) + +**Render:** +- Displays dialog title (dynamic based on lesson/session) +- Embeds `` with props: + - `newFormat={true}` + - `channel={lessonSessionId}` + - `hideHeader={true}` + - `rootClass="ChatDialog"` + - `showEmailNotice={true}` + - `showClose={true}` + +--- + +### 2. ChatWindow (Main UI Component) + +**File:** `web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee` + +**Purpose:** Core chat UI - renders messages, handles composition, tab switching. + +**Mixins:** +- `Reflux.listenTo(@AppStore, "onAppInit")` +- `Reflux.listenTo(@UserStore, "onUserChanged")` +- `Reflux.listenTo(@ChatStore, "onChatChanged")` + +**State Structure:** +```coffeescript +state = { + msgs: { + global: [msg1, msg2, ...], + session: [...], + lesson_123: [...] # lesson channels use lesson_session_id as key + }, + channel: 'global', # active channel + channelType: null, # 'lesson' or null + lessonSessionId: 123 # if lesson channel +} +``` + +**Message Object Format:** +```javascript +{ + msg_id: 456, + sender_id: "user_id", + sender_name: "John Doe", # or "me" for current user + msg: "Hello world", + created_at: "2026-01-26T12:00:00Z", + channel: "session", # or "global", "lesson" + purpose: null, # or "Notation File", "JamKazam Recording", etc. + music_notation: {...}, # if purpose is notation/audio + claimed_recording: {...} # if purpose is recording/video +} +``` + +**Rendering Logic:** + +1. **Channel Tabs** (if not hideHeader): + - Loops through `@state.msgs` keys + - Displays "Global", "Session", "Lesson" tabs + - Active class on current channel + - Click handler: `activateChannel(channel)` + +2. **Message List**: + - Gets active channel messages: `@state.msgs[activeChannel]` + - Maps messages to chat-message divs + - Uses jQuery timeago for timestamps: `$.timeago(msg.created_at)` + - Displays sender as "me" or sender_name + - Shows purpose badge (if present): "attached a notation file" + - Adds attachment links (clickable for notation/recording/video) + +3. **Message Composition**: + - Textarea with placeholder "enter message" + - SEND button (orange) + - CLOSE button (if showClose prop) + - Email notice (if showEmailNotice and other user offline) + - "Attach file" button (for lesson channels) + +**Key Methods:** + +- `sendMessage()`: Validates non-empty, checks connection, calls `ChatActions.sendMsg` +- `handleEnter(evt)`: Shift+Enter = newline, Enter = send +- `componentDidUpdate()`: Auto-scrolls to bottom on new messages + ```coffeescript + $scroller = @root.find('.chat-list-scroller') + $scroller.animate({scrollTop: $scroller[0].scrollHeight}, speed) + ``` + +**Attachment Handling:** +- Purpose types: "Notation File", "Audio File", "JamKazam Recording", "Video Uploaded" +- Converts purpose to friendly text: `convertPurpose(purpose)` +- Links trigger: `notationClicked`, `recordingClicked`, `videoClicked` +- Opens external links: `context.JK.popExternalLink(url)` + +**Lesson Actions Menu:** +- Uses jQuery plugin: `$node.lessonSessionActions(lesson)` +- Actions: attach-recording, attach-notation, attach-audio +- Triggers: `AttachmentActions.startAttachRecording`, etc. + +--- + +### 3. ChatStore (Reflux Store) + +**File:** `web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee` + +**Purpose:** Central state management for all chat channels. + +**State Properties:** +```coffeescript +{ + limit: 20, + currentPage: 0, + next: null, + channel: 'global', + systemMsgId: 0, + msgs: {global:[], session:[]}, + max_global_msgs: 100, + channelType: null, + lessonSessionId: null +} +``` + +**Key Actions:** + +1. **onSessionStarted(sessionId, lessonId):** + - Called when session joins + - If lessonId: sets channel='lesson', channelType='lesson' + - Else: sets channel='session' + - Empties channel messages + - Calls `fetchHistory()` + +2. **onActivateChannel(channel):** + - Switches active channel + - Triggers UI update + +3. **onSendMsg(msg, done, fail, target_user, channel):** + - Builds message payload + - Calls `rest.createChatMessage()` + - On success: adds message to local state (for session/lesson) + - Triggers `changed()` + +4. **onMsgReceived(msg):** + - Handles incoming WebSocket messages + - Routes to correct channel (global/session/lesson_id) + - Appends to channel's msg array + - For global: limits to max_global_msgs (trims old messages) + - Triggers `changed()` + +5. **fetchHistory(channel):** + - Loads previous messages via `rest.getChatMessages(buildQuery())` + - Calls `onLoadMessages(channel, response)` + +6. **onLoadMessages(channel, msgs):** + - Converts server format to local format: `convertServerMessages(chats)` + - Merges with existing messages (dedupes by msg_id) + - Sorts by created_at timestamp + - Triggers `changed()` + +**Server Message Conversion:** +```coffeescript +convertServerMessages: (chats) -> + for chat in chats + { + sender_name: chat.user?.name + sender_id: chat.user_id + msg: chat.message + msg_id: chat.id + created_at: chat.created_at + channel: chat.channel + purpose: chat.purpose + music_notation: chat.music_notation + claimed_recording: chat.claimed_recording + } +``` + +**System Messages:** +- Auto-generates system messages on user activity changes +- Shows "You've come back!" or "You've become inactive..." +- Only if `!gon.chat_blast` flag + +--- + +### 4. ChatActions (Reflux Actions) + +**File:** `web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee` + +**Actions Defined:** +```coffeescript +@ChatActions = Reflux.createActions({ + msgReceived: {} + sendMsg: {} + loadMessages: {} + emptyChannel: {} + sessionStarted: {} + activateChannel: {} + fullyOpened: {} + initializeLesson: {} +}) +``` + +**Usage Pattern:** +- `window.ChatActions.activateChannel('session')` +- `window.ChatActions.sendMsg(msg, doneCb, failCb, targetUser, channel)` +- `window.ChatActions.msgReceived(payload)` - called from WebSocket handler + +--- + +### 5. chatPanel.js (jQuery Sidebar Integration) + +**File:** `web/app/assets/javascripts/chatPanel.js` + +**Purpose:** Manages chat panel in sidebar (collapsed/expanded state). + +**Key Features:** + +1. **Panel Elements:** + - `$panel` = `[layout-id="panelChat"]` + - `$count` = `#sidebar-chat-count` badge + - `$chatMessagesScroller` = scrollable message list + - `$textBox` = textarea input + +2. **Unread Count Badge:** + - `incrementChatCount()` - adds 1 to badge + - `highlightCount()` - adds CSS class for visual highlight + - `lowlightCount()` - removes highlight on panel open + - `setCount(0)` - resets badge on open + +3. **Session Lifecycle:** + - `sessionStarted(e, data)`: + - Shows chat panel + - Resets state + - Calls `ChatActions.sessionStarted(sessionId, lessonId)` + - Sets `showing = true` + - `sessionStopped(e, data)`: + - Hides panel + - Resets state + +4. **WebSocket Integration:** + - Registers callback: `context.JK.JamServer.registerMessageCallback(CHAT_MESSAGE, handler)` + - `chatMessageReceived(payload)`: + - If panel visible: do nothing (already showing) + - Else: increment count, highlight, call `jamClient.UserAttention(true)` + - Calls `context.ChatActions.msgReceived(payload)` to update store + +5. **Infinite Scroll:** + - Uses jQuery infinitescroll plugin + - Loads older messages on scroll up + - Path: `/api/sessions/:id/chats?page=1` + - Handles pagination with `next` cursor + +**Integration Points:** +- Initialized from sidebar: `chatPanel.initialize(sidebar)` +- Event listeners: `$panel.on('open', opened)`, `$panel.on('fullyOpen', fullyOpened)` +- Hooked to session events via jQuery triggers + +--- + +## State Management Flow + +### Message Received (WebSocket) + +``` +WebSocket Gateway + ↓ +chatPanel.chatMessageReceived(payload) + ↓ (if hidden panel) +incrementChatCount() + highlightCount() + ↓ +ChatActions.msgReceived(payload) + ↓ +ChatStore.onMsgReceived(msg) + ↓ (appends to msgs[channel]) +ChatStore.changed() → trigger(state) + ↓ +ChatWindow.onChatChanged(state) + ↓ +setState() → re-render + ↓ +componentDidUpdate() → auto-scroll +``` + +### Message Sent (User Action) + +``` +User clicks SEND + ↓ +ChatWindow.sendMessage() + ↓ +ChatActions.sendMsg(msg, done, fail, targetUser, channel) + ↓ +ChatStore.onSendMsg(...) + ↓ +rest.createChatMessage(payload) → POST /api/chat + ↓ (on success) +ChatStore.onMsgReceived({local message}) + ↓ +ChatStore.changed() → trigger(state) + ↓ +ChatWindow.onChatChanged(state) → re-render +``` + +### Session Started + +``` +Session joins + ↓ +chatPanel.sessionStarted(e, data) + ↓ +ChatActions.sessionStarted(sessionId, lessonId) + ↓ +ChatStore.onSessionStarted(sessionId, lessonId) + ↓ (sets channel, empties msgs) +ChatStore.fetchHistory() + ↓ +rest.getChatMessages() → GET /api/chat + ↓ +ChatStore.onLoadMessages(channel, response) + ↓ (merges + sorts messages) +ChatStore.changed() → trigger(state) + ↓ +ChatWindow → re-render with history +``` + +--- + +## Data Structures + +### REST API Request (Create Message) +```javascript +{ + message: "Hello world", + music_session: 123, // if session channel + lesson_session: 456, // if lesson channel + channel: "session", // or "global", "lesson" + client_id: "uuid-client-id", + target_user: 789 // if lesson (direct message) +} +``` + +### REST API Response (Create Message) +```javascript +{ + id: 999, + message: "Hello world", + channel: "session", + created_at: "2026-01-26T12:00:00Z", + lesson_session_id: 456 // if lesson +} +``` + +### REST API Request (Get History) +```javascript +GET /api/chat?channel=session&music_session=123&limit=20&page=0 +``` + +### REST API Response (Get History) +```javascript +{ + chats: [ + { + id: 999, + message: "Hello", + user_id: 123, + user: {name: "John Doe"}, + created_at: "2026-01-26T12:00:00Z", + channel: "session", + purpose: null, + music_notation: null, + claimed_recording: null + } + ], + next: 20 // or null if no more +} +``` + +### WebSocket Message (Incoming) +```javascript +{ + sender_id: "123", + sender_name: "John Doe", + msg: "Hello world", + msg_id: "999", + created_at: "2026-01-26T12:00:00Z", + channel: "session", // or "global", "lesson" + lesson_session_id: "456", // if lesson + purpose: null, + attachment_id: null, + attachment_type: null, + attachment_name: null +} +``` + +--- + +## UI Patterns + +### Sidebar Chat Panel + +**Location:** Left sidebar, collapsible panel + +**Elements:** +- **Header:** "chat" title + unread count badge +- **Message list:** Scrollable div with chat-message items +- **Input area:** Textarea + SEND button + +**States:** +- Collapsed (header only) +- Expanded (full UI) +- Highlighted badge (unread messages) + +**Behavior:** +- Opens on session start +- Auto-collapses on session end +- Badge resets on panel open +- Auto-scrolls to bottom on new message + +### Expanded Chat Dialog + +**Location:** Modeless dialog (draggable, repositionable) + +**Opened via:** +- "Expand" link in sidebar chat +- `@app.layout.showDialog('chat-dialog', {d1: 'global'})` + +**Features:** +- Same ChatWindow component +- Larger view +- Shows tabs (Global/Session/Lesson) +- CLOSE button +- Email notice for offline users (lesson chat) + +**Use Cases:** +- Global chat (when not in session) +- Lesson chat (teacher-student) +- Expanded session chat (more room) + +--- + +## Integration with Session UI + +### Initialization +1. Sidebar renders with `<%= react_component 'ChatWindow', {} %>` +2. chatPanel.js initializes on sidebar load +3. ChatStore listens to AppStore, registers WebSocket callback +4. ChatWindow mounts and subscribes to ChatStore + +### Session Join +1. Session start event triggers +2. `chatPanel.sessionStarted(e, data)` called +3. Calls `ChatActions.sessionStarted(sessionId, lessonId)` +4. ChatStore switches to session/lesson channel +5. Fetches message history +6. Panel expands (if collapsed) + +### Session Leave +1. Session stop event triggers +2. `chatPanel.sessionStopped()` called +3. Resets state +4. ChatStore switches back to global (if afterHide callback triggers) + +### Chat Button (Unread Badge) +- Lives in sidebar header: `

chat

` +- Updated via `chatPanel.setCount(n)`, `incrementChatCount()` +- Highlighted via CSS class `.highlighted` +- Screenshot shows red badge with number + +--- + +## Key Observations + +### Modeless Dialog Pattern +- Uses custom layout system: `layout='dialog'`, `layout-id='chat-dialog'` +- Dialogs can be repositioned, stay open while using other UI +- Controlled via `@app.layout.showDialog()`, `closeDialog()` + +### Multi-Channel Architecture +- Single store manages all channels (global, session, lesson_N) +- Active channel tracked in state +- Messages keyed by channel ID +- Lesson channels use lesson_session_id as key (not string "lesson") + +### Message Deduplication +- `onLoadMessages` checks existing messages by msg_id +- Prevents duplicate rendering when merging history + real-time + +### Auto-Scroll Behavior +- Scrolls instantly on channel switch (`speed = 0`) +- Scrolls slowly on new message (`speed = 'slow'`) +- Uses jQuery animate to scroll to `scrollHeight` + +### Reflux Pattern +- Actions trigger store methods +- Store emits `changed()` → `trigger(state)` +- Components listen via `listenTo(Store, callback)` +- State flows one-way: Actions → Store → Components + +### jQuery-React Hybrid +- React components for rendering +- jQuery for DOM manipulation (scrolling, timeago, menu plugins) +- `getDOMNode()` used to get root element for jQuery + +### Attachment Support +- Messages can have `purpose` field +- Purpose types: Notation File, Audio File, JamKazam Recording, Video Uploaded +- Attachments linked via `music_notation` or `claimed_recording` +- Displays clickable links to open attachments + +### Read/Unread Tracking +- **NOT implemented in session/global chat** +- Only exists for **lesson chat** (teacher_unread_messages, student_unread_messages fields in lesson_session) +- Sidebar badge counts unread session messages while chat is closed +- Badge resets on panel open (no persistent tracking) +- **This is NEW functionality we need to add for session chat** + +--- + +## Quirks & Legacy Patterns + +### CoffeeScript Arrow Functions +- Fat arrows (`=>`) bind `this` context +- Slim arrows (`->`) don't bind context +- Used extensively in callbacks + +### Global Variables +- `window.ChatActions`, `window.ChatStore`, `window.JK.*` +- No module system, all globals +- `context = window` at top of files + +### jQuery Plugins +- Custom plugins like `.lessonSessionActions()`, `.btOn()` +- Defined elsewhere, not documented here + +### Timeago +- Uses jQuery timeago plugin: `$('.timeago').timeago()` +- Converts ISO timestamps to "5 minutes ago" + +### Template Strings in CoffeeScript +- Uses backticks for JSX: `` `
{foo}
` `` +- Interpolation with `{expression}` + +### Mixed Responsibilities +- ChatWindow handles both UI and business logic +- Direct jQuery DOM manipulation in React component +- No clear separation of concerns + +--- + +## Summary for React Redesign + +### Keep These Concepts: +- Multi-channel architecture (global, session, lesson) +- Message object format (sender, msg, timestamp, channel, purpose) +- Auto-scroll on new messages +- Unread badge in sidebar +- Modeless dialog for expanded view +- WebSocket + REST hybrid (history via API, real-time via WebSocket) + +### Modernize These Patterns: +- Replace Reflux with Redux +- Replace CoffeeScript with TypeScript +- Replace jQuery DOM manipulation with React hooks +- Replace getDOMNode() with refs +- Use React Router for dialog management (or WindowPortal) +- Use modern scroll APIs (scrollIntoView, scrollTo) +- Use date-fns or dayjs instead of jQuery timeago + +### Add These Features (NEW): +- Read/unread tracking for session chat (already exists for lessons) +- Persistent badge counts (store in localStorage or server) +- Notification sounds (optional) +- Typing indicators (optional, future milestone) + +### Remove These Patterns: +- Global variables +- jQuery plugins +- React.createClass (use functional components) +- Mixins (use custom hooks) +- Manual state synchronization (use Redux) + +--- + +## Files to Reference During Implementation + +**Essential:** +- `ChatWindow.js.jsx.coffee` - core UI patterns +- `ChatStore.js.coffee` - state management logic +- `chatPanel.js` - sidebar integration + +**Secondary:** +- `ChatDialog.js.jsx.coffee` - dialog wrapper +- `ChatActions.js.coffee` - action definitions + +**Ignore for MVP:** +- Attachment handling (out of scope Phase 6) +- Lesson-specific logic (may not port to jam-ui) +- Infinite scroll (can implement simpler pagination) + +--- + +**Next Steps:** +- Document API surface (CHAT_API.md) +- Analyze React patterns in jam-ui (CHAT_REACT_PATTERNS.md) +- Design Redux state structure (Phase 6 Plan 2)