# React Patterns for Session Chat in jam-ui

**Purpose:** Document available React patterns in jam-ui, identify what can be reused for session chat, and highlight gaps.

**Date:** 2026-01-26
**Phase:** 06-session-chat-research-design
**Plan:** 01

---

## Overview

This document analyzes existing jam-ui patterns from Phases 2-5 to identify:
- ✅ **What's Available** - Components, patterns, hooks we can reuse
- ❌ **What's Missing** - Gaps that need to be filled for session chat
- 🧪 **TDD Candidates** - What should follow Test-Driven Development

---

## Available Components

### 1. WindowPortal (Modeless Dialog)

**File:** `/jam-ui/src/components/common/WindowPortal.js`

**Purpose:** Creates a popup window for modeless dialogs (used by Metronome, JamTrack player)

**Features:**
- ✅ Opens new browser window with custom dimensions
- ✅ Copies all stylesheets from parent to popup
- ✅ Auto-detects window close and triggers callback
- ✅ Cross-window message passing (`postMessage` API)
- ✅ Auto-cleanup on parent window close
- ✅ Window ID tracking for multi-instance support

**Props:**
```javascript
{
  children: ReactNode,
  onClose: () => void,
  windowFeatures: string, // "width=400,height=300,left=200,top=200,..."
  title: string,
  onWindowReady: (window, sendMessage) => void,
  onWindowMessage: (data) => void,
  windowId: string
}
```

**Example Usage (from JKSessionMetronomePlayer):**
```javascript
{isWindowOpen && (
  <WindowPortal
    windowFeatures="width=400,height=580,left=200,top=200,menubar=no,toolbar=no"
    title="Metronome Controls"
    onClose={() => setIsWindowOpen(false)}
    windowId="metronome-controls"
  >
    <div className="metronome-popup-content">
      {/* Metronome controls */}
    </div>
  </WindowPortal>
)}
```

**Reuse for Chat:**
- ✅ Perfect for expanded chat dialog
- ✅ No modifications needed
- ✅ Already handles styling, lifecycle, cleanup
- **Recommendation:** Use as-is for Phase 8 (expanded chat window)

---

### 2. JKModal / Dialog Patterns

**Status:** ❌ NOT FOUND in jam-ui codebase

**Legacy Pattern (web app):**
- Uses custom layout system: `layout='dialog'`, `layout-id='chat-dialog'`
- Controlled via `@app.layout.showDialog()`, `closeDialog()`

**Gap:**
- jam-ui doesn't have equivalent modal/dialog component
- WindowPortal is used instead for modeless dialogs
- For modal-style dialogs, would need to create new component

**Recommendation:**
- For session chat, use WindowPortal (matches legacy behavior)
- If modal dialog needed later, create `JKModal.js` component (Phase 11)

---

### 3. Button Components

**Files:** Various throughout jam-ui (not centralized yet)

**Existing Patterns:**
```javascript
// Orange button (primary action)
<button className="button-orange">SEND</button>

// Grey button (secondary action)
<button className="button-grey">CLOSE</button>

// Icon buttons
<button className="icon-button">
  <img src="/assets/icon.png" />
</button>
```

**Reuse for Chat:**
- ✅ Use existing button classes
- ✅ No custom button component needed for MVP
- **Recommendation:** Reuse CSS classes from legacy (already styled)

---

### 4. Input Components

**Files:** Various throughout jam-ui

**Existing Patterns:**
```javascript
// Textarea
<textarea
  placeholder="enter message"
  onKeyDown={handleEnter}
  value={message}
  onChange={(e) => setMessage(e.target.value)}
/>

// Controlled input
<input
  type="text"
  value={value}
  onChange={handleChange}
/>
```

**Reuse for Chat:**
- ✅ Use standard HTML textarea
- ✅ Add Enter-to-send logic (Shift+Enter for newline)
- **Recommendation:** Follow existing patterns (no custom component)

---

## Redux State Patterns

### 1. Existing Chat Slices

#### lobbyChatMessagesSlice.js

**File:** `/jam-ui/src/store/features/lobbyChatMessagesSlice.js`

**Purpose:** Manages lobby (global) chat messages

**State Structure:**
```javascript
{
  records: {
    messages: [
      {
        id: "msg-uuid",
        message: "Hello",
        user_id: "user-uuid",
        user: { name: "John Doe" },
        created_at: "2026-01-26T12:00:00Z",
        channel: "lobby",
        status: "delivered" // added by reducer
      }
    ],
    next: 20 // pagination cursor
  },
  status: 'idle' | 'loading' | 'succeeded' | 'failed',
  error: null,
  create_status: 'idle' | 'loading' | 'succeeded' | 'failed'
}
```

**Async Thunks:**
```javascript
fetchLobbyChatMessages({ channel: 'lobby', lastOnly: false })
postNewChatMessage({ message, channel, music_session, client_id })
```

**Reducers:**
```javascript
addMessage(state, action) // Add message to state (for WebSocket)
```

**Key Features:**
- ✅ Sorts messages by `created_at` (oldest first)
- ✅ Maps server response to local format
- ✅ Adds `status: 'delivered'` field
- ✅ Handles pagination with `next` cursor

**Gaps:**
- ❌ No message deduplication (by msg_id)
- ❌ No channel switching logic
- ❌ No unread tracking
- ❌ Only supports lobby channel (not session/lesson)

---

#### textMessagesSlice.js

**File:** `/jam-ui/src/store/features/textMessagesSlice.js`

**Purpose:** Manages 1-on-1 text messages (direct messages, not chat)

**State Structure:**
```javascript
{
  messages: [
    {
      id: "msg-uuid",
      message: "Hello",
      senderId: "user-uuid",
      senderName: "John Doe",
      receiverId: "user-uuid",
      receiverName: "Jane Smith",
      createdAt: "2026-01-26T12:00:00Z",
      sent: true
    }
  ],
  status: 'idle' | 'loading' | 'succeeded' | 'failed',
  error: null
}
```

**Async Thunks:**
```javascript
fetchMessagesByReceiverId({ userId, offset, limit })
postNewMessage({ message, target_user_id })
```

**Key Features:**
- ✅ Message deduplication (by id)
- ✅ Restructures server messages to local format
- ✅ Merges fetched messages with existing state

**Gaps:**
- ❌ Only supports 1-on-1 messages (no channels)
- ❌ No WebSocket integration
- ❌ No unread tracking

---

### 2. Patterns from Other Slices

#### mediaSlice.js (Backing Track, JamTrack)

**File:** `/jam-ui/src/store/features/mediaSlice.js`

**Key Patterns:**

1. **Async Thunks with jamClient Integration:**
```javascript
export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, jamClient, jamServer }, { dispatch, rejectWithValue }) => {
    try {
      // Call native client methods
      const result = await jamClient.JamTrackPlay(fqId);
      return { jamTrack, fqId, result };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

2. **Redux State for Media:**
```javascript
{
  backingTrack: {
    file: null,
    status: 'idle' | 'loading' | 'playing' | 'error',
    error: null
  },
  jamTrack: {
    current: null,
    isPlaying: false,
    isPaused: false,
    state: 'idle' | 'downloading' | 'loading' | 'playing' | 'error'
  }
}
```

3. **Extra Reducers:**
```javascript
extraReducers: builder => {
  builder
    .addCase(loadJamTrack.pending, (state) => {
      state.jamTrack.state = 'loading';
    })
    .addCase(loadJamTrack.fulfilled, (state, action) => {
      state.jamTrack.current = action.payload.jamTrack;
      state.jamTrack.state = 'playing';
    })
    .addCase(loadJamTrack.rejected, (state, action) => {
      state.jamTrack.state = 'error';
      state.jamTrack.error = action.payload;
    });
}
```

**Reuse for Chat:**
- ✅ Follow async thunk pattern for API calls
- ✅ Use pending/fulfilled/rejected states
- ✅ Structure state by feature (not by data type)

---

#### activeSessionSlice.js

**File:** `/jam-ui/src/store/features/activeSessionSlice.js`

**Key Patterns:**

1. **Session State Structure:**
```javascript
{
  sessionId: null,
  participants: [],
  tracks: [],
  jamTracks: [],
  backingTracks: [],
  metronome: null,
  connectionStatus: 'disconnected' | 'connected'
}
```

2. **Reducers for Real-Time Updates:**
```javascript
addParticipant(state, action) {
  state.participants.push(action.payload);
}

removeParticipant(state, action) {
  state.participants = state.participants.filter(
    p => p.id !== action.payload
  );
}

updateParticipant(state, action) {
  const index = state.participants.findIndex(
    p => p.id === action.payload.id
  );
  if (index !== -1) {
    state.participants[index] = action.payload;
  }
}
```

**Reuse for Chat:**
- ✅ Follow add/remove/update pattern for messages
- ✅ Store session-related data in session slice
- ✅ Use connection status for chat availability

---

#### sessionUISlice.js

**File:** `/jam-ui/src/store/features/sessionUISlice.js`

**Key Patterns:**

1. **UI State (not data state):**
```javascript
{
  chatWindowOpen: false,
  chatWindowPosition: { x: 200, y: 200 },
  unreadChatCount: 0,
  activeChannel: 'session'
}
```

2. **UI Actions:**
```javascript
openChatWindow(state) {
  state.chatWindowOpen = true;
}

closeChatWindow(state) {
  state.chatWindowOpen = false;
  state.unreadChatCount = 0;
}

incrementUnreadCount(state) {
  state.unreadChatCount += 1;
}
```

**Reuse for Chat:**
- ✅ Store UI state separately from data
- ✅ Track window open/close in sessionUISlice
- ✅ Track unread count per channel

---

### 3. Recommended Redux Architecture for Chat

**New Slice: sessionChatSlice.js**

```javascript
{
  // Message storage (keyed by channel for efficient lookup)
  messagesByChannel: {
    'global': [msg1, msg2, ...],
    'session-uuid': [msg1, msg2, ...],
    'lesson-uuid': [msg1, msg2, ...]
  },

  // Active channel
  activeChannel: 'session-uuid',
  channelType: 'session', // 'global', 'session', 'lesson'

  // UI state
  isWindowOpen: false,
  windowPosition: { x: 200, y: 200 },

  // Unread counts (keyed by channel)
  unreadCounts: {
    'global': 0,
    'session-uuid': 3,
    'lesson-uuid': 0
  },

  // Fetch status per channel
  fetchStatus: {
    'global': 'idle',
    'session-uuid': 'loading'
  },

  // Send status
  sendStatus: 'idle' | 'loading' | 'succeeded' | 'failed',
  sendError: null,

  // Pagination cursors per channel
  nextCursors: {
    'global': 20,
    'session-uuid': null // no more messages
  }
}
```

**Async Thunks:**
```javascript
fetchChatHistory({ channel, channelType, sessionId, lessonId })
sendChatMessage({ message, channel, channelType, sessionId, targetUserId })
markAsRead({ channel })
```

**Reducers:**
```javascript
addMessageFromWebSocket(state, action) // Real-time message
setActiveChannel(state, action)
incrementUnreadCount(state, action)
resetUnreadCount(state, action)
openChatWindow(state)
closeChatWindow(state)
```

**Integration with activeSessionSlice:**
- ✅ Get sessionId from activeSessionSlice
- ✅ Activate session channel on session join
- ✅ Switch back to global on session leave

---

## WebSocket Integration

### 1. Existing Pattern: useSessionWebSocket Hook

**File:** `/jam-ui/src/hooks/useSessionWebSocket.js`

**Purpose:** Integrates WebSocket messages with Redux state

**Pattern:**
```javascript
export const useSessionWebSocket = (sessionId) => {
  const dispatch = useDispatch();
  const { jamServer, isConnected } = useJamServerContext();

  useEffect(() => {
    if (!jamServer || !sessionId) return;

    // Define message callbacks
    const callbacks = {
      MIXER_CHANGES: (sessionMixers) => {
        dispatch(updateMediaSummary(sessionMixers.mixers.mediaSummary));
      },
      JAM_TRACK_CHANGES: (changes) => {
        dispatch(updateJamTrackState(changes.jamTrackState));
      },
      MIXDOWN_CHANGES: (message) => {
        dispatch(setDownloadState(message.mixdownPackage));
      }
    };

    // Register callbacks with jamServer
    Object.entries(callbacks).forEach(([event, callback]) => {
      if (jamServer.registerMessageCallback) {
        jamServer.registerMessageCallback(event, callback);
      }
    });

    // Cleanup
    return () => {
      Object.entries(callbacks).forEach(([event, callback]) => {
        if (jamServer.unregisterMessageCallback) {
          jamServer.unregisterMessageCallback(event, callback);
        }
      });
    };
  }, [jamServer, sessionId, isConnected, dispatch]);
};
```

**Reuse for Chat:**
- ✅ Add `CHAT_MESSAGE` callback to useSessionWebSocket
- ✅ Dispatch `addMessageFromWebSocket(payload)` action
- ✅ Follow same cleanup pattern

**Example Chat Integration:**
```javascript
// In useSessionWebSocket.js
const callbacks = {
  // ... existing callbacks

  CHAT_MESSAGE: (message) => {
    console.log('Chat message received:', message);
    dispatch(addMessageFromWebSocket({
      id: message.msg_id,
      message: message.msg,
      senderId: message.sender_id,
      senderName: message.sender_name,
      createdAt: message.created_at,
      channel: message.channel,
      lessonSessionId: message.lesson_session_id,
      purpose: message.purpose
    }));

    // Increment unread count if chat window is closed
    const state = getState();
    if (!state.sessionUI.chatWindowOpen) {
      dispatch(incrementUnreadCount(message.channel));
    }
  }
};
```

---

### 2. JamServer Integration

**File:** `/jam-ui/src/helpers/JamServer.js`

**Key Features:**
- ✅ EventEmitter-based WebSocket wrapper
- ✅ Handles connection, heartbeat, reconnect
- ✅ Message routing via `registerMessageCallback(type, callback)`
- ✅ Connection status events: `connected`, `disconnected`, `connection_down`

**MessageType Constants:**
```javascript
const MessageType = {
  LOGIN_ACK: 'login_ack',
  HEARTBEAT: 'heartbeat',
  HEARTBEAT_ACK: 'heartbeat_ack',
  SERVER_REJECTION_ERROR: 'server_rejection_error',
  PEER_MESSAGE: 'peer_message',
  LOGOUT: 'logout',
  CHAT_MESSAGE: 'chat_message', // ✅ Already defined!
  USER_STATUS: 'user_status'
};
```

**Reuse for Chat:**
- ✅ `CHAT_MESSAGE` type already defined
- ✅ Use existing `registerMessageCallback` API
- ✅ No modifications needed to JamServer.js

---

### 3. JamServerContext

**File:** `/jam-ui/src/context/JamServerContext.js`

**Purpose:** Provides jamServer instance and connection status to components

**Usage:**
```javascript
const { jamServer, isConnected } = useJamServerContext();

// Check connection before sending message
if (!isConnected) {
  alert('Cannot send message: disconnected from server');
  return;
}
```

**Reuse for Chat:**
- ✅ Use context to access jamServer
- ✅ Disable send button if !isConnected
- ✅ Show connection status in chat UI

---

## API Integration

### 1. REST Client Pattern

**File:** `/jam-ui/src/helpers/rest.js` (inferred from imports)

**Existing Chat Methods:**
```javascript
// From lobbyChatMessagesSlice.js
import { getLobbyChatMessages, createLobbyChatMessage } from '../../helpers/rest';

export const getLobbyChatMessages = (params) => {
  return fetch(`/api/chat?${new URLSearchParams(params)}`, {
    credentials: 'include' // session cookie
  });
};

export const createLobbyChatMessage = (body) => {
  return fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify(body)
  });
};
```

**Reuse for Chat:**
- ✅ Add similar methods for session chat
- ✅ Follow same fetch pattern (credentials: 'include')
- ✅ Use URLSearchParams for query strings

**New Methods Needed:**
```javascript
// Get chat history (any channel)
export const getChatMessages = (params) => {
  return fetch(`/api/chat?${new URLSearchParams(params)}`, {
    credentials: 'include'
  });
};

// Send message (any channel)
export const sendChatMessage = (body) => {
  return fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify(body)
  });
};

// Mark messages as read (NEW - not in legacy API)
export const markChatAsRead = ({ channel, sessionId }) => {
  return fetch('/api/chat/mark_read', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ channel, sessionId })
  });
};
```

---

### 2. Async Thunk Pattern

**From mediaSlice.js:**
```javascript
export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      const result = await jamClient.JamTrackPlay(fqId);
      return { jamTrack, fqId, result };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Reuse for Chat:**
```javascript
export const fetchChatHistory = createAsyncThunk(
  'sessionChat/fetchHistory',
  async ({ channel, channelType, sessionId, lessonId }, { rejectWithValue }) => {
    try {
      const params = {
        channel: channelType, // 'global', 'session', 'lesson'
        limit: 20,
        page: 0
      };

      if (channelType === 'session' && sessionId) {
        params.music_session = sessionId;
      } else if (channelType === 'lesson' && lessonId) {
        params.lesson_session = lessonId;
      }

      const response = await getChatMessages(params);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();
      return { channel, messages: data.chats, next: data.next };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

export const sendMessage = createAsyncThunk(
  'sessionChat/sendMessage',
  async ({ message, channel, channelType, sessionId, targetUserId, clientId }, { rejectWithValue }) => {
    try {
      const body = {
        message,
        channel: channelType,
        client_id: clientId
      };

      if (channelType === 'session' && sessionId) {
        body.music_session = sessionId;
      } else if (channelType === 'lesson' && targetUserId) {
        body.target_user = targetUserId;
      }

      const response = await sendChatMessage(body);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();
      return { channel, message: data };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

---

## Component Patterns

### 1. Session Screen Structure

**File:** `/jam-ui/src/components/client/JKSessionScreen.js`

**Pattern:**
```javascript
export const JKSessionScreen = () => {
  const { sessionId } = useParams();
  const dispatch = useDispatch();

  // Load session data
  useEffect(() => {
    if (sessionId) {
      dispatch(loadSession(sessionId));
    }
  }, [sessionId, dispatch]);

  // WebSocket integration
  useSessionWebSocket(sessionId);

  return (
    <div className="session-screen">
      {/* Mixer, tracks, etc. */}
      <JKSessionJamTrackPlayer />
      <JKSessionBackingTrackPlayer />
      <JKSessionMetronomePlayer />

      {/* Chat window (to be added) */}
      <JKSessionChatWindow />
    </div>
  );
};
```

**Reuse for Chat:**
- ✅ Add `<JKSessionChatWindow />` to session screen
- ✅ Chat window conditionally renders based on Redux state
- ✅ Positioned absolutely (or as WindowPortal)

---

### 2. Player Component Pattern (JKSessionJamTrackPlayer)

**File:** `/jam-ui/src/components/client/JKSessionJamTrackPlayer.js`

**Pattern:**
```javascript
export const JKSessionJamTrackPlayer = () => {
  const dispatch = useDispatch();
  const jamTrack = useSelector(state => state.media.jamTrack.current);
  const [isWindowOpen, setIsWindowOpen] = useState(false);

  const handlePlay = () => {
    dispatch(loadJamTrack({ jamTrack, jamClient }));
  };

  return (
    <div className="jam-track-player">
      <button onClick={handlePlay}>Play</button>
      <button onClick={() => setIsWindowOpen(true)}>Pop Out</button>

      {isWindowOpen && (
        <WindowPortal
          title="JamTrack Player"
          onClose={() => setIsWindowOpen(false)}
        >
          {/* Player controls */}
        </WindowPortal>
      )}
    </div>
  );
};
```

**Reuse for Chat:**
```javascript
export const JKSessionChatWindow = () => {
  const dispatch = useDispatch();
  const sessionId = useSelector(state => state.activeSession.sessionId);
  const messages = useSelector(state =>
    state.sessionChat.messagesByChannel[sessionId] || []
  );
  const [isWindowOpen, setIsWindowOpen] = useState(false);
  const [message, setMessage] = useState('');

  const handleSend = () => {
    dispatch(sendMessage({
      message,
      channel: sessionId,
      channelType: 'session',
      sessionId
    }));
    setMessage('');
  };

  return (
    <div className="session-chat-button">
      <button onClick={() => setIsWindowOpen(true)}>
        Chat {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
      </button>

      {isWindowOpen && (
        <WindowPortal
          title="Session Chat"
          onClose={() => setIsWindowOpen(false)}
          windowFeatures="width=400,height=600,left=200,top=200"
        >
          <div className="chat-window">
            <div className="message-list">
              {messages.map(msg => (
                <div key={msg.id} className="chat-message">
                  <span className="sender">{msg.senderName}</span>
                  <span className="message">{msg.message}</span>
                  <span className="timestamp">{msg.createdAt}</span>
                </div>
              ))}
            </div>
            <div className="message-input">
              <textarea
                value={message}
                onChange={(e) => setMessage(e.target.value)}
                placeholder="enter message"
              />
              <button onClick={handleSend}>SEND</button>
            </div>
          </div>
        </WindowPortal>
      )}
    </div>
  );
};
```

---

## Gap Analysis

### What's Available ✅

**Components:**
- ✅ WindowPortal (modeless dialog)
- ✅ Button/input patterns (CSS classes)
- ✅ Layout patterns (from session screen)

**Redux:**
- ✅ lobbyChatMessagesSlice (lobby chat - can be reference)
- ✅ textMessagesSlice (1-on-1 messages - can be reference)
- ✅ mediaSlice (async thunk patterns)
- ✅ activeSessionSlice (session state)
- ✅ sessionUISlice (UI state patterns)

**WebSocket:**
- ✅ useSessionWebSocket hook
- ✅ JamServer helper (CHAT_MESSAGE type defined)
- ✅ JamServerContext

**API:**
- ✅ REST client pattern (fetch with credentials)
- ✅ Async thunk pattern
- ✅ Existing chat methods (lobby)

---

### What's Missing ❌

**Components:**
- ❌ JKSessionChatWindow component
- ❌ JKChatMessageList sub-component
- ❌ JKChatInput sub-component
- ❌ JKChatButton (with unread badge)

**Redux:**
- ❌ sessionChatSlice (multi-channel chat state)
- ❌ Channel switching logic
- ❌ Unread tracking per channel
- ❌ Message deduplication logic (by msg_id)

**API:**
- ❌ getChatMessages (generic, not lobby-specific)
- ❌ sendChatMessage (generic)
- ❌ markChatAsRead (NEW endpoint - not in legacy API)

**WebSocket:**
- ❌ CHAT_MESSAGE handler in useSessionWebSocket
- ❌ Unread count increment on message receive

**Utilities:**
- ❌ Timestamp formatting (e.g., "5 minutes ago")
- ❌ Auto-scroll to bottom logic
- ❌ Enter-to-send keyboard handler

---

### What Needs to Be Created

**Phase 7 (Data Layer):**
1. ✅ sessionChatSlice.js - Multi-channel chat state management
2. ✅ Async thunks: fetchChatHistory, sendMessage, markAsRead
3. ✅ REST API methods: getChatMessages, sendChatMessage, markChatAsRead
4. ✅ WebSocket handler: CHAT_MESSAGE callback in useSessionWebSocket

**Phase 8 (UI Components):**
1. ✅ JKSessionChatWindow.js - Main chat window component
2. ✅ JKChatMessageList.js - Scrollable message list
3. ✅ JKChatInput.js - Message composition area
4. ✅ JKChatButton.js - Chat button with unread badge (if needed)

**Phase 9 (Integration):**
1. ✅ Wire up Redux actions in components
2. ✅ Integrate WebSocket subscription
3. ✅ Handle session join/leave (activate/deactivate channel)
4. ✅ Sync unread counts

**Phase 10 (Testing):**
1. ✅ Unit tests: API client, Redux reducers
2. ✅ Integration tests: Message send/receive flow
3. ✅ E2E tests: Complete chat workflow

**Phase 11 (Error Handling & Polish):**
1. ✅ Handle API errors (404, 403, 422)
2. ✅ Handle WebSocket disconnection
3. ✅ Retry failed message sends
4. ✅ Show error messages to user
5. ✅ Add loading states
6. ✅ Add timestamp formatting utility
7. ✅ Add auto-scroll logic

---

## TDD Candidates

Per CLAUDE.md, all jam-ui code changes MUST follow TDD methodology.

### High Priority (MUST use TDD) 🧪

**Phase 7 (Data Layer):**
- 🧪 sessionChatSlice reducers (add/remove/update messages)
- 🧪 fetchChatHistory async thunk
- 🧪 sendMessage async thunk
- 🧪 Message deduplication logic (by msg_id)
- 🧪 Unread count increment/reset logic
- 🧪 CHAT_MESSAGE WebSocket handler

**Phase 9 (State Management):**
- 🧪 Redux integration tests (dispatch actions, check state)
- 🧪 WebSocket integration tests (mock server, verify Redux updates)

**Phase 10 (API Integration):**
- 🧪 getChatMessages API client (mock fetch)
- 🧪 sendChatMessage API client (mock fetch)
- 🧪 Error handling (404, 403, 422 responses)

---

### Medium Priority (Should use TDD) 🧪

**Phase 8 (UI Components):**
- 🧪 JKSessionChatWindow component behavior tests
  - Opens/closes window
  - Sends message on Enter key
  - Auto-scrolls to bottom on new message
  - Shows unread badge
- 🧪 JKChatInput keyboard handling (Enter vs Shift+Enter)

**Phase 11 (Error Handling):**
- 🧪 Retry logic for failed sends
- 🧪 Connection status handling (disable send when disconnected)

---

### Low Priority (Optional TDD) 🧪

**Phase 8 (UI Components):**
- Styling-only changes (CSS/SCSS)
- Message list rendering (mostly visual)
- Timestamp formatting (simple utility)

**Phase 11 (Polish):**
- Loading spinners
- Animations
- Accessibility (ARIA labels)

---

## Test Types for Chat

### 1. Unit Tests (Jest)

**Test:** Redux reducers, async thunks, API client

**Example:**
```javascript
// sessionChatSlice.test.js
describe('sessionChatSlice', () => {
  test('addMessageFromWebSocket adds message to correct channel', () => {
    const initialState = {
      messagesByChannel: { 'session-123': [] }
    };
    const action = {
      type: 'sessionChat/addMessageFromWebSocket',
      payload: {
        id: 'msg-1',
        message: 'Hello',
        channel: 'session-123'
      }
    };
    const newState = sessionChatReducer(initialState, action);
    expect(newState.messagesByChannel['session-123']).toHaveLength(1);
    expect(newState.messagesByChannel['session-123'][0].message).toBe('Hello');
  });

  test('incrementUnreadCount increments correct channel', () => {
    const initialState = {
      unreadCounts: { 'session-123': 0 }
    };
    const action = {
      type: 'sessionChat/incrementUnreadCount',
      payload: { channel: 'session-123' }
    };
    const newState = sessionChatReducer(initialState, action);
    expect(newState.unreadCounts['session-123']).toBe(1);
  });
});
```

---

### 2. Integration Tests (Playwright)

**Test:** API calls, WebSocket messages, user flows

**Example:**
```typescript
// test/session-chat/send-message.spec.ts
test('sends message when user clicks SEND button', async ({ page }) => {
  const interceptor = new APIInterceptor();
  interceptor.intercept(page);

  await loginToJamUI(page);
  await createAndJoinSession(page);

  // Open chat window
  await page.click('[data-testid="chat-button"]');

  // Type message
  await page.fill('[data-testid="chat-input"]', 'Hello world');

  // Send
  await page.click('[data-testid="chat-send"]');

  // Verify API call
  const chatCalls = interceptor.getCallsByPath('/api/chat');
  expect(chatCalls.length).toBe(1);
  expect(chatCalls[0].request.postDataJSON().message).toBe('Hello world');

  // Verify message appears in UI
  await expect(page.locator('.chat-message').last()).toContainText('Hello world');
});
```

---

### 3. E2E Tests (Playwright)

**Test:** Complete user workflows across multiple pages

**Example:**
```typescript
// test/e2e/chat-flow.spec.ts
test('complete chat flow: join session → send message → receive message', async ({ page }) => {
  await loginToJamUI(page);
  await createAndJoinSession(page);

  // Open chat
  await page.click('[data-testid="chat-button"]');

  // Send message
  await page.fill('[data-testid="chat-input"]', 'Test message');
  await page.keyboard.press('Enter');

  // Verify message sent
  await expect(page.locator('.chat-message').last()).toContainText('Test message');

  // Close chat
  await page.click('[data-testid="chat-close"]');

  // Simulate incoming message (via WebSocket mock)
  await page.evaluate(() => {
    window.__mockWebSocketMessage__({
      type: 'CHAT_MESSAGE',
      payload: {
        sender_name: 'Other User',
        msg: 'Reply message',
        msg_id: 'msg-999',
        created_at: new Date().toISOString(),
        channel: 'session'
      }
    });
  });

  // Verify unread badge appears
  await expect(page.locator('[data-testid="chat-button"] .badge')).toHaveText('1');

  // Open chat again
  await page.click('[data-testid="chat-button"]');

  // Verify unread badge cleared
  await expect(page.locator('[data-testid="chat-button"] .badge')).not.toBeVisible();

  // Verify reply message visible
  await expect(page.locator('.chat-message').last()).toContainText('Reply message');
});
```

---

## Summary for Implementation

### Phases 7-11 Roadmap

**Phase 7 (Data Layer):**
- [ ] Create sessionChatSlice.js with multi-channel state
- [ ] Add async thunks: fetchChatHistory, sendMessage
- [ ] Add REST API methods: getChatMessages, sendChatMessage
- [ ] Add CHAT_MESSAGE handler to useSessionWebSocket
- [ ] Write unit tests for all reducers and thunks

**Phase 8 (UI Components):**
- [ ] Create JKSessionChatWindow.js (main component)
- [ ] Create JKChatMessageList.js (message list)
- [ ] Create JKChatInput.js (composition area)
- [ ] Use WindowPortal for modeless dialog
- [ ] Write component behavior tests

**Phase 9 (State Management):**
- [ ] Wire up Redux actions in components
- [ ] Integrate WebSocket subscription
- [ ] Handle session join/leave (activate/deactivate channel)
- [ ] Sync unread counts with Redux
- [ ] Write integration tests for state flow

**Phase 10 (Testing):**
- [ ] Integration tests: Message send/receive flow
- [ ] E2E tests: Complete chat workflow
- [ ] Test error scenarios (API failures, disconnection)

**Phase 11 (Error Handling & Polish):**
- [ ] Handle API errors (404, 403, 422)
- [ ] Handle WebSocket disconnection
- [ ] Retry failed message sends
- [ ] Add loading states
- [ ] Add timestamp formatting utility (e.g., dayjs relative time)
- [ ] Add auto-scroll to bottom logic
- [ ] Accessibility improvements (ARIA, keyboard nav)

---

### Key Decisions Made

1. **WindowPortal for Chat Window:**
   - ✅ Reuse existing component (no modifications needed)
   - ✅ Matches legacy modeless dialog behavior
   - ✅ Already handles styling, lifecycle, cleanup

2. **sessionChatSlice for State:**
   - ✅ New slice (not extending lobbyChatMessagesSlice)
   - ✅ Multi-channel support (global, session, lesson)
   - ✅ Unread tracking per channel
   - ✅ Message deduplication by msg_id

3. **WebSocket Integration:**
   - ✅ Add CHAT_MESSAGE handler to useSessionWebSocket
   - ✅ No modifications to JamServer.js (already supports CHAT_MESSAGE)
   - ✅ Dispatch Redux actions from WebSocket handler

4. **TDD Approach:**
   - ✅ Follow TDD for all data layer (Redux, API)
   - ✅ Follow TDD for component behavior (send, scroll, unread)
   - ❌ Skip TDD for styling-only changes

5. **Read/Unread Tracking:**
   - ✅ Use client-side unread counts (Redux state)
   - ❌ No server-side tracking in Phase 6 (deferred to later milestone)
   - ✅ Reset unread count when chat window opened

---

### Next Steps

1. **Phase 6 Plan 2:** Design Redux architecture and implementation roadmap
2. **Phase 7:** Implement data layer (TDD approach)
3. **Phase 8:** Implement UI components (TDD approach)
4. **Phase 9:** Integrate state management
5. **Phase 10:** Comprehensive testing
6. **Phase 11:** Error handling and polish

---

**Document Complete!**
Ready for Phase 6 Plan 2 (React Architecture & Implementation Roadmap).
