mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 09:13:04 +02:00
Add favorites as contact field (dug)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user