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