Browse Source

handle freezing

imwald
Silberengel 2 weeks ago
parent
commit
e5b36499e9
  1. 7
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  2. 13
      src/components/PostEditor/PostContent.tsx
  3. 17
      src/components/PostEditor/PostRelaySelector.tsx
  4. 20
      src/services/client.service.ts
  5. 27
      src/services/post-editor-cache.service.ts

7
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -297,7 +297,8 @@ export default function AdvancedEventLabDialog({
setUndoUiTick((n) => n + 1) setUndoUiTick((n) => n + 1)
}, []) }, [])
const flushLabDraftNow = useCallback((key: string) => { /** Writes the live CodeMirror doc into the draft cache. `urgent` flushes localStorage synchronously (tab hide / unload only). */
const flushLabDraftNow = useCallback((key: string, urgent = false) => {
const v = markupView.current const v = markupView.current
const s = sliceRef.current const s = sliceRef.current
if (!v || !s) return if (!v || !s) return
@ -310,14 +311,14 @@ export default function AdvancedEventLabDialog({
content: v.state.doc.toString(), content: v.state.doc.toString(),
tags: s.tags.map((row) => [...row]) tags: s.tags.map((row) => [...row])
}) })
postEditorCache.flushPersist() if (urgent) postEditorCache.flushPersist()
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!open || !draftPersistenceKey) return if (!open || !draftPersistenceKey) return
const key = draftPersistenceKey const key = draftPersistenceKey
const onPageLeave = () => { const onPageLeave = () => {
flushLabDraftNow(key) flushLabDraftNow(key, true)
} }
window.addEventListener('pagehide', onPageLeave) window.addEventListener('pagehide', onPageLeave)
window.addEventListener('beforeunload', onPageLeave) window.addEventListener('beforeunload', onPageLeave)

13
src/components/PostEditor/PostContent.tsx

@ -120,6 +120,14 @@ import {
import { parseLabSlice, type AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' import { parseLabSlice, type AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice'
import { isAsciidocMarkupKind } from '@/lib/advanced-event-lab-kinds' import { isAsciidocMarkupKind } from '@/lib/advanced-event-lab-kinds'
/** Let the UI paint before heavy work. `requestAnimationFrame` alone can stall indefinitely in hidden or throttled documents. */
function yieldForPaintBeforeHeavyWork(): Promise<void> {
return Promise.race([
new Promise<void>((resolve) => requestAnimationFrame(() => resolve())),
new Promise<void>((resolve) => setTimeout(resolve, 50))
])
}
function stripUrlForImageExtensionCheck(url: string): string { function stripUrlForImageExtensionCheck(url: string): string {
return url.trim().split(/[#?]/)[0].toLowerCase() return url.trim().split(/[#?]/)[0].toLowerCase()
} }
@ -1217,6 +1225,8 @@ export default function PostContent({
return return
} }
try { try {
// Let the browser paint any loading/disabled UI before draft build + CodeMirror mount (can be heavy).
await yieldForPaintBeforeHeavyWork()
const body = textareaRef.current?.getText() ?? text const body = textareaRef.current?.getText() ?? text
const cleanedText = rewritePlainTextHttpUrls(body) const cleanedText = rewritePlainTextHttpUrls(body)
let d = await createDraftEvent(cleanedText) let d = await createDraftEvent(cleanedText)
@ -1308,6 +1318,9 @@ export default function PostContent({
let newEvent: any = null let newEvent: any = null
let draftEvent: any = null let draftEvent: any = null
// Allow "Publishing…" (and other posting UI) to paint before draft build + network work.
await yieldForPaintBeforeHeavyWork()
try { try {
// Clean tracking parameters from URLs in the post content // Clean tracking parameters from URLs in the post content
const cleanedText = rewritePlainTextHttpUrls(text) const cleanedText = rewritePlainTextHttpUrls(text)

17
src/components/PostEditor/PostRelaySelector.tsx

@ -19,6 +19,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { computePrePublishRelayCapPreview, type TPrePublishRelayCapPreview } from '@/lib/pre-publish-relay-cap' import { computePrePublishRelayCapPreview, type TPrePublishRelayCapPreview } from '@/lib/pre-publish-relay-cap'
import client from '@/services/client.service'
/** Stable default when `mentions` is omitted — inline `= []` is a new array every render and retriggers effects. */ /** Stable default when `mentions` is omitted — inline `= []` is a new array every render and retriggers effects. */
const NO_MENTIONS: string[] = [] const NO_MENTIONS: string[] = []
@ -265,6 +266,22 @@ export default function PostRelaySelector({
} }
}, [selectedRelayUrls, hasManualSelection, isLoading, describeRelaySelection]) }, [selectedRelayUrls, hasManualSelection, isLoading, describeRelaySelection])
/** Picker lists exclude global read-only relays; session-strike skips are cleared when a relay is newly chosen so publishes honor the list. */
const prevSelectedNormalizedRef = useRef<Set<string>>(new Set())
useEffect(() => {
const norm = (u: string) => normalizeAnyRelayUrl(u) || u
const prev = prevSelectedNormalizedRef.current
const newlyAdded: string[] = []
for (const url of selectedRelayUrls) {
const n = norm(url)
if (!prev.has(n)) newlyAdded.push(url)
}
prevSelectedNormalizedRef.current = new Set(selectedRelayUrls.map(norm))
if (newlyAdded.length > 0) {
client.clearSessionRelayStrikesForUrls(newlyAdded)
}
}, [selectedRelayUrls])
// Update parent component with selected relays // Update parent component with selected relays
useEffect(() => { useEffect(() => {
// An event is "protected" if we have selected relays that aren't the default user write relays // An event is "protected" if we have selected relays that aren't the default user write relays

20
src/services/client.service.ts

@ -1164,6 +1164,26 @@ class ClientService extends EventTarget {
return had return had
} }
/**
* Clear session strikes for several URLs at once (e.g. publish relay picker). One UI notification.
*/
clearSessionRelayStrikesForUrls(urls: string[]): number {
let cleared = 0
for (const url of urls) {
const n = normalizeAnyRelayUrl(url) || url
if (!n) continue
if (this.publishStrikeCount.delete(n)) cleared += 1
}
if (cleared > 0) {
logger.info('[Relay] Session strikes cleared for relays (added to publish selection)', {
cleared,
urlCount: urls.length
})
this.notifySessionRelayStrikesChanged()
}
return cleared
}
/** /**
* Apply strike filter; if that removes all candidates while some were provided, clear strikes **for those URLs * Apply strike filter; if that removes all candidates while some were provided, clear strikes **for those URLs
* only** and retry once. (A global clear here caused storms: e.g. NIP-65 outbox retry with 2 relays wiped strikes * only** and retry once. (A global clear here caused storms: e.g. NIP-65 outbox retry with 2 relays wiped strikes

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

@ -263,11 +263,10 @@ class PostEditorCacheService {
clearAdvancedLabDraft(key: string) { clearAdvancedLabDraft(key: string) {
this.restoreFromStorageIfNeeded() this.restoreFromStorageIfNeeded()
if (!this.advancedLabDrafts.delete(key)) return if (!this.advancedLabDrafts.delete(key)) return
if (this.persistTimeoutId) { // Avoid synchronous JSON.stringify(localStorage) of the full draft blob here — that blocks
clearTimeout(this.persistTimeoutId) // the main thread when TipTap caches are large. Debounced persist is enough; tab close still
this.persistTimeoutId = null // uses {@link flushPersist} via beforeunload.
} this.schedulePersist()
this.persistNow()
} }
clearPostCache({ kind, defaultContent, parentEvent }: TCacheKeyParams) { clearPostCache({ kind, defaultContent, parentEvent }: TCacheKeyParams) {
@ -276,11 +275,7 @@ class PostEditorCacheService {
this.postContentCache.delete(cacheKey) this.postContentCache.delete(cacheKey)
this.postSettingsCache.delete(cacheKey) this.postSettingsCache.delete(cacheKey)
this.advancedLabDrafts.delete(cacheKey) this.advancedLabDrafts.delete(cacheKey)
if (this.persistTimeoutId) { this.schedulePersist()
clearTimeout(this.persistTimeoutId)
this.persistTimeoutId = null
}
this.persistNow()
} }
/** Clear all post and settings drafts. Use when user explicitly clears caches. */ /** Clear all post and settings drafts. Use when user explicitly clears caches. */
@ -289,11 +284,7 @@ class PostEditorCacheService {
this.postContentCache.clear() this.postContentCache.clear()
this.postSettingsCache.clear() this.postSettingsCache.clear()
this.advancedLabDrafts.clear() this.advancedLabDrafts.clear()
if (this.persistTimeoutId) { this.schedulePersist()
clearTimeout(this.persistTimeoutId)
this.persistTimeoutId = null
}
this.persistNow()
} }
generateCacheKey({ kind, parentEvent }: TCacheKeyParams): string { generateCacheKey({ kind, parentEvent }: TCacheKeyParams): string {
@ -315,11 +306,7 @@ class PostEditorCacheService {
clearThreadDraft(): void { clearThreadDraft(): void {
this.threadDraftCache = null this.threadDraftCache = null
if (this.persistTimeoutId) { this.schedulePersist()
clearTimeout(this.persistTimeoutId)
this.persistTimeoutId = null
}
this.persistNow()
} }
} }

Loading…
Cancel
Save