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