chore: create Phase 8 plans (Chat Window UI & Message Display)

- Plan 8.1: Chat Window Shell & WindowPortal Integration (4 tasks)
- Plan 8.2: Message List & Auto-Scroll (5 tasks, mixed TDD)
- Plan 8.3: Chat Button & Unread Badge (3-4 tasks)

Total: 3 plans, ~12 tasks for Phase 8
Updated ROADMAP.md and STATE.md with Phase 8 breakdown
This commit is contained in:
Nuwan 2026-01-27 12:56:33 +05:30
parent 9be85da11a
commit f8a3a7bb84
5 changed files with 1765 additions and 9 deletions

View File

@ -137,10 +137,12 @@ Plans:
**Goal**: Build modeless chat dialog with message list and user information display
**Depends on**: Phase 7
**Research**: Unlikely (following established modal/dialog patterns)
**Plans**: TBD
**Plans**: 3 plans
Plans:
- [ ] 08-01: TBD (run /gsd:plan-phase 8 to break down)
- [ ] 08-01: Chat Window Shell & WindowPortal Integration (4 tasks)
- [ ] 08-02: Message List & Auto-Scroll (5 tasks, mixed TDD)
- [ ] 08-03: Chat Button & Unread Badge (3-4 tasks)
#### Phase 9: Message Composition & Sending
**Goal**: Implement text input, send functionality, and real-time message delivery
@ -183,6 +185,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 5. JamTrack Implementation | v1.0 | 5/5 | Complete | 2026-01-14 |
| 6. Session Chat Research & Design | v1.1 | 2/2 | Complete | 2026-01-26 |
| 7. Chat Infrastructure & State Management | v1.1 | 3/3 | Complete | 2026-01-27 |
| 8. Chat Window UI & Message Display | v1.1 | 0/3 | Not started | - |
| 8. Chat Window UI & Message Display | v1.1 | 0/TBD | Not started | - |
| 9. Message Composition & Sending | v1.1 | 0/TBD | Not started | - |
| 10. Read/Unread Status Management | v1.1 | 0/TBD | Not started | - |

View File

@ -9,10 +9,10 @@ See: .planning/PROJECT.md (updated 2026-01-13)
## Current Position
Phase: 7 of 11 (Chat Infrastructure & State Management)
Plan: 07-03 Complete
Status: Phase 7 complete (3/3 plans), ready for Phase 8 (Chat Window UI & Message Display)
Last activity: 2026-01-27 — Plan 3 (WebSocket Integration & Selectors) complete
Phase: 8 of 11 (Chat Window UI & Message Display)
Plan: Phase 8 planning complete
Status: Phase 7 complete (3/3 plans), Phase 8 plans created (3 plans), ready to execute Plan 08-01
Last activity: 2026-01-27 — Phase 8 planning complete (3 plans created)
Progress: █████░░░░░ 50% (v1.1)
@ -37,7 +37,7 @@ Progress: █████░░░░░ 50% (v1.1)
**v1.1 Music Session Chat (In Progress):**
- Total plans completed: 5 (Phase 6: 2 plans, Phase 7: 3 plans)
- Total phases: 6 (phases 6-11)
- Progress: 50% (Phase 6 complete, Phase 7 complete, Phase 8 ready to start)
- Progress: 50% (Phase 6 complete, Phase 7 complete, Phase 8 planned with 3 plans)
**Recent Trend:**
- Last milestone: v1.0 completed 2026-01-14 with excellent velocity
@ -249,7 +249,7 @@ None yet.
## Session Continuity
Last session: 2026-01-27
Stopped at: Phase 7 Plan 2 complete (Async Thunks & API Integration)
Stopped at: Phase 8 planning complete (3 plans created)
Resume file: None
**Next:** Phase 7 Plan 3 (WebSocket Integration & Selectors) - Implement CHAT_MESSAGE handler, 8 memoized selectors, localStorage persistence for unread counts
**Next:** Execute Plan 08-01 (Chat Window Shell & WindowPortal Integration) - Create JKSessionChatWindow, JKChatHeader, integrate with JKSessionScreen, write integration tests

View File

@ -0,0 +1,423 @@
---
phase: 08-chat-window-ui
plan: 01
type: standard
---
<objective>
Create chat window shell with WindowPortal integration.
Purpose: Build the foundational chat window UI that opens in a popup window, with header and close functionality. This establishes the chat UI foundation that Plans 8.2 and 8.3 will build upon.
Output: JKSessionChatWindow component with WindowPortal wrapper, JKChatHeader component, integration with JKSessionScreen.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-phase.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Phase 6 design documents
@.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md
@.planning/phases/06-session-chat-research-design/IMPLEMENTATION_ROADMAP.md
# Phase 7 summary
@.planning/phases/07-chat-infrastructure/07-03-SUMMARY.md
# WindowPortal reference
@jam-ui/src/components/common/WindowPortal.js
@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js (WindowPortal usage example)
@jam-ui/src/components/client/JKSessionScreen.js (integration point)
# Redux selectors from Phase 7
@jam-ui/src/store/features/sessionChatSlice.js
# Codebase conventions
@.planning/codebase/ARCHITECTURE.md
@.planning/codebase/CONVENTIONS.md
@CLAUDE.md
**Key context from Phase 6 design:**
- Chat window uses WindowPortal for modeless popup (follows Backing Track/JamTrack pattern)
- Window dimensions: 450px width × 600px height (per CHAT_COMPONENT_DESIGN.md)
- Header shows channel name, close button
- Component hierarchy: JKSessionChatWindow → WindowPortal → JKChatHeader + [content area]
- Redux integration: `isWindowOpen`, `activeChannel`, `openChatWindow`, `closeChatWindow` actions
**Key context from Phase 7:**
- Redux infrastructure complete: sessionChatSlice with reducers, thunks, selectors
- Selectors available: `selectIsChatWindowOpen`, `selectActiveChannel`, `selectChatMessages`
- Actions available: `openChatWindow`, `closeChatWindow`, `setActiveChannel`, `setWindowPosition`
</context>
<tasks>
<task>
<name>Task 1: Create JKChatHeader component</name>
<files>jam-ui/src/components/client/chat/JKChatHeader.js</files>
<action>
Create the chat window header component:
1. Create directory `jam-ui/src/components/client/chat/`
2. Create `JKChatHeader.js` with:
- Props: `channelName` (string), `onClose` (function)
- Render: Title bar with channel name and close button (X)
- Style: Inline styles for now (SCSS in Plan 8.3)
- Example structure:
```javascript
const JKChatHeader = ({ channelName, onClose }) => {
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e0e0e0',
backgroundColor: '#f8f9fa'
}}>
<h5 style={{ margin: 0, fontSize: '16px', fontWeight: 600 }}>
{channelName || 'Session Chat'}
</h5>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
padding: '0 8px',
color: '#6c757d'
}}
aria-label="Close chat"
>
×
</button>
</div>
);
};
```
3. Add PropTypes validation
4. Export component
**Verification:**
- Component renders header with title
- Close button calls onClose callback
- Inline styles applied correctly
</action>
<verify>
JKChatHeader.js exists with channel name display and close button. PropTypes defined.
</verify>
<done>
Chat header component complete with close functionality.
</done>
</task>
<task>
<name>Task 2: Create JKSessionChatWindow component with WindowPortal</name>
<files>jam-ui/src/components/client/JKSessionChatWindow.js</files>
<action>
Create the main chat window component with WindowPortal wrapper:
1. Create `jam-ui/src/components/client/JKSessionChatWindow.js`
2. Import dependencies:
- React, Redux (useSelector, useDispatch)
- WindowPortal component
- JKChatHeader component
- sessionChatSlice actions and selectors
3. Component structure:
```javascript
const JKSessionChatWindow = () => {
const dispatch = useDispatch();
// Selectors
const isWindowOpen = useSelector(selectIsChatWindowOpen);
const activeChannel = useSelector(selectActiveChannel);
// Handlers
const handleClose = useCallback(() => {
dispatch(closeChatWindow());
}, [dispatch]);
// Channel name formatting
const getChannelName = () => {
if (!activeChannel) return 'Session Chat';
if (activeChannel === 'global') return 'Global Chat';
return 'Session Chat';
};
if (!isWindowOpen) return null;
return (
<WindowPortal
title="JamKazam Chat"
windowFeatures="width=450,height=600,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=no,resizable=yes"
onClose={handleClose}
windowId="jamkazam-chat"
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<JKChatHeader
channelName={getChannelName()}
onClose={handleClose}
/>
<div style={{ flex: 1, padding: '16px', backgroundColor: '#ffffff' }}>
{/* Message list placeholder - Plan 8.2 */}
<p style={{ color: '#6c757d', textAlign: 'center', marginTop: '24px' }}>
Chat messages will appear here
</p>
</div>
</div>
</WindowPortal>
);
};
```
4. Add PropTypes (none needed - uses Redux)
5. Export component with React.memo for performance
**Implementation notes:**
- Window dimensions: 450×600 per design spec
- Placeholder content area for message list (Plan 8.2)
- windowId for identification (useful for debugging)
- Conditional render: only if isWindowOpen is true
- Use useCallback for handleClose to prevent re-renders
**Verification:**
- WindowPortal opens when isWindowOpen is true
- Header displays correct channel name
- Close button calls closeChatWindow action
- Window has correct dimensions
- Popup window closes properly
</action>
<verify>
JKSessionChatWindow.js exists with WindowPortal wrapper. Window opens/closes correctly. Header displays channel name.
</verify>
<done>
Chat window component complete with WindowPortal integration and placeholder content.
</done>
</task>
<task>
<name>Task 3: Integrate chat window with JKSessionScreen</name>
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
<action>
Add chat window to JKSessionScreen with conditional rendering:
1. Open `jam-ui/src/components/client/JKSessionScreen.js`
2. Import JKSessionChatWindow at top:
```javascript
import JKSessionChatWindow from './JKSessionChatWindow.js';
```
3. Add chat window render at bottom of component JSX (after all other modals):
- Find the location after JKSessionMetronomePlayer or other popup components
- Add: `<JKSessionChatWindow />`
- Example placement (near end of return statement):
```javascript
return (
<>
{/* ... existing session UI ... */}
{/* Media players */}
{backingTrackPlayerOpen && <JKSessionBackingTrackPlayer />}
{jamTrackPlayerOpen && <JKSessionJamTrackPlayer />}
{metronomePlayerOpen && <JKSessionMetronomePlayer />}
{/* Chat window */}
<JKSessionChatWindow />
</>
);
```
**Integration notes:**
- No props needed (component uses Redux internally)
- Conditional rendering handled inside JKSessionChatWindow
- Follows same pattern as BackingTrackPlayer/JamTrackPlayer/MetronomePlayer
- Window lifecycle managed by WindowPortal
**Verification:**
- Component renders without errors
- No console warnings
- Chat window doesn't show by default (isWindowOpen = false)
- Other session UI unaffected
</action>
<verify>
JKSessionChatWindow integrated into JKSessionScreen. Component renders without errors. Window lifecycle works correctly.
</verify>
<done>
Chat window integrated into session screen. Ready for message list implementation (Plan 8.2).
</done>
</task>
<task>
<name>Task 4: Write integration test for window open/close behavior</name>
<files>jam-ui/test/chat/chat-window.spec.ts</files>
<action>
Create Playwright integration test for chat window behavior:
1. Create directory `jam-ui/test/chat/`
2. Create `chat-window.spec.ts` with:
```typescript
import { test, expect } from '@playwright/test';
import { loginToJamUI, createAndJoinSession } from '../helpers/sessionHelpers';
test.describe('Chat Window', () => {
test.beforeEach(async ({ page }) => {
await loginToJamUI(page);
await createAndJoinSession(page);
});
test('opens chat window when dispatching openChatWindow action', async ({ page }) => {
// Dispatch openChatWindow action via Redux DevTools or direct call
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
store.dispatch({ type: 'sessionChat/openChatWindow' });
});
// Wait for popup window
const popupPromise = page.waitForEvent('popup');
const popup = await popupPromise;
// Verify popup opened
expect(popup).toBeTruthy();
await expect(popup).toHaveTitle('JamKazam Chat');
// Verify header content
const header = popup.locator('h5');
await expect(header).toBeVisible();
await expect(header).toContainText('Chat');
});
test('closes chat window when clicking close button', async ({ page }) => {
// Open chat window
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
store.dispatch({ type: 'sessionChat/openChatWindow' });
});
const popupPromise = page.waitForEvent('popup');
const popup = await popupPromise;
// Click close button
await popup.locator('button[aria-label="Close chat"]').click();
// Verify popup closed
await expect(popup).not.toBeAttached();
// Verify Redux state updated
const isWindowOpen = await page.evaluate(() => {
const store = window.__REDUX_STORE__;
return store.getState().sessionChat.isWindowOpen;
});
expect(isWindowOpen).toBe(false);
});
test('shows placeholder message before messages loaded', async ({ page }) => {
// Open chat window
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
store.dispatch({ type: 'sessionChat/openChatWindow' });
});
const popupPromise = page.waitForEvent('popup');
const popup = await popupPromise;
// Verify placeholder text
const placeholder = popup.locator('p:has-text("Chat messages will appear here")');
await expect(placeholder).toBeVisible();
});
});
```
3. Add Redux store exposure for testing (if not already present):
- In `jam-ui/src/store/store.js`, add:
```javascript
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
window.__REDUX_STORE__ = store;
}
```
**Test coverage:**
- Window opens on openChatWindow action
- Window closes on close button click
- Header displays correct title
- Placeholder message visible
- Redux state updated correctly
**Verification:**
- All 3 tests pass
- No console errors
- Window lifecycle works correctly
</action>
<verify>
Integration tests written and passing. Window open/close behavior validated. Redux state updates confirmed.
</verify>
<done>
Integration tests complete for chat window shell. Plan 8.1 complete.
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] JKChatHeader component exists with channel name and close button
- [ ] JKSessionChatWindow component exists with WindowPortal wrapper
- [ ] Chat window renders with correct dimensions (450×600)
- [ ] Window opens only when isWindowOpen is true
- [ ] Close button triggers closeChatWindow action
- [ ] JKSessionChatWindow integrated into JKSessionScreen
- [ ] Integration tests written and passing
- [ ] No console errors or warnings
- [ ] Popup window closes properly on unmount
</verification>
<success_criteria>
- JKChatHeader.js created with close button
- JKSessionChatWindow.js created with WindowPortal integration
- Chat window integrated into JKSessionScreen
- Integration tests passing (3 tests minimum)
- Window opens/closes correctly via Redux actions
- Header displays channel name
- Placeholder content visible
- Ready for Plan 8.2 (Message List & Auto-Scroll)
</success_criteria>
<output>
After completion, create `.planning/phases/08-chat-window-ui/08-01-SUMMARY.md`:
# Phase 8 Plan 1: Chat Window Shell & WindowPortal Integration Summary
**Created chat window foundation with popup window integration**
## Accomplishments
- Created JKChatHeader component with channel name display and close button
- Created JKSessionChatWindow component with WindowPortal wrapper
- Integrated chat window into JKSessionScreen (conditional rendering)
- Wrote integration tests for window open/close behavior
- Window lifecycle managed via Redux (openChatWindow/closeChatWindow actions)
## Files Created/Modified
- `jam-ui/src/components/client/chat/JKChatHeader.js` - Header component (NEW)
- `jam-ui/src/components/client/JKSessionChatWindow.js` - Main chat window (NEW)
- `jam-ui/src/components/client/JKSessionScreen.js` - Added chat window render (MODIFIED)
- `jam-ui/test/chat/chat-window.spec.ts` - Integration tests (NEW)
## Decisions Made
[Document any implementation decisions: Window dimensions? Styling approach? Redux integration patterns?]
## Issues Encountered
[Any challenges during implementation, or "None"]
## Next Phase Readiness
Ready for Plan 2 (Message List & Auto-Scroll)
</output>

View File

@ -0,0 +1,738 @@
---
phase: 08-chat-window-ui
plan: 02
type: mixed
---
<objective>
Build message list with auto-scroll behavior and message display components.
Purpose: Create the core chat UI for displaying messages with auto-scroll logic, message formatting, and loading/empty states. This completes the read-only chat experience.
Output: JKChatMessageList, JKChatMessage, loading/empty components, auto-scroll logic, timestamp formatting utility.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-phase.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Phase 6 design documents
@.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md
@.planning/phases/06-session-chat-research-design/IMPLEMENTATION_ROADMAP.md
# Phase 7 summary
@.planning/phases/07-chat-infrastructure/07-03-SUMMARY.md
# Phase 8 Plan 1 summary
@.planning/phases/08-chat-window-ui/08-01-SUMMARY.md
# Redux selectors and state
@jam-ui/src/store/features/sessionChatSlice.js
# Chat window shell
@jam-ui/src/components/client/JKSessionChatWindow.js
# Codebase conventions
@.planning/codebase/ARCHITECTURE.md
@.planning/codebase/CONVENTIONS.md
@CLAUDE.md
**Key context from Phase 6 design:**
- Message list: Scrollable container with auto-scroll to bottom on new messages
- Auto-scroll logic: Respect user manual scrolling (disable auto-scroll when scrolled up)
- Message format: Avatar (or initials), sender name, message text, timestamp (relative)
- Loading state: Spinner while fetching history
- Empty state: "No messages yet" when messagesByChannel[channel] is empty
- Timestamp formatting: Use dayjs for relative time (e.g., "2 minutes ago", "Yesterday")
**Key context from Phase 7:**
- Selectors available: `selectChatMessages(state, channel)`, `selectFetchStatus(state, channel)`
- Message structure: `{ id, senderId, senderName, message, createdAt, channel }`
- fetchChatHistory thunk: Available for loading history (optional for now)
**Auto-scroll requirements from design:**
- Scroll to bottom on component mount
- Scroll to bottom when new message arrives (if user at bottom)
- Don't auto-scroll if user manually scrolled up (preserve position)
- Re-enable auto-scroll when user scrolls back to bottom
- Detect "at bottom" threshold: within 50px of bottom
**TDD requirements from CLAUDE.md:**
- Auto-scroll logic should be tested (unit test for scroll behavior state machine)
- Timestamp formatting utility must have unit tests
- Message rendering can skip TDD (visual component)
</context>
<tasks>
<task type="tdd">
<name>Task 1: Create timestamp formatting utility (TDD)</name>
<files>jam-ui/src/utils/formatTimestamp.js, jam-ui/src/utils/__tests__/formatTimestamp.test.js</files>
<action>
Using TDD, create utility function for relative timestamp formatting:
**RED Phase - Write failing tests first:**
1. Create directory `jam-ui/src/utils/` (if not exists)
2. Create test file `jam-ui/src/utils/__tests__/formatTimestamp.test.js`:
```javascript
import { formatTimestamp } from '../formatTimestamp';
describe('formatTimestamp', () => {
test('returns "Just now" for messages within last minute', () => {
const now = new Date();
expect(formatTimestamp(now.toISOString())).toBe('Just now');
});
test('returns "X minutes ago" for messages within last hour', () => {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
expect(formatTimestamp(fiveMinutesAgo.toISOString())).toBe('5 minutes ago');
});
test('returns "X hours ago" for messages within last day', () => {
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
expect(formatTimestamp(twoHoursAgo.toISOString())).toBe('2 hours ago');
});
test('returns "Yesterday" for messages from previous day', () => {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
expect(formatTimestamp(yesterday.toISOString())).toMatch(/Yesterday/);
});
test('returns date for older messages', () => {
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const result = formatTimestamp(lastWeek.toISOString());
expect(result).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/); // MM/DD/YYYY format
});
test('handles invalid dates gracefully', () => {
expect(formatTimestamp(null)).toBe('');
expect(formatTimestamp(undefined)).toBe('');
expect(formatTimestamp('invalid')).toBe('');
});
});
```
3. Run tests - should FAIL (utility doesn't exist yet)
**GREEN Phase - Implement utility:**
4. Install dayjs if not already present:
```bash
cd jam-ui
npm install dayjs
```
5. Create `jam-ui/src/utils/formatTimestamp.js`:
```javascript
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
/**
* Formats a timestamp as relative time (e.g., "2 minutes ago", "Yesterday")
* @param {string} timestamp - ISO timestamp string
* @returns {string} Formatted relative time
*/
export const formatTimestamp = (timestamp) => {
if (!timestamp) return '';
try {
const date = dayjs(timestamp);
if (!date.isValid()) return '';
const now = dayjs();
const diffInMinutes = now.diff(date, 'minute');
const diffInHours = now.diff(date, 'hour');
const diffInDays = now.diff(date, 'day');
if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes > 1 ? 's' : ''} ago`;
if (diffInHours < 24) return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`;
if (diffInDays === 1) return 'Yesterday';
if (diffInDays < 7) return date.format('dddd'); // Day name (e.g., "Monday")
return date.format('MM/DD/YYYY');
} catch (error) {
console.error('formatTimestamp error:', error);
return '';
}
};
```
6. Run tests - should PASS
**REFACTOR Phase:**
7. Add JSDoc comments for clarity
8. Consider edge cases (future dates, very old dates)
9. Ensure tests still pass
**Verification:**
- All 6 tests pass
- Handles invalid input gracefully
- Returns expected formats for all time ranges
</action>
<verify>
formatTimestamp utility created with passing unit tests. All time ranges handled correctly. Invalid input handled gracefully.
</verify>
<done>
Timestamp formatting utility complete with TDD validation.
</done>
</task>
<task>
<name>Task 2: Create JKChatMessage component</name>
<files>jam-ui/src/components/client/chat/JKChatMessage.js</files>
<action>
Create individual message display component:
1. Create `jam-ui/src/components/client/chat/JKChatMessage.js`
2. Import formatTimestamp utility
3. Component structure:
```javascript
import React from 'react';
import PropTypes from 'prop-types';
import { formatTimestamp } from '../../../utils/formatTimestamp';
const JKChatMessage = ({ message }) => {
const { senderName, message: text, createdAt } = message;
// Get initials for avatar (first letter of first and last name)
const getInitials = (name) => {
if (!name) return '?';
const parts = name.trim().split(' ');
if (parts.length === 1) return parts[0][0].toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
};
return (
<div style={{
display: 'flex',
gap: '12px',
marginBottom: '16px',
padding: '8px',
borderRadius: '4px',
backgroundColor: '#f8f9fa'
}}>
{/* Avatar */}
<div style={{
width: '36px',
height: '36px',
borderRadius: '50%',
backgroundColor: '#007bff',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: '14px',
flexShrink: 0
}}>
{getInitials(senderName)}
</div>
{/* Message content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '4px' }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>
{senderName || 'Unknown'}
</span>
<span style={{ fontSize: '12px', color: '#6c757d' }}>
{formatTimestamp(createdAt)}
</span>
</div>
<div style={{ fontSize: '14px', color: '#212529', wordWrap: 'break-word' }}>
{text}
</div>
</div>
</div>
);
};
JKChatMessage.propTypes = {
message: PropTypes.shape({
id: PropTypes.string.isRequired,
senderId: PropTypes.string.isRequired,
senderName: PropTypes.string,
message: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired
}).isRequired
};
export default React.memo(JKChatMessage);
```
**Component features:**
- Avatar with initials (first + last name initials)
- Sender name in bold
- Relative timestamp (via formatTimestamp utility)
- Message text with word wrapping
- React.memo for performance (prevents re-renders on list scroll)
- Inline styles (SCSS in Plan 8.3 if needed)
**Verification:**
- Component renders message correctly
- Initials generated properly
- Timestamp formatted correctly
- No PropTypes warnings
</action>
<verify>
JKChatMessage component created with avatar, name, message, and timestamp display. React.memo applied for performance.
</verify>
<done>
Message display component complete with formatting and performance optimization.
</done>
</task>
<task>
<name>Task 3: Create loading and empty state components</name>
<files>jam-ui/src/components/client/chat/JKChatLoadingSpinner.js, jam-ui/src/components/client/chat/JKChatEmptyState.js</files>
<action>
Create loading spinner and empty state components:
**1. JKChatLoadingSpinner.js:**
```javascript
import React from 'react';
import { Spinner } from 'reactstrap';
const JKChatLoadingSpinner = () => {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '48px 24px',
color: '#6c757d'
}}>
<Spinner color="primary" />
<p style={{ marginTop: '16px', fontSize: '14px' }}>Loading messages...</p>
</div>
);
};
export default JKChatLoadingSpinner;
```
**2. JKChatEmptyState.js:**
```javascript
import React from 'react';
const JKChatEmptyState = () => {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '48px 24px',
color: '#6c757d',
textAlign: 'center'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>💬</div>
<p style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
No messages yet
</p>
<p style={{ fontSize: '14px', margin: 0 }}>
Be the first to send a message in this chat!
</p>
</div>
);
};
export default JKChatEmptyState;
```
**Component features:**
- Loading: Spinner from reactstrap + "Loading messages..." text
- Empty: Chat emoji + encouraging message
- Centered layout with padding
- Simple, clear messaging
- No props needed (stateless components)
**Verification:**
- Both components render without errors
- Styles applied correctly
- Spinner animates
- No console warnings
</action>
<verify>
Loading and empty state components created. Both render correctly with appropriate styling.
</verify>
<done>
Loading and empty state components complete.
</done>
</task>
<task type="tdd">
<name>Task 4: Create JKChatMessageList with auto-scroll logic (TDD for auto-scroll behavior)</name>
<files>jam-ui/src/components/client/chat/JKChatMessageList.js, jam-ui/src/components/client/chat/__tests__/JKChatMessageList.test.js</files>
<action>
Create message list component with auto-scroll logic. Use TDD for auto-scroll behavior.
**RED Phase - Write failing tests for auto-scroll logic:**
1. Create test file `jam-ui/src/components/client/chat/__tests__/JKChatMessageList.test.js`:
```javascript
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import JKChatMessageList from '../JKChatMessageList';
import sessionChatReducer from '../../../../store/features/sessionChatSlice';
const createMockStore = (messages = []) => {
return configureStore({
reducer: {
sessionChat: sessionChatReducer
},
preloadedState: {
sessionChat: {
messagesByChannel: { 'test-channel': messages },
activeChannel: 'test-channel',
fetchStatus: { 'test-channel': 'idle' }
}
}
});
};
describe('JKChatMessageList auto-scroll behavior', () => {
test('scrolls to bottom on mount if messages exist', () => {
const messages = [
{ id: '1', senderId: 'user1', senderName: 'User 1', message: 'Hello', createdAt: new Date().toISOString() }
];
const store = createMockStore(messages);
const scrollToMock = jest.fn();
HTMLDivElement.prototype.scrollTo = scrollToMock;
render(
<Provider store={store}>
<JKChatMessageList />
</Provider>
);
// Should call scrollTo after messages render
// Note: May need to wait for useEffect
// expect(scrollToMock).toHaveBeenCalled();
});
test('shows empty state when no messages', () => {
const store = createMockStore([]);
render(
<Provider store={store}>
<JKChatMessageList />
</Provider>
);
expect(screen.getByText('No messages yet')).toBeInTheDocument();
});
test('shows loading spinner when fetching', () => {
const store = configureStore({
reducer: { sessionChat: sessionChatReducer },
preloadedState: {
sessionChat: {
messagesByChannel: {},
activeChannel: 'test-channel',
fetchStatus: { 'test-channel': 'loading' }
}
}
});
render(
<Provider store={store}>
<JKChatMessageList />
</Provider>
);
expect(screen.getByText('Loading messages...')).toBeInTheDocument();
});
});
```
2. Run tests - should FAIL (component doesn't exist yet)
**GREEN Phase - Implement component:**
3. Create `jam-ui/src/components/client/chat/JKChatMessageList.js`:
```javascript
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import {
selectChatMessages,
selectFetchStatus,
selectActiveChannel
} from '../../../store/features/sessionChatSlice';
import JKChatMessage from './JKChatMessage';
import JKChatLoadingSpinner from './JKChatLoadingSpinner';
import JKChatEmptyState from './JKChatEmptyState';
const JKChatMessageList = () => {
const activeChannel = useSelector(selectActiveChannel);
const messages = useSelector((state) => selectChatMessages(state, activeChannel));
const fetchStatus = useSelector((state) => selectFetchStatus(state, activeChannel));
const listRef = useRef(null);
const [isUserScrolling, setIsUserScrolling] = useState(false);
const scrollTimeoutRef = useRef(null);
// Scroll to bottom helper
const scrollToBottom = useCallback(() => {
if (listRef.current) {
listRef.current.scrollTo({
top: listRef.current.scrollHeight,
behavior: 'smooth'
});
}
}, []);
// Detect if user is at bottom (within 50px threshold)
const isAtBottom = useCallback(() => {
if (!listRef.current) return false;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
return scrollHeight - scrollTop - clientHeight < 50;
}, []);
// Handle scroll events
const handleScroll = useCallback(() => {
if (!listRef.current) return;
// Clear existing timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// Set user scrolling flag
setIsUserScrolling(true);
// Reset flag after scroll stops (300ms debounce)
scrollTimeoutRef.current = setTimeout(() => {
setIsUserScrolling(false);
// Re-enable auto-scroll if user scrolled to bottom
if (isAtBottom()) {
setIsUserScrolling(false);
}
}, 300);
}, [isAtBottom]);
// Auto-scroll on new messages (if user at bottom or not manually scrolling)
useEffect(() => {
if (!isUserScrolling && messages.length > 0) {
scrollToBottom();
}
}, [messages.length, isUserScrolling, scrollToBottom]);
// Scroll to bottom on mount
useEffect(() => {
if (messages.length > 0) {
setTimeout(scrollToBottom, 100); // Delay for DOM render
}
}, [scrollToBottom]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, []);
// Loading state
if (fetchStatus === 'loading') {
return <JKChatLoadingSpinner />;
}
// Empty state
if (messages.length === 0) {
return <JKChatEmptyState />;
}
// Message list
return (
<div
ref={listRef}
onScroll={handleScroll}
style={{
flex: 1,
overflowY: 'auto',
padding: '16px',
backgroundColor: '#ffffff'
}}
>
{messages.map((message) => (
<JKChatMessage key={message.id} message={message} />
))}
</div>
);
};
export default JKChatMessageList;
```
4. Run tests - should PASS (at least empty/loading tests)
**REFACTOR Phase:**
5. Review auto-scroll logic:
- Scroll threshold (50px) is reasonable
- Debounce timeout (300ms) feels responsive
- useCallback prevents unnecessary re-renders
6. Add JSDoc comments for complex functions
7. Ensure tests still pass
**Implementation notes:**
- Auto-scroll disabled when user manually scrolls up
- Re-enabled when user scrolls back to bottom (within 50px)
- Smooth scrolling for better UX
- 300ms debounce prevents jittery scroll detection
- scrollTimeoutRef cleanup on unmount prevents memory leaks
**Verification:**
- Tests pass (empty state, loading state)
- Auto-scroll works on mount
- Auto-scroll works on new messages (if at bottom)
- Manual scroll disables auto-scroll
- Scrolling to bottom re-enables auto-scroll
</action>
<verify>
JKChatMessageList component created with auto-scroll logic. Unit tests passing. Auto-scroll behavior validated.
</verify>
<done>
Message list component complete with auto-scroll behavior and TDD validation.
</done>
</task>
<task>
<name>Task 5: Integrate message list into JKSessionChatWindow</name>
<files>jam-ui/src/components/client/JKSessionChatWindow.js</files>
<action>
Replace placeholder content with JKChatMessageList:
1. Open `jam-ui/src/components/client/JKSessionChatWindow.js`
2. Import JKChatMessageList:
```javascript
import JKChatMessageList from './chat/JKChatMessageList.js';
```
3. Replace placeholder content:
```javascript
// OLD:
<div style={{ flex: 1, padding: '16px', backgroundColor: '#ffffff' }}>
<p style={{ color: '#6c757d', textAlign: 'center', marginTop: '24px' }}>
Chat messages will appear here
</p>
</div>
// NEW:
<JKChatMessageList />
```
4. Update component structure (remove padding wrapper, JKChatMessageList has its own styling):
```javascript
return (
<WindowPortal
title="JamKazam Chat"
windowFeatures="width=450,height=600,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=no,resizable=yes"
onClose={handleClose}
windowId="jamkazam-chat"
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<JKChatHeader
channelName={getChannelName()}
onClose={handleClose}
/>
<JKChatMessageList />
</div>
</WindowPortal>
);
```
**Verification:**
- Message list renders inside chat window
- Auto-scroll works on window open
- Header remains fixed at top
- Message list scrolls independently
- No layout issues
</action>
<verify>
JKChatMessageList integrated into chat window. Messages display correctly. Auto-scroll works. Layout correct.
</verify>
<done>
Message list integrated. Chat window now displays messages with auto-scroll. Plan 8.2 complete.
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] formatTimestamp utility created with passing unit tests
- [ ] JKChatMessage component renders message correctly
- [ ] JKChatLoadingSpinner and JKChatEmptyState components created
- [ ] JKChatMessageList component created with auto-scroll logic
- [ ] Auto-scroll unit tests written and passing
- [ ] Auto-scroll works on mount and new messages
- [ ] Manual scrolling disables auto-scroll
- [ ] Message list integrated into JKSessionChatWindow
- [ ] No console errors or warnings
- [ ] All existing tests still pass
</verification>
<success_criteria>
- formatTimestamp utility with 6+ passing tests
- JKChatMessage component with initials avatar
- Loading and empty state components
- JKChatMessageList with auto-scroll behavior
- Auto-scroll logic tested (TDD)
- Message list integrated into chat window
- All tests passing
- Ready for Plan 8.3 (Chat Button & Unread Badge)
</success_criteria>
<output>
After completion, create `.planning/phases/08-chat-window-ui/08-02-SUMMARY.md`:
# Phase 8 Plan 2: Message List & Auto-Scroll Summary
**Built message display with auto-scroll behavior**
## Accomplishments
- Created formatTimestamp utility with relative time formatting (TDD)
- Created JKChatMessage component with avatar, name, message, timestamp
- Created loading spinner and empty state components
- Created JKChatMessageList with auto-scroll logic (TDD for behavior)
- Integrated message list into chat window
- Auto-scroll respects user manual scrolling
## Files Created/Modified
- `jam-ui/src/utils/formatTimestamp.js` - Timestamp formatting utility (NEW)
- `jam-ui/src/utils/__tests__/formatTimestamp.test.js` - Unit tests (NEW)
- `jam-ui/src/components/client/chat/JKChatMessage.js` - Message component (NEW)
- `jam-ui/src/components/client/chat/JKChatLoadingSpinner.js` - Loading state (NEW)
- `jam-ui/src/components/client/chat/JKChatEmptyState.js` - Empty state (NEW)
- `jam-ui/src/components/client/chat/JKChatMessageList.js` - Message list with auto-scroll (NEW)
- `jam-ui/src/components/client/chat/__tests__/JKChatMessageList.test.js` - Unit tests (NEW)
- `jam-ui/src/components/client/JKSessionChatWindow.js` - Integrated message list (MODIFIED)
## Decisions Made
[Document any implementation decisions: Auto-scroll threshold? Debounce duration? Timestamp format choices?]
## Issues Encountered
[Any challenges with auto-scroll logic, or "None"]
## Next Phase Readiness
Ready for Plan 3 (Chat Button & Unread Badge)
</output>

View File

@ -0,0 +1,592 @@
---
phase: 08-chat-window-ui
plan: 03
type: standard
---
<objective>
Create chat button with unread badge and integrate into session screen navigation.
Purpose: Add the chat button to JKSessionScreen top navigation with unread count badge, completing the chat window trigger UI and Phase 8.
Output: JKSessionChatButton component with badge, navigation integration, styling, integration tests.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-phase.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Phase 6 design documents
@.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md
@.planning/phases/06-session-chat-research-design/IMPLEMENTATION_ROADMAP.md
# Phase 7 summary
@.planning/phases/07-chat-infrastructure/07-03-SUMMARY.md
# Phase 8 Plan 1 & 2 summaries
@.planning/phases/08-chat-window-ui/08-01-SUMMARY.md
@.planning/phases/08-chat-window-ui/08-02-SUMMARY.md
# Redux selectors and state
@jam-ui/src/store/features/sessionChatSlice.js
# Session screen (integration point)
@jam-ui/src/components/client/JKSessionScreen.js
# Codebase conventions
@.planning/codebase/ARCHITECTURE.md
@.planning/codebase/CONVENTIONS.md
@CLAUDE.md
**Key context from Phase 6 design:**
- Chat button: Icon button with badge showing unread count
- Badge: Only visible when unreadCount > 0
- Badge text: Number (1-99) or "99+" for counts >= 100
- Button placement: Top navigation bar in JKSessionScreen (near other action buttons)
- Button behavior: Click opens chat window (dispatch openChatWindow + setActiveChannel)
- Icon: Use existing chatIcon from assets/img/client/chat.svg
**Key context from Phase 7:**
- Selectors available: `selectTotalUnreadCount`, `selectIsChatWindowOpen`, `selectActiveChannel`
- Actions available: `openChatWindow`, `setActiveChannel`
- Unread count: Sum across all channels (global + session + lesson)
**Session screen navigation:**
- Top bar has action buttons: gear (settings), invite, volume, video, record, broadcast, open, chat, attach, resync
- Icons imported from assets/img/client/*.svg
- Button pattern: `<img src={icon} alt="description" onClick={handler} style={...} />`
- Badge pattern: Position badge absolutely over icon top-right corner
**Integration test requirements:**
- Badge visibility (hidden when count = 0, visible when count > 0)
- Badge text ("1", "5", "99+")
- Button click opens chat window
- Redux state updated on click
</context>
<tasks>
<task>
<name>Task 1: Create JKSessionChatButton component</name>
<files>jam-ui/src/components/client/JKSessionChatButton.js</files>
<action>
Create chat button component with unread badge:
1. Create `jam-ui/src/components/client/JKSessionChatButton.js`
2. Import dependencies:
- React, Redux (useSelector, useDispatch)
- sessionChatSlice actions and selectors
- chatIcon from assets
3. Component structure:
```javascript
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
openChatWindow,
setActiveChannel,
selectTotalUnreadCount,
selectIsChatWindowOpen
} from '../../store/features/sessionChatSlice';
import chatIcon from '../../assets/img/client/chat.svg';
const JKSessionChatButton = ({ sessionId }) => {
const dispatch = useDispatch();
const unreadCount = useSelector(selectTotalUnreadCount);
const isWindowOpen = useSelector(selectIsChatWindowOpen);
const handleClick = useCallback(() => {
if (isWindowOpen) {
// Window already open - do nothing (or focus window if possible)
return;
}
// Set active channel to session chat
dispatch(setActiveChannel({
channel: sessionId,
channelType: 'session'
}));
// Open chat window
dispatch(openChatWindow());
}, [dispatch, sessionId, isWindowOpen]);
// Format badge text
const getBadgeText = () => {
if (unreadCount === 0) return '';
if (unreadCount >= 100) return '99+';
return String(unreadCount);
};
return (
<div style={{ position: 'relative', display: 'inline-block' }}>
<img
src={chatIcon}
alt="Chat"
onClick={handleClick}
style={{
cursor: 'pointer',
width: '24px',
height: '24px',
opacity: isWindowOpen ? 0.6 : 1
}}
title="Open session chat"
/>
{unreadCount > 0 && (
<div
style={{
position: 'absolute',
top: '-6px',
right: '-6px',
backgroundColor: '#dc3545', // Bootstrap danger red
color: 'white',
borderRadius: '10px',
padding: '2px 6px',
fontSize: '11px',
fontWeight: 'bold',
lineHeight: '1',
minWidth: '18px',
textAlign: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.3)'
}}
>
{getBadgeText()}
</div>
)}
</div>
);
};
export default JKSessionChatButton;
```
**Component features:**
- sessionId prop: Used to set active channel on click
- unreadCount badge: Only visible when count > 0
- Badge formatting: "1", "5", "99+" (for 100+)
- Reduced opacity when window already open (visual feedback)
- Tooltip on hover ("Open session chat")
- Badge styling: Red background, white text, positioned top-right
- useCallback for handleClick to prevent re-renders
**Verification:**
- Component renders chat icon
- Badge hidden when unreadCount = 0
- Badge visible with correct text when unreadCount > 0
- Click handler dispatches actions
- No PropTypes warnings
</action>
<verify>
JKSessionChatButton component created with unread badge. Badge visibility and text correct. Click handler works.
</verify>
<done>
Chat button component complete with unread badge functionality.
</done>
</task>
<task>
<name>Task 2: Integrate chat button into JKSessionScreen navigation</name>
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
<action>
Add chat button to session screen top navigation:
1. Open `jam-ui/src/components/client/JKSessionScreen.js`
2. Import JKSessionChatButton near other component imports:
```javascript
import JKSessionChatButton from './JKSessionChatButton.js';
```
3. Locate the top navigation bar (look for action button icons: gear, invite, volume, etc.)
- Search for `gearIcon`, `inviteIcon`, `volumeIcon` to find the navigation section
- Likely in a Row/Col layout or flexbox container
4. Add JKSessionChatButton after the "open" button (openIcon) and before "attach" button (attachIcon):
```javascript
{/* Example placement - adjust based on actual layout */}
<img src={openIcon} alt="Open" onClick={handleOpen} style={{...}} />
<JKSessionChatButton sessionId={sessionId} />
<img src={attachIcon} alt="Attach" onClick={handleAttach} style={{...}} />
```
5. Pass sessionId prop from component state/props:
- sessionId should be available from `selectSessionId` selector or `activeSession.id`
- Example: `const sessionId = useSelector(selectSessionId);`
**Integration notes:**
- Button renders inline with other action buttons
- Spacing should match other icons (margin/padding)
- sessionId required for setting active channel
- Button order: ...open, chat, attach, resync (per design)
**Verification:**
- Chat button visible in session screen top bar
- Button aligned with other action buttons
- sessionId passed correctly
- No layout issues
- Other buttons unaffected
</action>
<verify>
Chat button integrated into JKSessionScreen navigation. Button visible and aligned. sessionId prop passed correctly.
</verify>
<done>
Chat button integrated into session screen. UI complete.
</done>
</task>
<task>
<name>Task 3: Write integration tests for chat button behavior</name>
<files>jam-ui/test/chat/chat-button.spec.ts</files>
<action>
Create Playwright integration tests for chat button:
1. Create `jam-ui/test/chat/chat-button.spec.ts`
2. Write tests for badge visibility and click behavior:
```typescript
import { test, expect } from '@playwright/test';
import { loginToJamUI, createAndJoinSession } from '../helpers/sessionHelpers';
test.describe('Chat Button', () => {
test.beforeEach(async ({ page }) => {
await loginToJamUI(page);
await createAndJoinSession(page);
});
test('shows chat button in session screen navigation', async ({ page }) => {
// Verify chat button is visible
const chatButton = page.locator('img[alt="Chat"]');
await expect(chatButton).toBeVisible();
});
test('hides badge when unread count is 0', async ({ page }) => {
// Ensure unread count is 0 in Redux state
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
const state = store.getState().sessionChat;
expect(state.unreadCounts).toEqual({});
});
// Badge should not be visible
const badge = page.locator('div:has-text(/^\\d+$/)').filter({ hasText: /^\\d+$/ });
await expect(badge).not.toBeVisible();
});
test('shows badge with correct count when messages arrive', async ({ page }) => {
// Simulate receiving a message (increment unread count via Redux)
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
store.dispatch({
type: 'sessionChat/incrementUnreadCount',
payload: { channel: 'test-session-id' }
});
});
// Badge should be visible with "1"
const badge = page.locator('div:has-text("1")');
await expect(badge).toBeVisible();
});
test('shows "99+" for counts >= 100', async ({ page }) => {
// Set unread count to 150
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
for (let i = 0; i < 150; i++) {
store.dispatch({
type: 'sessionChat/incrementUnreadCount',
payload: { channel: 'test-session-id' }
});
}
});
// Badge should show "99+"
const badge = page.locator('div:has-text("99+")');
await expect(badge).toBeVisible();
});
test('opens chat window when button clicked', async ({ page }) => {
// Click chat button
const chatButton = page.locator('img[alt="Chat"]');
await chatButton.click();
// Wait for popup window
const popupPromise = page.waitForEvent('popup', { timeout: 5000 });
const popup = await popupPromise;
// Verify popup opened
expect(popup).toBeTruthy();
await expect(popup).toHaveTitle('JamKazam Chat');
// Verify Redux state updated
const isWindowOpen = await page.evaluate(() => {
const store = window.__REDUX_STORE__;
return store.getState().sessionChat.isWindowOpen;
});
expect(isWindowOpen).toBe(true);
});
test('does not open duplicate window when button clicked again', async ({ page }) => {
// Click chat button
const chatButton = page.locator('img[alt="Chat"]');
await chatButton.click();
// Wait for popup
const popupPromise = page.waitForEvent('popup');
const popup = await popupPromise;
expect(popup).toBeTruthy();
// Click button again
await chatButton.click();
// No new popup should open (timeout after 2 seconds)
await expect(async () => {
await page.waitForEvent('popup', { timeout: 2000 });
}).rejects.toThrow();
});
test('resets badge count when chat window opened', async ({ page }) => {
// Add unread messages
await page.evaluate(() => {
const store = window.__REDUX_STORE__;
for (let i = 0; i < 5; i++) {
store.dispatch({
type: 'sessionChat/incrementUnreadCount',
payload: { channel: 'test-session-id' }
});
}
});
// Verify badge shows "5"
let badge = page.locator('div:has-text("5")');
await expect(badge).toBeVisible();
// Open chat window
const chatButton = page.locator('img[alt="Chat"]');
await chatButton.click();
await page.waitForEvent('popup');
// Wait for Redux state to update
await page.waitForTimeout(500);
// Badge should be hidden (count reset to 0)
badge = page.locator('div:has-text("5")');
await expect(badge).not.toBeVisible();
});
});
```
**Test coverage:**
- Button visibility
- Badge hidden when count = 0
- Badge visible with correct text (1-99)
- Badge shows "99+" for counts >= 100
- Button click opens chat window
- Duplicate window prevention
- Badge resets on window open
**Verification:**
- All 7 tests pass
- No flaky tests
- Redux state validated correctly
</action>
<verify>
Integration tests written and passing. Badge behavior validated. Window open behavior confirmed. Redux state updates verified.
</verify>
<done>
Integration tests complete for chat button. Plan 8.3 complete.
</done>
</task>
<task>
<name>Task 4: Add SCSS styling for chat button (optional)</name>
<files>jam-ui/src/components/client/JKSessionChatButton.scss</files>
<action>
Optionally create SCSS file for chat button styling:
**Note:** This task is optional. Inline styles from Task 1 are sufficient for MVP. SCSS provides better organization for future maintenance.
1. Create `jam-ui/src/components/client/JKSessionChatButton.scss`:
```scss
.jk-session-chat-button {
position: relative;
display: inline-block;
&__icon {
cursor: pointer;
width: 24px;
height: 24px;
opacity: 1;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
&--open {
opacity: 0.6;
}
}
&__badge {
position: absolute;
top: -6px;
right: -6px;
background-color: #dc3545; // Bootstrap danger red
color: white;
border-radius: 10px;
padding: 2px 6px;
font-size: 11px;
font-weight: bold;
line-height: 1;
min-width: 18px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
animation: badge-pulse 0.3s ease-out;
}
}
@keyframes badge-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
```
2. Import SCSS in component:
```javascript
import './JKSessionChatButton.scss';
```
3. Update component JSX to use class names:
```javascript
<div className="jk-session-chat-button">
<img
src={chatIcon}
alt="Chat"
onClick={handleClick}
className={`jk-session-chat-button__icon ${isWindowOpen ? 'jk-session-chat-button__icon--open' : ''}`}
title="Open session chat"
/>
{unreadCount > 0 && (
<div className="jk-session-chat-button__badge">
{getBadgeText()}
</div>
)}
</div>
```
4. Run SCSS compilation:
```bash
cd jam-ui
npm run scss
```
**SCSS benefits:**
- Better organization (BEM naming convention)
- Hover effects for icon
- Badge pulse animation on new messages
- Easier to maintain and extend
**Skip this task if:**
- Inline styles are sufficient for MVP
- Want to batch styling updates later
- Time constraints
**Verification:**
- SCSS compiles without errors
- Styles applied correctly
- Hover effects work
- Badge animation smooth
- No visual regressions
</action>
<verify>
SCSS styling added (or skipped). Chat button styled consistently. Animations work (if added).
</verify>
<done>
Styling complete (or deferred). Chat button UI polished.
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] JKSessionChatButton component created with badge
- [ ] Badge hidden when unreadCount = 0
- [ ] Badge visible with correct text (1-99, "99+")
- [ ] Chat button integrated into JKSessionScreen navigation
- [ ] sessionId prop passed correctly
- [ ] Integration tests written and passing (7 tests)
- [ ] Button click opens chat window
- [ ] Badge resets when window opens
- [ ] SCSS styling added (optional)
- [ ] No console errors or warnings
</verification>
<success_criteria>
- JKSessionChatButton component with unread badge
- Badge visibility logic working correctly
- Chat button integrated into session navigation
- Integration tests passing (7+ tests)
- Button opens chat window with correct channel
- Badge resets on window open
- Styling complete (inline or SCSS)
- Phase 8 complete - Chat window UI fully functional
- Ready for Phase 9 (Message Composition & Sending)
</success_criteria>
<output>
After completion, create `.planning/phases/08-chat-window-ui/08-03-SUMMARY.md`:
# Phase 8 Plan 3: Chat Button & Unread Badge Summary
**Created chat button with unread badge and integrated into session navigation**
## Accomplishments
- Created JKSessionChatButton component with unread count badge
- Badge shows correct text (1-99, "99+") and hides when count = 0
- Integrated chat button into JKSessionScreen top navigation
- Wrote integration tests for badge visibility and click behavior
- Badge resets when chat window opens
- Optional SCSS styling (if completed)
## Files Created/Modified
- `jam-ui/src/components/client/JKSessionChatButton.js` - Chat button with badge (NEW)
- `jam-ui/src/components/client/JKSessionScreen.js` - Integrated chat button (MODIFIED)
- `jam-ui/test/chat/chat-button.spec.ts` - Integration tests (NEW)
- `jam-ui/src/components/client/JKSessionChatButton.scss` - SCSS styling (OPTIONAL, NEW)
## Decisions Made
[Document any implementation decisions: Badge positioning? Badge color? Button placement in navigation? SCSS vs inline styles?]
## Issues Encountered
[Any challenges with badge visibility, or "None"]
## Phase 8 Complete!
**Chat Window UI fully functional:**
- ✅ Chat window opens in popup (Plan 8.1)
- ✅ Messages display with auto-scroll (Plan 8.2)
- ✅ Chat button with unread badge (Plan 8.3)
**Ready for Phase 9 (Message Composition & Sending).**
Next phase will add:
- Text input composer
- Send button functionality
- Real-time message delivery
- Enter key to send
- Character count/limits
</output>