revoke media URLs
This commit is contained in:
@@ -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}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user