Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions sources/components/MainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { useRouter } from 'expo-router';
import { EmptySessionsTablet } from './EmptySessionsTablet';
import { SessionsList } from './SessionsList';
import { FABWide } from './FABWide';
import { TabBar, TabType } from './TabBar';
import { TabBar } from './TabBar';
import { useTabState, TabType } from '@/hooks/useTabState';
import { InboxView } from './InboxView';
import { SettingsViewWrapper } from './SettingsViewWrapper';
import { SessionsListWrapper } from './SessionsListWrapper';
Expand Down Expand Up @@ -231,17 +232,17 @@ export const MainView = React.memo(({ variant }: MainViewProps) => {
const friendRequests = useFriendRequests();
const realtimeStatus = useRealtimeStatus();

// Tab state management
// Tab state management - persisted to server via KV API
// NOTE: Zen tab removed - the feature never got to a useful state
const [activeTab, setActiveTab] = React.useState<TabType>('sessions');
const { activeTab, setActiveTab, isLoading: isTabLoading } = useTabState();

const handleNewSession = React.useCallback(() => {
router.push('/new');
}, [router]);

const handleTabPress = React.useCallback((tab: TabType) => {
setActiveTab(tab);
}, []);
}, [setActiveTab]);

// Regular phone mode with tabs - define this before any conditional returns
const renderTabContent = React.useCallback(() => {
Expand Down
4 changes: 3 additions & 1 deletion sources/components/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { Typography } from '@/constants/Typography';
import { layout } from '@/components/layout';
import { useInboxHasContent } from '@/hooks/useInboxHasContent';

export type TabType = 'zen' | 'inbox' | 'sessions' | 'settings';
// Import and re-export TabType from the hook for backward compatibility
import type { TabType } from '@/hooks/useTabState';
export type { TabType };

interface TabBarProps {
activeTab: TabType;
Expand Down
129 changes: 129 additions & 0 deletions sources/hooks/useTabState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from 'react';
import { useAuth } from '@/auth/AuthContext';
import { kvGet, kvSet, KvItem } from '@/sync/apiKv';

/**
* Tab types for the main navigation
* Note: 'zen' is included for TabBar compatibility but not actively used
*/
export type TabType = 'zen' | 'inbox' | 'sessions' | 'settings';

const TAB_STATE_KEY = 'ui:active-tab';
const DEFAULT_TAB: TabType = 'sessions';

interface TabState {
activeTab: TabType;
version: number;
}

/**
* Hook for persistent tab state that syncs to the server.
*
* Features:
* - Loads tab state from server on mount
* - Saves tab changes to server with optimistic updates
* - Handles version conflicts gracefully
* - Falls back to 'sessions' if no saved state
*
* Usage:
* const { activeTab, setActiveTab, isLoading } = useTabState();
*/
export function useTabState() {
const { credentials } = useAuth();
const [state, setState] = React.useState<TabState>({
activeTab: DEFAULT_TAB,
version: -1
});
const [isLoading, setIsLoading] = React.useState(true);

// Load initial state from server
React.useEffect(() => {
if (!credentials) {
setIsLoading(false);
return;
}

let mounted = true;

async function loadTabState() {
try {
const item = await kvGet(credentials!, TAB_STATE_KEY);

if (!mounted) return;

if (item) {
const tab = item.value as TabType;
// Validate the tab value (zen is excluded as it's not active)
if (tab === 'sessions' || tab === 'inbox' || tab === 'settings') {
setState({
activeTab: tab,
version: item.version
});
}
}
} catch (error) {
console.warn('[TabState] Failed to load tab state:', error);
} finally {
if (mounted) {
setIsLoading(false);
}
}
}

loadTabState();

return () => {
mounted = false;
};
}, [credentials]);

// Set active tab with server sync
const setActiveTab = React.useCallback(async (tab: TabType) => {
// Optimistic update
setState(prev => ({
activeTab: tab,
version: prev.version
}));

if (!credentials) return;

try {
const newVersion = await kvSet(
credentials,
TAB_STATE_KEY,
tab,
state.version
);

setState(prev => ({
...prev,
version: newVersion
}));
} catch (error) {
console.warn('[TabState] Failed to save tab state:', error);
// On conflict, reload the current state from server
if (String(error).includes('version-mismatch')) {
try {
const item = await kvGet(credentials, TAB_STATE_KEY);
if (item) {
const serverTab = item.value as TabType;
if (serverTab === 'sessions' || serverTab === 'inbox' || serverTab === 'settings' || serverTab === 'zen') {
setState({
activeTab: serverTab,
version: item.version
});
}
}
} catch {
// Ignore reload errors
}
}
}
}, [credentials, state.version]);

return {
activeTab: state.activeTab,
setActiveTab,
isLoading
};
}