Move to server side preference and read indicator management

This commit is contained in:
Jack Kingsman
2026-01-18 23:44:56 -08:00
parent 43b7e94b0a
commit 9c071dbc53
24 changed files with 1339 additions and 703 deletions
+62 -28
View File
@@ -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
}
}
+32 -57
View File
@@ -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 };