Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d8e4c95
Refactor: Implement Optimistic UI for Spotify Controls
google-labs-jules[bot] Mar 16, 2026
c3af0aa
Refactor: Implement Optimistic UI for Spotify Controls and fix CI
google-labs-jules[bot] Mar 16, 2026
2a5fb59
Refactor: Implement Optimistic UI for Spotify Controls and Fix CI
google-labs-jules[bot] Mar 16, 2026
71013a1
Refactor: Optimistic UI for Spotify Controls and CI Fixes
google-labs-jules[bot] Mar 16, 2026
957657c
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 17, 2026
3168e9b
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 17, 2026
00def15
feat(large): Refactor optimistic UI for Spotify controls (#9574)
arii Mar 18, 2026
b2a0cb0
feat(large): Refactor optimistic UI for Spotify controls (#9569)
arii Mar 18, 2026
b6a2912
feat(large): Refactor Spotify UI optimistic state and volume sync (#9…
arii Mar 19, 2026
d052734
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 20, 2026
ead9f65
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 23, 2026
5384589
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 23, 2026
0c65504
Refactor: Enhance Spotify Optimistic UI Architecture (#9664)
arii Mar 26, 2026
51be57a
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 26, 2026
9656ab3
Refactor Spotify optimistic UI and minimize overhead (#9683)
arii Mar 27, 2026
7135ba3
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 27, 2026
fedad5a
Merge branch 'leader' into refactor/spotify-optimistic-ui-11189282566…
arii Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 21 additions & 95 deletions app/client/control/components/SpotifyControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import Select from '@mui/material/Select'
import Typography from '@mui/material/Typography'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import useVolumePreference, { clampVolume } from '@/hooks/useVolumePreference'
import { useAppSnackbar } from '@/hooks/useAppSnackbar'
import { clampVolume } from '@/utils/audioManager'
import { useSpotifyVolume } from '@/hooks/useSpotifyVolume'

import { useWebSocket } from '@/context/WebSocketContext'
import { useSpotifyCommand } from '@/hooks/useSpotifyCommand'
import { SpotifyCommand } from '@/types/websocket'
import {
HRM_WEB_PLAYER_NAME,
VOLUME_SYNC_GRACE_PERIOD_MS,
} from '@/constants/spotify'
import { HRM_WEB_PLAYER_NAME } from '@/constants/spotify'
import PlaybackControls from '@/components/shared/PlaybackControls'
import SpotifySearchInput from '@/components/SpotifySearchInput'
import VolumeSlider from '@/components/shared/VolumeSlider'
Expand All @@ -34,15 +32,11 @@ const SpotifyControls = () => {
useWebSocket()
const { execute: executeSpotify } = useSpotifyCommand()
const { devices = [] } = spotifyData // Default to empty array if undefined
const { volume, setVolume, muted, toggleMute } = useVolumePreference()
const { showWarning } = useAppSnackbar()

const lastSentVolumeRef = useRef<string | null>(null)
const lastWarningTimeRef = useRef<number>(0)

const [selectedDeviceId, setSelectedDeviceId] = useState<string>('')
const [isSliding, setIsSliding] = useState(false)
const prevActiveIdRef = useRef<string | undefined>(undefined)
const lastVolumeSyncTimeRef = useRef<number>(0)
const hasPendingSendRef = useRef<boolean>(false)

const [optimisticIsPlaying, setOptimisticIsPlaying] = useState<
boolean | null
>(null)
Expand Down Expand Up @@ -83,63 +77,15 @@ const SpotifyControls = () => {
}
}, [connectionStatus, sendData, spotifyServiceInitialized])

// 4. Sync selected device and volume with active device
// Auto-select HRM Web Player if no active device is available
useEffect(() => {
const activeDevice = devices.find((d) => d.is_active)
const activeId = activeDevice?.id

// Helper: determine if device should be updated to activeId
const shouldUpdateToActive = () => {
// Initial sync or active device changed externally
if (!prevActiveIdRef.current || activeId !== prevActiveIdRef.current) {
return Boolean(activeId)
}
// Selected device no longer exists or no device selected
const selectedStillExists = devices.some((d) => d.id === selectedDeviceId)
return (!selectedDeviceId || !selectedStillExists) && Boolean(activeId)
}

if (shouldUpdateToActive()) {
setSelectedDeviceId(activeId!)
}
prevActiveIdRef.current = activeId

// Sync Volume (if not dragging and not within grace period after send)
// We rely on the server as the source of truth for volume, but use a grace period
// to prevent local sliders from "jumping" while the user is actively adjusting them.
const playbackVolume = spotifyData.playback.volume_percent

if (isSliding) return

const timeSinceLastVolumeSend = Date.now() - lastVolumeSyncTimeRef.current

// Only sync if we haven't sent a volume command recently.
// The server broadcasts a SPOTIFY_UPDATE immediately after a SET_VOLUME command,
// confirming the new state to all clients.
const shouldRespectGracePeriod =
hasPendingSendRef.current &&
timeSinceLastVolumeSend < VOLUME_SYNC_GRACE_PERIOD_MS

if (shouldRespectGracePeriod) {
return
if (activeId && selectedDeviceId !== activeId) {
setSelectedDeviceId(activeId)
}

// Clear pending flag after grace period
if (
hasPendingSendRef.current &&
timeSinceLastVolumeSend >= VOLUME_SYNC_GRACE_PERIOD_MS
) {
hasPendingSendRef.current = false
}

if (activeDevice && typeof playbackVolume === 'number') {
if (playbackVolume !== volume) {
setVolume(playbackVolume)
}
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [devices]) // Rely on devices update to trigger sync
}, [devices.map((d) => `${d.id}:${d.is_active}`).join(','), selectedDeviceId])

// Auto-select HRM Web Player if no active device is available
useEffect(() => {
Expand Down Expand Up @@ -234,37 +180,17 @@ const SpotifyControls = () => {
}
}, [])

const handleVolumeChange = useCallback(
(val: number) => {
setIsSliding(true)
setVolume(val)
if (connectionStatus !== 'Connected') {
const now = Date.now()
// Throttle warning to once every 3 seconds to avoid spam during sliding
if (now - lastWarningTimeRef.current > 3000) {
showWarning('Changes not saved: Offline')
lastWarningTimeRef.current = now
}
}
},
[connectionStatus, showWarning, setVolume]
)

const sendVolumeCommand = useCallback(
(value: number) => {
if (connectionStatus !== 'Connected') return
const targetDeviceId = resolveTargetDeviceId()

// Prevent sending volume command if no device is targeted
if (!targetDeviceId) return

const sanitized = clampVolume(value)
const messageKey = `${targetDeviceId}:${sanitized}`
if (lastSentVolumeRef.current === messageKey) return

hasPendingSendRef.current = true
lastVolumeSyncTimeRef.current = Date.now()

executeSpotify('SET_VOLUME', {
volume: sanitized,
deviceId: targetDeviceId,
Expand All @@ -275,13 +201,13 @@ const SpotifyControls = () => {
[connectionStatus, resolveTargetDeviceId, executeSpotify]
)

const handleVolumeChangeCommitted = useCallback(
(val: number) => {
setIsSliding(false)
sendVolumeCommand(val)
},
[sendVolumeCommand]
)
const {
displayVolume,
isMuted,
handleVolumeChange,
handleVolumeChangeCommitted,
handleToggleMute,
} = useSpotifyVolume(spotifyData.playback.volume_percent, sendVolumeCommand)

useEffect(() => {
if (connectionStatus !== 'Connected') {
Expand Down Expand Up @@ -355,11 +281,11 @@ const SpotifyControls = () => {
/>

<VolumeSlider
volume={volume}
muted={muted}
volume={displayVolume}
muted={isMuted}
onVolumeChange={handleVolumeChange}
onVolumeChangeCommitted={handleVolumeChangeCommitted}
onToggleMute={toggleMute}
onToggleMute={handleToggleMute}
showValue
sliderColor={SPOTIFY_BRAND_COLOR}
size="medium"
Expand Down
Loading
Loading