diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 78f0e61e72a9..d4a0b8a21d66 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -272,6 +272,9 @@ const ONYXKEYS = { /** Indicates whether we should store logs or not */ SHOULD_STORE_LOGS: 'shouldStoreLogs', + // Paths of PDF file that has been cached during one session + CACHED_PDF_PATHS: 'cachedPDFPaths', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -564,6 +567,7 @@ type OnyxValuesMapping = { [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; + [ONYXKEYS.CACHED_PDF_PATHS]: Record; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index edc8ab11fd27..b2c9fed64467 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -105,6 +105,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} transactionID={item.transactionID} + reportActionID={item.reportActionID} isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index c871628f65e7..56425f64a51c 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -17,6 +17,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -57,6 +58,9 @@ const propTypes = { // eslint-disable-next-line react/no-unused-prop-types transactionID: PropTypes.string, + /** The id of the report action related to the attachment */ + reportActionID: PropTypes.string, + isHovered: PropTypes.bool, optionalVideoDuration: PropTypes.number, @@ -71,6 +75,7 @@ const defaultProps = { isWorkspaceAvatar: false, maybeIcon: false, transactionID: '', + reportActionID: '', isHovered: false, optionalVideoDuration: 0, }; @@ -92,6 +97,7 @@ function AttachmentView({ maybeIcon, fallbackSource, transaction, + reportActionID, isHovered, optionalVideoDuration, }) { @@ -153,6 +159,15 @@ function AttachmentView({ if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; + const onPDFLoadComplete = (path) => { + if (path && (transaction.transactionID || reportActionID)) { + CachedPDFPaths.add(transaction.transactionID || reportActionID, path); + } + if (!loadComplete) { + setLoadComplete(true); + } + }; + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -166,7 +181,7 @@ function AttachmentView({ encryptedSourceUrl={encryptedSourceUrl} onPress={onPress} onToggleKeyboard={onToggleKeyboard} - onLoadComplete={() => !loadComplete && setLoadComplete(true)} + onLoadComplete={onPDFLoadComplete} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 7339718e7073..558f6636a325 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -104,12 +104,14 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused /** * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. + * @param {Number} numberOfPages + * @param {Number} path - Path to cache location */ - const finishPDFLoad = () => { + const finishPDFLoad = (numberOfPages, path) => { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); setSuccessToLoadPDF(true); - onLoadComplete(); + onLoadComplete(path); }; function renderPDFView() { @@ -137,7 +139,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused fitPolicy={0} trustAllCerts={false} renderActivityIndicator={() => } - source={{uri: sourceURL}} + source={{uri: sourceURL, cache: true, expiration: 864000}} style={pdfStyles} onError={handleFailureToLoadPDF} password={password} diff --git a/src/libs/actions/CachedPDFPaths/index.native.ts b/src/libs/actions/CachedPDFPaths/index.native.ts new file mode 100644 index 000000000000..09203995e9a1 --- /dev/null +++ b/src/libs/actions/CachedPDFPaths/index.native.ts @@ -0,0 +1,47 @@ +import {exists, unlink} from 'react-native-fs'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Add, Clear, ClearAll, ClearByKey} from './types'; + +/* + * We need to save the paths of PDF files so we can delete them later. + * This is to remove the cached PDFs when an attachment is deleted or the user logs out. + */ +let pdfPaths: Record = {}; +Onyx.connect({ + key: ONYXKEYS.CACHED_PDF_PATHS, + callback: (val) => { + pdfPaths = val ?? {}; + }, +}); + +const add: Add = (id: string, path: string) => { + if (pdfPaths[id]) { + return Promise.resolve(); + } + return Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[id]: path}); +}; + +const clear: Clear = (path: string) => { + if (!path) { + return Promise.resolve(); + } + return new Promise((resolve) => { + exists(path).then((exist) => { + if (!exist) { + resolve(); + } + return unlink(path); + }); + }); +}; + +const clearByKey: ClearByKey = (id: string) => { + clear(pdfPaths[id] ?? '').then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[id]: null})); +}; + +const clearAll: ClearAll = () => { + Promise.all(Object.values(pdfPaths).map(clear)).then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {})); +}; + +export {add, clearByKey, clearAll}; diff --git a/src/libs/actions/CachedPDFPaths/index.ts b/src/libs/actions/CachedPDFPaths/index.ts new file mode 100644 index 000000000000..3cac21bf3c25 --- /dev/null +++ b/src/libs/actions/CachedPDFPaths/index.ts @@ -0,0 +1,9 @@ +import type {Add, ClearAll, ClearByKey} from './types'; + +const add: Add = () => Promise.resolve(); + +const clearByKey: ClearByKey = () => {}; + +const clearAll: ClearAll = () => {}; + +export {add, clearByKey, clearAll}; diff --git a/src/libs/actions/CachedPDFPaths/types.ts b/src/libs/actions/CachedPDFPaths/types.ts new file mode 100644 index 000000000000..98b768c4645e --- /dev/null +++ b/src/libs/actions/CachedPDFPaths/types.ts @@ -0,0 +1,6 @@ +type Add = (id: string, path: string) => Promise; +type Clear = (path: string) => Promise; +type ClearAll = () => void; +type ClearByKey = (id: string) => void; + +export type {Add, Clear, ClearAll, ClearByKey}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 99e4e512bfb8..0486792df466 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -58,6 +58,7 @@ import type {OnyxData} from '@src/types/onyx/Request'; import type {Comment, Receipt, ReceiptSource, TaxRate, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import * as CachedPDFPaths from './CachedPDFPaths'; import * as Policy from './Policy'; import * as Report from './Report'; @@ -3162,6 +3163,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor // STEP 6: Make the API request API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); + CachedPDFPaths.clearByKey(transactionID); // STEP 7: Navigate the user depending on which page they are on and which resources were deleted if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 69f37972ea1a..502e2c69f71d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -77,6 +77,7 @@ import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/Rep import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; @@ -1224,6 +1225,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { reportActionID, }; + CachedPDFPaths.clearByKey(reportActionID); API.write(WRITE_COMMANDS.DELETE_COMMENT, parameters, {optimisticData, successData, failureData}); }