diff --git a/client/src/mobile/screens/admin/MAdmin.tsx b/client/src/mobile/screens/admin/MAdmin.tsx index 5f4923281..5130ba5f2 100644 --- a/client/src/mobile/screens/admin/MAdmin.tsx +++ b/client/src/mobile/screens/admin/MAdmin.tsx @@ -230,12 +230,14 @@ export default function MAdmin() { }} collabFeatures={collabFeatures} onToggleCollabFeature={async (key: string) => { - const next = { ...collabFeatures, [key]: !collabFeatures[key] } - setCollabFeatures(next) + const previous = collabFeatures[key] + setCollabFeatures({ ...collabFeatures, [key]: !previous }) try { - await adminApi.updateCollabFeatures({ [key]: next[key] }) + await adminApi.updateCollabFeatures({ [key]: !previous }) } catch { - setCollabFeatures(collabFeatures) + // Only this key rolls back — a slower request must not undo a toggle + // the admin made in the meantime. + setCollabFeatures(prev => ({ ...prev, [key]: previous })) } }} /> diff --git a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx index 654e054e0..e046b0a54 100644 --- a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx +++ b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx @@ -89,6 +89,13 @@ export default function MNewTripSheet({ open, trip, onClose, onSave, onCoverUpda setError('') }, [trip, open]) + // The local file preview is a blob url; release it once a new cover replaces it + // or the sheet goes away. Server and Unsplash urls are left alone. + useEffect(() => { + if (!coverPreview?.startsWith('blob:')) return + return () => { URL.revokeObjectURL(coverPreview) } + }, [coverPreview]) + // Moving the start keeps the trip length (same rule as TripFormModal). const changeStart = (value: string) => { if (value && endDate && startDate && endDate >= startDate) { diff --git a/client/src/mobile/screens/journey/MJourneyDetail.tsx b/client/src/mobile/screens/journey/MJourneyDetail.tsx index 012c17dd3..a98175d3c 100644 --- a/client/src/mobile/screens/journey/MJourneyDetail.tsx +++ b/client/src/mobile/screens/journey/MJourneyDetail.tsx @@ -135,7 +135,9 @@ export default function MJourneyDetail() { const [uploading, setUploading] = useState(false) useEffect(() => { - (async () => { + let active = true + const ac = new AbortController() + ;(async () => { try { const addonsData = await addonsApi.enabled() const enabled = (addonsData.addons || []).filter( @@ -144,13 +146,14 @@ export default function MJourneyDetail() { const connected: { id: string; name: string }[] = [] for (const p of enabled) { try { - const res = await fetch(`/api/integrations/memories/${p.id}/status`, { credentials: 'include' }) + const res = await fetch(`/api/integrations/memories/${p.id}/status`, { credentials: 'include', signal: ac.signal }) if (res.ok && (await res.json()).connected) connected.push({ id: p.id, name: p.name }) } catch { /* provider stays hidden */ } } - setAvailableProviders(connected) + if (active) setAvailableProviders(connected) } catch { /* no providers */ } })() + return () => { active = false; ac.abort() } }, []) const handleGalleryUpload = async (e: React.ChangeEvent) => { diff --git a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx index 33f4db055..9692cb6ab 100644 --- a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx +++ b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx @@ -101,6 +101,11 @@ export default function MJourneyEntrySheet({ [addons], ) + // Minting the preview inside the tile markup would hand out a fresh blob URL on + // every keystroke in the story field, and nothing would ever release them. + const pendingPreviews = useMemo(() => pendingFiles.map(f => URL.createObjectURL(f)), [pendingFiles]) + useEffect(() => () => { pendingPreviews.forEach(url => URL.revokeObjectURL(url)) }, [pendingPreviews]) + useEffect(() => { if (readOnly) return // App.tsx loads the addon list on boot, so this only reacts to it. Kicking off @@ -536,13 +541,13 @@ export default function MJourneyEntrySheet({