1
1
import _ from 'underscore' ;
2
2
import React , { Component } from 'react' ;
3
- import { View , Dimensions } from 'react-native' ;
3
+ import { View } from 'react-native' ;
4
4
import 'core-js/features/array/at' ;
5
5
import { Document , Page , pdfjs } from 'react-pdf/dist/esm/entry.webpack' ;
6
6
import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker' ;
7
+ import { VariableSizeList as List } from 'react-window' ;
7
8
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator' ;
8
9
import styles from '../../styles/styles' ;
9
10
import variables from '../../styles/variables' ;
@@ -15,13 +16,27 @@ import withLocalize from '../withLocalize';
15
16
import Text from '../Text' ;
16
17
import compose from '../../libs/compose' ;
17
18
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback' ;
19
+ import Log from '../../libs/Log' ;
20
+
21
+ /**
22
+ * Each page has a default border. The app should take this size into account
23
+ * when calculates the page width and height.
24
+ */
25
+ const PAGE_BORDER = 9 ;
26
+ /**
27
+ * Pages should be more narrow than the container on large screens. The app should take this size into account
28
+ * when calculates the page width.
29
+ */
30
+ const LARGE_SCREEN_SIDE_SPACING = 40 ;
18
31
19
32
class PDFView extends Component {
20
33
constructor ( props ) {
21
34
super ( props ) ;
22
35
this . state = {
23
36
numPages : null ,
24
- windowWidth : Dimensions . get ( 'window' ) . width ,
37
+ pageViewports : [ ] ,
38
+ containerWidth : props . windowWidth ,
39
+ containerHeight : props . windowHeight ,
25
40
shouldRequestPassword : false ,
26
41
isPasswordInvalid : false ,
27
42
isKeyboardOpen : false ,
@@ -30,6 +45,9 @@ class PDFView extends Component {
30
45
this . initiatePasswordChallenge = this . initiatePasswordChallenge . bind ( this ) ;
31
46
this . attemptPDFLoad = this . attemptPDFLoad . bind ( this ) ;
32
47
this . toggleKeyboardOnSmallScreens = this . toggleKeyboardOnSmallScreens . bind ( this ) ;
48
+ this . calculatePageHeight = this . calculatePageHeight . bind ( this ) ;
49
+ this . calculatePageWidth = this . calculatePageWidth . bind ( this ) ;
50
+ this . renderPage = this . renderPage . bind ( this ) ;
33
51
34
52
const workerBlob = new Blob ( [ pdfWorkerSource ] , { type : 'text/javascript' } ) ;
35
53
pdfjs . GlobalWorkerOptions . workerSrc = URL . createObjectURL ( workerBlob ) ;
@@ -50,21 +68,70 @@ class PDFView extends Component {
50
68
}
51
69
52
70
/**
53
- * Upon successful document load, set the number of pages on PDF,
71
+ * Upon successful document load, combine an array of page viewports,
72
+ * set the number of pages on PDF,
54
73
* hide/reset PDF password form, and notify parent component that
55
74
* user input is no longer required.
56
75
*
57
- * @param {* } {numPages } No of pages in the rendered PDF
76
+ * @param {Object } pdf - The PDF file instance
77
+ * @param {Number } pdf.numPages - Number of pages of the PDF file
78
+ * @param {Function } pdf.getPage - A method to get page by its number. It requires to have the context. It should be the pdf itself.
58
79
* @memberof PDFView
59
80
*/
60
- onDocumentLoadSuccess ( { numPages} ) {
61
- this . setState ( {
62
- numPages,
63
- shouldRequestPassword : false ,
64
- isPasswordInvalid : false ,
81
+ onDocumentLoadSuccess ( pdf ) {
82
+ const { numPages} = pdf ;
83
+
84
+ Promise . all (
85
+ _ . times ( numPages , ( index ) => {
86
+ const pageNumber = index + 1 ;
87
+
88
+ return pdf . getPage ( pageNumber ) . then ( ( page ) => page . getViewport ( { scale : 1 } ) ) ;
89
+ } ) ,
90
+ ) . then ( ( pageViewports ) => {
91
+ this . setState ( {
92
+ pageViewports,
93
+ numPages,
94
+ shouldRequestPassword : false ,
95
+ isPasswordInvalid : false ,
96
+ } ) ;
65
97
} ) ;
66
98
}
67
99
100
+ /**
101
+ * Calculates a proper page height. The method should be called only when there are page viewports.
102
+ * It is based on a ratio between the specific page viewport width and provided page width.
103
+ * Also, the app should take into account the page borders.
104
+ * @param {* } pageIndex
105
+ * @returns {Number }
106
+ */
107
+ calculatePageHeight ( pageIndex ) {
108
+ if ( this . state . pageViewports . length === 0 ) {
109
+ Log . warn ( 'Dev error: calculatePageHeight() in PDFView called too early' ) ;
110
+
111
+ return 0 ;
112
+ }
113
+
114
+ const pageViewport = this . state . pageViewports [ pageIndex ] ;
115
+ const pageWidth = this . calculatePageWidth ( ) ;
116
+ const scale = pageWidth / pageViewport . width ;
117
+ const actualHeight = pageViewport . height * scale + PAGE_BORDER * 2 ;
118
+
119
+ return actualHeight ;
120
+ }
121
+
122
+ /**
123
+ * Calculates a proper page width.
124
+ * It depends on a screen size. Also, the app should take into account the page borders.
125
+ * @returns {Number }
126
+ */
127
+ calculatePageWidth ( ) {
128
+ const pdfContainerWidth = this . state . containerWidth ;
129
+ const pageWidthOnLargeScreen = Math . min ( pdfContainerWidth - LARGE_SCREEN_SIDE_SPACING * 2 , variables . pdfPageMaxWidth ) ;
130
+ const pageWidth = this . props . isSmallScreenWidth ? this . state . containerWidth : pageWidthOnLargeScreen ;
131
+
132
+ return pageWidth + PAGE_BORDER * 2 ;
133
+ }
134
+
68
135
/**
69
136
* Initiate password challenge process. The react-pdf/Document
70
137
* component calls this handler to indicate that a PDF requires a
@@ -110,10 +177,29 @@ class PDFView extends Component {
110
177
this . props . onToggleKeyboard ( isKeyboardOpen ) ;
111
178
}
112
179
180
+ /**
181
+ * It is a currying method that returns a function that renders a specific page based on its index.
182
+ * The function includes a wrapper to apply virtualized styles.
183
+ * @param {Number } pageWidth
184
+ * @returns {JSX.Element }
185
+ */
186
+ renderPage ( pageWidth ) {
187
+ return ( { index, style} ) => (
188
+ < View style = { style } >
189
+ < Page
190
+ key = { `page_${ index } ` }
191
+ width = { pageWidth }
192
+ pageIndex = { index }
193
+ // This needs to be empty to avoid multiple loading texts which show per page and look ugly
194
+ // See https://github.com/Expensify/App/issues/14358 for more details
195
+ loading = ""
196
+ />
197
+ </ View >
198
+ ) ;
199
+ }
200
+
113
201
renderPDFView ( ) {
114
- const pdfContainerWidth = this . state . windowWidth - 100 ;
115
- const pageWidthOnLargeScreen = pdfContainerWidth <= variables . pdfPageMaxWidth ? pdfContainerWidth : variables . pdfPageMaxWidth ;
116
- const pageWidth = this . props . isSmallScreenWidth ? this . state . windowWidth : pageWidthOnLargeScreen ;
202
+ const pageWidth = this . calculatePageWidth ( ) ;
117
203
const outerContainerStyle = [ styles . w100 , styles . h100 , styles . justifyContentCenter , styles . alignItemsCenter ] ;
118
204
119
205
// If we're requesting a password then we need to hide - but still render -
@@ -127,7 +213,11 @@ class PDFView extends Component {
127
213
< View
128
214
focusable
129
215
style = { pdfContainerStyle }
130
- onLayout = { ( event ) => this . setState ( { windowWidth : event . nativeEvent . layout . width } ) }
216
+ onLayout = { ( {
217
+ nativeEvent : {
218
+ layout : { width, height} ,
219
+ } ,
220
+ } ) => this . setState ( { containerWidth : width , containerHeight : height } ) }
131
221
>
132
222
< Document
133
223
error = { < Text style = { [ styles . textLabel , styles . textLarge ] } > { this . props . translate ( 'attachmentView.failedToLoadPDF' ) } </ Text > }
@@ -141,16 +231,18 @@ class PDFView extends Component {
141
231
onLoadSuccess = { this . onDocumentLoadSuccess }
142
232
onPassword = { this . initiatePasswordChallenge }
143
233
>
144
- { _ . map ( _ . range ( this . state . numPages ) , ( v , index ) => (
145
- < Page
146
- width = { pageWidth }
147
- key = { `page_${ index + 1 } ` }
148
- pageNumber = { index + 1 }
149
- // This needs to be empty to avoid multiple loading texts which show per page and look ugly
150
- // See https://github.com/Expensify/App/issues/14358 for more details
151
- loading = ""
152
- />
153
- ) ) }
234
+ { this . state . pageViewports . length > 0 && (
235
+ < List
236
+ style = { styles . PDFViewList }
237
+ width = { this . props . isSmallScreenWidth ? pageWidth : this . state . containerWidth }
238
+ height = { this . state . containerHeight }
239
+ estimatedItemSize = { this . calculatePageHeight ( 0 ) }
240
+ itemCount = { this . state . numPages }
241
+ itemSize = { this . calculatePageHeight }
242
+ >
243
+ { this . renderPage ( pageWidth ) }
244
+ </ List >
245
+ ) }
154
246
</ Document >
155
247
</ View >
156
248
{ this . state . shouldRequestPassword && (
0 commit comments