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:
parent
eca7f95c23
commit
940009c22d
|
|
@ -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)
|
||||
Loading…
Reference in New Issue