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:
parent
9be85da11a
commit
f8a3a7bb84
|
|
@ -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 | - |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue