diff --git a/webapp/channels/src/components/__snapshots__/pdf_preview.test.jsx.snap b/webapp/channels/src/components/__snapshots__/pdf_preview.test.tsx.snap similarity index 58% rename from webapp/channels/src/components/__snapshots__/pdf_preview.test.jsx.snap rename to webapp/channels/src/components/__snapshots__/pdf_preview.test.tsx.snap index 88f60c7450..e0111b47bb 100644 --- a/webapp/channels/src/components/__snapshots__/pdf_preview.test.jsx.snap +++ b/webapp/channels/src/components/__snapshots__/pdf_preview.test.tsx.snap @@ -12,7 +12,20 @@ exports[`component/PDFPreview should match snapshot, not successful 1`] = ` {}} /> diff --git a/webapp/channels/src/components/file_preview_modal/file_preview_modal.tsx b/webapp/channels/src/components/file_preview_modal/file_preview_modal.tsx index b050080b7d..96f7a4ee7f 100644 --- a/webapp/channels/src/components/file_preview_modal/file_preview_modal.tsx +++ b/webapp/channels/src/components/file_preview_modal/file_preview_modal.tsx @@ -15,6 +15,7 @@ import AudioVideoPreview from 'components/audio_video_preview'; import CodePreview from 'components/code_preview'; import FileInfoPreview from 'components/file_info_preview'; import LoadingImagePreview from 'components/loading_image_preview'; +import type {Props as PDFPreviewComponentProps} from 'components/pdf_preview'; import Constants, {FileTypes, ZoomSettings} from 'utils/constants'; import * as Keyboard from 'utils/keyboard'; @@ -31,7 +32,7 @@ import type {LinkInfo} from './types'; import './file_preview_modal.scss'; -const PDFPreview = React.lazy(() => import('components/pdf_preview')); +const PDFPreview = React.lazy>(() => import('components/pdf_preview')); const KeyCodes = Constants.KeyCodes; @@ -326,7 +327,7 @@ export default class FilePreviewModal extends React.PureComponent > ({ getDocument: () => Promise.resolve({ numPages: 3, - getPage: (i) => Promise.resolve({ + getPage: (i: number) => Promise.resolve({ pageIndex: i, - getContext: (s) => Promise.resolve({s}), + getContext: (s: string) => Promise.resolve({s}), }), }), })); describe('component/PDFPreview', () => { - const requiredProps = { - fileInfo: {extension: 'pdf'}, + const requiredProps: Props = { + fileInfo: TestHelper.getFileInfoMock({extension: 'pdf'}), fileUrl: 'https://pre-release.mattermost.com/api/v4/files/ips59w4w9jnfbrs3o94m1dbdie', scale: 1, handleBgClose: jest.fn(), @@ -40,7 +44,7 @@ describe('component/PDFPreview', () => { }); test('should update state with new value from props when prop changes', () => { - const wrapper = shallow( + const wrapper = shallow( , ); const newFileUrl = 'https://some-new-url'; @@ -51,19 +55,22 @@ describe('component/PDFPreview', () => { }); test('should return correct state when onDocumentLoad is called', () => { - const wrapper = shallow( + const wrapper = shallow( , ); - let pdf = {numPages: 0}; + let pdf = {numPages: 0} as PDFDocumentProxy; wrapper.instance().onDocumentLoad(pdf); expect(wrapper.state('pdf')).toEqual(pdf); expect(wrapper.state('numPages')).toEqual(pdf.numPages); pdf = { numPages: 100, - getPage: (i) => Promise.resolve(i), - }; + getPage: async (i) => { + const page = {pageNumber: i} as PDFPageProxy; + return Promise.resolve(page); + }, + } as PDFDocumentProxy; wrapper.instance().onDocumentLoad(pdf); expect(wrapper.state('pdf')).toEqual(pdf); expect(wrapper.state('numPages')).toEqual(pdf.numPages); diff --git a/webapp/channels/src/components/pdf_preview.jsx b/webapp/channels/src/components/pdf_preview.tsx similarity index 75% rename from webapp/channels/src/components/pdf_preview.jsx rename to webapp/channels/src/components/pdf_preview.tsx index bbaafc7884..e6fe18530c 100644 --- a/webapp/channels/src/components/pdf_preview.jsx +++ b/webapp/channels/src/components/pdf_preview.tsx @@ -2,9 +2,12 @@ // See LICENSE.txt for license information. import debounce from 'lodash/debounce'; -import PropTypes from 'prop-types'; +import type {PDFDocumentProxy, PDFPageProxy} from 'pdfjs-dist'; +import type {RenderParameters} from 'pdfjs-dist/types/src/display/api'; import React from 'react'; +import type {FileInfo} from '@mattermost/types/files'; + import {getFileDownloadUrl} from 'mattermost-redux/utils/file_utils'; import FileInfoPreview from 'components/file_info_preview'; @@ -14,23 +17,38 @@ import {getSiteURL} from 'utils/url'; const INITIAL_RENDERED_PAGES = 3; -export default class PDFPreview extends React.PureComponent { - static propTypes = { +export type Props = { - /** + /** * Compare file types - */ - fileInfo: PropTypes.object.isRequired, + */ + fileInfo: FileInfo; - /** + /** * URL of pdf file to output and compare to update props url - */ - fileUrl: PropTypes.string.isRequired, - scale: PropTypes.number.isRequired, - handleBgClose: PropTypes.func.isRequired, - }; + */ + fileUrl: string; + scale: number; + handleBgClose: (e: React.MouseEvent) => void; +} - constructor(props) { +type State = { + pdf: PDFDocumentProxy | null; + pdfPages: Record; + pdfPagesLoaded: Record; + numPages: number; + loading: boolean; + success: boolean; + prevFileUrl: string; +} + +export default class PDFPreview extends React.PureComponent { + public pdfPagesRendered: Record; + public container: React.RefObject; + public parentNode: HTMLElement|null = null; + public pdfCanvasRef: {[key: string]: React.RefObject} = {}; + + constructor(props: Props) { super(props); this.pdfPagesRendered = {}; @@ -38,11 +56,12 @@ export default class PDFPreview extends React.PureComponent { this.state = { pdf: null, - pdfPages: {}, + pdfPages: [], pdfPagesLoaded: {}, numPages: 0, loading: true, success: false, + prevFileUrl: '', }; } @@ -50,7 +69,7 @@ export default class PDFPreview extends React.PureComponent { this.getPdfDocument(); if (this.container.current) { this.parentNode = this.container.current.parentElement; - this.parentNode.addEventListener('scroll', this.handleScroll); + this.parentNode?.addEventListener('scroll', this.handleScroll); } } @@ -60,7 +79,7 @@ export default class PDFPreview extends React.PureComponent { } } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: Props, state: State) { if (props.fileUrl !== state.prevFileUrl) { return { pdf: null, @@ -75,7 +94,7 @@ export default class PDFPreview extends React.PureComponent { return null; } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: Props, prevState: State) { if (this.props.fileUrl !== prevProps.fileUrl) { this.getPdfDocument(); this.pdfPagesRendered = {}; @@ -96,16 +115,16 @@ export default class PDFPreview extends React.PureComponent { } } - downloadFile = (e) => { + downloadFile = (e: React.FormEvent) => { const fileDownloadUrl = this.props.fileInfo.link || getFileDownloadUrl(this.props.fileInfo.id); e.preventDefault(); window.location.href = fileDownloadUrl; }; - isInViewport = (page) => { + isInViewport = (page: Element) => { const bounding = page.getBoundingClientRect(); - const viewportTop = this.container.current.scrollTop; - const viewportBottom = viewportTop + this.container.current.parentElement.clientHeight; + const viewportTop = this.container.current?.scrollTop ?? 0; + const viewportBottom = viewportTop + (this.parentNode?.clientHeight ?? 0); return ( (bounding.top >= viewportTop && bounding.top <= viewportBottom) || (bounding.bottom >= viewportTop && bounding.bottom <= viewportBottom) || @@ -113,8 +132,8 @@ export default class PDFPreview extends React.PureComponent { ); }; - renderPDFPage = async (pageIndex) => { - const canvas = this[`pdfCanvasRef-${pageIndex}`].current; + renderPDFPage = async (pageIndex: number) => { + const canvas = this.pdfCanvasRef[`pdfCanvasRef-${pageIndex}`].current; if (!canvas) { // Refs are undefined when testing return; @@ -130,16 +149,16 @@ export default class PDFPreview extends React.PureComponent { return; } - const page = await this.loadPage(this.state.pdf, pageIndex); + const page = await this.loadPage(this.state.pdf!, pageIndex); const context = canvas.getContext('2d'); const viewport = page.getViewport({scale: this.props.scale}); canvas.height = viewport.height; canvas.width = viewport.width; const renderContext = { - canvasContext: context, + canvasContext: context as object, viewport, - }; + } as RenderParameters; await page.render(renderContext).promise; this.pdfPagesRendered[pageIndex] = true; @@ -162,20 +181,20 @@ export default class PDFPreview extends React.PureComponent { } }; - onDocumentLoad = (pdf) => { + onDocumentLoad = (pdf: PDFDocumentProxy) => { this.setState({pdf, numPages: pdf.numPages}); for (let i = 0; i < pdf.numPages; i++) { - this[`pdfCanvasRef-${i}`] = React.createRef(); + this.pdfCanvasRef[`pdfCanvasRef-${i}`] = React.createRef(); } this.setState({loading: false, success: true}); }; - onDocumentLoadError = (reason) => { + onDocumentLoadError = (reason: string) => { console.log('Unable to load PDF preview: ' + reason); //eslint-disable-line no-console this.setState({loading: false, success: false}); }; - loadPage = async (pdf, pageIndex) => { + loadPage = async (pdf: PDFDocumentProxy, pageIndex: number) => { if (this.state.pdfPagesLoaded[pageIndex]) { return this.state.pdfPages[pageIndex]; } @@ -225,7 +244,7 @@ export default class PDFPreview extends React.PureComponent { for (let i = 0; i < this.state.numPages; i++) { pdfCanvases.push( , ); diff --git a/webapp/channels/src/types/external/pdf-worker-entry.d.ts b/webapp/channels/src/types/external/pdf-worker-entry.d.ts new file mode 100644 index 0000000000..0fbbb42e7d --- /dev/null +++ b/webapp/channels/src/types/external/pdf-worker-entry.d.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +declare module 'pdfjs-dist/build/pdf.worker.entry.js';