diff --git a/components/file_attachment.jsx b/components/file_attachment.jsx index 33bf60ffccd8..6aa9b181c1bf 100644 --- a/components/file_attachment.jsx +++ b/components/file_attachment.jsx @@ -5,11 +5,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils'; -import Constants, {FileTypes} from 'utils/constants.jsx'; -import * as FileUtils from 'utils/file_utils'; -import * as Utils from 'utils/utils.jsx'; +import {FileTypes} from 'utils/constants.jsx'; +import { + canDownloadFiles, + trimFilename +} from 'utils/file_utils'; +import { + fileSizeToString, + getFileType, + loadImage +} from 'utils/utils.jsx'; import FilenameOverlay from 'components/file_attachment/filename_overlay.jsx'; +import FileThumbnail from 'components/file_attachment/file_thumbnail.jsx'; import DownloadIcon from 'components/svg/download_icon'; export default class FileAttachment extends React.PureComponent { @@ -40,7 +48,7 @@ export default class FileAttachment extends React.PureComponent { super(props); this.state = { - loaded: Utils.getFileType(props.fileInfo.extension) !== FileTypes.IMAGE + loaded: getFileType(props.fileInfo.extension) !== FileTypes.IMAGE }; } @@ -53,7 +61,7 @@ export default class FileAttachment extends React.PureComponent { const extension = nextProps.fileInfo.extension; this.setState({ - loaded: Utils.getFileType(extension) !== FileTypes.IMAGE && extension !== FileTypes.SVG + loaded: getFileType(extension) !== FileTypes.IMAGE && extension !== FileTypes.SVG }); } } @@ -66,14 +74,14 @@ export default class FileAttachment extends React.PureComponent { loadFiles = () => { const fileInfo = this.props.fileInfo; - const fileType = Utils.getFileType(fileInfo.extension); + const fileType = getFileType(fileInfo.extension); if (fileType === FileTypes.IMAGE) { const thumbnailUrl = getFileThumbnailUrl(fileInfo.id); - Utils.loadImage(thumbnailUrl, this.handleImageLoaded); + loadImage(thumbnailUrl, this.handleImageLoaded); } else if (fileInfo.extension === FileTypes.SVG) { - Utils.loadImage(getFileUrl(fileInfo.id), this.handleImageLoaded); + loadImage(getFileUrl(fileInfo.id), this.handleImageLoaded); } } @@ -89,67 +97,14 @@ export default class FileAttachment extends React.PureComponent { } render() { - const fileInfo = this.props.fileInfo; - const fileName = fileInfo.name; - const fileUrl = getFileUrl(fileInfo.id); - - let thumbnail; - if (this.state.loaded) { - const type = Utils.getFileType(fileInfo.extension); - - if (type === FileTypes.IMAGE) { - let className = 'post-image'; - - if (fileInfo.width < Constants.THUMBNAIL_WIDTH && fileInfo.height < Constants.THUMBNAIL_HEIGHT) { - className += ' small'; - } else { - className += ' normal'; - } - - let thumbnailUrl = getFileThumbnailUrl(fileInfo.id); - if (Utils.isGIFImage(fileInfo.extension) && !fileInfo.has_preview_image) { - thumbnailUrl = getFileUrl(fileInfo.id); - } - - thumbnail = ( -
- ); - } else if (fileInfo.extension === FileTypes.SVG) { - thumbnail = ( - - ); - } else { - thumbnail =
; - } - } else { - thumbnail =
; - } + const { + compactDisplay, + fileInfo, + index + } = this.props; - const canDownloadFiles = FileUtils.canDownloadFiles(); - - let downloadButton = null; - if (canDownloadFiles) { - downloadButton = ( - - - - ); - } + const trimmedFilename = trimFilename(fileInfo.name); + const canDownload = canDownloadFiles(); return (
@@ -158,7 +113,11 @@ export default class FileAttachment extends React.PureComponent { href='#' onClick={this.onAttachmentClick} > - {thumbnail} + {this.state.loaded ? ( + + ) : ( +
+ )}
- + + {trimmedFilename} + {fileInfo.extension.toUpperCase()} - {Utils.fileSizeToString(fileInfo.size)} + {fileSizeToString(fileInfo.size)}
- {downloadButton} + {canDownload && + + + + }
); diff --git a/components/file_attachment/file_thumbnail.jsx b/components/file_attachment/file_thumbnail.jsx new file mode 100644 index 000000000000..527c1648b652 --- /dev/null +++ b/components/file_attachment/file_thumbnail.jsx @@ -0,0 +1,67 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import PropTypes from 'prop-types'; +import React from 'react'; + +import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils'; + +import Constants, {FileTypes} from 'utils/constants.jsx'; +import { + getFileType, + getIconClassName, + isGIFImage +} from 'utils/utils.jsx'; + +export default class FileThumbnail extends React.PureComponent { + + static propTypes = { + + /* + * File detailed information + */ + fileInfo: PropTypes.object.isRequired + } + + render() { + const {fileInfo} = this.props; + const type = getFileType(fileInfo.extension); + + let thumbnail; + if (type === FileTypes.IMAGE) { + let className = 'post-image'; + + if (fileInfo.width < Constants.THUMBNAIL_WIDTH && fileInfo.height < Constants.THUMBNAIL_HEIGHT) { + className += ' small'; + } else { + className += ' normal'; + } + + let thumbnailUrl = getFileThumbnailUrl(fileInfo.id); + if (isGIFImage(fileInfo.extension) && !fileInfo.has_preview_image) { + thumbnailUrl = getFileUrl(fileInfo.id); + } + + return ( +
+ ); + } else if (fileInfo.extension === FileTypes.SVG) { + thumbnail = ( + + ); + } else { + thumbnail =
; + } + + return thumbnail; + } +} diff --git a/components/file_attachment/filename_overlay.jsx b/components/file_attachment/filename_overlay.jsx index 4c8166fb5e06..b0e9994f34f9 100644 --- a/components/file_attachment/filename_overlay.jsx +++ b/components/file_attachment/filename_overlay.jsx @@ -7,7 +7,8 @@ import {OverlayTrigger, Tooltip} from 'react-bootstrap'; import {getFileUrl} from 'mattermost-redux/utils/file_utils'; import AttachmentIcon from 'components/svg/attachment_icon'; -import * as Utils from 'utils/utils.jsx'; +import {trimFilename} from 'utils/file_utils'; +import {localizeMessage} from 'utils/utils.jsx'; export default class FilenameOverlay extends React.PureComponent { static propTypes = { @@ -35,7 +36,17 @@ export default class FilenameOverlay extends React.PureComponent { /* * If it should display link to download on file name */ - canDownload: PropTypes.bool + canDownload: PropTypes.bool, + + /** + * Optional children like download icon + */ + children: PropTypes.element, + + /** + * Optional class like for icon + */ + iconClass: PropTypes.string }; onAttachmentClick = (e) => { @@ -44,19 +55,20 @@ export default class FilenameOverlay extends React.PureComponent { } render() { - const fileInfo = this.props.fileInfo; + const { + canDownload, + children, + compactDisplay, + fileInfo, + iconClass + } = this.props; + const fileName = fileInfo.name; + const trimmedFilename = trimFilename(fileName); const fileUrl = getFileUrl(fileInfo.id); - let trimmedFilename; - if (fileName.length > 35) { - trimmedFilename = fileName.substring(0, Math.min(35, fileName.length)) + '...'; - } else { - trimmedFilename = fileName; - } - let filenameOverlay; - if (this.props.compactDisplay) { + if (compactDisplay) { filenameOverlay = ( ); - } else if (this.props.canDownload) { + } else if (canDownload) { filenameOverlay = ( - {Utils.localizeMessage('file_attachment.download', 'Download') + ' "' + fileName + '"'}} + - + {localizeMessage('file_attachment.download', 'Download')} + + } > - {trimmedFilename} - - + {children || trimmedFilename} + + ); } else { filenameOverlay = ( diff --git a/sass/components/_files.scss b/sass/components/_files.scss index f7d303bd2e2e..bd418fc6187b 100644 --- a/sass/components/_files.scss +++ b/sass/components/_files.scss @@ -357,7 +357,7 @@ height: 100%; justify-content: center; line-height: 0; - padding-right: 10px; + padding: 0 5px; position: absolute; right: 0; text-align: center; diff --git a/tests/components/__snapshots__/file_attachment.test.jsx.snap b/tests/components/__snapshots__/file_attachment.test.jsx.snap index e8411c64071e..44c66745292f 100644 --- a/tests/components/__snapshots__/file_attachment.test.jsx.snap +++ b/tests/components/__snapshots__/file_attachment.test.jsx.snap @@ -9,12 +9,15 @@ exports[`component/FileAttachment should match snapshot, after change from file href="#" onClick={[Function]} > -
@@ -29,21 +32,11 @@ exports[`component/FileAttachment should match snapshot, after change from file
- + + test.png + @@ -56,15 +49,23 @@ exports[`component/FileAttachment should match snapshot, after change from file
- - +
`; @@ -78,8 +79,15 @@ exports[`component/FileAttachment should match snapshot, file with long name 1`] href="#" onClick={[Function]} > -
- + + a-quite-long-filename-to-test-the-f... + @@ -117,15 +117,21 @@ exports[`component/FileAttachment should match snapshot, file with long name 1`]
- - +
`; @@ -139,8 +145,15 @@ exports[`component/FileAttachment should match snapshot, regular file 1`] = ` href="#" onClick={[Function]} > -
- + + test.pdf + @@ -178,15 +183,21 @@ exports[`component/FileAttachment should match snapshot, regular file 1`] = `
- - +
`; @@ -200,12 +211,15 @@ exports[`component/FileAttachment should match snapshot, regular image 1`] = ` href="#" onClick={[Function]} > -
@@ -220,21 +234,11 @@ exports[`component/FileAttachment should match snapshot, regular image 1`] = `
- + + test.png + @@ -247,15 +251,23 @@ exports[`component/FileAttachment should match snapshot, regular image 1`] = `
- - + `; @@ -269,12 +281,15 @@ exports[`component/FileAttachment should match snapshot, small image 1`] = ` href="#" onClick={[Function]} > -
@@ -289,21 +304,11 @@ exports[`component/FileAttachment should match snapshot, small image 1`] = `
- + + test.png + @@ -316,15 +321,23 @@ exports[`component/FileAttachment should match snapshot, small image 1`] = `
- - + `; @@ -338,9 +351,17 @@ exports[`component/FileAttachment should match snapshot, svg image 1`] = ` href="#" onClick={[Function]} > -
- + + test.svg + @@ -380,15 +391,23 @@ exports[`component/FileAttachment should match snapshot, svg image 1`] = `
- - + `; @@ -416,19 +435,11 @@ exports[`component/FileAttachment should match snapshot, when file is not loaded
- + + test.pdf + @@ -441,15 +452,21 @@ exports[`component/FileAttachment should match snapshot, when file is not loaded
- - + `; @@ -463,8 +480,15 @@ exports[`component/FileAttachment should match snapshot, with compact display 1` href="#" onClick={[Function]} > -
- + + test.pdf + @@ -503,15 +518,22 @@ exports[`component/FileAttachment should match snapshot, with compact display 1`
- - + `; @@ -525,8 +547,15 @@ exports[`component/FileAttachment should match snapshot, without compact display href="#" onClick={[Function]} > -
- + + test.pdf + diff --git a/tests/components/file_attachment/__snapshots__/file_thumbnail.test.jsx.snap b/tests/components/file_attachment/__snapshots__/file_thumbnail.test.jsx.snap new file mode 100644 index 000000000000..6b5eb87d7648 --- /dev/null +++ b/tests/components/file_attachment/__snapshots__/file_thumbnail.test.jsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/file_attachment/FileThumbnail should match snapshot, normal size image 1`] = ` +
+`; + +exports[`components/file_attachment/FileThumbnail should match snapshot, pdf 1`] = ` +
+`; + +exports[`components/file_attachment/FileThumbnail should match snapshot, small size image 1`] = ` +
+`; + +exports[`components/file_attachment/FileThumbnail should match snapshot, svg 1`] = ` + +`; diff --git a/tests/components/file_attachment/file_thumbnail.test.jsx b/tests/components/file_attachment/file_thumbnail.test.jsx new file mode 100644 index 000000000000..659bb5ff8355 --- /dev/null +++ b/tests/components/file_attachment/file_thumbnail.test.jsx @@ -0,0 +1,52 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import FileThumbnail from 'components/file_attachment/file_thumbnail.jsx'; + +describe('components/file_attachment/FileThumbnail', () => { + const fileInfo = { + id: 'thumbnail_id', + extension: 'jpg', + width: 100, + height: 80, + has_preview_image: true + }; + + test('should match snapshot, small size image', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, normal size image', () => { + const newFileInfo = {...fileInfo, height: 150, width: 150}; + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, svg', () => { + const newFileInfo = {...fileInfo, extension: 'svg'}; + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, pdf', () => { + const newFileInfo = {...fileInfo, extension: 'pdf'}; + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/tests/utils/file_utils.test.jsx b/tests/utils/file_utils.test.jsx new file mode 100644 index 000000000000..ff282ed39927 --- /dev/null +++ b/tests/utils/file_utils.test.jsx @@ -0,0 +1,22 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import {trimFilename} from 'utils/file_utils.jsx'; + +describe('FileUtils.trimFilename', function() { + it('trimFilename', function() { + assert.equal( + trimFilename('abcdefghijklmnopqrstuvwxyz'), + 'abcdefghijklmnopqrstuvwxyz', + 'should return same filename' + ); + + assert.equal( + trimFilename('abcdefghijklmnopqrstuvwxyz0123456789'), + 'abcdefghijklmnopqrstuvwxyz012345678...', + 'should return trimmed filename' + ); + }); +}); diff --git a/utils/constants.jsx b/utils/constants.jsx index f1437885c1fb..7546097f144b 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -540,6 +540,7 @@ export const Constants = { }, MAX_DISPLAY_FILES: 5, MAX_UPLOAD_FILES: 5, + MAX_FILENAME_LENGTH: 35, THUMBNAIL_WIDTH: 128, THUMBNAIL_HEIGHT: 100, PROFILE_WIDTH: 128, diff --git a/utils/file_utils.jsx b/utils/file_utils.jsx index 42feb11be260..ee0b196ef1ff 100644 --- a/utils/file_utils.jsx +++ b/utils/file_utils.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import Constants from 'utils/constants.jsx'; import * as UserAgent from 'utils/user_agent'; export function canUploadFiles() { @@ -22,3 +23,12 @@ export function canDownloadFiles() { return true; } + +export function trimFilename(filename) { + let trimmedFilename = filename; + if (filename.length > Constants.MAX_FILENAME_LENGTH) { + trimmedFilename = filename.substring(0, Math.min(Constants.MAX_FILENAME_LENGTH, filename.length)) + '...'; + } + + return trimmedFilename; +}