# 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)
