From 461bbf23130ff30017c7814d3a43d3d47cab13dc Mon Sep 17 00:00:00 2001 From: bennybrainless <242895261+bennybrainless@users.noreply.github.com> Date: Sat, 22 Nov 2025 01:42:29 -0600 Subject: [PATCH] Add optimistic voice control updates and loading states --- .../components/controls/ControlWrapper.tsx | 34 ++++++++- .../src/components/controls/DeafenButton.tsx | 8 ++- .../src/components/controls/MuteButton.tsx | 8 ++- discord/src/stores/callStore.ts | 72 +++++++++++++++++++ discord/src/stores/controlStore.ts | 11 +++ 5 files changed, 128 insertions(+), 5 deletions(-) diff --git a/discord/src/components/controls/ControlWrapper.tsx b/discord/src/components/controls/ControlWrapper.tsx index 930e040..f79b2fd 100644 --- a/discord/src/components/controls/ControlWrapper.tsx +++ b/discord/src/components/controls/ControlWrapper.tsx @@ -5,6 +5,7 @@ interface ControlWrapperProps { iconDisabled: React.ReactNode; onClick: () => void; isEnabled: boolean; + isLoading?: boolean; } export const ControlWrapper: React.FC = ({ @@ -12,7 +13,33 @@ export const ControlWrapper: React.FC = ({ iconDisabled, onClick, isEnabled, + isLoading = false, }) => { + const LoadingSpinner = () => ( + + + + + ); + return ( ); }; diff --git a/discord/src/components/controls/DeafenButton.tsx b/discord/src/components/controls/DeafenButton.tsx index c6dc9f7..29adbc4 100644 --- a/discord/src/components/controls/DeafenButton.tsx +++ b/discord/src/components/controls/DeafenButton.tsx @@ -7,7 +7,12 @@ import { ControlWrapper } from "./ControlWrapper" export const DeafenButton = () => { useInitializeCallStore() const toggleDeafen = useControlStore((state) => state.toggleDeafen) - const isDeafened = useCallStore((state) => state.callStatus?.user?.isDeafened) || false + const { callStatus, isLoading } = useCallStore((state) => ({ + callStatus: state.callStatus, + isLoading: state.isLoading, + })) + const isDeafened = callStatus?.user?.isDeafened || false + const showLoading = isLoading && !callStatus return ( { iconDisabled={} onClick={toggleDeafen} isEnabled={!isDeafened} + isLoading={showLoading} /> ) } \ No newline at end of file diff --git a/discord/src/components/controls/MuteButton.tsx b/discord/src/components/controls/MuteButton.tsx index ce77a63..6857549 100644 --- a/discord/src/components/controls/MuteButton.tsx +++ b/discord/src/components/controls/MuteButton.tsx @@ -7,7 +7,12 @@ import { ControlWrapper } from "./ControlWrapper" export const MuteButton = () => { useInitializeCallStore() const toggleMute = useControlStore((state) => state.toggleMute) - const isMuted = useCallStore((state) => state.callStatus?.user?.isMuted) || false + const { callStatus, isLoading } = useCallStore((state) => ({ + callStatus: state.callStatus, + isLoading: state.isLoading, + })) + const isMuted = callStatus?.user?.isMuted || false + const showLoading = isLoading && !callStatus return ( { iconDisabled={} onClick={toggleMute} isEnabled={!isMuted} + isLoading={showLoading} /> ) } \ No newline at end of file diff --git a/discord/src/stores/callStore.ts b/discord/src/stores/callStore.ts index 992d77f..a3a2f62 100644 --- a/discord/src/stores/callStore.ts +++ b/discord/src/stores/callStore.ts @@ -9,6 +9,10 @@ type CallStoreState = { callStatus: CallStatus | null; selfUserId: string | null; pollingIntervalId: ReturnType | null; + updateLocalVoiceState: (update: { + isMuted?: boolean; + isDeafened?: boolean; + }) => void; initialize: () => void; refreshCallStatus: () => void; setCallStatus: (callStatus: CallStatus) => void; @@ -127,6 +131,73 @@ export const useCallStore = create((set, get) => { callStatus: null, pollingIntervalId: null, + updateLocalVoiceState: (update) => { + set((state) => { + if (!state.callStatus) return {}; + + const targetUserId = state.callStatus.user?.id ?? state.selfUserId; + if (!targetUserId) return {}; + + const participantExists = state.callStatus.participants.some( + (participant) => participant.id === targetUserId + ); + + const updatedParticipants = state.callStatus.participants.map( + (participant) => + participant.id === targetUserId + ? { + ...participant, + ...(update.isMuted !== undefined + ? { isMuted: update.isMuted } + : {}), + ...(update.isDeafened !== undefined + ? { isDeafened: update.isDeafened } + : {}), + } + : participant + ); + + const participants = participantExists + ? updatedParticipants + : [ + ...updatedParticipants, + { + id: targetUserId, + username: + state.callStatus.user?.username ?? + state.callStatus.user?.id ?? + targetUserId, + profileUrl: state.callStatus.user?.profileUrl, + isSpeaking: false, + isMuted: update.isMuted ?? false, + isDeafened: update.isDeafened ?? false, + }, + ]; + + const nextUser = + state.callStatus.user && state.callStatus.user.id === targetUserId + ? { + ...state.callStatus.user, + ...(update.isMuted !== undefined + ? { isMuted: update.isMuted } + : {}), + ...(update.isDeafened !== undefined + ? { isDeafened: update.isDeafened } + : {}), + } + : state.callStatus.user; + + return { + callStatus: { + ...state.callStatus, + participants, + ...(nextUser ? { user: nextUser } : {}), + }, + selfUserId: targetUserId, + }; + }); + }, + initialize: () => { if (get().initialized) return; set({ initialized: true }); @@ -215,6 +286,7 @@ export const useCallStore = create((set, get) => { selfUserId: shouldUpdateUser ? event.payload.userId : state.selfUserId, + isLoading: false, }; }); } diff --git a/discord/src/stores/controlStore.ts b/discord/src/stores/controlStore.ts index eb831eb..bac75a6 100644 --- a/discord/src/stores/controlStore.ts +++ b/discord/src/stores/controlStore.ts @@ -3,6 +3,7 @@ import { createDeskThing } from '@deskthing/client'; import { DISCORD_ACTIONS } from '@shared/types/discord'; import { ToClientTypes, ToServerTypes } from '@shared/types/transit'; import { DISCORD_APP_ID } from '@src/constants/app'; +import { useCallStore } from './callStore'; const DeskThing = createDeskThing(); const APP_ID = DISCORD_APP_ID; @@ -28,26 +29,36 @@ interface ControlStore { export const useControlStore = create(() => ({ mute: () => { DeskThing.debug('Muting user'); + useCallStore.getState().updateLocalVoiceState({ isMuted: true }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.MUTE, value: 'mute', source: APP_ID }); }, unmute: () => { DeskThing.debug('Unmuting user'); + useCallStore.getState().updateLocalVoiceState({ isMuted: false }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.MUTE, value: 'unmute', source: APP_ID }); }, toggleMute: () => { DeskThing.debug('Toggling mute'); + const currentState = useCallStore.getState(); + const isMuted = currentState.callStatus?.user?.isMuted ?? false; + currentState.updateLocalVoiceState({ isMuted: !isMuted }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.MUTE, value: 'toggle', source: APP_ID }); }, deafen: () => { DeskThing.debug('Deafening user'); + useCallStore.getState().updateLocalVoiceState({ isDeafened: true }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.DEAFEN, value: 'deafen', source: APP_ID }); }, undeafen: () => { DeskThing.debug('Undeafening user'); + useCallStore.getState().updateLocalVoiceState({ isDeafened: false }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.DEAFEN, value: 'undeafen', source: APP_ID }); }, toggleDeafen: () => { DeskThing.debug('Toggling deafen'); + const currentState = useCallStore.getState(); + const isDeafened = currentState.callStatus?.user?.isDeafened ?? false; + currentState.updateLocalVoiceState({ isDeafened: !isDeafened }); DeskThing.triggerAction({ id: DISCORD_ACTIONS.DEAFEN, value: 'toggle', source: APP_ID }); }, disconnect: () => {