Add favorites as contact field (dug)

This commit is contained in:
Jack Kingsman
2026-04-05 20:50:27 -07:00
parent c2e1a3cbe6
commit a351c86ccb
55 changed files with 482 additions and 466 deletions
+12 -42
View File
@@ -3,16 +3,11 @@ import { api } from '../api';
import { takePrefetchOrFetch } from '../prefetch';
import { toast } from '../components/ui/sonner';
import { initLastMessageTimes } from '../utils/conversationState';
import { isFavorite } from '../utils/favorites';
import type { AppSettings, AppSettingsUpdate, Favorite } from '../types';
import type { AppSettings, AppSettingsUpdate } from '../types';
export function useAppSettings() {
const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
// Stable empty array prevents a new reference every render when there are none.
const emptyFavorites = useRef<Favorite[]>([]).current;
const favorites: Favorite[] = appSettings?.favorites ?? emptyFavorites;
// One-time migration guard
const hasMigratedRef = useRef(false);
@@ -85,32 +80,6 @@ export function useAppSettings() {
}
}, []);
const handleToggleFavorite = useCallback(async (type: 'channel' | 'contact', id: string) => {
setAppSettings((prev) => {
if (!prev) return prev;
const currentFavorites = prev.favorites ?? [];
const wasFavorited = isFavorite(currentFavorites, type, id);
const optimisticFavorites = wasFavorited
? currentFavorites.filter((f) => !(f.type === type && f.id === id))
: [...currentFavorites, { type, id }];
return { ...prev, favorites: optimisticFavorites };
});
try {
const updatedSettings = await api.toggleFavorite(type, id);
setAppSettings(updatedSettings);
} catch (err) {
console.error('Failed to toggle favorite:', err);
try {
const settings = await api.getSettings();
setAppSettings(settings);
} catch {
// If refetch also fails, leave optimistic state
}
toast.error('Failed to update favorite');
}
}, []);
const handleToggleTrackedTelemetry = useCallback(async (publicKey: string) => {
const key = publicKey.toLowerCase();
setAppSettings((prev) => {
@@ -151,7 +120,7 @@ export function useAppSettings() {
hasMigratedRef.current = true;
const FAVORITES_KEY = 'remoteterm-favorites';
let localFavorites: Favorite[] = [];
let localFavorites: Array<{ type: 'channel' | 'contact'; id: string }> = [];
try {
const stored = localStorage.getItem(FAVORITES_KEY);
if (stored) localFavorites = JSON.parse(stored);
@@ -161,25 +130,26 @@ export function useAppSettings() {
if (localFavorites.length === 0) return;
const migrate = async () => {
try {
for (const f of localFavorites) {
let migrated = 0;
for (const f of localFavorites) {
try {
await api.toggleFavorite(f.type, f.id);
migrated++;
} catch {
// Entity may have been deleted; skip and continue
}
localStorage.removeItem(FAVORITES_KEY);
await fetchAppSettings();
} catch (err) {
console.error('Failed to migrate legacy favorites:', err);
}
localStorage.removeItem(FAVORITES_KEY);
// Reload so contacts/channels pick up the new favorite flags
if (migrated > 0) window.location.reload();
};
migrate();
}, [appSettings, fetchAppSettings]);
}, [appSettings]);
return {
appSettings,
favorites,
fetchAppSettings,
handleSaveAppSettings,
handleToggleFavorite,
handleToggleBlockedKey,
handleToggleBlockedName,
handleToggleTrackedTelemetry,
+36 -20
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef } from 'react';
import type { Favorite } from '../types';
import type { Channel, Contact } from '../types';
import { getStateKey } from '../utils/conversationState';
const APP_TITLE = 'RemoteTerm for MeshCore';
@@ -25,12 +25,11 @@ function getUnreadDirectMessageCount(unreadCounts: Record<string, number>): numb
function getUnreadFavoriteChannelCount(
unreadCounts: Record<string, number>,
favorites: Favorite[]
channels: Channel[]
): number {
return favorites.reduce(
(sum, favorite) =>
sum +
(favorite.type === 'channel' ? unreadCounts[getStateKey('channel', favorite.id)] || 0 : 0),
return channels.reduce(
(sum, channel) =>
sum + (channel.favorite ? unreadCounts[getStateKey('channel', channel.key)] || 0 : 0),
0
);
}
@@ -41,19 +40,29 @@ export function getTotalUnreadCount(unreadCounts: Record<string, number>): numbe
export function getFavoriteUnreadCount(
unreadCounts: Record<string, number>,
favorites: Favorite[]
contacts: Contact[],
channels: Channel[]
): number {
return favorites.reduce((sum, favorite) => {
const stateKey = getStateKey(favorite.type, favorite.id);
return sum + (unreadCounts[stateKey] || 0);
}, 0);
let sum = 0;
for (const contact of contacts) {
if (contact.favorite) {
sum += unreadCounts[getStateKey('contact', contact.public_key)] || 0;
}
}
for (const channel of channels) {
if (channel.favorite) {
sum += unreadCounts[getStateKey('channel', channel.key)] || 0;
}
}
return sum;
}
export function getUnreadTitle(
unreadCounts: Record<string, number>,
favorites: Favorite[]
contacts: Contact[],
channels: Channel[]
): string {
const unreadCount = getFavoriteUnreadCount(unreadCounts, favorites);
const unreadCount = getFavoriteUnreadCount(unreadCounts, contacts, channels);
if (unreadCount <= 0) {
return APP_TITLE;
}
@@ -65,13 +74,13 @@ export function getUnreadTitle(
export function deriveFaviconBadgeState(
unreadCounts: Record<string, number>,
mentions: Record<string, boolean>,
favorites: Favorite[]
channels: Channel[]
): FaviconBadgeState {
if (Object.values(mentions).some(Boolean) || getUnreadDirectMessageCount(unreadCounts) > 0) {
return 'red';
}
if (getUnreadFavoriteChannelCount(unreadCounts, favorites) > 0) {
if (getUnreadFavoriteChannelCount(unreadCounts, channels) > 0) {
return 'green';
}
@@ -128,8 +137,15 @@ function applyFaviconHref(href: string): void {
upsertFaviconLinks('shortcut icon', href);
}
export function useUnreadTitle(unreadCounts: Record<string, number>, favorites: Favorite[]): void {
const title = useMemo(() => getUnreadTitle(unreadCounts, favorites), [favorites, unreadCounts]);
export function useUnreadTitle(
unreadCounts: Record<string, number>,
contacts: Contact[],
channels: Channel[]
): void {
const title = useMemo(
() => getUnreadTitle(unreadCounts, contacts, channels),
[contacts, channels, unreadCounts]
);
useEffect(() => {
document.title = title;
@@ -143,12 +159,12 @@ export function useUnreadTitle(unreadCounts: Record<string, number>, favorites:
export function useFaviconBadge(
unreadCounts: Record<string, number>,
mentions: Record<string, boolean>,
favorites: Favorite[]
channels: Channel[]
): void {
const objectUrlRef = useRef<string | null>(null);
const badgeState = useMemo(
() => deriveFaviconBadgeState(unreadCounts, mentions, favorites),
[favorites, mentions, unreadCounts]
() => deriveFaviconBadgeState(unreadCounts, mentions, channels),
[channels, mentions, unreadCounts]
);
useEffect(() => {