From 192703f5925bb6e0c4bc9e04394a9b5a44fb03b1 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Mon, 26 Jan 2026 15:46:15 +0530 Subject: [PATCH] feat(06-02): design session chat component architecture Create comprehensive component design for session chat following JamTrack and Backing Track patterns: - 8 components: JKSessionChatButton, JKSessionChatWindow, 6 sub-components - WindowPortal integration for modeless dialog (reuse existing) - Props vs Redux decision matrix (heavy Redux, minimal props) - Auto-scroll logic for message list - Enter-to-send keyboard handling for composer - Integration plan with JKSessionScreen - Component lifecycle patterns documented - Testing strategy defined (unit, integration, E2E) Component hierarchy: - JKSessionChatButton (top nav with unread badge) - JKSessionChatWindow (WindowPortal wrapper) - JKChatHeader - JKChatMessageList (with auto-scroll) - JKChatMessage (avatar, name, text, timestamp) - JKChatLoadingSpinner - JKChatEmptyState - JKChatComposer (textarea + send button) File structure: jam-ui/src/components/client/chat/ Co-Authored-By: Claude Sonnet 4.5 --- .../CHAT_COMPONENT_DESIGN.md | 954 ++++++++++++++++++ 1 file changed, 954 insertions(+) create mode 100644 .planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md diff --git a/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md b/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md new file mode 100644 index 000000000..21204c7a7 --- /dev/null +++ b/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md @@ -0,0 +1,954 @@ +# Session Chat Component Architecture Design + +**Purpose:** Design React component hierarchy for session chat, following established jam-ui patterns from Backing Track and JamTrack. + +**Date:** 2026-01-26 +**Phase:** 06-session-chat-research-design +**Plan:** 02 + +--- + +## Overview + +This document designs the component architecture for session chat in jam-ui, reusing existing patterns from Phase 3 (Backing Track) and Phase 5 (JamTrack), with WindowPortal for modeless dialog support. + +**Key Design Principles:** +- Follow JamTrack player pattern: Main component with specialized sub-components +- Use WindowPortal for modeless chat window (already proven for Metronome, JamTrack) +- Heavy Redux integration for state management (minimal props) +- Clear separation: UI components vs data/behavior (Redux) + +--- + +## Component Hierarchy + +``` +JKSessionChatButton (in session screen top nav) +├── Badge (unread count indicator) +└── onClick → dispatch(openChatWindow) + +JKSessionChatWindow (WindowPortal wrapper) +├── WindowPortal (from jam-ui/src/components/common/WindowPortal.js) +│ └── Chat content (rendered inside popup window) +│ ├── JKChatHeader +│ │ ├── Title ("Session Chat") +│ │ └── Close button +│ ├── JKChatMessageList (scrollable container) +│ │ ├── JKChatLoadingSpinner (conditional: fetchStatus === 'loading') +│ │ ├── JKChatEmptyState (conditional: messages.length === 0) +│ │ └── JKChatMessage[] (repeated for each message) +│ │ ├── Avatar (user photo) +│ │ ├── Sender name +│ │ ├── Message text +│ │ └── Timestamp +│ └── JKChatComposer +│ ├── Textarea (message input) +│ ├── Character count (255 max) +│ └── Send button +``` + +--- + +## Component Specifications + +### 1. JKSessionChatButton + +**File:** `jam-ui/src/components/client/JKSessionChatButton.js` + +**Purpose:** Chat button in session screen top nav, shows unread badge. + +**Props:** +```javascript +{ + // No props - all state from Redux +} +``` + +**Redux Selectors:** +```javascript +const unreadCount = useSelector(selectUnreadCount(sessionId)); +const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen); +``` + +**Redux Actions:** +```javascript +dispatch(openChatWindow()); +dispatch(closeChatWindow()); +``` + +**Render:** +```jsx + +``` + +**State Ownership:** +- ✅ Redux: unreadCount, isWindowOpen +- ❌ Props: None +- ✅ Local: None (purely presentational) + +**Integration Point:** +- Add to `JKSessionScreen` top navigation area (near mixer, metronome buttons) +- Position: Absolute or flex layout (TBD in implementation) + +--- + +### 2. JKSessionChatWindow + +**File:** `jam-ui/src/components/client/JKSessionChatWindow.js` + +**Purpose:** Main chat window component, wraps WindowPortal for modeless dialog. + +**Props:** +```javascript +{ + // No props - all state from Redux +} +``` + +**Redux Selectors:** +```javascript +const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen); +const sessionId = useSelector(state => state.activeSession.sessionId); +const messages = useSelector(selectChatMessages(sessionId)); +const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]); +const currentUser = useSelector(state => state.auth.user); +``` + +**Redux Actions:** +```javascript +dispatch(closeChatWindow()); +dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId })); +dispatch(markAsRead({ channel: sessionId })); +``` + +**Lifecycle:** +```javascript +useEffect(() => { + if (isWindowOpen && sessionId) { + // Fetch message history on window open + dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId })); + + // Mark messages as read + dispatch(markAsRead({ channel: sessionId })); + } +}, [isWindowOpen, sessionId, dispatch]); +``` + +**Render:** +```jsx +{isWindowOpen && ( + +
+ + + +
+
+)} +``` + +**State Ownership:** +- ✅ Redux: isWindowOpen, messages, fetchStatus +- ❌ Props: None +- ✅ Local: None (delegates to sub-components) + +**Window Management:** +- Uses WindowPortal (no modifications needed) +- Dimensions: 400x600px (similar to Metronome popup) +- Position: User-configurable (WindowPortal handles) +- Auto-cleanup: WindowPortal handles on close + +--- + +### 3. JKChatHeader + +**File:** `jam-ui/src/components/client/chat/JKChatHeader.js` + +**Purpose:** Chat window title bar with close button. + +**Props:** +```javascript +{ + onClose: () => void // Callback to close window +} +``` + +**Redux Selectors:** +```javascript +// None - purely presentational +``` + +**Render:** +```jsx +
+

Session Chat

+ +
+``` + +**State Ownership:** +- ❌ Redux: None +- ✅ Props: onClose callback +- ✅ Local: None + +**Styling:** +- Fixed height (e.g., 48px) +- Flex layout: title left, close button right +- Border-bottom separator + +--- + +### 4. JKChatMessageList + +**File:** `jam-ui/src/components/client/chat/JKChatMessageList.js` + +**Purpose:** Scrollable message container with auto-scroll to bottom. + +**Props:** +```javascript +{ + // No props - all state from Redux +} +``` + +**Redux Selectors:** +```javascript +const sessionId = useSelector(state => state.activeSession.sessionId); +const messages = useSelector(selectChatMessages(sessionId)); // Sorted by created_at ASC +const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]); +const currentUser = useSelector(state => state.auth.user); +``` + +**Local State:** +```javascript +const [isUserScrolling, setIsUserScrolling] = useState(false); +const scrollContainerRef = useRef(null); +``` + +**Auto-Scroll Logic:** +```javascript +useEffect(() => { + if (!isUserScrolling && scrollContainerRef.current) { + // Auto-scroll to bottom on new messages + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + behavior: 'smooth' + }); + } +}, [messages.length, isUserScrolling]); + +const handleScroll = (e) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; + setIsUserScrolling(!isAtBottom); +}; +``` + +**Render:** +```jsx +
+ {fetchStatus === 'loading' && } + + {fetchStatus === 'succeeded' && messages.length === 0 && ( + + )} + + {messages.map(message => ( + + ))} +
+``` + +**State Ownership:** +- ✅ Redux: messages, fetchStatus, currentUser +- ❌ Props: None +- ✅ Local: isUserScrolling, scrollContainerRef + +**Styling:** +- Flex-grow: 1 (fills space between header and composer) +- Overflow-y: auto (scrollable) +- Padding: 16px +- Background: Light gray (#f5f5f5) + +--- + +### 5. JKChatMessage + +**File:** `jam-ui/src/components/client/chat/JKChatMessage.js` + +**Purpose:** Single message item (avatar, name, text, timestamp). + +**Props:** +```javascript +{ + message: { + id: string, + senderId: string, + senderName: string, + message: string, + createdAt: string, // ISO 8601 timestamp + channel: string + }, + isCurrentUser: boolean // True if message from current user +} +``` + +**Redux Selectors:** +```javascript +// None - all data passed via props +``` + +**Helper Functions:** +```javascript +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +dayjs.extend(relativeTime); + +const formatTimestamp = (isoTimestamp) => { + return dayjs(isoTimestamp).fromNow(); // "5 minutes ago" +}; +``` + +**Render:** +```jsx +
+ {!isCurrentUser && ( +
+ {/* Placeholder: Use first letter of name */} + + {message.senderName.charAt(0).toUpperCase()} + +
+ )} + +
+
+ + {isCurrentUser ? 'You' : message.senderName} + + + {formatTimestamp(message.createdAt)} + +
+ +
+ {message.message} +
+
+ + {isCurrentUser && ( +
+ + {message.senderName.charAt(0).toUpperCase()} + +
+ )} +
+``` + +**State Ownership:** +- ❌ Redux: None +- ✅ Props: message, isCurrentUser +- ✅ Local: None (purely presentational) + +**Styling:** +- Layout: Flex row +- Current user messages: Avatar on right, text bubble right-aligned (blue) +- Other user messages: Avatar on left, text bubble left-aligned (gray) +- Text bubble: Rounded corners, padding, max-width 70% +- Timestamp: Small, gray, relative time format + +**Future Enhancement (Phase 11):** +- Replace avatar placeholder with actual user photo from API +- Add attachment support (music_notation, claimed_recording) + +--- + +### 6. JKChatComposer + +**File:** `jam-ui/src/components/client/chat/JKChatComposer.js` + +**Purpose:** Message composition area (textarea + send button). + +**Props:** +```javascript +{ + // No props - all state from Redux +} +``` + +**Redux Selectors:** +```javascript +const sessionId = useSelector(state => state.activeSession.sessionId); +const sendStatus = useSelector(state => state.sessionChat.sendStatus); +const sendError = useSelector(state => state.sessionChat.sendError); +const clientId = useSelector(state => state.auth.clientId); // For WebSocket routing +const isConnected = useSelector(state => state.jamServer.isConnected); +``` + +**Redux Actions:** +```javascript +dispatch(sendMessage({ + message, + channel: sessionId, + channelType: 'session', + sessionId, + clientId +})); +``` + +**Local State:** +```javascript +const [message, setMessage] = useState(''); +const textareaRef = useRef(null); +``` + +**Keyboard Handling:** +```javascript +const handleKeyDown = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); // Prevent newline + handleSend(); + } + // Shift+Enter allows newline +}; +``` + +**Send Handler:** +```javascript +const handleSend = () => { + const trimmed = message.trim(); + + // Validation + if (!trimmed) return; + if (trimmed.length > 255) { + alert('Message too long (max 255 characters)'); + return; + } + if (!isConnected) { + alert('Cannot send message: disconnected from server'); + return; + } + + // Send message + dispatch(sendMessage({ + message: trimmed, + channel: sessionId, + channelType: 'session', + sessionId, + clientId + })); + + // Clear input + setMessage(''); + textareaRef.current?.focus(); +}; +``` + +**Render:** +```jsx +
+ {sendError && ( +
+ Error: {sendError} +
+ )} + +
+