From 82181ffb43a29db7c858f4031fe8e46cc2d12bfb Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 24 Mar 2026 13:38:56 +0100 Subject: [PATCH] bug-fixes --- src/hooks/useQuoteEvents.tsx | 17 +++---- src/providers/NostrProvider/index.tsx | 4 +- src/services/post-editor-cache.service.ts | 57 +++++++++++++++++++++-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/hooks/useQuoteEvents.tsx b/src/hooks/useQuoteEvents.tsx index a42e4144..c7c1e4b7 100644 --- a/src/hooks/useQuoteEvents.tsx +++ b/src/hooks/useQuoteEvents.tsx @@ -35,11 +35,12 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) { return } + const ev = event let cancelled = false let loadTimeoutId: ReturnType | undefined async function init() { - const noteRowId = event.id + const noteRowId = ev.id const isNewTarget = lastSubscribedEventIdRef.current !== noteRowId lastSubscribedEventIdRef.current = noteRowId @@ -60,7 +61,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) { const userRelays = userRelayList?.read || [] const fromFeed = browsingRelayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean) - const seenOn = client.getSeenEventRelayUrls(event.id) + const seenOn = client.getSeenEventRelayUrls(ev.id) const eTagBlockedSet = new Set( E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u) ) @@ -76,12 +77,12 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) { .filter(Boolean) .filter((u) => !eTagBlockedSet.has(normalizeUrl(u) || u)) - const filterQeId = isReplaceableEvent(event.kind) - ? getReplaceableCoordinateFromEvent(event) - : event.id - const eventCoordinate = isReplaceableEvent(event.kind) - ? getReplaceableCoordinateFromEvent(event) - : `${event.kind}:${event.pubkey}:${event.id}` + const filterQeId = isReplaceableEvent(ev.kind) + ? getReplaceableCoordinateFromEvent(ev) + : ev.id + const eventCoordinate = isReplaceableEvent(ev.kind) + ? getReplaceableCoordinateFromEvent(ev) + : `${ev.kind}:${ev.pubkey}:${ev.id}` const { closer, timelineKey } = await client.subscribeTimeline( [ diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 2d01d452..63bd988e 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -695,7 +695,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const prev = prevAccountPubkeyRef.current const curr = account?.pubkey ?? null prevAccountPubkeyRef.current = curr - if (prev !== undefined && prev !== curr) { + if (prev != null && curr != null && prev !== curr) { + postEditorCache.clearOnAccountChange() + } else if (prev != null && curr === null) { postEditorCache.clearOnAccountChange() } }, [account?.pubkey]) diff --git a/src/services/post-editor-cache.service.ts b/src/services/post-editor-cache.service.ts index b872a446..f1a7633d 100644 --- a/src/services/post-editor-cache.service.ts +++ b/src/services/post-editor-cache.service.ts @@ -3,8 +3,9 @@ import storage from '@/services/local-storage.service' import { TPollCreateData } from '@/types' import { Content } from '@tiptap/react' import { Event } from 'nostr-tools' +import { parseEditorJsonToText } from '@/lib/tiptap' -const PERSIST_DEBOUNCE_MS = 30_000 +const PERSIST_DEBOUNCE_MS = 5_000 type TPostSettings = { isNsfw?: boolean @@ -41,14 +42,27 @@ class PostEditorCacheService { private threadDraftCache: TThreadDraft | null = null private persistTimeoutId: ReturnType | null = null private restoredFromStorage = false + private keysRestoredThisSession = new Set() constructor() { if (!PostEditorCacheService.instance) { PostEditorCacheService.instance = this + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', () => this.flushPersist()) + } } return PostEditorCacheService.instance } + /** Flush pending draft to localStorage immediately. Called on beforeunload so drafts survive reload. */ + flushPersist() { + if (this.persistTimeoutId) { + clearTimeout(this.persistTimeoutId) + this.persistTimeoutId = null + } + this.persistNow() + } + /** * Escape ampersands so that when TipTap parses initial content as HTML, * sequences like ¬ify in URLs are not interpreted as the ¬ entity (¬). @@ -57,6 +71,14 @@ class PostEditorCacheService { return text.replace(/&/g, '&') } + /** Normalize cache key so hex event ids are lowercase; ensures consistent lookup across sessions. */ + private normalizeCacheKey(key: string): string { + const [, parentPart] = key.split(':', 2) + if (!parentPart) return key + const normalized = /^[0-9a-f]{64}$/i.test(parentPart) ? parentPart.toLowerCase() : parentPart + return `${key.split(':')[0]}:${normalized}` + } + private restoreFromStorageIfNeeded() { if (this.restoredFromStorage) return this.restoredFromStorage = true @@ -69,12 +91,16 @@ class PostEditorCacheService { if (data.accountPubkey !== account.pubkey) return if (data.postContentCache && typeof data.postContentCache === 'object') { Object.entries(data.postContentCache).forEach(([k, v]) => { - if (v) this.postContentCache.set(k, v) + if (v) { + const key = this.normalizeCacheKey(k) + this.postContentCache.set(key, v) + this.keysRestoredThisSession.add(key) + } }) } if (data.postSettingsCache && typeof data.postSettingsCache === 'object') { Object.entries(data.postSettingsCache).forEach(([k, v]) => { - if (v) this.postSettingsCache.set(k, v) + if (v) this.postSettingsCache.set(this.normalizeCacheKey(k), v) }) } if (data.threadDraft) { @@ -128,6 +154,7 @@ class PostEditorCacheService { this.postContentCache.clear() this.postSettingsCache.clear() this.threadDraftCache = null + this.keysRestoredThisSession.clear() this.restoredFromStorage = false try { window.localStorage.removeItem(StorageKey.POST_EDITOR_DRAFT) @@ -148,7 +175,23 @@ class PostEditorCacheService { } setPostContentCache({ kind, defaultContent, parentEvent }: TCacheKeyParams, content: Content) { + this.restoreFromStorageIfNeeded() const cacheKey = this.generateCacheKey({ kind, defaultContent, parentEvent }) + const incomingText = ( + typeof content === 'string' ? content : parseEditorJsonToText(content ?? undefined) + ).trim() + const existing = this.postContentCache.get(cacheKey) + const existingText = existing + ? (typeof existing === 'string' ? existing : parseEditorJsonToText(existing)).trim() + : '' + if ( + incomingText === '' && + existingText !== '' && + this.keysRestoredThisSession.has(cacheKey) + ) { + return + } + this.keysRestoredThisSession.delete(cacheKey) this.postContentCache.set(cacheKey, content) this.schedulePersist() } @@ -166,6 +209,7 @@ class PostEditorCacheService { clearPostCache({ kind, defaultContent, parentEvent }: TCacheKeyParams) { const cacheKey = this.generateCacheKey({ kind, defaultContent, parentEvent }) + this.keysRestoredThisSession.delete(cacheKey) this.postContentCache.delete(cacheKey) this.postSettingsCache.delete(cacheKey) if (this.persistTimeoutId) { @@ -177,6 +221,7 @@ class PostEditorCacheService { /** Clear all post and settings drafts. Use when user explicitly clears caches. */ clearAllPostCaches() { + this.keysRestoredThisSession.clear() this.postContentCache.clear() this.postSettingsCache.clear() if (this.persistTimeoutId) { @@ -186,8 +231,10 @@ class PostEditorCacheService { this.persistNow() } - generateCacheKey({ kind, defaultContent = '', parentEvent }: TCacheKeyParams): string { - const parentPart = parentEvent ? parentEvent.id : '' + generateCacheKey({ kind, parentEvent }: TCacheKeyParams): string { + if (!parentEvent?.id) return `${kind}:` + const id = parentEvent.id.trim() + const parentPart = /^[0-9a-f]{64}$/i.test(id) ? id.toLowerCase() : id return `${kind}:${parentPart}` }