feat(06-01): document legacy chat implementation patterns

- Analyze React CoffeeScript components (ChatDialog, ChatWindow)
- Document Reflux state management (ChatStore, ChatActions)
- Map jQuery sidebar integration (chatPanel.js)
- Capture message flow and data structures
- Identify UI patterns and integration points
- Note quirks and modernization opportunities

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-26 15:17:26 +05:30
parent eca7f95c23
commit 940009c22d
1 changed files with 679 additions and 0 deletions

View File

@ -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 `<ChatWindow>` 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: `<h2>chat<div id="sidebar-chat-count" class="badge">0</div></h2>`
- 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: `` `<div>{foo}</div>` ``
- 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)