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,
} from './drafts/state/queries'
import {type DraftSummary} from './drafts/state/schema'
import {revokeAllMediaUrls} from './drafts/state/storage'
import {PostLanguageSelect} from './select-language/PostLanguageSelect'
import {
type AssetType,
@@ -504,6 +505,7 @@ export const ComposePost = ({
const onClose = useCallback(() => {
closeComposer()
clearThumbnailCache(queryClient)
revokeAllMediaUrls()
}, [closeComposer, queryClient])
const handleSaveDraft = React.useCallback(async () => {
@@ -14,6 +14,7 @@ import {IS_NATIVE} from '#/env'
import {DraftItem} from './DraftItem'
import {useDeleteDraftMutation, useDraftsQuery} from './state/queries'
import {type DraftSummary} from './state/schema'
import {revokeAllMediaUrls} from './state/storage'
export function DraftsListDialog({
control,
@@ -36,6 +37,12 @@ export function DraftsListDialog({
const handleSelectDraft = useCallback(
(summary: DraftSummary) => {
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)
})
},
@@ -53,7 +60,11 @@ export function DraftsListDialog({
() => (
<Button
label={_(msg`Back`)}
onPress={() => control.close()}
onPress={() => {
control.close(() => {
revokeAllMediaUrls()
})
}}
size="small"
color="primary"
variant="ghost">
@@ -93,7 +104,7 @@ export function DraftsListDialog({
const onEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
void fetchNextPage()
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
@@ -132,7 +143,7 @@ export function DraftsListDialog({
<Dialog.InnerFlatList
data={drafts}
renderItem={renderItem}
keyExtractor={item => item.id}
keyExtractor={(item: DraftSummary) => item.id}
ListHeaderComponent={web(header)}
stickyHeaderIndices={web([0])}
ListEmptyComponent={emptyComponent}
@@ -155,3 +155,17 @@ export function clearMediaCache(): void {
cachePopulated = false
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
* @returns A blob URL for the saved media
@@ -97,7 +102,10 @@ export async function loadMediaFromLocal(
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 {
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)
}
createdBlobUrls.clear()
}
@@ -9,7 +9,7 @@ export const RQKEY = 'video-thumbnail'
export function clearThumbnailCache(queryClient: QueryClient) {
clearCache().catch(() => {})
queryClient.resetQueries({queryKey: [RQKEY]})
void queryClient.resetQueries({queryKey: [RQKEY]})
}
export function VideoTranscodeBackdrop({uri}: {uri: string}) {