Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
82181ffb43
  1. 17
      src/hooks/useQuoteEvents.tsx
  2. 4
      src/providers/NostrProvider/index.tsx
  3. 57
      src/services/post-editor-cache.service.ts

17
src/hooks/useQuoteEvents.tsx

@ -35,11 +35,12 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) {
return return
} }
const ev = event
let cancelled = false let cancelled = false
let loadTimeoutId: ReturnType<typeof setTimeout> | undefined let loadTimeoutId: ReturnType<typeof setTimeout> | undefined
async function init() { async function init() {
const noteRowId = event.id const noteRowId = ev.id
const isNewTarget = lastSubscribedEventIdRef.current !== noteRowId const isNewTarget = lastSubscribedEventIdRef.current !== noteRowId
lastSubscribedEventIdRef.current = noteRowId lastSubscribedEventIdRef.current = noteRowId
@ -60,7 +61,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) {
const userRelays = userRelayList?.read || [] const userRelays = userRelayList?.read || []
const fromFeed = browsingRelayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean) 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( const eTagBlockedSet = new Set(
E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u) 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(Boolean)
.filter((u) => !eTagBlockedSet.has(normalizeUrl(u) || u)) .filter((u) => !eTagBlockedSet.has(normalizeUrl(u) || u))
const filterQeId = isReplaceableEvent(event.kind) const filterQeId = isReplaceableEvent(ev.kind)
? getReplaceableCoordinateFromEvent(event) ? getReplaceableCoordinateFromEvent(ev)
: event.id : ev.id
const eventCoordinate = isReplaceableEvent(event.kind) const eventCoordinate = isReplaceableEvent(ev.kind)
? getReplaceableCoordinateFromEvent(event) ? getReplaceableCoordinateFromEvent(ev)
: `${event.kind}:${event.pubkey}:${event.id}` : `${ev.kind}:${ev.pubkey}:${ev.id}`
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
[ [

4
src/providers/NostrProvider/index.tsx

@ -695,7 +695,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const prev = prevAccountPubkeyRef.current const prev = prevAccountPubkeyRef.current
const curr = account?.pubkey ?? null const curr = account?.pubkey ?? null
prevAccountPubkeyRef.current = curr 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() postEditorCache.clearOnAccountChange()
} }
}, [account?.pubkey]) }, [account?.pubkey])

57
src/services/post-editor-cache.service.ts

@ -3,8 +3,9 @@ import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { Content } from '@tiptap/react' import { Content } from '@tiptap/react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { parseEditorJsonToText } from '@/lib/tiptap'
const PERSIST_DEBOUNCE_MS = 30_000 const PERSIST_DEBOUNCE_MS = 5_000
type TPostSettings = { type TPostSettings = {
isNsfw?: boolean isNsfw?: boolean
@ -41,14 +42,27 @@ class PostEditorCacheService {
private threadDraftCache: TThreadDraft | null = null private threadDraftCache: TThreadDraft | null = null
private persistTimeoutId: ReturnType<typeof setTimeout> | null = null private persistTimeoutId: ReturnType<typeof setTimeout> | null = null
private restoredFromStorage = false private restoredFromStorage = false
private keysRestoredThisSession = new Set<string>()
constructor() { constructor() {
if (!PostEditorCacheService.instance) { if (!PostEditorCacheService.instance) {
PostEditorCacheService.instance = this PostEditorCacheService.instance = this
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => this.flushPersist())
}
} }
return PostEditorCacheService.instance 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, * Escape ampersands so that when TipTap parses initial content as HTML,
* sequences like &notify in URLs are not interpreted as the &not; entity (¬). * sequences like &notify in URLs are not interpreted as the &not; entity (¬).
@ -57,6 +71,14 @@ class PostEditorCacheService {
return text.replace(/&/g, '&amp;') return text.replace(/&/g, '&amp;')
} }
/** 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() { private restoreFromStorageIfNeeded() {
if (this.restoredFromStorage) return if (this.restoredFromStorage) return
this.restoredFromStorage = true this.restoredFromStorage = true
@ -69,12 +91,16 @@ class PostEditorCacheService {
if (data.accountPubkey !== account.pubkey) return if (data.accountPubkey !== account.pubkey) return
if (data.postContentCache && typeof data.postContentCache === 'object') { if (data.postContentCache && typeof data.postContentCache === 'object') {
Object.entries(data.postContentCache).forEach(([k, v]) => { 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') { if (data.postSettingsCache && typeof data.postSettingsCache === 'object') {
Object.entries(data.postSettingsCache).forEach(([k, v]) => { 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) { if (data.threadDraft) {
@ -128,6 +154,7 @@ class PostEditorCacheService {
this.postContentCache.clear() this.postContentCache.clear()
this.postSettingsCache.clear() this.postSettingsCache.clear()
this.threadDraftCache = null this.threadDraftCache = null
this.keysRestoredThisSession.clear()
this.restoredFromStorage = false this.restoredFromStorage = false
try { try {
window.localStorage.removeItem(StorageKey.POST_EDITOR_DRAFT) window.localStorage.removeItem(StorageKey.POST_EDITOR_DRAFT)
@ -148,7 +175,23 @@ class PostEditorCacheService {
} }
setPostContentCache({ kind, defaultContent, parentEvent }: TCacheKeyParams, content: Content) { setPostContentCache({ kind, defaultContent, parentEvent }: TCacheKeyParams, content: Content) {
this.restoreFromStorageIfNeeded()
const cacheKey = this.generateCacheKey({ kind, defaultContent, parentEvent }) 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.postContentCache.set(cacheKey, content)
this.schedulePersist() this.schedulePersist()
} }
@ -166,6 +209,7 @@ class PostEditorCacheService {
clearPostCache({ kind, defaultContent, parentEvent }: TCacheKeyParams) { clearPostCache({ kind, defaultContent, parentEvent }: TCacheKeyParams) {
const cacheKey = this.generateCacheKey({ kind, defaultContent, parentEvent }) const cacheKey = this.generateCacheKey({ kind, defaultContent, parentEvent })
this.keysRestoredThisSession.delete(cacheKey)
this.postContentCache.delete(cacheKey) this.postContentCache.delete(cacheKey)
this.postSettingsCache.delete(cacheKey) this.postSettingsCache.delete(cacheKey)
if (this.persistTimeoutId) { if (this.persistTimeoutId) {
@ -177,6 +221,7 @@ class PostEditorCacheService {
/** Clear all post and settings drafts. Use when user explicitly clears caches. */ /** Clear all post and settings drafts. Use when user explicitly clears caches. */
clearAllPostCaches() { clearAllPostCaches() {
this.keysRestoredThisSession.clear()
this.postContentCache.clear() this.postContentCache.clear()
this.postSettingsCache.clear() this.postSettingsCache.clear()
if (this.persistTimeoutId) { if (this.persistTimeoutId) {
@ -186,8 +231,10 @@ class PostEditorCacheService {
this.persistNow() this.persistNow()
} }
generateCacheKey({ kind, defaultContent = '', parentEvent }: TCacheKeyParams): string { generateCacheKey({ kind, parentEvent }: TCacheKeyParams): string {
const parentPart = parentEvent ? parentEvent.id : '' 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}` return `${kind}:${parentPart}`
} }

Loading…
Cancel
Save