This commit is contained in:
legop3
2026-08-19 21:35:02 -04:00
parent fab0673d9e
commit 5ab62c3633
14 changed files with 146 additions and 112 deletions
@@ -16,6 +16,7 @@ import { LinkButtonsPanel } from '../../../components/UserListPanel/index.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../../../components/Tabs/index.jsx';
import ActivitiesTab from '../tabs/shared/ActivitiesTab/index.jsx';
import VipTab from '../tabs/shared/VipTab/index.jsx';
import VipTabButton from '../tabs/shared/VipTabButton/index.jsx';
import HelpTab from '../tabs/shared/HelpTab/index.jsx';
import SettingsTab from '../tabs/shared/SettingsTab/index.jsx';
import { themeGapClass } from '../../../themes/index.js';
@@ -26,7 +27,7 @@ function LeftColumnTabs() {
<TabList className="sticky top-0 z-30 shrink-0 bg-neutral-950">
<Tab id="room">Room</Tab>
<Tab id="activities">Activities</Tab>
<Tab id="vip">Vip</Tab>
<VipTabButton />
</TabList>
<TabPanels>
<TabPanel id="room">
@@ -19,7 +19,6 @@ import GPIOToggleControl from '../../../components/GPIOToggleControl/index.jsx';
import HornControl from '../../../components/HornControl/index.jsx';
import CameraTiltControl from '../../../components/CameraTiltControl/index.jsx';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../../settings/index.js';
import OverseerPreferencePanel from '../../../components/OverseerPreferencePanel/index.jsx';
import CardFrame from '../../../components/CardFrame/index.jsx';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -27,6 +26,7 @@ import { useManualDockAssist } from '../../../features/manualDockAssist/useManua
import { themeGapClass, themeStackClass } from '../../../themes/index.js';
import ActivitiesTab from '../tabs/shared/ActivitiesTab/index.jsx';
import VipTab from '../tabs/shared/VipTab/index.jsx';
import VipTabButton from '../tabs/shared/VipTabButton/index.jsx';
import HelpTab from '../tabs/shared/HelpTab/index.jsx';
import SettingsTab from '../tabs/shared/SettingsTab/index.jsx';
@@ -214,29 +214,6 @@ export default function RightPaneTabs() {
const [activeTab, setActiveTab] = useState('telemetry');
const chatDockRef = useRef(null);
const [chatDockHeight, setChatDockHeight] = useState(CHAT_DOCK_INITIAL_HEIGHT);
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600';
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean(
ownRoverId &&
isVerified &&
(pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)),
);
const vipClipPlaying = Boolean(
ownRoverId &&
isVerified &&
pttMode === 'clip' &&
ownAudioForward?.source === 'upload' &&
ownAudioForward?.state === 'playing',
);
const showOverseerPreferencePanel = useSessionSelector((state) => {
const vote = state.session?.overseerVote;
@@ -345,16 +322,7 @@ export default function RightPaneTabs() {
<TabList>
<Tab id="telemetry">Controls</Tab>
<Tab id="activities">Activities</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
<span className="inline-flex items-center gap-2">
<span>VIP</span>
<span
className={`inline-block h-3 w-3 rounded-full ${vipDotClass}`}
aria-hidden="true"
title={isVerified ? 'Verified' : 'Not verified'}
/>
</span>
</Tab>
<VipTabButton />
<Tab id="help">Help</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
+2 -17
View File
@@ -2,26 +2,16 @@
// Purpose: Owns the shared mobile tab selector, state, and per-tab modules.
import { useCallback, useState } from 'react';
import Tabs, { Tab, TabList, TabPanels } from '../../../components/Tabs/index.jsx';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useControlSelector } from '../../../controls/index.js';
import { useSettingsNamespace } from '../../../settings/index.js';
import MobileChatTab from '../tabs/mobile/ChatTab/index.jsx';
import RoomControlsTab from '../tabs/mobile/RoomControlsTab/index.jsx';
import ActivitiesTab from '../tabs/shared/ActivitiesTab/index.jsx';
import VipTab from '../tabs/shared/VipTab/index.jsx';
import VipTabButton from '../tabs/shared/VipTabButton/index.jsx';
import HelpTab from '../tabs/shared/HelpTab/index.jsx';
import SettingsTab from '../tabs/shared/SettingsTab/index.jsx';
export default function MobileTabs() {
const [activeTab, setActiveTab] = useState('chat');
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => ownRoverId ? state.session?.audioForward?.[ownRoverId] || null : null);
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean(ownRoverId && isVerified && (pttMode === 'clip' ? pttActive : (vipAudio?.openMicEnabled || pttActive)));
const vipClipPlaying = Boolean(ownRoverId && isVerified && pttMode === 'clip' && ownAudioForward?.source === 'upload' && ownAudioForward?.state === 'playing');
const handleTabChange = useCallback((tab) => setActiveTab(tab), []);
return (
@@ -30,12 +20,7 @@ export default function MobileTabs() {
<TabList>
<Tab id="chat">Chat</Tab>
<Tab id="activities">Activities</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
<span className="inline-flex items-center gap-0.5">
<span>VIP</span>
<span className={`inline-block h-1.5 w-1.5 rounded-full ${isVerified ? 'bg-emerald-400' : 'bg-amber-400'}`} aria-hidden="true" title={isVerified ? 'Verified' : 'Not verified'} />
</span>
</Tab>
<VipTabButton compact />
<Tab id="roomcontrols">Room Controls</Tab>
<Tab id="help">Help</Tab>
<Tab id="settings">Settings</Tab>
@@ -0,0 +1,48 @@
// Driver VIP Tab Button
// Purpose: Keeps verification and active-audio status identical across every driver layout.
import { Tab } from '../../../../../components/Tabs/index.jsx';
import { useControlSelector } from '../../../../../controls/index.js';
import { useSessionSelector } from '../../../../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../../../../settings/index.js';
export default function VipTabButton({ compact = false }) {
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => (
ownRoverId ? state.session?.audioForward?.[ownRoverId] || null : null
));
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const clipMode = vipAudio?.pttMode === 'clip';
// Pink represents a live microphone path, while green means an uploaded clip
// is actively forwarding. Keeping these conditions here prevents desktop and
// mobile selectors from drifting away from the mounted VIP audio lifecycle.
const micActive = Boolean(
ownRoverId
&& isVerified
&& (clipMode ? pttActive : (vipAudio?.openMicEnabled || pttActive)),
);
const clipPlaying = Boolean(
ownRoverId
&& isVerified
&& clipMode
&& ownAudioForward?.source === 'upload'
&& ownAudioForward?.state === 'playing',
);
const dotSizeClass = compact ? 'h-1.5 w-1.5' : 'h-3 w-3';
const gapClass = compact ? 'gap-0.5' : 'gap-2';
return (
<Tab id="vip" highlight={clipPlaying ? 'green' : micActive ? 'pink' : 'none'}>
<span className={`inline-flex items-center ${gapClass}`}>
<span>VIP</span>
<span
className={`inline-block rounded-full ${dotSizeClass} ${isVerified ? 'bg-emerald-400' : 'bg-red-600'}`}
aria-hidden="true"
title={isVerified ? 'Verified' : 'Not verified'}
/>
</span>
</Tab>
);
}