diff --git a/package-lock.json b/package-lock.json
index 7a7368316d53..5e8db3002290 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -73,6 +73,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
+ "react-fast-pdf": "^1.0.6",
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^2.0.1",
@@ -25183,7 +25184,6 @@
},
"node_modules/dequal": {
"version": "2.0.3",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -38922,6 +38922,74 @@
"react": ">=16.13.1"
}
},
+ "node_modules/react-fast-pdf": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz",
+ "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==",
+ "dependencies": {
+ "react-pdf": "^7.7.0",
+ "react-window": "^1.8.10"
+ },
+ "engines": {
+ "node": "20.10.0",
+ "npm": "10.2.3"
+ },
+ "peerDependencies": {
+ "lodash": "4.x",
+ "prop-types": "15.x",
+ "react": "18.x",
+ "react-dom": "18.x"
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/pdfjs-dist": {
+ "version": "3.11.174",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
+ "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "canvas": "^2.11.2",
+ "path2d-polyfill": "^2.0.1"
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/react-pdf": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz",
+ "integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "dequal": "^2.0.3",
+ "make-cancellable-promise": "^1.3.1",
+ "make-event-props": "^1.6.0",
+ "merge-refs": "^1.2.1",
+ "pdfjs-dist": "3.11.174",
+ "prop-types": "^15.6.2",
+ "tiny-invariant": "^1.0.0",
+ "warning": "^4.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -40398,8 +40466,9 @@
}
},
"node_modules/react-window": {
- "version": "1.8.9",
- "license": "MIT",
+ "version": "1.8.10",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
+ "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
diff --git a/package.json b/package.json
index 9ceda36371c2..7d91f9995ee4 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
+ "react-fast-pdf": "^1.0.6",
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^2.0.1",
diff --git a/src/CONST.ts b/src/CONST.ts
index 12dbf87f79fb..3109b9ea90ca 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1851,13 +1851,6 @@ const CONST = {
MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000,
IOS_KEYBOARD_SPACE_OFFSET: -30,
- PDF_PASSWORD_FORM: {
- // Constants for password-related error responses received from react-pdf.
- REACT_PDF_PASSWORD_RESPONSES: {
- NEED_PASSWORD: 1,
- INCORRECT_PASSWORD: 2,
- },
- },
API_REQUEST_TYPE: {
READ: 'read',
WRITE: 'write',
diff --git a/src/components/PDFView/WebPDFDocument.js b/src/components/PDFView/WebPDFDocument.js
deleted file mode 100644
index dd9d1e066b19..000000000000
--- a/src/components/PDFView/WebPDFDocument.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import 'core-js/features/array/at';
-import PropTypes from 'prop-types';
-import React, {memo, useCallback} from 'react';
-import {Document} from 'react-pdf';
-import {VariableSizeList as List} from 'react-window';
-import _ from 'underscore';
-import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import Text from '@components/Text';
-import stylePropTypes from '@styles/stylePropTypes';
-import CONST from '@src/CONST';
-import PageRenderer from './WebPDFPageRenderer';
-
-const propTypes = {
- /** Index of the PDF page to be displayed passed by VariableSizeList */
- errorLabelStyles: stylePropTypes,
- /** Returns translated string for given locale and phrase */
- translate: PropTypes.func.isRequired,
- /** The source URL from which to load PDF file to be displayed */
- sourceURL: PropTypes.string.isRequired,
- /** Callback invoked when the PDF document is loaded successfully */
- onDocumentLoadSuccess: PropTypes.func.isRequired,
- /** Viewport info of all PDF pages */
- pageViewportsLength: PropTypes.number.isRequired,
- /** Sets attributes to list container */
- setListAttributes: PropTypes.func.isRequired,
- /** Indicates, whether the screen is of small width */
- isSmallScreenWidth: PropTypes.bool.isRequired,
- /** Height of PDF document container view */
- containerHeight: PropTypes.number.isRequired,
- /** Width of PDF document container view */
- containerWidth: PropTypes.number.isRequired,
- /** The number of pages of the PDF file to be rendered */
- numPages: PropTypes.number,
- /** Function that calculates the height of a page of the PDF document */
- calculatePageHeight: PropTypes.func.isRequired,
- /** Function that calculates the devicePixelRatio the page should be rendered with */
- getDevicePixelRatio: PropTypes.func.isRequired,
- /** The estimated height of a single PDF page for virtualized rendering purposes */
- estimatedItemSize: PropTypes.number.isRequired,
- /** The width of a page in the PDF file */
- pageWidth: PropTypes.number.isRequired,
- /** The style applied to the list component */
- listStyle: stylePropTypes,
- /** Function that should initiate that the user should be prompted for password to the PDF file */
- initiatePasswordChallenge: PropTypes.func.isRequired,
- /** Either:
- * - `string` - the password provided by the user to unlock the PDF file
- * - `undefined` if password isn't needed to view the PDF file
- * - `null` if the password is required but hasn't been provided yet */
- password: PropTypes.string,
-};
-
-const defaultProps = {
- errorLabelStyles: [],
- numPages: null,
- listStyle: undefined,
- password: undefined,
-};
-
-const WebPDFDocument = memo(
- ({
- errorLabelStyles,
- translate,
- sourceURL,
- onDocumentLoadSuccess,
- pageViewportsLength,
- setListAttributes,
- isSmallScreenWidth,
- containerHeight,
- containerWidth,
- numPages,
- calculatePageHeight,
- getDevicePixelRatio,
- estimatedItemSize,
- pageWidth,
- listStyle,
- initiatePasswordChallenge,
- password,
- }) => {
- const onPassword = useCallback(
- (callback, reason) => {
- if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) {
- if (password) {
- callback(password);
- } else {
- initiatePasswordChallenge(reason);
- }
- } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) {
- initiatePasswordChallenge(reason);
- }
- },
- [password, initiatePasswordChallenge],
- );
-
- return (
- }
- error={{translate('attachmentView.failedToLoadPDF')}}
- file={sourceURL}
- options={{
- cMapUrl: 'cmaps/',
- cMapPacked: true,
- }}
- externalLinkTarget="_blank"
- onLoadSuccess={onDocumentLoadSuccess}
- onPassword={onPassword}
- >
- {!!pageViewportsLength && (
-
- {PageRenderer}
-
- )}
-
- );
- },
- (prevProps, nextProps) => _.isEqual(prevProps, nextProps),
-);
-
-WebPDFDocument.displayName = 'WebPDFDocument';
-WebPDFDocument.propTypes = propTypes;
-WebPDFDocument.defaultProps = defaultProps;
-
-export default WebPDFDocument;
diff --git a/src/components/PDFView/WebPDFPageRenderer.js b/src/components/PDFView/WebPDFPageRenderer.js
deleted file mode 100644
index 15af0bb88e39..000000000000
--- a/src/components/PDFView/WebPDFPageRenderer.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {memo} from 'react';
-import {View} from 'react-native';
-import {Page} from 'react-pdf';
-import _ from 'underscore';
-import stylePropTypes from '@styles/stylePropTypes';
-import PDFViewConstants from './constants';
-
-const propTypes = {
- /** Index of the PDF page to be displayed passed by VariableSizeList */
- index: PropTypes.number.isRequired,
-
- /** Page extra data passed by VariableSizeList's data prop */
- data: PropTypes.shape({
- /** Width of a single page in the document */
- pageWidth: PropTypes.number.isRequired,
- /** Function that calculates the height of a page given its index */
- calculatePageHeight: PropTypes.func.isRequired,
- /** Function that calculates the pixel ratio for a page given its calculated width and height */
- getDevicePixelRatio: PropTypes.func.isRequired,
- /** The estimated height of a single page in the document */
- estimatedItemSize: PropTypes.number.isRequired,
- }).isRequired,
-
- /** Additional style props passed by VariableSizeList */
- style: stylePropTypes.isRequired,
-};
-
-const WebPDFPageRenderer = memo(
- ({index: pageIndex, data, style}) => {
- const {pageWidth, calculatePageHeight, getDevicePixelRatio, estimatedItemSize} = data;
-
- const pageHeight = calculatePageHeight(pageIndex);
- const devicePixelRatio = getDevicePixelRatio(pageWidth, pageHeight);
-
- return (
-
-
-
- );
- },
- (prevProps, nextProps) => _.isEqual(prevProps, nextProps),
-);
-
-WebPDFPageRenderer.displayName = 'WebPDFPageRenderer';
-WebPDFPageRenderer.propTypes = propTypes;
-
-export default WebPDFPageRenderer;
diff --git a/src/components/PDFView/constants.js b/src/components/PDFView/constants.js
deleted file mode 100644
index a45beddfbb68..000000000000
--- a/src/components/PDFView/constants.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Each page has a default border. The app should take this size into account
- * when calculates the page width and height.
- */
-const PAGE_BORDER = 9;
-
-/**
- * Pages should be more narrow than the container on large screens. The app should take this size into account
- * when calculates the page width.
- */
-const LARGE_SCREEN_SIDE_SPACING = 40;
-
-const REQUIRED_PASSWORD_MISSING = null;
-
-export default {PAGE_BORDER, LARGE_SCREEN_SIDE_SPACING, REQUIRED_PASSWORD_MISSING};
diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js
index 9706f8e06cc1..e69b52b74e95 100644
--- a/src/components/PDFView/index.js
+++ b/src/components/PDFView/index.js
@@ -1,53 +1,30 @@
import 'core-js/features/array/at';
-import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
import React, {Component} from 'react';
+import {PDFPreviewer} from 'react-fast-pdf';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import {pdfjs} from 'react-pdf';
import _ from 'underscore';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import Text from '@components/Text';
import withLocalize from '@components/withLocalize';
import withThemeStyles from '@components/withThemeStyles';
import withWindowDimensions from '@components/withWindowDimensions';
import compose from '@libs/compose';
-import Log from '@libs/Log';
import variables from '@styles/variables';
import * as CanvasSize from '@userActions/CanvasSize';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import PDFViewConstants from './constants';
import PDFPasswordForm from './PDFPasswordForm';
import * as pdfViewPropTypes from './pdfViewPropTypes';
-import PDFDocument from './WebPDFDocument';
class PDFView extends Component {
constructor(props) {
super(props);
this.state = {
- numPages: null,
- pageViewports: [],
- containerWidth: props.windowWidth,
- containerHeight: props.windowHeight,
- password: undefined,
- /** used to keep the PDFPasswordForm mounted (for it to maintain state) while password is being verified */
- isCheckingPassword: false,
- isPasswordInvalid: false,
isKeyboardOpen: false,
};
- this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
- this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this);
- this.attemptPDFLoad = this.attemptPDFLoad.bind(this);
this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this);
- this.calculatePageHeight = this.calculatePageHeight.bind(this);
- this.calculatePageWidth = this.calculatePageWidth.bind(this);
- this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio.bind(this));
- this.setListAttributes = this.setListAttributes.bind(this);
-
- const workerURL = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'}));
- if (pdfjs.GlobalWorkerOptions.workerSrc !== workerURL) {
- pdfjs.GlobalWorkerOptions.workerSrc = workerURL;
- }
-
this.retrieveCanvasLimits();
}
@@ -69,134 +46,6 @@ class PDFView extends Component {
}
}
- /**
- * Upon successful document load, combine an array of page viewports,
- * set the number of pages on PDF,
- * hide/reset PDF password form, and notify parent component that
- * user input is no longer required.
- *
- * @param {Object} pdf - The PDF file instance
- * @param {Number} pdf.numPages - Number of pages of the PDF file
- * @param {Function} pdf.getPage - A method to get page by its number. It requires to have the context. It should be the pdf itself.
- * @memberof PDFView
- */
- onDocumentLoadSuccess(pdf) {
- const {numPages} = pdf;
-
- Promise.all(
- _.times(numPages, (index) => {
- const pageNumber = index + 1;
-
- return pdf.getPage(pageNumber).then((page) => page.getViewport({scale: 1}));
- }),
- ).then((pageViewports) => {
- this.setState({
- pageViewports,
- numPages,
- isPasswordInvalid: false,
- isCheckingPassword: false,
- });
- });
- }
-
- /**
- * Sets attributes to list container.
- * It unblocks a default scroll by keyboard of browsers.
- * @param {Object|undefined} ref
- */
- setListAttributes(ref) {
- if (!ref) {
- return;
- }
-
- // Useful for elements that should not be navigated to directly using the "Tab" key,
- // but need to have keyboard focus set to them.
- // eslint-disable-next-line no-param-reassign
- ref.tabIndex = -1;
- }
-
- /**
- * Calculate the devicePixelRatio the page should be rendered with
- * Each platform has a different default devicePixelRatio and different canvas limits, we need to verify that
- * with the default devicePixelRatio it will be able to diplay the pdf correctly, if not we must change the devicePixelRatio.
- * @param {Number} width of the page
- * @param {Number} height of the page
- * @returns {Number} devicePixelRatio for this page on this platform
- */
- getDevicePixelRatio(width, height) {
- const nbPixels = width * height;
- const ratioHeight = this.props.maxCanvasHeight / height;
- const ratioWidth = this.props.maxCanvasWidth / width;
- const ratioArea = Math.sqrt(this.props.maxCanvasArea / nbPixels);
- const ratio = Math.min(ratioHeight, ratioArea, ratioWidth);
-
- return ratio > window.devicePixelRatio ? undefined : ratio;
- }
-
- /**
- * Calculates a proper page height. The method should be called only when there are page viewports.
- * It is based on a ratio between the specific page viewport width and provided page width.
- * Also, the app should take into account the page borders.
- * @param {Number} pageIndex
- * @returns {Number}
- */
- calculatePageHeight(pageIndex) {
- if (this.state.pageViewports.length === 0 || _.some(this.state.pageViewports, (viewport) => !viewport)) {
- Log.warn('Dev error: calculatePageHeight() in PDFView called too early');
-
- return 0;
- }
-
- const pageViewport = this.state.pageViewports[pageIndex];
- const pageWidth = this.calculatePageWidth();
- const scale = pageWidth / pageViewport.width;
- const actualHeight = pageViewport.height * scale + PDFViewConstants.PAGE_BORDER * 2;
-
- return actualHeight;
- }
-
- /**
- * Calculates a proper page width.
- * It depends on a screen size. Also, the app should take into account the page borders.
- * @returns {Number}
- */
- calculatePageWidth() {
- const pdfContainerWidth = this.state.containerWidth;
- const pageWidthOnLargeScreen = Math.min(pdfContainerWidth - PDFViewConstants.LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth);
- const pageWidth = this.props.isSmallScreenWidth ? this.state.containerWidth : pageWidthOnLargeScreen;
-
- return pageWidth + PDFViewConstants.PAGE_BORDER * 2;
- }
-
- /**
- * Initiate password challenge process. The WebPDFDocument
- * component calls this handler to indicate that a PDF requires a
- * password, or to indicate that a previously provided password was
- * invalid.
- *
- * The PasswordResponses constants used below were copied from react-pdf
- * because they're not exported in entry.webpack.
- *
- * @param {Number} reason Reason code for password request
- */
- initiatePasswordChallenge(reason) {
- if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) {
- this.setState({password: PDFViewConstants.REQUIRED_PASSWORD_MISSING, isCheckingPassword: false});
- } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) {
- this.setState({password: PDFViewConstants.REQUIRED_PASSWORD_MISSING, isPasswordInvalid: true, isCheckingPassword: false});
- }
- }
-
- /**
- * Send password to react-pdf via its callback so that it can attempt to load
- * the PDF.
- *
- * @param {String} password Password to send via callback to react-pdf
- */
- attemptPDFLoad(password) {
- this.setState({password, isCheckingPassword: true});
- }
-
/**
* On small screens notify parent that the keyboard has opened or closed.
*
@@ -229,59 +78,33 @@ class PDFView extends Component {
renderPDFView() {
const styles = this.props.themeStyles;
- const pageWidth = this.calculatePageWidth();
const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter];
- const pdfContainerStyle = [styles.PDFView, styles.noSelect, this.props.style];
- // If we're requesting a password then we need to hide - but still render -
- // the PDF component.
- if (this.state.password === PDFViewConstants.REQUIRED_PASSWORD_MISSING || this.state.isCheckingPassword) {
- pdfContainerStyle.push(styles.invisible);
- }
-
- const estimatedItemSize = this.calculatePageHeight(0);
-
return (
-
- this.setState({containerWidth: width, containerHeight: height})}
- >
-
-
- {(this.state.password === PDFViewConstants.REQUIRED_PASSWORD_MISSING || this.state.isCheckingPassword) && (
- this.setState({isPasswordInvalid: false})}
- isPasswordInvalid={this.state.isPasswordInvalid}
- onPasswordFieldFocused={this.toggleKeyboardOnSmallScreens}
- />
- )}
+
+ }
+ ErrorComponent={{this.props.translate('attachmentView.failedToLoadPDF')}}
+ renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => (
+
+ )}
+ />
);
}
@@ -302,6 +125,7 @@ class PDFView extends Component {
);
}
}
+
PDFView.propTypes = pdfViewPropTypes.propTypes;
PDFView.defaultProps = pdfViewPropTypes.defaultProps;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index a56a858d1707..31e3941cc4e5 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2373,29 +2373,12 @@ const styles = (theme: ThemeColors) =>
backgroundColor: theme.modalBackground,
},
- PDFView: {
- // `display: grid` is not supported in native platforms!
- // It's being used on Web/Desktop only to vertically center short PDFs,
- // while preventing the overflow of the top of long PDF files.
- ...display.dGrid,
- width: '100%',
- height: '100%',
- justifyContent: 'center',
- overflow: 'hidden',
- alignItems: 'center',
- },
-
- PDFViewList: {
- overflowX: 'hidden',
- // There properties disable "focus" effect on list
- boxShadow: 'none',
- outline: 'none',
- },
-
getPDFPasswordFormStyle: (isSmallScreenWidth: boolean) =>
({
width: isSmallScreenWidth ? '100%' : 350,
- ...(isSmallScreenWidth && flex.flex1),
+ flexBasis: isSmallScreenWidth ? '100%' : 350,
+ flexGrow: 0,
+ alignSelf: 'flex-start',
} satisfies ViewStyle),
centeredModalStyles: (isSmallScreenWidth: boolean, isFullScreenWhenSmall: boolean) =>