diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index e18c52b06972..a56f85f73450 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -9,10 +9,15 @@ 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 usePrevious from '@hooks/usePrevious'; +import useLocalize from '@components/useLocalize'; +import useKeyboardState from '@hooks/useKeyboardState'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import compose from '@libs/compose'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; 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'; @@ -32,46 +37,48 @@ const PAGE_BORDER = 9; */ const LARGE_SCREEN_SIDE_SPACING = 40; -class PDFView extends Component { - constructor(props) { - super(props); - this.state = { - numPages: null, - pageViewports: [], - containerWidth: props.windowWidth, - containerHeight: props.windowHeight, - shouldRequestPassword: 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.renderPage = this.renderPage.bind(this); - this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio); - this.setListAttributes = this.setListAttributes.bind(this); +function PDFView(props) { + const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const isKeyboardShown = useKeyboardState(); + const StyleUtils = useStyleUtils(); + const [numPages, setNumPages] = null; + const [pageViewports, setPageViewports] = []; + const [containerWidth, setContainerWidth] = windowWidth; + const [containerHeight, setContainerHeight] = windowHeight; + const [shouldRequestPassword, setShouldRequestPassword] = false; + const [isPasswordInvalid, setIsPasswordInvalid] = false; + const [isKeyboardOpen, setIsKeyboardOpen] = false; + let onPasswordCallback; - const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'}); - pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); - this.retrieveCanvasLimits(); + /** + * On small screens notify parent that the keyboard has opened or closed. + * + * @param {Boolean} keyboardState True if keyboard is open + */ + function toggleKeyboardOnSmallScreens(keyboardState) { + if (isSmallScreenWidth) { + return; + } + setIsKeyboardOpen(keyboardState); + onToggleKeyboard(keyboardState); } - componentDidUpdate(prevProps) { + useEffect(() => { + // Use window height changes to toggle the keyboard. To maintain keyboard state // on all platforms we also use focus/blur events. So we need to make sure here // that we avoid redundant keyboard toggling. // Minus 100px is needed to make sure that when the internet connection is // disabled in android chrome and a small 'No internet connection' text box appears, // we do not take it as a sign to open the keyboard - if (!this.state.isKeyboardOpen && this.props.windowHeight < prevProps.windowHeight - 100) { - this.toggleKeyboardOnSmallScreens(true); - } else if (this.state.isKeyboardOpen && this.props.windowHeight > prevProps.windowHeight) { - this.toggleKeyboardOnSmallScreens(false); + if (!isKeyboardOpen && windowHeight < usePrevious(windowHeight) - 100) { + toggleKeyboardOnSmallScreens(true); + } else if (isKeyboardOpen && windowHeight > usePrevious(windowHeight)) { + toggleKeyboardOnSmallScreens(false); } - } + }); /** * Upon successful document load, combine an array of page viewports, @@ -84,22 +91,20 @@ class PDFView extends Component { * @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; + function onDocumentLoadSuccess(pdf) { + const {numberOfPages} = pdf; Promise.all( - _.times(numPages, (index) => { + _.times(numberOfPages, (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, - }); + ).then((pgViewports) => { + setPageViewports(pgViewports); + setNumPages(numberOfPages); + setShouldRequestPassword(false); + setIsPasswordInvalid(false); }); } @@ -108,7 +113,7 @@ class PDFView extends Component { * It unblocks a default scroll by keyboard of browsers. * @param {Object|undefined} ref */ - setListAttributes(ref) { + function setListAttributes(ref) { if (!ref) { return; } @@ -127,15 +132,28 @@ class PDFView extends Component { * @param {Number} height of the page * @returns {Number} devicePixelRatio for this page on this platform */ - getDevicePixelRatio(width, height) { + function 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 ratioHeight = props.maxCanvasHeight / height; + const ratioWidth = props.maxCanvasWidth / width; + const ratioArea = Math.sqrt(props.maxCanvasArea / nbPixels); const ratio = Math.min(ratioHeight, ratioArea, ratioWidth); return ratio > window.devicePixelRatio ? undefined : ratio; } + /** + * Calculates a proper page width. + * It depends on a screen size. Also, the app should take into account the page borders. + * @returns {Number} + */ + function calculatePageWidth() { + const pdfContainerWidth = containerWidth; + const pageWidthOnLargeScreen = Math.min(pdfContainerWidth - LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth); + const pageWidth = isSmallScreenWidth ? containerWidth : pageWidthOnLargeScreen; + + return pageWidth + PAGE_BORDER * 2; + } + /** * 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. @@ -143,34 +161,21 @@ class PDFView extends Component { * @param {Number} pageIndex * @returns {Number} */ - calculatePageHeight(pageIndex) { - if (this.state.pageViewports.length === 0) { + function calculatePageHeight(pageIndex) { + if (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 pageViewport = pageViewports[pageIndex]; + const pageWidth = 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 @@ -183,13 +188,14 @@ class PDFView extends Component { * @param {Function} callback Callback used to send password to react-pdf * @param {Number} reason Reason code for password request */ - initiatePasswordChallenge(callback, reason) { - this.onPasswordCallback = callback; + function initiatePasswordChallenge(callback, reason) { + onPasswordCallback = callback; if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) { - this.setState({shouldRequestPassword: true}); + setShouldRequestPassword(true); } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) { - this.setState({shouldRequestPassword: true, isPasswordInvalid: true}); + setShouldRequestPassword(true); + setIsPasswordInvalid(true); } } @@ -199,40 +205,31 @@ class PDFView extends Component { * * @param {String} password Password to send via callback to react-pdf */ - attemptPDFLoad(password) { - this.onPasswordCallback(password); - } - - /** - * On small screens notify parent that the keyboard has opened or closed. - * - * @param {Boolean} isKeyboardOpen True if keyboard is open - */ - toggleKeyboardOnSmallScreens(isKeyboardOpen) { - if (!this.props.isSmallScreenWidth) { - return; - } - this.setState({isKeyboardOpen}); - this.props.onToggleKeyboard(isKeyboardOpen); + function attemptPDFLoad(password) { + onPasswordCallback(password); } /** * Verify that the canvas limits have been calculated already, if not calculate them and put them in Onyx */ - retrieveCanvasLimits() { - if (!this.props.maxCanvasArea) { + function retrieveCanvasLimits() { + if (!props.maxCanvasArea) { CanvasSize.retrieveMaxCanvasArea(); } - if (!this.props.maxCanvasHeight) { + if (!props.maxCanvasHeight) { CanvasSize.retrieveMaxCanvasHeight(); } - if (!this.props.maxCanvasWidth) { + if (!props.maxCanvasWidth) { CanvasSize.retrieveMaxCanvasWidth(); } } + const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'}); + pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); + retrieveCanvasLimits(); + /** * Render a specific page based on its index. * The method includes a wrapper to apply virtualized styles. @@ -241,10 +238,10 @@ class PDFView extends Component { * @param {Object} page.style virtualized styles * @returns {JSX.Element} */ - renderPage({index, style}) { - const pageWidth = this.calculatePageWidth(); - const pageHeight = this.calculatePageHeight(index); - const devicePixelRatio = this.getDevicePixelRatio(pageWidth, pageHeight); + function renderPage({index, style}) { + const pageWidth = calculatePageWidth(); + const pageHeight = calculatePageHeight(index); + const devicePixelRatio = getDevicePixelRatio(pageWidth, pageHeight); return ( @@ -262,15 +259,14 @@ class PDFView extends Component { } renderPDFView() { - const styles = this.props.themeStyles; 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 - // the PDF component. - const pdfContainerStyle = this.state.shouldRequestPassword - ? [styles.PDFView, styles.noSelect, this.props.style, styles.invisible] - : [styles.PDFView, styles.noSelect, this.props.style]; + const pdfContainerStyle = shouldRequestPassword + ? [styles.PDFView, styles.noSelect, props.style, styles.invisible] + : [styles.PDFView, styles.noSelect, props.style]; return ( @@ -281,42 +277,46 @@ class PDFView extends Component { nativeEvent: { layout: {width, height}, }, - }) => this.setState({containerWidth: width, containerHeight: height})} + }) => { + setContainerWidth(width); + setContainerHeight(height); + } + } > {this.props.translate('attachmentView.failedToLoadPDF')}} + error={{translate('attachmentView.failedToLoadPDF')}} loading={} - file={this.props.sourceURL} + file={props.sourceURL} options={{ cMapUrl: 'cmaps/', cMapPacked: true, }} externalLinkTarget="_blank" - onLoadSuccess={this.onDocumentLoadSuccess} - onPassword={this.initiatePasswordChallenge} + onLoadSuccess={() => onDocumentLoadSuccess} + onPassword={() => initiatePasswordChallenge} > - {this.state.pageViewports.length > 0 && ( + {pageViewports.length > 0 && ( setListAttributes} style={styles.PDFViewList} - width={this.props.isSmallScreenWidth ? pageWidth : this.state.containerWidth} - height={this.state.containerHeight} - estimatedItemSize={this.calculatePageHeight(0)} - itemCount={this.state.numPages} - itemSize={this.calculatePageHeight} + width={isSmallScreenWidth ? pageWidth : containerWidth} + height={containerHeight} + estimatedItemSize={calculatePageHeight(0)} + itemCount={() => numPages} + itemSize={() => calculatePageHeight} > - {this.renderPage} + {renderPage} )} - {this.state.shouldRequestPassword && ( + {shouldRequestPassword && ( this.setState({isPasswordInvalid: false})} - isPasswordInvalid={this.state.isPasswordInvalid} - onPasswordFieldFocused={this.toggleKeyboardOnSmallScreens} + isFocused={props.isFocused} + onSubmit={() => attemptPDFLoad} + onPasswordUpdated={() => setIsPasswordInvalid(false)} + isPasswordInvalid={isPasswordInvalid} + onPasswordFieldFocused={() => toggleKeyboardOnSmallScreens} /> )} @@ -324,18 +324,18 @@ class PDFView extends Component { } render() { - const styles = this.props.themeStyles; - return this.props.onPress ? ( + const styles = props.themeStyles; + return props.onPress ? ( - {this.renderPDFView()} + {renderPDFView()} ) : ( - this.renderPDFView() + renderPDFView() ); } } @@ -344,9 +344,6 @@ PDFView.propTypes = pdfViewPropTypes.propTypes; PDFView.defaultProps = pdfViewPropTypes.defaultProps; export default compose( - withLocalize, - withWindowDimensions, - withThemeStyles, withOnyx({ maxCanvasArea: { key: ONYXKEYS.MAX_CANVAS_AREA,