mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 09:13:04 +02:00
Fix sidebar sort order. Closes #61
This commit is contained in:
@@ -135,7 +135,6 @@ export function App() {
|
||||
favorites,
|
||||
fetchAppSettings,
|
||||
handleSaveAppSettings,
|
||||
handleSortOrderChange,
|
||||
handleToggleFavorite,
|
||||
handleToggleBlockedKey,
|
||||
handleToggleBlockedName,
|
||||
@@ -401,10 +400,7 @@ export function App() {
|
||||
void markAllRead();
|
||||
},
|
||||
favorites,
|
||||
sortOrder: appSettings?.sidebar_sort_order ?? 'recent',
|
||||
onSortOrderChange: (sortOrder: 'recent' | 'alpha') => {
|
||||
void handleSortOrderChange(sortOrder);
|
||||
},
|
||||
legacySortOrder: appSettings?.sidebar_sort_order,
|
||||
isConversationNotificationsEnabled,
|
||||
};
|
||||
const conversationPaneProps = {
|
||||
|
||||
@@ -19,7 +19,17 @@ import {
|
||||
type Conversation,
|
||||
type Favorite,
|
||||
} from '../types';
|
||||
import { getStateKey, type ConversationTimes, type SortOrder } from '../utils/conversationState';
|
||||
import {
|
||||
buildSidebarSectionSortOrders,
|
||||
getStateKey,
|
||||
loadLegacyLocalStorageSortOrder,
|
||||
loadLocalStorageSidebarSectionSortOrders,
|
||||
saveLocalStorageSidebarSectionSortOrders,
|
||||
type ConversationTimes,
|
||||
type SidebarSectionSortOrders,
|
||||
type SidebarSortableSection,
|
||||
type SortOrder,
|
||||
} from '../utils/conversationState';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import { handleKeyboardActivate } from '../utils/a11y';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
@@ -91,13 +101,36 @@ interface SidebarProps {
|
||||
onToggleCracker: () => void;
|
||||
onMarkAllRead: () => void;
|
||||
favorites: Favorite[];
|
||||
/** Sort order from server settings */
|
||||
sortOrder?: SortOrder;
|
||||
/** Callback when sort order changes */
|
||||
onSortOrderChange?: (order: SortOrder) => void;
|
||||
/** Legacy global sort order, used only to seed per-section local preferences. */
|
||||
legacySortOrder?: SortOrder;
|
||||
isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean;
|
||||
}
|
||||
|
||||
type InitialSectionSortState = {
|
||||
orders: SidebarSectionSortOrders;
|
||||
source: 'section' | 'legacy' | 'none';
|
||||
};
|
||||
|
||||
function loadInitialSectionSortOrders(): InitialSectionSortState {
|
||||
const storedOrders = loadLocalStorageSidebarSectionSortOrders();
|
||||
if (storedOrders) {
|
||||
return { orders: storedOrders, source: 'section' };
|
||||
}
|
||||
|
||||
const legacyOrder = loadLegacyLocalStorageSortOrder();
|
||||
if (legacyOrder) {
|
||||
return {
|
||||
orders: buildSidebarSectionSortOrders(legacyOrder),
|
||||
source: 'legacy',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
orders: buildSidebarSectionSortOrders(),
|
||||
source: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
contacts,
|
||||
channels,
|
||||
@@ -112,12 +145,12 @@ export function Sidebar({
|
||||
onToggleCracker,
|
||||
onMarkAllRead,
|
||||
favorites,
|
||||
sortOrder: sortOrderProp = 'recent',
|
||||
onSortOrderChange,
|
||||
legacySortOrder,
|
||||
isConversationNotificationsEnabled,
|
||||
}: SidebarProps) {
|
||||
const sortOrder = sortOrderProp;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const initialSectionSortState = useMemo(loadInitialSectionSortOrders, []);
|
||||
const [sectionSortOrders, setSectionSortOrders] = useState(initialSectionSortState.orders);
|
||||
const initialCollapsedState = useMemo(loadCollapsedState, []);
|
||||
const [toolsCollapsed, setToolsCollapsed] = useState(initialCollapsedState.tools);
|
||||
const [favoritesCollapsed, setFavoritesCollapsed] = useState(initialCollapsedState.favorites);
|
||||
@@ -125,10 +158,31 @@ export function Sidebar({
|
||||
const [contactsCollapsed, setContactsCollapsed] = useState(initialCollapsedState.contacts);
|
||||
const [repeatersCollapsed, setRepeatersCollapsed] = useState(initialCollapsedState.repeaters);
|
||||
const collapseSnapshotRef = useRef<CollapseState | null>(null);
|
||||
const sectionSortSourceRef = useRef(initialSectionSortState.source);
|
||||
|
||||
const handleSortToggle = () => {
|
||||
const newOrder = sortOrder === 'alpha' ? 'recent' : 'alpha';
|
||||
onSortOrderChange?.(newOrder);
|
||||
useEffect(() => {
|
||||
if (sectionSortSourceRef.current === 'legacy') {
|
||||
saveLocalStorageSidebarSectionSortOrders(sectionSortOrders);
|
||||
sectionSortSourceRef.current = 'section';
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionSortSourceRef.current !== 'none' || legacySortOrder === undefined) return;
|
||||
|
||||
const seededOrders = buildSidebarSectionSortOrders(legacySortOrder);
|
||||
setSectionSortOrders(seededOrders);
|
||||
saveLocalStorageSidebarSectionSortOrders(seededOrders);
|
||||
sectionSortSourceRef.current = 'section';
|
||||
}, [legacySortOrder, sectionSortOrders]);
|
||||
|
||||
const handleSortToggle = (section: SidebarSortableSection) => {
|
||||
setSectionSortOrders((prev) => {
|
||||
const nextOrder = prev[section] === 'alpha' ? 'recent' : 'alpha';
|
||||
const updated = { ...prev, [section]: nextOrder };
|
||||
saveLocalStorageSidebarSectionSortOrders(updated);
|
||||
sectionSortSourceRef.current = 'section';
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectConversation = (conversation: Conversation) => {
|
||||
@@ -203,7 +257,7 @@ export function Sidebar({
|
||||
if (a.name === 'Public') return -1;
|
||||
if (b.name === 'Public') return 1;
|
||||
|
||||
if (sortOrder === 'recent') {
|
||||
if (sectionSortOrders.channels === 'recent') {
|
||||
const timeA = getLastMessageTime('channel', a.key);
|
||||
const timeB = getLastMessageTime('channel', b.key);
|
||||
if (timeA && timeB) return timeB - timeA;
|
||||
@@ -212,13 +266,13 @@ export function Sidebar({
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
}),
|
||||
[uniqueChannels, sortOrder, getLastMessageTime]
|
||||
[uniqueChannels, sectionSortOrders.channels, getLastMessageTime]
|
||||
);
|
||||
|
||||
const sortContactsByOrder = useCallback(
|
||||
(items: Contact[]) =>
|
||||
(items: Contact[], order: SortOrder) =>
|
||||
[...items].sort((a, b) => {
|
||||
if (sortOrder === 'recent') {
|
||||
if (order === 'recent') {
|
||||
const timeA = getLastMessageTime('contact', a.public_key);
|
||||
const timeB = getLastMessageTime('contact', b.public_key);
|
||||
if (timeA && timeB) return timeB - timeA;
|
||||
@@ -227,18 +281,26 @@ export function Sidebar({
|
||||
}
|
||||
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
|
||||
}),
|
||||
[sortOrder, getLastMessageTime]
|
||||
[getLastMessageTime]
|
||||
);
|
||||
|
||||
// Split non-repeater contacts and repeater contacts into separate sorted lists
|
||||
const sortedNonRepeaterContacts = useMemo(
|
||||
() => sortContactsByOrder(uniqueContacts.filter((c) => c.type !== CONTACT_TYPE_REPEATER)),
|
||||
[uniqueContacts, sortContactsByOrder]
|
||||
() =>
|
||||
sortContactsByOrder(
|
||||
uniqueContacts.filter((c) => c.type !== CONTACT_TYPE_REPEATER),
|
||||
sectionSortOrders.contacts
|
||||
),
|
||||
[uniqueContacts, sectionSortOrders.contacts, sortContactsByOrder]
|
||||
);
|
||||
|
||||
const sortedRepeaters = useMemo(
|
||||
() => sortContactsByOrder(uniqueContacts.filter((c) => c.type === CONTACT_TYPE_REPEATER)),
|
||||
[uniqueContacts, sortContactsByOrder]
|
||||
() =>
|
||||
sortContactsByOrder(
|
||||
uniqueContacts.filter((c) => c.type === CONTACT_TYPE_REPEATER),
|
||||
sectionSortOrders.repeaters
|
||||
),
|
||||
[uniqueContacts, sectionSortOrders.repeaters, sortContactsByOrder]
|
||||
);
|
||||
|
||||
// Filter by search query
|
||||
@@ -604,11 +666,12 @@ export function Sidebar({
|
||||
title: string,
|
||||
collapsed: boolean,
|
||||
onToggle: () => void,
|
||||
showSortToggle = false,
|
||||
sortSection: SidebarSortableSection | null = null,
|
||||
unreadCount = 0,
|
||||
highlightUnread = false
|
||||
) => {
|
||||
const effectiveCollapsed = isSearching ? false : collapsed;
|
||||
const sectionSortOrder = sortSection ? sectionSortOrders[sortSection] : null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center px-3 py-2 pt-3.5">
|
||||
@@ -630,16 +693,24 @@ export function Sidebar({
|
||||
)}
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
{(showSortToggle || unreadCount > 0) && (
|
||||
{(sortSection || unreadCount > 0) && (
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{showSortToggle && (
|
||||
{sortSection && sectionSortOrder && (
|
||||
<button
|
||||
className="bg-transparent text-muted-foreground/60 px-1 py-0.5 text-[10px] rounded hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={handleSortToggle}
|
||||
aria-label={sortOrder === 'alpha' ? 'Sort by recent' : 'Sort alphabetically'}
|
||||
title={sortOrder === 'alpha' ? 'Sort by recent' : 'Sort alphabetically'}
|
||||
onClick={() => handleSortToggle(sortSection)}
|
||||
aria-label={
|
||||
sectionSortOrder === 'alpha'
|
||||
? `Sort ${title} by recent`
|
||||
: `Sort ${title} alphabetically`
|
||||
}
|
||||
title={
|
||||
sectionSortOrder === 'alpha'
|
||||
? `Sort ${title} by recent`
|
||||
: `Sort ${title} alphabetically`
|
||||
}
|
||||
>
|
||||
{sortOrder === 'alpha' ? 'A-Z' : '⏱'}
|
||||
{sectionSortOrder === 'alpha' ? 'A-Z' : '⏱'}
|
||||
</button>
|
||||
)}
|
||||
{unreadCount > 0 && (
|
||||
@@ -731,7 +802,7 @@ export function Sidebar({
|
||||
'Favorites',
|
||||
favoritesCollapsed,
|
||||
() => setFavoritesCollapsed((prev) => !prev),
|
||||
false,
|
||||
null,
|
||||
favoritesUnreadCount,
|
||||
favoritesHasMention
|
||||
)}
|
||||
@@ -747,7 +818,7 @@ export function Sidebar({
|
||||
'Channels',
|
||||
channelsCollapsed,
|
||||
() => setChannelsCollapsed((prev) => !prev),
|
||||
true,
|
||||
'channels',
|
||||
channelsUnreadCount,
|
||||
channelsHasMention
|
||||
)}
|
||||
@@ -763,7 +834,7 @@ export function Sidebar({
|
||||
'Contacts',
|
||||
contactsCollapsed,
|
||||
() => setContactsCollapsed((prev) => !prev),
|
||||
true,
|
||||
'contacts',
|
||||
contactsUnreadCount,
|
||||
contactsUnreadCount > 0
|
||||
)}
|
||||
@@ -779,7 +850,7 @@ export function Sidebar({
|
||||
'Repeaters',
|
||||
repeatersCollapsed,
|
||||
() => setRepeatersCollapsed((prev) => !prev),
|
||||
true,
|
||||
'repeaters',
|
||||
repeatersUnreadCount
|
||||
)}
|
||||
{(isSearching || !repeatersCollapsed) &&
|
||||
|
||||
@@ -43,25 +43,6 @@ export function useAppSettings() {
|
||||
[fetchAppSettings]
|
||||
);
|
||||
|
||||
const handleSortOrderChange = useCallback(
|
||||
async (order: 'recent' | 'alpha') => {
|
||||
const previousOrder = appSettings?.sidebar_sort_order ?? 'recent';
|
||||
|
||||
// Optimistic update for responsive UI
|
||||
setAppSettings((prev) => (prev ? { ...prev, sidebar_sort_order: order } : prev));
|
||||
|
||||
try {
|
||||
const updatedSettings = await api.updateSettings({ sidebar_sort_order: order });
|
||||
setAppSettings(updatedSettings);
|
||||
} catch (err) {
|
||||
console.error('Failed to update sort order:', err);
|
||||
setAppSettings((prev) => (prev ? { ...prev, sidebar_sort_order: previousOrder } : prev));
|
||||
toast.error('Failed to save sort preference');
|
||||
}
|
||||
},
|
||||
[appSettings?.sidebar_sort_order]
|
||||
);
|
||||
|
||||
const handleToggleBlockedKey = useCallback(async (key: string) => {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
setAppSettings((prev) => {
|
||||
@@ -198,7 +179,6 @@ export function useAppSettings() {
|
||||
favorites,
|
||||
fetchAppSettings,
|
||||
handleSaveAppSettings,
|
||||
handleSortOrderChange,
|
||||
handleToggleFavorite,
|
||||
handleToggleBlockedKey,
|
||||
handleToggleBlockedName,
|
||||
|
||||
@@ -75,8 +75,7 @@ function renderSidebar(overrides?: {
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={favorites}
|
||||
sortOrder="recent"
|
||||
onSortOrderChange={vi.fn()}
|
||||
legacySortOrder="recent"
|
||||
isConversationNotificationsEnabled={overrides?.isConversationNotificationsEnabled}
|
||||
/>
|
||||
);
|
||||
@@ -85,7 +84,7 @@ function renderSidebar(overrides?: {
|
||||
}
|
||||
|
||||
function getSectionHeaderContainer(title: string): HTMLElement {
|
||||
const btn = screen.getByRole('button', { name: new RegExp(title, 'i') });
|
||||
const btn = screen.getByRole('button', { name: title });
|
||||
const container = btn.closest('div');
|
||||
if (!container) throw new Error(`Missing header container for section ${title}`);
|
||||
return container;
|
||||
@@ -142,9 +141,9 @@ describe('Sidebar section summaries', () => {
|
||||
it('expands collapsed sections during search and restores collapse state after clearing search', async () => {
|
||||
const { opsChannel, aliceName } = renderSidebar();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Tools/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Channels/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Contacts/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Tools' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Channels' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Contacts' }));
|
||||
|
||||
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
||||
@@ -169,9 +168,9 @@ describe('Sidebar section summaries', () => {
|
||||
it('persists collapsed section state across unmount and remount', () => {
|
||||
const { opsChannel, aliceName, unmount } = renderSidebar();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Tools/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Channels/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Contacts/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Tools' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Channels' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Contacts' }));
|
||||
|
||||
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
||||
@@ -206,8 +205,7 @@ describe('Sidebar section summaries', () => {
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={[]}
|
||||
sortOrder="recent"
|
||||
onSortOrderChange={vi.fn()}
|
||||
legacySortOrder="recent"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -253,4 +251,69 @@ describe('Sidebar section summaries', () => {
|
||||
const unread = within(aliceRow).getByText('3');
|
||||
expect(bell.compareDocumentPosition(unread) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sorts each section independently and persists per-section sort preferences', () => {
|
||||
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
||||
const zebraChannel = makeChannel('BB'.repeat(16), '#zebra');
|
||||
const alphaChannel = makeChannel('CC'.repeat(16), '#alpha');
|
||||
const zed = makeContact('11'.repeat(32), 'Zed');
|
||||
const amy = makeContact('22'.repeat(32), 'Amy');
|
||||
const relayZulu = makeContact('33'.repeat(32), 'Zulu Relay', CONTACT_TYPE_REPEATER);
|
||||
const relayAlpha = makeContact('44'.repeat(32), 'Alpha Relay', CONTACT_TYPE_REPEATER);
|
||||
|
||||
const props = {
|
||||
contacts: [zed, amy, relayZulu, relayAlpha],
|
||||
channels: [publicChannel, zebraChannel, alphaChannel],
|
||||
activeConversation: null,
|
||||
onSelectConversation: vi.fn(),
|
||||
onNewMessage: vi.fn(),
|
||||
lastMessageTimes: {
|
||||
[getStateKey('channel', zebraChannel.key)]: 300,
|
||||
[getStateKey('channel', alphaChannel.key)]: 100,
|
||||
[getStateKey('contact', zed.public_key)]: 200,
|
||||
[getStateKey('contact', amy.public_key)]: 100,
|
||||
[getStateKey('contact', relayZulu.public_key)]: 300,
|
||||
[getStateKey('contact', relayAlpha.public_key)]: 100,
|
||||
},
|
||||
unreadCounts: {},
|
||||
mentions: {},
|
||||
showCracker: false,
|
||||
crackerRunning: false,
|
||||
onToggleCracker: vi.fn(),
|
||||
onMarkAllRead: vi.fn(),
|
||||
favorites: [],
|
||||
legacySortOrder: 'recent' as const,
|
||||
};
|
||||
|
||||
const getChannelsOrder = () => screen.getAllByText(/^#/).map((node) => node.textContent);
|
||||
const getContactsOrder = () =>
|
||||
screen
|
||||
.getAllByText(/^(Amy|Zed)$/)
|
||||
.map((node) => node.textContent)
|
||||
.filter((text): text is string => Boolean(text));
|
||||
const getRepeatersOrder = () =>
|
||||
screen
|
||||
.getAllByText(/Relay$/)
|
||||
.map((node) => node.textContent)
|
||||
.filter((text): text is string => Boolean(text));
|
||||
|
||||
const { unmount } = render(<Sidebar {...props} />);
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#zebra', '#alpha']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
|
||||
unmount();
|
||||
render(<Sidebar {...props} />);
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
const LAST_MESSAGE_KEY = 'remoteterm-lastMessageTime';
|
||||
const SORT_ORDER_KEY = 'remoteterm-sortOrder';
|
||||
const SIDEBAR_SECTION_SORT_ORDERS_KEY = 'remoteterm-sidebar-section-sort-orders';
|
||||
|
||||
export type ConversationTimes = Record<string, number>;
|
||||
export type SortOrder = 'recent' | 'alpha';
|
||||
export type SidebarSortableSection = 'channels' | 'contacts' | 'repeaters';
|
||||
export type SidebarSectionSortOrders = Record<SidebarSortableSection, SortOrder>;
|
||||
|
||||
// In-memory cache of last message times (loaded from server on init)
|
||||
let lastMessageTimesCache: ConversationTimes = {};
|
||||
@@ -93,6 +96,56 @@ export function loadLocalStorageSortOrder(): SortOrder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the legacy single sidebar sort order from localStorage, if present.
|
||||
*/
|
||||
export function loadLegacyLocalStorageSortOrder(): SortOrder | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(SORT_ORDER_KEY);
|
||||
if (!stored) return null;
|
||||
return stored === 'alpha' ? 'alpha' : 'recent';
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSidebarSectionSortOrders(
|
||||
defaultOrder: SortOrder = 'recent'
|
||||
): SidebarSectionSortOrders {
|
||||
return {
|
||||
channels: defaultOrder,
|
||||
contacts: defaultOrder,
|
||||
repeaters: defaultOrder,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load per-section sidebar sort orders from localStorage.
|
||||
*/
|
||||
export function loadLocalStorageSidebarSectionSortOrders(): SidebarSectionSortOrders | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(SIDEBAR_SECTION_SORT_ORDERS_KEY);
|
||||
if (!stored) return null;
|
||||
|
||||
const parsed = JSON.parse(stored) as Partial<SidebarSectionSortOrders>;
|
||||
return {
|
||||
channels: parsed.channels === 'alpha' ? 'alpha' : 'recent',
|
||||
contacts: parsed.contacts === 'alpha' ? 'alpha' : 'recent',
|
||||
repeaters: parsed.repeaters === 'alpha' ? 'alpha' : 'recent',
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLocalStorageSidebarSectionSortOrders(orders: SidebarSectionSortOrders): void {
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_SECTION_SORT_ORDERS_KEY, JSON.stringify(orders));
|
||||
} catch {
|
||||
// localStorage might be disabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversation state from localStorage (after migration)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user