19 KiB
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:
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-dialoglayout 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:
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:
{
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:
-
Channel Tabs (if not hideHeader):
- Loops through
@state.msgskeys - Displays "Global", "Session", "Lesson" tabs
- Active class on current channel
- Click handler:
activateChannel(channel)
- Loops through
-
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)
- Gets active channel messages:
-
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, callsChatActions.sendMsghandleEnter(evt): Shift+Enter = newline, Enter = sendcomponentDidUpdate(): Auto-scrolls to bottom on new messages$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:
{
limit: 20,
currentPage: 0,
next: null,
channel: 'global',
systemMsgId: 0,
msgs: {global:[], session:[]},
max_global_msgs: 100,
channelType: null,
lessonSessionId: null
}
Key Actions:
-
onSessionStarted(sessionId, lessonId):
- Called when session joins
- If lessonId: sets channel='lesson', channelType='lesson'
- Else: sets channel='session'
- Empties channel messages
- Calls
fetchHistory()
-
onActivateChannel(channel):
- Switches active channel
- Triggers UI update
-
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()
-
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()
-
fetchHistory(channel):
- Loads previous messages via
rest.getChatMessages(buildQuery()) - Calls
onLoadMessages(channel, response)
- Loads previous messages via
-
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()
- Converts server format to local format:
Server Message Conversion:
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_blastflag
4. ChatActions (Reflux Actions)
File: web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee
Actions Defined:
@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:
-
Panel Elements:
$panel=[layout-id="panelChat"]$count=#sidebar-chat-countbadge$chatMessagesScroller= scrollable message list$textBox= textarea input
-
Unread Count Badge:
incrementChatCount()- adds 1 to badgehighlightCount()- adds CSS class for visual highlightlowlightCount()- removes highlight on panel opensetCount(0)- resets badge on open
-
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
-
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
- Registers callback:
-
Infinite Scroll:
- Uses jQuery infinitescroll plugin
- Loads older messages on scroll up
- Path:
/api/sessions/:id/chats?page=1 - Handles pagination with
nextcursor
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)
{
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)
{
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)
GET /api/chat?channel=session&music_session=123&limit=20&page=0
REST API Response (Get History)
{
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)
{
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
- Sidebar renders with
<%= react_component 'ChatWindow', {} %> - chatPanel.js initializes on sidebar load
- ChatStore listens to AppStore, registers WebSocket callback
- ChatWindow mounts and subscribes to ChatStore
Session Join
- Session start event triggers
chatPanel.sessionStarted(e, data)called- Calls
ChatActions.sessionStarted(sessionId, lessonId) - ChatStore switches to session/lesson channel
- Fetches message history
- Panel expands (if collapsed)
Session Leave
- Session stop event triggers
chatPanel.sessionStopped()called- Resets state
- 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
onLoadMessageschecks 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
purposefield - Purpose types: Notation File, Audio File, JamKazam Recording, Video Uploaded
- Attachments linked via
music_notationorclaimed_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 (
=>) bindthiscontext - Slim arrows (
->) don't bind context - Used extensively in callbacks
Global Variables
window.ChatActions,window.ChatStore,window.JK.*- No module system, all globals
context = windowat 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 patternsChatStore.js.coffee- state management logicchatPanel.js- sidebar integration
Secondary:
ChatDialog.js.jsx.coffee- dialog wrapperChatActions.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)