jam-cloud/.planning/codebase/CHAT_LEGACY.md

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-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:

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:

  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
    $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:

  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:

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:

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

{
  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

  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)