diff --git a/webapp/components/pdf_preview.jsx b/webapp/components/pdf_preview.jsx new file mode 100644 index 0000000000..fd1ca7758a --- /dev/null +++ b/webapp/components/pdf_preview.jsx @@ -0,0 +1,189 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import FileInfoPreview from './file_info_preview.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +import loadingGif from 'images/load.gif'; + +import React from 'react'; +import PDFJS from 'pdfjs-dist'; +import {FormattedMessage} from 'react-intl'; + +const MAX_PDF_PAGES = 5; + +export default class PDFPreview extends React.Component { + constructor(props) { + super(props); + + this.updateStateFromProps = this.updateStateFromProps.bind(this); + this.onDocumentLoad = this.onDocumentLoad.bind(this); + this.onPageLoad = this.onPageLoad.bind(this); + this.renderPDFPage = this.renderPDFPage.bind(this); + + this.pdfPagesRendered = {}; + + this.state = { + pdf: null, + pdfPages: {}, + pdfPagesLoaded: {}, + numPages: 0, + loading: true, + success: false + }; + } + + componentDidMount() { + this.updateStateFromProps(this.props); + } + + componentWillReceiveProps(nextProps) { + if (this.props.fileUrl !== nextProps.fileUrl) { + this.updateStateFromProps(nextProps); + this.pdfPagesRendered = {}; + } + } + + componentDidUpdate() { + if (this.state.success) { + for (let i = 0; i < this.state.numPages; i++) { + this.renderPDFPage(i); + } + } + } + + renderPDFPage(pageIndex) { + if (this.pdfPagesRendered[pageIndex] || !this.state.pdfPagesLoaded[pageIndex]) { + return; + } + + const canvas = this.refs['pdfCanvas' + pageIndex]; + const context = canvas.getContext('2d'); + const viewport = this.state.pdfPages[pageIndex].getViewport(1); + + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport + }; + + this.state.pdfPages[pageIndex].render(renderContext); + this.pdfPagesRendered[pageIndex] = true; + } + + updateStateFromProps(props) { + this.setState({ + pdf: null, + pdfPages: {}, + pdfPagesLoaded: {}, + numPages: 0, + loading: true, + success: false + }); + + PDFJS.getDocument(window.mm_config.SiteURL + props.fileUrl).then(this.onDocumentLoad); + } + + onDocumentLoad(pdf) { + const numPages = pdf.numPages <= MAX_PDF_PAGES ? pdf.numPages : MAX_PDF_PAGES; + this.setState({pdf, numPages}); + for (let i = 1; i <= pdf.numPages; i++) { + pdf.getPage(i).then(this.onPageLoad); + } + } + + onPageLoad(page) { + const pdfPages = Object.assign({}, this.state.pdfPages); + pdfPages[page.pageIndex] = page; + + const pdfPagesLoaded = Object.assign({}, this.state.pdfPagesLoaded); + pdfPagesLoaded[page.pageIndex] = true; + + this.setState({pdfPages, pdfPagesLoaded}); + + if (page.pageIndex === 0) { + this.setState({success: true, loading: false}); + } + } + + static support(filename) { + const fileInfo = Utils.splitFileLocation(filename); + const ext = fileInfo.ext; + if (!ext) { + return false; + } + + if (ext === 'pdf') { + return true; + } + + return false; + } + + render() { + if (this.state.loading) { + return ( +
+ +
+ ); + } + + if (!this.state.success) { + return ( + + ); + } + + const pdfCanvases = []; + for (let i = 0; i < this.state.numPages; i++) { + pdfCanvases.push( + + ); + + if (i < this.state.numPages - 1 && this.state.numPages > 1) { + pdfCanvases.push( +
+ ); + } + } + + if (this.state.pdf.numPages > MAX_PDF_PAGES) { + pdfCanvases.push( +
+ +
+ ); + } + + return ( +
+ {pdfCanvases} +
+ ); + } +} + +PDFPreview.propTypes = { + filename: React.PropTypes.string.isRequired, + fileUrl: React.PropTypes.string.isRequired, + fileInfo: React.PropTypes.object.isRequired, + formatMessage: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx index 7b827ac0e3..c9f558725a 100644 --- a/webapp/components/view_image.jsx +++ b/webapp/components/view_image.jsx @@ -1,23 +1,29 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import * as AsyncClient from 'utils/async_client.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; -import * as Utils from 'utils/utils.jsx'; import AudioVideoPreview from './audio_video_preview.jsx'; -import Constants from 'utils/constants.jsx'; import CodePreview from './code_preview.jsx'; +import PDFPreview from './pdf_preview.jsx'; import FileInfoPreview from './file_info_preview.jsx'; -import FileStore from 'stores/file_store.jsx'; import ViewImagePopoverBar from './view_image_popover_bar.jsx'; -import loadingGif from 'images/load.gif'; -import {intlShape, injectIntl, defineMessages} from 'react-intl'; +import * as GlobalActions from 'actions/global_actions.jsx'; -import {Modal} from 'react-bootstrap'; +import FileStore from 'stores/file_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import Constants from 'utils/constants.jsx'; const KeyCodes = Constants.KeyCodes; +import $ from 'jquery'; +import React from 'react'; +import {intlShape, injectIntl, defineMessages} from 'react-intl'; +import {Modal} from 'react-bootstrap'; + +import loadingGif from 'images/load.gif'; + const holders = defineMessages({ loading: { id: 'view_image.loading', @@ -25,8 +31,6 @@ const holders = defineMessages({ } }); -import React from 'react'; - class ViewImageModal extends React.Component { constructor(props) { super(props); @@ -241,6 +245,15 @@ class ViewImageModal extends React.Component { formatMessage={this.props.intl.formatMessage} /> ); + } else if (PDFPreview.support(filename)) { + content = ( + + ); } else if (CodePreview.support(filename)) { content = ( here to log in.", "password_form.enter": "Enter a new password for your {siteName} account.", diff --git a/webapp/package.json b/webapp/package.json index dce79447ae..2ad477f4db 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -19,6 +19,7 @@ "marked": "mattermost/marked#69736482dbad685c398a5eec33a59b5ab06057ac", "match-at": "0.1.0", "object-assign": "4.1.0", + "pdfjs-dist": "1.5.374", "perfect-scrollbar": "0.6.12", "react": "15.2.1", "react-addons-pure-render-mixin": "15.2.1", diff --git a/webapp/sass/components/_files.scss b/webapp/sass/components/_files.scss index 9a65693daf..2239bdd1bf 100644 --- a/webapp/sass/components/_files.scss +++ b/webapp/sass/components/_files.scss @@ -134,6 +134,30 @@ } } +.pdf-preview-spacer { + height: 5px; +} + +.pdf-max-pages { + bottom: 0; + background: $white; + left: 0; + position: relative; + width: 100%; + + span { + @include border-radius(3px); + background: alpha-color($black, .8); + bottom: 15px; + color: $white; + display: inline-block; + height: 3em; + line-height: 3em; + padding: 0 1.5em; + position: relative; + } +} + .post-image__column { border: 1px solid alpha-color($black, .2); float: left; diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index bda11dc2c9..5505aa353f 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -393,7 +393,7 @@ padding: 5px; } - .image-wrapper { + .modal-image__wrapper { > div { font-size: 12px; min-width: 250px; @@ -406,6 +406,7 @@ .modal-button-bar { @include opacity(1); + padding-right: 10px; } }