diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; +import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { logInActionSources, @@ -14,6 +15,7 @@ import { getMessageForException } from 'lib/utils/errors.js'; import { convertClientDBThreadInfosToRawThreadInfos } from 'lib/utils/thread-ops-utils.js'; +import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -36,6 +38,7 @@ const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); + const mediaCacheContext = React.useContext(MediaCacheContext); const callFetchNewCookieFromNativeCredentials = React.useCallback( async (source: LogInActionSource) => { @@ -69,6 +72,7 @@ Alert.alert('Starting SQLite database deletion process'); } await commCoreModule.clearSensitiveData(); + await filesystemMediaCache.clearCache(); if (staffCanSee || staffUserHasBeenLoggedIn) { Alert.alert( 'SQLite database successfully deleted', @@ -142,7 +146,10 @@ return; } (async () => { - await sensitiveDataHandled; + await Promise.all([ + sensitiveDataHandled, + mediaCacheContext?.evictCache(), + ]); try { const { threads, messages, drafts } = await commCoreModule.getClientDBStore(); @@ -188,6 +195,7 @@ staffUserHasBeenLoggedIn, callFetchNewCookieFromNativeCredentials, callClearSensitiveData, + mediaCacheContext, ]); return null; diff --git a/native/media/encrypted-image.react.js b/native/media/encrypted-image.react.js --- a/native/media/encrypted-image.react.js +++ b/native/media/encrypted-image.react.js @@ -2,6 +2,8 @@ import * as React from 'react'; +import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; + import { decryptMedia } from './encryption-utils.js'; import LoadableImage from './loadable-image.react.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -22,6 +24,7 @@ function EncryptedImage(props: Props): React.Node { const { holder, encryptionKey, onLoad: onLoadProp } = props; + const mediaCache = React.useContext(MediaCacheContext); const [source, setSource] = React.useState(null); const connectionStatus = useSelector(state => state.connection.status); @@ -40,11 +43,18 @@ setSource(null); const loadDecrypted = async () => { + const cached = await mediaCache?.get(holder); + if (cached && isMounted) { + setSource({ uri: cached }); + return; + } + const { result } = await decryptMedia(holder, encryptionKey, { destination: 'data_uri', }); // TODO: decide what to do if decryption fails if (result.success && isMounted) { + mediaCache?.set(holder, result.uri); setSource({ uri: result.uri }); } }; @@ -54,7 +64,7 @@ return () => { isMounted = false; }; - }, [attempt, holder, encryptionKey]); + }, [attempt, holder, encryptionKey, mediaCache]); const onLoad = React.useCallback(() => { onLoadProp && onLoadProp(holder); diff --git a/native/media/video-playback-modal.react.js b/native/media/video-playback-modal.react.js --- a/native/media/video-playback-modal.react.js +++ b/native/media/video-playback-modal.react.js @@ -12,6 +12,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import Video from 'react-native-video'; +import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import { useIsAppBackgroundedOrInactive } from 'lib/shared/lifecycle-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; @@ -78,11 +79,13 @@ function VideoPlaybackModal(props: Props): React.Node { const { mediaInfo } = props.route.params; - const { uri, holder, encryptionKey } = mediaInfo; + const { uri: videoUri, holder, encryptionKey } = mediaInfo; const [videoSource, setVideoSource] = React.useState( - uri ? { uri } : undefined, + videoUri ? { uri: videoUri } : undefined, ); + const mediaCache = React.useContext(MediaCacheContext); + React.useEffect(() => { // skip for unencrypted videos if (!holder || !encryptionKey) { @@ -94,12 +97,27 @@ setVideoSource(undefined); const loadDecrypted = async () => { + const cached = await mediaCache?.get(holder); + if (cached && isMounted) { + setVideoSource({ uri: cached }); + return; + } + const { result } = await decryptMedia(holder, encryptionKey, { destination: 'file', }); - if (result.success && isMounted) { - uriToDispose = result.uri; - setVideoSource({ uri: result.uri }); + if (result.success) { + const { uri } = result; + const cacheSetPromise = mediaCache?.set(holder, uri); + if (isMounted) { + uriToDispose = uri; + setVideoSource({ uri }); + } else { + // dispose of the temporary file immediately when unmounted + // but wait for the cache to be set + await cacheSetPromise; + filesystem.unlink(uri); + } } }; loadDecrypted(); @@ -111,7 +129,7 @@ filesystem.unlink(uriToDispose); } }; - }, [holder, encryptionKey]); + }, [holder, encryptionKey, mediaCache]); const closeButtonX = useValue(-1); const closeButtonY = useValue(-1); diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -19,6 +19,7 @@ import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; +import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; @@ -30,6 +31,7 @@ import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; +import { filesystemMediaCache } from './media/media-cache.js'; import { defaultNavigationState } from './navigation/default-state.js'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; @@ -253,23 +255,25 @@ - - - - - - {gated} - - - - - {navigation} - - + + + + + + + {gated} + + + + + {navigation} + + +