394 lines
12 KiB
Markdown
394 lines
12 KiB
Markdown
---
|
|
phase: 32-state-update-optimization
|
|
plan: 03
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- jam-ui/src/components/client/JKSessionScreen.js
|
|
- jam-ui/src/components/client/JKResyncButton.js
|
|
- jam-ui/src/components/client/JKVideoButton.js
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "resyncLoading state lives in JKResyncButton (not JKSessionScreen)"
|
|
- "videoLoading state lives in JKVideoButton (not JKSessionScreen)"
|
|
- "Loading state changes don't re-render JKSessionScreen"
|
|
artifacts:
|
|
- path: "jam-ui/src/components/client/JKResyncButton.js"
|
|
provides: "Self-contained resync button with loading state"
|
|
exports: ["JKResyncButton"]
|
|
- path: "jam-ui/src/components/client/JKVideoButton.js"
|
|
provides: "Self-contained video button with loading state"
|
|
exports: ["JKVideoButton"]
|
|
- path: "jam-ui/src/components/client/JKSessionScreen.js"
|
|
provides: "Uses extracted button components"
|
|
contains: "JKResyncButton"
|
|
key_links:
|
|
- from: "jam-ui/src/components/client/JKSessionScreen.js"
|
|
to: "JKResyncButton"
|
|
via: "import and render"
|
|
pattern: "<JKResyncButton"
|
|
- from: "jam-ui/src/components/client/JKSessionScreen.js"
|
|
to: "JKVideoButton"
|
|
via: "import and render"
|
|
pattern: "<JKVideoButton"
|
|
---
|
|
|
|
<objective>
|
|
Colocate loading states to button components
|
|
|
|
Purpose: Move resyncLoading (COLOC-01) and videoLoading (COLOC-02) from JKSessionScreen to their respective button components. This follows state colocation principles - state should live in the component that uses it, preventing parent re-renders.
|
|
|
|
Output: JKResyncButton and JKVideoButton components with colocated loading state, JKSessionScreen simplified
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/32-state-update-optimization/32-RESEARCH.md
|
|
|
|
# Source files
|
|
@jam-ui/src/components/client/JKSessionScreen.js
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create JKResyncButton component</name>
|
|
<files>jam-ui/src/components/client/JKResyncButton.js</files>
|
|
<action>
|
|
Create a new self-contained button component with colocated loading state.
|
|
|
|
```javascript
|
|
import React, { useState, useCallback, memo } from 'react';
|
|
import { Button, Spinner } from 'reactstrap';
|
|
import { toast } from 'react-toastify';
|
|
import PropTypes from 'prop-types';
|
|
|
|
/**
|
|
* Self-contained resync button with colocated loading state.
|
|
* Loading state changes only re-render this component, not the parent.
|
|
*
|
|
* State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
|
|
*/
|
|
const JKResyncButton = memo(({ resyncAudio, className }) => {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleClick = useCallback(async (e) => {
|
|
e.preventDefault();
|
|
if (loading) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
await resyncAudio();
|
|
// Silent success (matches legacy behavior)
|
|
} catch (error) {
|
|
if (error.message === 'timeout') {
|
|
toast.error('Audio resync timed out. Please try again.');
|
|
} else {
|
|
toast.error('Audio resync failed: ' + (error.message || 'Unknown error'));
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [resyncAudio, loading]);
|
|
|
|
return (
|
|
<Button
|
|
className={className || 'btn-custom-outline'}
|
|
outline
|
|
size="md"
|
|
onClick={handleClick}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Spinner size="sm" /> Resyncing...
|
|
</>
|
|
) : (
|
|
'Resync'
|
|
)}
|
|
</Button>
|
|
);
|
|
});
|
|
|
|
JKResyncButton.displayName = 'JKResyncButton';
|
|
|
|
JKResyncButton.propTypes = {
|
|
resyncAudio: PropTypes.func.isRequired,
|
|
className: PropTypes.string
|
|
};
|
|
|
|
export default JKResyncButton;
|
|
```
|
|
|
|
Key design decisions:
|
|
- memo() wrapper prevents re-renders from parent prop stability
|
|
- Loading state is local - changes don't propagate up
|
|
- Same error handling as original handleResync
|
|
- displayName for React DevTools debugging
|
|
- PropTypes for documentation
|
|
</action>
|
|
<verify>
|
|
File exists and exports component:
|
|
`ls -la jam-ui/src/components/client/JKResyncButton.js`
|
|
|
|
Has useState for loading:
|
|
`grep "useState(false)" jam-ui/src/components/client/JKResyncButton.js`
|
|
|
|
Has memo wrapper:
|
|
`grep "memo(" jam-ui/src/components/client/JKResyncButton.js`
|
|
</verify>
|
|
<done>
|
|
JKResyncButton component created with colocated loading state
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create JKVideoButton component</name>
|
|
<files>jam-ui/src/components/client/JKVideoButton.js</files>
|
|
<action>
|
|
Create a new self-contained video button component with colocated loading state.
|
|
|
|
```javascript
|
|
import React, { useState, useCallback, memo } from 'react';
|
|
import { Button, Spinner } from 'reactstrap';
|
|
import { toast } from 'react-toastify';
|
|
import PropTypes from 'prop-types';
|
|
import videoIcon from '../../assets/images/icons8-video-call-50.png';
|
|
|
|
/**
|
|
* Self-contained video button with colocated loading state.
|
|
* Loading state changes only re-render this component, not the parent.
|
|
*
|
|
* State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
|
|
*/
|
|
const JKVideoButton = memo(({
|
|
canVideo,
|
|
getVideoUrl,
|
|
onUpgradePrompt,
|
|
className
|
|
}) => {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Open external link in new window/tab
|
|
const openExternalLink = useCallback((url) => {
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
}, []);
|
|
|
|
const handleClick = useCallback(async () => {
|
|
if (!canVideo()) {
|
|
onUpgradePrompt();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Get video conferencing room URL from server
|
|
const response = await getVideoUrl();
|
|
const videoUrl = `${response.url}&audiooff=true`;
|
|
|
|
// Open video URL in new browser window/tab
|
|
openExternalLink(videoUrl);
|
|
|
|
} catch (error) {
|
|
toast.error('Failed to start video session');
|
|
} finally {
|
|
// Keep loading state for 10 seconds to prevent multiple clicks
|
|
setTimeout(() => setLoading(false), 10000);
|
|
}
|
|
}, [canVideo, getVideoUrl, onUpgradePrompt, openExternalLink]);
|
|
|
|
return (
|
|
<Button
|
|
className={className || 'btn-custom-outline'}
|
|
outline
|
|
size="md"
|
|
onClick={handleClick}
|
|
disabled={loading}
|
|
>
|
|
<img
|
|
src={videoIcon}
|
|
alt="Video"
|
|
style={{ width: '20px', height: '20px', marginRight: '0.3rem' }}
|
|
/>
|
|
{loading && <Spinner size="sm" />}
|
|
Video
|
|
</Button>
|
|
);
|
|
});
|
|
|
|
JKVideoButton.displayName = 'JKVideoButton';
|
|
|
|
JKVideoButton.propTypes = {
|
|
canVideo: PropTypes.func.isRequired,
|
|
getVideoUrl: PropTypes.func.isRequired,
|
|
onUpgradePrompt: PropTypes.func.isRequired,
|
|
className: PropTypes.string
|
|
};
|
|
|
|
export default JKVideoButton;
|
|
```
|
|
|
|
Key design decisions:
|
|
- 10-second loading timeout preserved from original
|
|
- canVideo and onUpgradePrompt as props for flexibility
|
|
- Video icon imported directly (same path as original)
|
|
- memo() for render optimization
|
|
</action>
|
|
<verify>
|
|
File exists and exports component:
|
|
`ls -la jam-ui/src/components/client/JKVideoButton.js`
|
|
|
|
Has useState for loading:
|
|
`grep "useState(false)" jam-ui/src/components/client/JKVideoButton.js`
|
|
|
|
Has 10-second timeout:
|
|
`grep "10000" jam-ui/src/components/client/JKVideoButton.js`
|
|
</verify>
|
|
<done>
|
|
JKVideoButton component created with colocated loading state
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Refactor JKSessionScreen to use extracted components</name>
|
|
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
|
|
<action>
|
|
Replace inline button implementations with the new components.
|
|
|
|
1. Add imports at top:
|
|
```javascript
|
|
import JKResyncButton from './JKResyncButton';
|
|
import JKVideoButton from './JKVideoButton';
|
|
```
|
|
|
|
2. Remove useState declarations (around lines 202-205):
|
|
```javascript
|
|
// DELETE these lines:
|
|
const [videoLoading, setVideoLoading] = useState(false);
|
|
const [resyncLoading, setResyncLoading] = useState(false);
|
|
```
|
|
|
|
3. Remove handleResync function (around lines 1056-1075):
|
|
```javascript
|
|
// DELETE the entire handleResync function
|
|
// const handleResync = useCallback(async (e) => { ... });
|
|
```
|
|
|
|
4. Remove handleVideoClick function (around lines 977-1002):
|
|
```javascript
|
|
// DELETE the entire handleVideoClick function
|
|
// const handleVideoClick = async () => { ... };
|
|
```
|
|
|
|
5. Update the button JSX in the toolbar section (around lines 1341-1388).
|
|
|
|
Find the Video button:
|
|
```jsx
|
|
<Button className='btn-custom-outline' outline size="md" onClick={handleVideoClick} disabled={videoLoading}>
|
|
<img src={videoIcon} alt="Video" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
|
|
{videoLoading && (<Spinner size="sm" />)}
|
|
Video
|
|
</Button>
|
|
```
|
|
|
|
Replace with:
|
|
```jsx
|
|
<JKVideoButton
|
|
canVideo={canVideo}
|
|
getVideoUrl={() => getVideoConferencingRoomUrl(currentSession.id)}
|
|
onUpgradePrompt={showVideoUpgradePrompt}
|
|
/>
|
|
```
|
|
|
|
Find the Resync button:
|
|
```jsx
|
|
<Button className='btn-custom-outline' outline size="md" onClick={handleResync} disabled={resyncLoading}>
|
|
<img src={resyncIcon} alt="Resync" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
|
|
{resyncLoading ? <><Spinner size="sm" /> Resyncing...</> : 'Resync'}
|
|
</Button>
|
|
```
|
|
|
|
Replace with:
|
|
```jsx
|
|
<JKResyncButton resyncAudio={resyncAudio} />
|
|
```
|
|
|
|
6. Keep the following functions in JKSessionScreen (they're still needed):
|
|
- `canVideo` (permission check)
|
|
- `showVideoUpgradePrompt` (toast display)
|
|
- `resyncAudio` (from useMediaActions)
|
|
|
|
7. Remove videoIcon import if no longer used elsewhere:
|
|
```javascript
|
|
// Check if videoIcon is used elsewhere, if not remove:
|
|
// import videoIcon from '../../assets/images/icons8-video-call-50.png';
|
|
```
|
|
</action>
|
|
<verify>
|
|
New components imported:
|
|
`grep "JKResyncButton\|JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js | head -5`
|
|
|
|
Old state removed:
|
|
`grep "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js | wc -l` should be 0
|
|
|
|
Components used in JSX:
|
|
`grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js`
|
|
|
|
ESLint passes:
|
|
`cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0`
|
|
</verify>
|
|
<done>
|
|
JKSessionScreen refactored to use JKResyncButton and JKVideoButton, loading state no longer in parent
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. New components exist:
|
|
```bash
|
|
ls -la jam-ui/src/components/client/JKResyncButton.js jam-ui/src/components/client/JKVideoButton.js
|
|
```
|
|
|
|
2. Loading state removed from JKSessionScreen:
|
|
```bash
|
|
grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js
|
|
```
|
|
Should return 0
|
|
|
|
3. Components used:
|
|
```bash
|
|
grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js
|
|
```
|
|
Should show both component usages
|
|
|
|
4. ESLint passes:
|
|
```bash
|
|
cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- [ ] JKResyncButton.js created with local loading state
|
|
- [ ] JKVideoButton.js created with local loading state
|
|
- [ ] JKSessionScreen imports and uses both new components
|
|
- [ ] videoLoading and resyncLoading useState removed from JKSessionScreen
|
|
- [ ] handleResync and handleVideoClick removed from JKSessionScreen
|
|
- [ ] All files pass ESLint
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/32-state-update-optimization/32-03-SUMMARY.md`
|
|
</output>
|