diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx index 9164f5ce0f..0c227c7b44 100644 --- a/src/screens/Settings/AboutSettings.tsx +++ b/src/screens/Settings/AboutSettings.tsx @@ -10,6 +10,7 @@ import {useMutation} from '@tanstack/react-query' import {STATUS_PAGE_URL} from '#/lib/constants' import {type CommonNavigatorParams} from '#/lib/routes/types' +import {purgeTemporaryImageFiles} from '#/state/gallery' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle' @@ -41,7 +42,13 @@ export function AboutSettingsScreen({}: Props) { useMutation({ mutationFn: async () => { const freeSpaceBefore = await FileSystem.getFreeDiskStorageAsync() - await Image.clearDiskCache() + await Promise.all([ + // expo-image's disk cache + Image.clearDiskCache(), + // full-resolution media-upload leftovers (picker/manipulator copies); + // the only in-app way for iOS users to reclaim this space + purgeTemporaryImageFiles(), + ]) const freeSpaceAfter = await FileSystem.getFreeDiskStorageAsync() const spaceDiff = freeSpaceBefore - freeSpaceAfter return spaceDiff * -1 diff --git a/src/state/gallery.ts b/src/state/gallery.ts index e6c5b50d36..09bc299f07 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -358,14 +358,37 @@ function blobToDataUri(blob: Blob): Promise { }) } +/** + * Caches that the OS image picker and manipulator write into when attaching + * media to a post. They live alongside our own `bsky-composer` dir under the OS + * cache directory. expo-image-picker copies every originally selected photo and + * video here, and expo-image-manipulator leaves intermediate full-resolution + * outputs here (compressImage makes several manipulateAsync passes, only the + * last of which gets moved into `bsky-composer`). Nothing else cleans these up, + * so on iOS - where the OS exposes no "clear cache" - they accumulate + * indefinitely, one full-resolution copy per attached item. + */ +const SYSTEM_MEDIA_CACHE_DIRS = ['ImagePicker', 'ImageManipulator'] + /** Purge files that were created to accomodate image manipulation */ export async function purgeTemporaryImageFiles() { - const cacheDir = IS_NATIVE && getImageCacheDirectory() + if (!IS_NATIVE) { + return + } + const cacheDir = getImageCacheDirectory() if (cacheDir) { await deleteAsync(cacheDir, {idempotent: true}) await makeDirectoryAsync(cacheDir) } + + // We don't recreate these - the respective expo modules recreate them on + // demand the next time they run. + await Promise.all( + SYSTEM_MEDIA_CACHE_DIRS.map(dir => + deleteAsync(joinPath(cacheDirectory!, dir), {idempotent: true}), + ), + ) } function joinPath(a: string, b: string) {