mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-02 06:53:09 +02:00
Move to server side preference and read indicator management
This commit is contained in:
+105
-6
@@ -18,12 +18,22 @@ import { MapView } from './components/MapView';
|
||||
import { CrackerPanel } from './components/CrackerPanel';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './components/ui/sheet';
|
||||
import { Toaster, toast } from './components/ui/sonner';
|
||||
import { getStateKey } from './utils/conversationState';
|
||||
import {
|
||||
getStateKey,
|
||||
initLastMessageTimes,
|
||||
loadLocalStorageLastMessageTimes,
|
||||
loadLocalStorageSortOrder,
|
||||
clearLocalStorageConversationState,
|
||||
} from './utils/conversationState';
|
||||
import { formatTime } from './utils/messageParser';
|
||||
import { pubkeysMatch, getContactDisplayName } from './utils/pubkey';
|
||||
import { parseHashConversation, updateUrlHash, getMapFocusHash } from './utils/urlHash';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from './utils/pathUtils';
|
||||
import { loadFavorites, toggleFavorite, isFavorite, type Favorite } from './utils/favorites';
|
||||
import {
|
||||
isFavorite,
|
||||
loadLocalStorageFavorites,
|
||||
clearLocalStorageFavorites,
|
||||
} from './utils/favorites';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
AppSettings,
|
||||
@@ -31,6 +41,7 @@ import type {
|
||||
Contact,
|
||||
Channel,
|
||||
Conversation,
|
||||
Favorite,
|
||||
HealthStatus,
|
||||
Message,
|
||||
MessagePath,
|
||||
@@ -60,7 +71,9 @@ export function App() {
|
||||
const [undecryptedCount, setUndecryptedCount] = useState(0);
|
||||
const [showCracker, setShowCracker] = useState(false);
|
||||
const [crackerRunning, setCrackerRunning] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Favorite[]>(loadFavorites);
|
||||
|
||||
// Favorites are now stored server-side in appSettings
|
||||
const favorites: Favorite[] = appSettings?.favorites ?? [];
|
||||
|
||||
// Track previous health status to detect changes
|
||||
const prevHealthRef = useRef<HealthStatus | null>(null);
|
||||
@@ -251,6 +264,8 @@ export function App() {
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
setAppSettings(data);
|
||||
// Initialize in-memory cache with server data
|
||||
initLastMessageTimes(data.last_message_times ?? {});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch app settings:', err);
|
||||
}
|
||||
@@ -273,6 +288,72 @@ export function App() {
|
||||
fetchUndecryptedCount();
|
||||
}, [fetchConfig, fetchAppSettings, fetchUndecryptedCount]);
|
||||
|
||||
// One-time migration of localStorage preferences to server
|
||||
const hasMigratedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
// Only run once we have appSettings loaded
|
||||
if (!appSettings || hasMigratedRef.current) return;
|
||||
|
||||
// Skip if already migrated on server
|
||||
if (appSettings.preferences_migrated) {
|
||||
// Just clear any leftover localStorage
|
||||
clearLocalStorageFavorites();
|
||||
clearLocalStorageConversationState();
|
||||
hasMigratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have any localStorage data to migrate
|
||||
const localFavorites = loadLocalStorageFavorites();
|
||||
const localSortOrder = loadLocalStorageSortOrder();
|
||||
const localLastMessageTimes = loadLocalStorageLastMessageTimes();
|
||||
|
||||
const hasLocalData =
|
||||
localFavorites.length > 0 ||
|
||||
localSortOrder !== 'recent' ||
|
||||
Object.keys(localLastMessageTimes).length > 0;
|
||||
|
||||
if (!hasLocalData) {
|
||||
// No local data to migrate, just mark as done
|
||||
hasMigratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as migrating immediately to prevent duplicate calls
|
||||
hasMigratedRef.current = true;
|
||||
|
||||
// Migrate localStorage to server
|
||||
const migratePreferences = async () => {
|
||||
try {
|
||||
const result = await api.migratePreferences({
|
||||
favorites: localFavorites,
|
||||
sort_order: localSortOrder,
|
||||
last_message_times: localLastMessageTimes,
|
||||
});
|
||||
|
||||
if (result.migrated) {
|
||||
toast.success('Preferences migrated', {
|
||||
description: `Migrated ${localFavorites.length} favorites to server`,
|
||||
});
|
||||
}
|
||||
|
||||
// Update local state with migrated settings
|
||||
setAppSettings(result.settings);
|
||||
// Reinitialize cache with migrated data
|
||||
initLastMessageTimes(result.settings.last_message_times ?? {});
|
||||
|
||||
// Clear localStorage after successful migration
|
||||
clearLocalStorageFavorites();
|
||||
clearLocalStorageConversationState();
|
||||
} catch (err) {
|
||||
console.error('Failed to migrate preferences:', err);
|
||||
// Don't block the app on migration failure
|
||||
}
|
||||
};
|
||||
|
||||
migratePreferences();
|
||||
}, [appSettings]);
|
||||
|
||||
// Resolve URL hash to a conversation
|
||||
const resolveHashToConversation = useCallback((): Conversation | null => {
|
||||
const hashConv = parseHashConversation();
|
||||
@@ -432,9 +513,15 @@ export function App() {
|
||||
setSidebarOpen(false);
|
||||
}, []);
|
||||
|
||||
// Toggle favorite status for a conversation
|
||||
const handleToggleFavorite = useCallback((type: 'channel' | 'contact', id: string) => {
|
||||
setFavorites(toggleFavorite(type, id));
|
||||
// Toggle favorite status for a conversation (via API)
|
||||
const handleToggleFavorite = useCallback(async (type: 'channel' | 'contact', id: string) => {
|
||||
try {
|
||||
const updatedSettings = await api.toggleFavorite(type, id);
|
||||
setAppSettings(updatedSettings);
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle favorite:', err);
|
||||
toast.error('Failed to update favorite');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Delete channel handler
|
||||
@@ -535,6 +622,16 @@ export function App() {
|
||||
[fetchUndecryptedCount]
|
||||
);
|
||||
|
||||
// Handle sort order change via API
|
||||
const handleSortOrderChange = useCallback(async (order: 'recent' | 'alpha') => {
|
||||
try {
|
||||
const updatedSettings = await api.updateSettings({ sidebar_sort_order: order });
|
||||
setAppSettings(updatedSettings);
|
||||
} catch (err) {
|
||||
console.error('Failed to update sort order:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sidebar content (shared between desktop and mobile)
|
||||
const sidebarContent = (
|
||||
<Sidebar
|
||||
@@ -554,6 +651,8 @@ export function App() {
|
||||
onToggleCracker={() => setShowCracker((prev) => !prev)}
|
||||
onMarkAllRead={markAllRead}
|
||||
favorites={favorites}
|
||||
sortOrder={appSettings?.sidebar_sort_order ?? 'recent'}
|
||||
onSortOrderChange={handleSortOrderChange}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ import type {
|
||||
Channel,
|
||||
CommandResponse,
|
||||
Contact,
|
||||
Favorite,
|
||||
HealthStatus,
|
||||
MaintenanceResult,
|
||||
Message,
|
||||
MigratePreferencesRequest,
|
||||
MigratePreferencesResponse,
|
||||
RadioConfig,
|
||||
RadioConfigUpdate,
|
||||
TelemetryResponse,
|
||||
@@ -197,4 +200,35 @@ export const api = {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(settings),
|
||||
}),
|
||||
|
||||
// Favorites
|
||||
addFavorite: (type: Favorite['type'], id: string) =>
|
||||
fetchJson<AppSettings>('/settings/favorites', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, id }),
|
||||
}),
|
||||
removeFavorite: (type: Favorite['type'], id: string) =>
|
||||
fetchJson<AppSettings>('/settings/favorites', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ type, id }),
|
||||
}),
|
||||
toggleFavorite: (type: Favorite['type'], id: string) =>
|
||||
fetchJson<AppSettings>('/settings/favorites/toggle', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, id }),
|
||||
}),
|
||||
|
||||
// Last message time tracking
|
||||
updateLastMessageTime: (stateKey: string, timestamp: number) =>
|
||||
fetchJson<{ status: string }>('/settings/last-message-time', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ state_key: stateKey, timestamp }),
|
||||
}),
|
||||
|
||||
// Preferences migration (one-time, from localStorage to database)
|
||||
migratePreferences: (request: MigratePreferencesRequest) =>
|
||||
fetchJson<MigratePreferencesResponse>('/settings/migrate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -96,6 +96,7 @@ export function SettingsModal({
|
||||
// Database maintenance state
|
||||
const [retentionDays, setRetentionDays] = useState('14');
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
@@ -113,6 +114,7 @@ export function SettingsModal({
|
||||
useEffect(() => {
|
||||
if (appSettings) {
|
||||
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||
}
|
||||
}, [appSettings]);
|
||||
|
||||
@@ -314,6 +316,19 @@ export function SettingsModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAutoDecrypt = async () => {
|
||||
const newValue = !autoDecryptOnAdvert;
|
||||
setAutoDecryptOnAdvert(newValue); // Optimistic update
|
||||
|
||||
try {
|
||||
await onSaveAppSettings({ auto_decrypt_dm_on_advert: newValue });
|
||||
} catch (err) {
|
||||
console.error('Failed to save auto-decrypt setting:', err);
|
||||
setAutoDecryptOnAdvert(!newValue); // Revert on error
|
||||
toast.error('Failed to save setting');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
@@ -638,6 +653,31 @@ export function SettingsModal({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>DM Decryption</Label>
|
||||
<div
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
onClick={handleToggleAutoDecrypt}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoDecryptOnAdvert}
|
||||
readOnly
|
||||
className="w-4 h-4 rounded border-input accent-primary pointer-events-none"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
Auto-decrypt historical DMs when new contact advertises
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, the server will automatically try to decrypt stored DM packets when
|
||||
a new contact sends an advertisement. This may cause brief delays on large packet
|
||||
backlogs.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Advertise Tab */}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import type { Contact, Channel, Conversation } from '../types';
|
||||
import type { Contact, Channel, Conversation, Favorite } from '../types';
|
||||
import { getStateKey, type ConversationTimes } from '../utils/conversationState';
|
||||
import { getPubkeyPrefix, getContactDisplayName } from '../utils/pubkey';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
import { CONTACT_TYPE_REPEATER } from '../utils/contactAvatar';
|
||||
import { isFavorite, type Favorite } from '../utils/favorites';
|
||||
import { isFavorite } from '../utils/favorites';
|
||||
import { UNREAD_FETCH_LIMIT } from '../api';
|
||||
import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
@@ -27,6 +27,10 @@ interface SidebarProps {
|
||||
onToggleCracker: () => void;
|
||||
onMarkAllRead: () => void;
|
||||
favorites: Favorite[];
|
||||
/** Sort order from server settings */
|
||||
sortOrder?: SortOrder;
|
||||
/** Callback when sort order changes */
|
||||
onSortOrderChange?: (order: SortOrder) => void;
|
||||
}
|
||||
|
||||
/** Format unread count, showing "X+" if at the fetch limit (indicating there may be more) */
|
||||
@@ -34,25 +38,6 @@ function formatUnreadCount(count: number): string {
|
||||
return count >= UNREAD_FETCH_LIMIT ? `${count}+` : `${count}`;
|
||||
}
|
||||
|
||||
// Load sort preference from localStorage (default to 'recent')
|
||||
function loadSortOrder(): SortOrder {
|
||||
try {
|
||||
const stored = localStorage.getItem('remoteterm-sortOrder');
|
||||
return stored === 'alpha' ? 'alpha' : 'recent';
|
||||
} catch {
|
||||
return 'recent';
|
||||
}
|
||||
}
|
||||
|
||||
// Save sort preference to localStorage
|
||||
function saveSortOrder(order: SortOrder): void {
|
||||
try {
|
||||
localStorage.setItem('remoteterm-sortOrder', order);
|
||||
} catch {
|
||||
// localStorage might be full or disabled
|
||||
}
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
contacts,
|
||||
channels,
|
||||
@@ -67,14 +52,15 @@ export function Sidebar({
|
||||
onToggleCracker,
|
||||
onMarkAllRead,
|
||||
favorites,
|
||||
sortOrder: sortOrderProp = 'recent',
|
||||
onSortOrderChange,
|
||||
}: SidebarProps) {
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>(loadSortOrder);
|
||||
const sortOrder = sortOrderProp;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const handleSortToggle = () => {
|
||||
const newOrder = sortOrder === 'alpha' ? 'recent' : 'alpha';
|
||||
setSortOrder(newOrder);
|
||||
saveSortOrder(newOrder);
|
||||
onSortOrderChange?.(newOrder);
|
||||
};
|
||||
|
||||
const handleSelectConversation = (conversation: Conversation) => {
|
||||
|
||||
@@ -124,12 +124,35 @@ export interface RawPacket {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface Favorite {
|
||||
type: 'channel' | 'contact';
|
||||
id: string; // channel key or contact public key
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
max_radio_contacts: number;
|
||||
favorites: Favorite[];
|
||||
auto_decrypt_dm_on_advert: boolean;
|
||||
sidebar_sort_order: 'recent' | 'alpha';
|
||||
last_message_times: Record<string, number>;
|
||||
preferences_migrated: boolean;
|
||||
}
|
||||
|
||||
export interface AppSettingsUpdate {
|
||||
max_radio_contacts?: number;
|
||||
auto_decrypt_dm_on_advert?: boolean;
|
||||
sidebar_sort_order?: 'recent' | 'alpha';
|
||||
}
|
||||
|
||||
export interface MigratePreferencesRequest {
|
||||
favorites: Favorite[];
|
||||
sort_order: string;
|
||||
last_message_times: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface MigratePreferencesResponse {
|
||||
migrated: boolean;
|
||||
settings: AppSettings;
|
||||
}
|
||||
|
||||
/** Contact type constants */
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* localStorage utilities for tracking conversation message times.
|
||||
* Conversation state utilities.
|
||||
*
|
||||
* Stores when each conversation last received a message, used for
|
||||
* sorting conversations by recency in the sidebar.
|
||||
* Last message times are tracked in-memory and persisted server-side.
|
||||
* This file provides helper functions for generating state keys
|
||||
* and managing conversation times.
|
||||
*
|
||||
* Read state (last_read_at) is tracked server-side for consistency
|
||||
* across devices - see useUnreadCounts hook.
|
||||
@@ -11,45 +12,42 @@
|
||||
import { getPubkeyPrefix } from './pubkey';
|
||||
|
||||
const LAST_MESSAGE_KEY = 'remoteterm-lastMessageTime';
|
||||
const SORT_ORDER_KEY = 'remoteterm-sortOrder';
|
||||
|
||||
export type ConversationTimes = Record<string, number>;
|
||||
export type SortOrder = 'recent' | 'alpha';
|
||||
|
||||
function loadTimes(key: string): ConversationTimes {
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveTimes(key: string, times: ConversationTimes): void {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(times));
|
||||
} catch {
|
||||
// localStorage might be full or disabled
|
||||
}
|
||||
// In-memory cache of last message times (loaded from server on init)
|
||||
let lastMessageTimesCache: ConversationTimes = {};
|
||||
|
||||
/**
|
||||
* Initialize the last message times cache from server data
|
||||
*/
|
||||
export function initLastMessageTimes(times: ConversationTimes): void {
|
||||
lastMessageTimesCache = { ...times };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all last message times from the cache
|
||||
*/
|
||||
export function getLastMessageTimes(): ConversationTimes {
|
||||
return loadTimes(LAST_MESSAGE_KEY);
|
||||
return { ...lastMessageTimesCache };
|
||||
}
|
||||
|
||||
export function setLastMessageTime(stateKey: string, timestamp: number): ConversationTimes {
|
||||
const times = loadTimes(LAST_MESSAGE_KEY);
|
||||
// Only update if this is a newer message
|
||||
if (!times[stateKey] || timestamp > times[stateKey]) {
|
||||
times[stateKey] = timestamp;
|
||||
saveTimes(LAST_MESSAGE_KEY, times);
|
||||
}
|
||||
return times;
|
||||
/**
|
||||
* Update a single message time in the cache and return the updated cache.
|
||||
* Note: This does NOT persist to server - caller should sync if needed.
|
||||
*/
|
||||
export function setLastMessageTime(key: string, timestamp: number): ConversationTimes {
|
||||
lastMessageTimesCache[key] = timestamp;
|
||||
return { ...lastMessageTimesCache };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a state tracking key for message times.
|
||||
*
|
||||
* This is NOT the same as Message.conversation_key (the database field).
|
||||
* This creates prefixed keys for localStorage/state tracking:
|
||||
* This creates prefixed keys for state tracking:
|
||||
* - Channels: "channel-{channelKey}"
|
||||
* - Contacts: "contact-{12-char-pubkey-prefix}"
|
||||
*
|
||||
@@ -63,3 +61,39 @@ export function getStateKey(type: 'channel' | 'contact', id: string): string {
|
||||
// For contacts, use 12-char prefix for consistent matching
|
||||
return `contact-${getPubkeyPrefix(id)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load last message times from localStorage (for migration only)
|
||||
*/
|
||||
export function loadLocalStorageLastMessageTimes(): ConversationTimes {
|
||||
try {
|
||||
const stored = localStorage.getItem(LAST_MESSAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load sort order from localStorage (for migration only)
|
||||
*/
|
||||
export function loadLocalStorageSortOrder(): SortOrder {
|
||||
try {
|
||||
const stored = localStorage.getItem(SORT_ORDER_KEY);
|
||||
return stored === 'alpha' ? 'alpha' : 'recent';
|
||||
} catch {
|
||||
return 'recent';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversation state from localStorage (after migration)
|
||||
*/
|
||||
export function clearLocalStorageConversationState(): void {
|
||||
try {
|
||||
localStorage.removeItem(LAST_MESSAGE_KEY);
|
||||
localStorage.removeItem(SORT_ORDER_KEY);
|
||||
} catch {
|
||||
// localStorage might be disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
/**
|
||||
* localStorage utilities for managing favorite conversations.
|
||||
* Favorites utilities.
|
||||
*
|
||||
* Favorites are stored client-side and displayed in a dedicated section
|
||||
* above channels in the sidebar, always sorted by most recent message.
|
||||
* Favorites are now stored server-side in the database.
|
||||
* This file provides helper functions for checking favorites
|
||||
* and loading legacy localStorage data for migration.
|
||||
*/
|
||||
|
||||
import type { Favorite } from '../types';
|
||||
import { pubkeysMatch } from './pubkey';
|
||||
|
||||
const FAVORITES_KEY = 'remoteterm-favorites';
|
||||
|
||||
export interface Favorite {
|
||||
type: 'channel' | 'contact';
|
||||
id: string; // channel key or contact public key
|
||||
/**
|
||||
* Check if a conversation is favorited (from provided favorites array)
|
||||
*
|
||||
* For contacts, uses prefix matching to handle full pubkeys vs 12-char prefixes.
|
||||
*/
|
||||
export function isFavorite(
|
||||
favorites: Favorite[],
|
||||
type: 'channel' | 'contact',
|
||||
id: string
|
||||
): boolean {
|
||||
return favorites.some((f) => {
|
||||
if (f.type !== type) return false;
|
||||
// For contacts, use prefix matching (handles full keys vs prefixes)
|
||||
if (type === 'contact') return pubkeysMatch(f.id, id);
|
||||
// For channels, exact match
|
||||
return f.id === id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load favorites from localStorage
|
||||
* Load favorites from localStorage (for migration only)
|
||||
*/
|
||||
export function loadFavorites(): Favorite[] {
|
||||
export function loadLocalStorageFavorites(): Favorite[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(FAVORITES_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
@@ -25,58 +43,15 @@ export function loadFavorites(): Favorite[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save favorites to localStorage
|
||||
* Clear favorites from localStorage (after migration)
|
||||
*/
|
||||
function saveFavorites(favorites: Favorite[]): void {
|
||||
export function clearLocalStorageFavorites(): void {
|
||||
try {
|
||||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
|
||||
localStorage.removeItem(FAVORITES_KEY);
|
||||
} catch {
|
||||
// localStorage might be full or disabled
|
||||
// localStorage might be disabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a conversation to favorites
|
||||
*/
|
||||
export function addFavorite(type: 'channel' | 'contact', id: string): Favorite[] {
|
||||
const favorites = loadFavorites();
|
||||
// Check if already favorited
|
||||
if (favorites.some((f) => f.type === type && f.id === id)) {
|
||||
return favorites;
|
||||
}
|
||||
const updated = [...favorites, { type, id }];
|
||||
saveFavorites(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a conversation from favorites
|
||||
*/
|
||||
export function removeFavorite(type: 'channel' | 'contact', id: string): Favorite[] {
|
||||
const favorites = loadFavorites();
|
||||
const updated = favorites.filter((f) => !(f.type === type && f.id === id));
|
||||
saveFavorites(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a conversation is favorited
|
||||
*/
|
||||
export function isFavorite(
|
||||
favorites: Favorite[],
|
||||
type: 'channel' | 'contact',
|
||||
id: string
|
||||
): boolean {
|
||||
return favorites.some((f) => f.type === type && f.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a conversation's favorite status
|
||||
*/
|
||||
export function toggleFavorite(type: 'channel' | 'contact', id: string): Favorite[] {
|
||||
const favorites = loadFavorites();
|
||||
if (favorites.some((f) => f.type === type && f.id === id)) {
|
||||
return removeFavorite(type, id);
|
||||
}
|
||||
return addFavorite(type, id);
|
||||
}
|
||||
// Re-export the Favorite type for convenience
|
||||
export type { Favorite };
|
||||
|
||||
Reference in New Issue
Block a user