diff --git a/package-lock.json b/package-lock.json
index 7c3a82d9f09d..df746a65f3ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -102,6 +102,7 @@
"react-pdf": "^6.2.2",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
+ "react-window": "^1.8.9",
"save": "^2.4.0",
"semver": "^7.3.8",
"shim-keyboard-event-key": "^1.0.3",
@@ -43792,6 +43793,22 @@
"react-dom": ">=16.2.0"
}
},
+ "node_modules/react-window": {
+ "version": "1.8.9",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz",
+ "integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ },
+ "engines": {
+ "node": ">8.0.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/read-config-file": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
@@ -79708,6 +79725,15 @@
"integrity": "sha512-2W5WN8wmEv8ZlxvyAlOxVuw6new8Bi7+KSPqoq5oa7z1KSKZ72ucaKqCFRtHSuFjZ5sh5ioS9lp4BGwnaZ6lDg==",
"requires": {}
},
+ "react-window": {
+ "version": "1.8.9",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz",
+ "integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ }
+ },
"read-config-file": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
diff --git a/package.json b/package.json
index d361fc7bad65..56ac7926f0e5 100644
--- a/package.json
+++ b/package.json
@@ -141,6 +141,7 @@
"react-pdf": "^6.2.2",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
+ "react-window": "^1.8.9",
"save": "^2.4.0",
"semver": "^7.3.8",
"shim-keyboard-event-key": "^1.0.3",
diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js
index 94dff2b01796..75c99757e7e0 100644
--- a/src/components/PDFView/index.js
+++ b/src/components/PDFView/index.js
@@ -1,9 +1,10 @@
import _ from 'underscore';
import React, {Component} from 'react';
-import {View, Dimensions} from 'react-native';
+import {View} from 'react-native';
import 'core-js/features/array/at';
import {Document, Page, pdfjs} from 'react-pdf/dist/esm/entry.webpack';
import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
+import {VariableSizeList as List} from 'react-window';
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
import styles from '../../styles/styles';
import variables from '../../styles/variables';
@@ -15,13 +16,27 @@ import withLocalize from '../withLocalize';
import Text from '../Text';
import compose from '../../libs/compose';
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';
+import Log from '../../libs/Log';
+
+/**
+ * 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;
class PDFView extends Component {
constructor(props) {
super(props);
this.state = {
numPages: null,
- windowWidth: Dimensions.get('window').width,
+ pageViewports: [],
+ containerWidth: props.windowWidth,
+ containerHeight: props.windowHeight,
shouldRequestPassword: false,
isPasswordInvalid: false,
isKeyboardOpen: false,
@@ -30,6 +45,9 @@ class PDFView extends Component {
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.renderPage = this.renderPage.bind(this);
const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'});
pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob);
@@ -50,21 +68,70 @@ class PDFView extends Component {
}
/**
- * Upon successful document load, set the number of pages on PDF,
+ * 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 {*} {numPages} No of pages in the rendered PDF
+ * @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({numPages}) {
- this.setState({
- numPages,
- shouldRequestPassword: false,
- isPasswordInvalid: false,
+ 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,
+ shouldRequestPassword: false,
+ isPasswordInvalid: false,
+ });
});
}
+ /**
+ * 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 {*} pageIndex
+ * @returns {Number}
+ */
+ calculatePageHeight(pageIndex) {
+ if (this.state.pageViewports.length === 0) {
+ 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 + 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 - LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth);
+ const pageWidth = this.props.isSmallScreenWidth ? this.state.containerWidth : pageWidthOnLargeScreen;
+
+ return pageWidth + PAGE_BORDER * 2;
+ }
+
/**
* Initiate password challenge process. The react-pdf/Document
* component calls this handler to indicate that a PDF requires a
@@ -110,10 +177,29 @@ class PDFView extends Component {
this.props.onToggleKeyboard(isKeyboardOpen);
}
+ /**
+ * It is a currying method that returns a function that renders a specific page based on its index.
+ * The function includes a wrapper to apply virtualized styles.
+ * @param {Number} pageWidth
+ * @returns {JSX.Element}
+ */
+ renderPage(pageWidth) {
+ return ({index, style}) => (
+
+
+
+ );
+ }
+
renderPDFView() {
- const pdfContainerWidth = this.state.windowWidth - 100;
- const pageWidthOnLargeScreen = pdfContainerWidth <= variables.pdfPageMaxWidth ? pdfContainerWidth : variables.pdfPageMaxWidth;
- const pageWidth = this.props.isSmallScreenWidth ? this.state.windowWidth : pageWidthOnLargeScreen;
+ const pageWidth = this.calculatePageWidth();
const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter];
// If we're requesting a password then we need to hide - but still render -
@@ -127,7 +213,11 @@ class PDFView extends Component {
this.setState({windowWidth: event.nativeEvent.layout.width})}
+ onLayout={({
+ nativeEvent: {
+ layout: {width, height},
+ },
+ }) => this.setState({containerWidth: width, containerHeight: height})}
>
{this.props.translate('attachmentView.failedToLoadPDF')}}
@@ -141,16 +231,18 @@ class PDFView extends Component {
onLoadSuccess={this.onDocumentLoadSuccess}
onPassword={this.initiatePasswordChallenge}
>
- {_.map(_.range(this.state.numPages), (v, index) => (
-
- ))}
+ {this.state.pageViewports.length > 0 && (
+
+ {this.renderPage(pageWidth)}
+
+ )}
{this.state.shouldRequestPassword && (
diff --git a/src/styles/styles.js b/src/styles/styles.js
index ac12ff9819da..535d59bcd468 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2104,10 +2104,13 @@ const styles = {
height: '100%',
justifyContent: 'center',
overflow: 'hidden',
- overflowY: 'auto',
alignItems: 'center',
},
+ PDFViewList: {
+ overflowX: 'hidden',
+ },
+
pdfPasswordForm: {
wideScreenWidth: {
width: 350,