revoke media URLs

This commit is contained in:
Samuel Newman
2026-01-29 22:02:32 +02:00
parent 23f7e18aa3
commit 68e1a8e506
5 changed files with 54 additions and 5 deletions
+2
View File
@@ -142,6 +142,7 @@ import {
useSaveDraftMutation, useSaveDraftMutation,
} from './drafts/state/queries' } from './drafts/state/queries'
import {type DraftSummary} from './drafts/state/schema' import {type DraftSummary} from './drafts/state/schema'
import {revokeAllMediaUrls} from './drafts/state/storage'
import {PostLanguageSelect} from './select-language/PostLanguageSelect' import {PostLanguageSelect} from './select-language/PostLanguageSelect'
import { import {
type AssetType, type AssetType,
@@ -504,6 +505,7 @@ export const ComposePost = ({
const onClose = useCallback(() => { const onClose = useCallback(() => {
closeComposer() closeComposer()
clearThumbnailCache(queryClient) clearThumbnailCache(queryClient)
revokeAllMediaUrls()
}, [closeComposer, queryClient]) }, [closeComposer, queryClient])
const handleSaveDraft = React.useCallback(async () => { const handleSaveDraft = React.useCallback(async () => {
@@ -14,6 +14,7 @@ import {IS_NATIVE} from '#/env'
import {DraftItem} from './DraftItem' import {DraftItem} from './DraftItem'
import {useDeleteDraftMutation, useDraftsQuery} from './state/queries' import {useDeleteDraftMutation, useDraftsQuery} from './state/queries'
import {type DraftSummary} from './state/schema' import {type DraftSummary} from './state/schema'
import {revokeAllMediaUrls} from './state/storage'
export function DraftsListDialog({ export function DraftsListDialog({
control, control,
@@ -36,6 +37,12 @@ export function DraftsListDialog({
const handleSelectDraft = useCallback( const handleSelectDraft = useCallback(
(summary: DraftSummary) => { (summary: DraftSummary) => {
control.close(() => { control.close(() => {
// Revoke preview URLs
// It would be neater to do it in the `onClose` callback
// on the dialog itself, but this would wipe out the new ones
// that will be created by `onSelectDraft`
// ndb: if we miss any, it'll be handled by the composer closing
revokeAllMediaUrls()
onSelectDraft(summary) onSelectDraft(summary)
}) })
}, },
@@ -53,7 +60,11 @@ export function DraftsListDialog({
() => ( () => (
<Button <Button
label={_(msg`Back`)} label={_(msg`Back`)}
onPress={() => control.close()} onPress={() => {
control.close(() => {
revokeAllMediaUrls()
})
}}
size="small" size="small"
color="primary" color="primary"
variant="ghost"> variant="ghost">
@@ -93,7 +104,7 @@ export function DraftsListDialog({
const onEndReached = useCallback(() => { const onEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) { if (hasNextPage && !isFetchingNextPage) {
fetchNextPage() void fetchNextPage()
} }
}, [hasNextPage, isFetchingNextPage, fetchNextPage]) }, [hasNextPage, isFetchingNextPage, fetchNextPage])
@@ -132,7 +143,7 @@ export function DraftsListDialog({
<Dialog.InnerFlatList <Dialog.InnerFlatList
data={drafts} data={drafts}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={item => item.id} keyExtractor={(item: DraftSummary) => item.id}
ListHeaderComponent={web(header)} ListHeaderComponent={web(header)}
stickyHeaderIndices={web([0])} stickyHeaderIndices={web([0])}
ListEmptyComponent={emptyComponent} ListEmptyComponent={emptyComponent}
@@ -155,3 +155,17 @@ export function clearMediaCache(): void {
cachePopulated = false cachePopulated = false
populateCachePromise = null populateCachePromise = null
} }
/**
* Revoke a media URL (no-op on native - only needed for web blob URLs)
*/
export function revokeMediaUrl(_url: string): void {
// No-op on native - file URIs don't need revocation
}
/**
* Revoke all media URLs (no-op on native - only needed for web blob URLs)
*/
export function revokeAllMediaUrls(): void {
// No-op on native - file URIs don't need revocation
}
@@ -84,6 +84,11 @@ export async function saveMediaToLocal(
} }
} }
/**
* Track blob URLs created by loadMediaFromLocal for cleanup
*/
const createdBlobUrls = new Set<string>()
/** /**
* Load a media file from IndexedDB * Load a media file from IndexedDB
* @returns A blob URL for the saved media * @returns A blob URL for the saved media
@@ -97,7 +102,10 @@ export async function loadMediaFromLocal(
throw new Error(`Media file not found: ${localRefPath}`) throw new Error(`Media file not found: ${localRefPath}`)
} }
return URL.createObjectURL(record.blob) const url = URL.createObjectURL(record.blob)
logger.debug('Created blob URL', {url})
createdBlobUrls.add(url)
return url
} }
/** /**
@@ -165,6 +173,20 @@ export function clearMediaCache(): void {
*/ */
export function revokeMediaUrl(url: string): void { export function revokeMediaUrl(url: string): void {
if (url.startsWith('blob:')) { if (url.startsWith('blob:')) {
logger.debug('Revoking blob URL', {url})
URL.revokeObjectURL(url)
createdBlobUrls.delete(url)
}
}
/**
* Revoke all blob URLs created by loadMediaFromLocal.
* Call this when closing the drafts list dialog to prevent memory leaks.
*/
export function revokeAllMediaUrls(): void {
logger.debug(`Revoking ${createdBlobUrls.size} blob URLs`)
for (const url of createdBlobUrls) {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
createdBlobUrls.clear()
} }
@@ -9,7 +9,7 @@ export const RQKEY = 'video-thumbnail'
export function clearThumbnailCache(queryClient: QueryClient) { export function clearThumbnailCache(queryClient: QueryClient) {
clearCache().catch(() => {}) clearCache().catch(() => {})
queryClient.resetQueries({queryKey: [RQKEY]}) void queryClient.resetQueries({queryKey: [RQKEY]})
} }
export function VideoTranscodeBackdrop({uri}: {uri: string}) { export function VideoTranscodeBackdrop({uri}: {uri: string}) {