diff --git a/src/components/ContentSettings.tsx b/src/components/ContentSettings.tsx index 626b3987..1474b662 100644 --- a/src/components/ContentSettings.tsx +++ b/src/components/ContentSettings.tsx @@ -236,13 +236,17 @@ function KindBadge({ kind }: { kind: number }) { function SubKindRow({ sub, parentEnabled }: { sub: SubKindDef; parentEnabled: boolean }) { const { feedSettings, updateFeedSettings } = useFeedSettings(); - const { updateSettings } = useEncryptedSettings(); + const { updateSettings, settings: nostrSettings } = useEncryptedSettings(); const { user } = useCurrentUser(); const handleToggle = async (key: string, value: boolean) => { updateFeedSettings({ [key]: value }); if (user) { - const updatedFeedSettings = { ...feedSettings, [key]: value }; + // Merge against the Nostr-cached feed settings (most up-to-date persisted + // state) rather than the local React state, which may lag behind when + // multiple toggles fire in quick succession. + const baseFeed = nostrSettings?.feedSettings ?? feedSettings; + const updatedFeedSettings = { ...baseFeed, [key]: value }; await updateSettings.mutateAsync({ feedSettings: updatedFeedSettings }); } }; @@ -280,7 +284,7 @@ function SubKindRow({ sub, parentEnabled }: { sub: SubKindDef; parentEnabled: bo function ContentTypeRow({ def }: { def: ExtraKindDef }) { const { feedSettings, updateFeedSettings } = useFeedSettings(); - const { updateSettings } = useEncryptedSettings(); + const { updateSettings, settings: nostrSettings } = useEncryptedSettings(); const { user } = useCurrentUser(); const icon = ICONS[def.route ?? String(def.kind)] ?? ; const hasSubKinds = !!def.subKinds; @@ -289,7 +293,11 @@ function ContentTypeRow({ def }: { def: ExtraKindDef }) { const handleToggle = async (key: string, value: boolean) => { updateFeedSettings({ [key]: value }); if (user) { - const updatedFeedSettings = { ...feedSettings, [key]: value }; + // Merge against the Nostr-cached feed settings (most up-to-date persisted + // state) rather than the local React state, which may lag behind when + // multiple toggles fire in quick succession. + const baseFeed = nostrSettings?.feedSettings ?? feedSettings; + const updatedFeedSettings = { ...baseFeed, [key]: value }; await updateSettings.mutateAsync({ feedSettings: updatedFeedSettings }); } }; diff --git a/src/components/NostrSync.tsx b/src/components/NostrSync.tsx index d94adcd3..fc497f0a 100644 --- a/src/components/NostrSync.tsx +++ b/src/components/NostrSync.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; @@ -17,9 +17,13 @@ export function NostrSync() { const { user } = useCurrentUser(); const { config, updateConfig } = useAppContext(); const { settings: encryptedSettings, isLoading: settingsLoading, recentlyWritten } = useEncryptedSettings(); - - // Track the last synced settings timestamp to prevent re-syncing the same data + + // Track the last synced settings timestamp to prevent re-syncing the same data. + // Seeded to the remote lastSync on first load so that a stale relay event + // (older than what useInitialSync already applied) does not overwrite local + // settings after a page reload. const lastSyncedTimestamp = useRef(0); + const [seededTimestamp, setSeededTimestamp] = useState(false); useEffect(() => { if (!user) return; @@ -73,16 +77,27 @@ export function NostrSync() { useEffect(() => { if (!user || settingsLoading || !encryptedSettings) return; - // Don't overwrite local config if we just saved settings + // Get the remote sync timestamp + const remoteSync = encryptedSettings.lastSync || 0; + + // On first load, seed the ref with the current remote timestamp so that + // we don't re-apply settings that were already applied by useInitialSync. + // This prevents stale relay events from overwriting local changes after a + // page reload (where lastWriteTs module variable resets to 0). + if (!seededTimestamp) { + lastSyncedTimestamp.current = remoteSync; + setSeededTimestamp(true); + return; + } + + // Don't overwrite local config if we just saved settings (short-circuit for + // the immediate write window, e.g. before the new event propagates back). if (recentlyWritten()) { console.log('Skipping settings sync - recent write'); return; } - // Get the remote sync timestamp - const remoteSync = encryptedSettings.lastSync || 0; - - // Only sync if we haven't already synced this exact timestamp + // Skip if the remote snapshot is older than what we last applied. if (remoteSync <= lastSyncedTimestamp.current) { return; } @@ -127,7 +142,7 @@ export function NostrSync() { // Return the same reference if nothing changed to prevent re-render return changed ? updates : current; }); - }, [user, encryptedSettings, settingsLoading, updateConfig, recentlyWritten]); + }, [user, encryptedSettings, settingsLoading, updateConfig, recentlyWritten, seededTimestamp]); return null; } \ No newline at end of file diff --git a/src/hooks/useEncryptedSettings.ts b/src/hooks/useEncryptedSettings.ts index c42fa219..beb395cf 100644 --- a/src/hooks/useEncryptedSettings.ts +++ b/src/hooks/useEncryptedSettings.ts @@ -156,15 +156,19 @@ export function useEncryptedSettings() { console.error('Failed to publish encrypted settings:', error); }); - return updatedSettings; + return { updatedSettings, signedEvent }; }, // Update cache in-place instead of refetching, which avoids // NostrSync re-running and causing a re-render loop. // Do NOT invalidate the encryptedSettings query here — doing so triggers a // relay refetch that can return the old event before the new one propagates, // which causes NostrSync to overwrite the theme the user just selected. - onSuccess: (data) => { - queryClient.setQueryData(['parsedSettings', query.data?.id], data); + // + // Use the signed event's ID (not the old query event ID) so the parsed + // settings cache entry is keyed correctly and NostrSync picks it up. + onSuccess: ({ updatedSettings, signedEvent }) => { + queryClient.setQueryData(['encryptedSettings', user?.pubkey], signedEvent); + queryClient.setQueryData(['parsedSettings', signedEvent.id], updatedSettings); }, });