diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2016-09-30 11:06:30 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-30 11:06:30 -0400 |
commit | 8a0e649f989a824bb3bbfd1900a5b8e5383b47e1 (patch) | |
tree | 4b424929fe13ebec438d2f41a2729e37e5160720 /webapp | |
parent | a2deeed597dea15d9b7ca237be71988469f58cdd (diff) | |
download | chat-8a0e649f989a824bb3bbfd1900a5b8e5383b47e1.tar.gz chat-8a0e649f989a824bb3bbfd1900a5b8e5383b47e1.tar.bz2 chat-8a0e649f989a824bb3bbfd1900a5b8e5383b47e1.zip |
PLT-3105 Files table migration (#4068)
* Implemented initial changes for files table
* Removed *_benchmark_test.go files
* Re-implemented GetPublicFile and added support for old path
* Localization for files table
* Moved file system code into utils package
* Finished server-side changes and added initial upgrade script
* Added getPostFiles api
* Re-add Extension and HasPreviewImage fields to FileInfo
* Removed unused translation
* Fixed merge conflicts left over after permissions changes
* Forced FileInfo.extension to be lower case
* Changed FileUploadResponse to contain the FileInfos instead of FileIds
* Fixed permissions on getFile* calls
* Fixed notifications for file uploads
* Added initial version of client code for files changes
* Permanently added FileIds field to Post object and removed Post.HasFiles
* Updated PostStore.Update to be usable in more circumstances
* Re-added Filenames field and switched file migration to be entirely lazy-loaded
* Increased max listener count for FileStore
* Removed unused fileInfoCache
* Moved file system code back into api
* Removed duplicate test case
* Fixed unit test running on ports other than 8065
* Renamed HasPermissionToPostContext to HasPermissionToChannelByPostContext
* Refactored handleImages to make it more easily understandable
* Renamed getPostFiles to getFileInfosForPost
* Re-added pre-FileIds posts to analytics
* Changed files to be saved as their ids as opposed to id/filename.ext
* Renamed FileInfo.UserId to FileInfo.CreatorId
* Fixed detection of language in CodePreview
* Fixed switching between threads in the RHS not loading new files
* Add serverside protection against a rare bug where the client sends the same file twice for a single post
* Refactored the important parts of uploadFile api call into a function that can be called without a web context
Diffstat (limited to 'webapp')
30 files changed, 866 insertions, 643 deletions
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index bc7562d44..81c06fe93 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -294,11 +294,11 @@ export function showGetPostLinkModal(post) { }); } -export function showGetPublicLinkModal(filename) { +export function showGetPublicLinkModal(fileId) { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL, value: true, - filename + fileId }); } @@ -388,7 +388,6 @@ export function sendEphemeralPost(message, channelId) { type: Constants.POST_TYPE_EPHEMERAL, create_at: timestamp, update_at: timestamp, - filenames: [], props: {} }; diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 439d41f78..334f8374d 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -113,10 +113,14 @@ export default class Client { return `${this.url}${this.urlVersion}/users`; } - getFilesRoute() { + getTeamFilesRoute() { return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/files`; } + getFileRoute(fileId) { + return `${this.url}${this.urlVersion}/files/${fileId}`; + } + getOAuthRoute() { return `${this.url}${this.urlVersion}/oauth`; } @@ -1520,40 +1524,71 @@ export default class Client { end(this.handleResponse.bind(this, 'getFlaggedPosts', success, error)); } + getFileInfosForPost(channelId, postId, success, error) { + request. + get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getFileInfosForPost', success, error)); + } + // Routes for Files - getFileInfo(filename, success, error) { + uploadFile(file, filename, channelId, clientId, success, error) { + return request. + post(`${this.getTeamFilesRoute()}/upload`). + set(this.defaultHeaders). + attach('files', file, filename). + field('channel_id', channelId). + field('client_ids', clientId). + accept('application/json'). + end(this.handleResponse.bind(this, 'uploadFile', success, error)); + } + + getFile(fileId, success, error) { request. - get(`${this.getFilesRoute()}/get_info${filename}`). + get(`${this.getFileRoute(fileId)}/get`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getFileInfo', success, error)); + end(this.handleResponse.bind(this, 'getFile', success, error)); } - getPublicLink(filename, success, error) { - const data = { - filename - }; + getFileThumbnail(fileId, success, error) { + request. + get(`${this.getFileRoute(fileId)}/get_thumbnail`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getFileThumbnail', success, error)); + } + getFilePreview(fileId, success, error) { request. - post(`${this.getFilesRoute()}/get_public_link`). + get(`${this.getFileRoute(fileId)}/get`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - send(data). - end(this.handleResponse.bind(this, 'getPublicLink', success, error)); + end(this.handleResponse.bind(this, 'getFilePreview', success, error)); } - uploadFile(file, filename, channelId, clientId, success, error) { - return request. - post(`${this.getFilesRoute()}/upload`). + getFileInfo(fileId, success, error) { + request. + get(`${this.getFileRoute(fileId)}/get_info`). set(this.defaultHeaders). - attach('files', file, filename). - field('channel_id', channelId). - field('client_ids', clientId). + type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'uploadFile', success, error)); + end(this.handleResponse.bind(this, 'getFileInfo', success, error)); + } + + getPublicLink(fileId, success, error) { + request. + get(`${this.getFileRoute(fileId)}/get_public_link`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPublicLink', success, error)); } // Routes for OAuth diff --git a/webapp/components/audio_video_preview.jsx b/webapp/components/audio_video_preview.jsx index dd2e910b3..4956900a9 100644 --- a/webapp/components/audio_video_preview.jsx +++ b/webapp/components/audio_video_preview.jsx @@ -76,10 +76,8 @@ export default class AudioVideoPreview extends React.Component { if (!this.state.canPlay) { return ( <FileInfoPreview - filename={this.props.filename} - fileUrl={this.props.fileUrl} fileInfo={this.props.fileInfo} - formatMessage={this.props.formatMessage} + fileUrl={this.props.fileUrl} /> ); } @@ -94,7 +92,7 @@ export default class AudioVideoPreview extends React.Component { // add a key to the video to prevent React from using an old video source while a new one is loading return ( <video - key={this.props.filename} + key={this.props.fileInfo.id} ref='video' style={{maxHeight: this.props.maxHeight}} data-setup='{}' @@ -112,9 +110,7 @@ export default class AudioVideoPreview extends React.Component { } AudioVideoPreview.propTypes = { - filename: React.PropTypes.string.isRequired, - fileUrl: React.PropTypes.string.isRequired, fileInfo: React.PropTypes.object.isRequired, - maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired, - formatMessage: React.PropTypes.func.isRequired + fileUrl: React.PropTypes.string.isRequired, + maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired }; diff --git a/webapp/components/code_preview.jsx b/webapp/components/code_preview.jsx index 6625f45f4..852b26e25 100644 --- a/webapp/components/code_preview.jsx +++ b/webapp/components/code_preview.jsx @@ -38,7 +38,7 @@ export default class CodePreview extends React.Component { } updateStateFromProps(props) { - var usedLanguage = SyntaxHighlighting.getLanguageFromFilename(props.filename); + const usedLanguage = SyntaxHighlighting.getLanguageFromFileExtension(props.fileInfo.extension); if (!usedLanguage || props.fileInfo.size > Constants.CODE_PREVIEW_MAX_FILE_SIZE) { this.setState({code: '', lang: '', loading: false, success: false}); @@ -64,8 +64,8 @@ export default class CodePreview extends React.Component { this.setState({loading: false, success: false}); } - static support(filename) { - return Boolean(SyntaxHighlighting.getLanguageFromFilename(filename)); + static supports(fileInfo) { + return Boolean(SyntaxHighlighting.getLanguageFromFileExtension(fileInfo.extension)); } render() { @@ -83,10 +83,8 @@ export default class CodePreview extends React.Component { if (!this.state.success) { return ( <FileInfoPreview - filename={this.props.filename} - fileUrl={this.props.fileUrl} fileInfo={this.props.fileInfo} - formatMessage={this.props.formatMessage} + fileUrl={this.props.fileUrl} /> ); } @@ -106,12 +104,10 @@ export default class CodePreview extends React.Component { const highlighted = SyntaxHighlighting.highlight(this.state.lang, this.state.code); - const fileName = this.props.filename.substring(this.props.filename.lastIndexOf('/') + 1, this.props.filename.length); - return ( <div className='post-code'> <span className='post-code__language'> - {`${fileName} - ${language}`} + {`${this.props.fileInfo.name} - ${language}`} </span> <code className='hljs'> <table> @@ -129,8 +125,6 @@ export default class CodePreview extends React.Component { } CodePreview.propTypes = { - filename: React.PropTypes.string.isRequired, - fileUrl: React.PropTypes.string.isRequired, fileInfo: React.PropTypes.object.isRequired, - formatMessage: React.PropTypes.func.isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index 2f0698510..133c2e6d2 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -55,7 +55,7 @@ export default class CreateComment extends React.Component { this.state = { messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, - previews: draft.previews, + fileInfos: draft.fileInfos, submitting: false, ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), showPostDeletedModal: false @@ -99,10 +99,10 @@ export default class CreateComment extends React.Component { } const post = {}; - post.filenames = []; + post.file_ids = []; post.message = this.state.messageText; - if (post.message.trim().length === 0 && this.state.previews.length === 0) { + if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) { return; } @@ -126,7 +126,7 @@ export default class CreateComment extends React.Component { post.channel_id = this.props.channelId; post.root_id = this.props.rootId; post.parent_id = this.props.rootId; - post.filenames = this.state.previews; + post.file_ids = this.state.fileInfos.map((info) => info.id); const time = Utils.getTimestamp(); post.pending_post_id = `${userId}:${time}`; post.user_id = userId; @@ -163,7 +163,7 @@ export default class CreateComment extends React.Component { messageText: '', submitting: false, postError: null, - previews: [], + fileInfos: [], serverError: null }); } @@ -245,7 +245,7 @@ export default class CreateComment extends React.Component { this.focusTextbox(); } - handleFileUploadComplete(filenames, clientIds) { + handleFileUploadComplete(fileInfos, clientIds) { const draft = PostStore.getCommentDraft(this.props.rootId); // remove each finished file from uploads @@ -257,10 +257,10 @@ export default class CreateComment extends React.Component { } } - draft.previews = draft.previews.concat(filenames); + draft.fileInfos = draft.fileInfos.concat(fileInfos); PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos}); } handleUploadError(err, clientId) { @@ -281,11 +281,11 @@ export default class CreateComment extends React.Component { } removePreview(id) { - const previews = this.state.previews; + const fileInfos = this.state.fileInfos; const uploadsInProgress = this.state.uploadsInProgress; - // id can either be the path of an uploaded file or the client id of an in progress upload - let index = previews.indexOf(id); + // id can either be the id of an uploaded file or the client id of an in progress upload + let index = fileInfos.findIndex((info) => info.id === id); if (index === -1) { index = uploadsInProgress.indexOf(id); @@ -294,26 +294,26 @@ export default class CreateComment extends React.Component { this.refs.fileUpload.getWrappedInstance().cancelUpload(id); } } else { - previews.splice(index, 1); + fileInfos.splice(index, 1); } const draft = PostStore.getCommentDraft(this.props.rootId); - draft.previews = previews; + draft.fileInfos = fileInfos; draft.uploadsInProgress = uploadsInProgress; PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({previews, uploadsInProgress}); + this.setState({fileInfos, uploadsInProgress}); } componentWillReceiveProps(newProps) { if (newProps.rootId !== this.props.rootId) { const draft = PostStore.getCommentDraft(newProps.rootId); - this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos}); } } getFileCount() { - return this.state.previews.length + this.state.uploadsInProgress.length; + return this.state.fileInfos.length + this.state.uploadsInProgress.length; } focusTextbox() { @@ -350,10 +350,10 @@ export default class CreateComment extends React.Component { } let preview = null; - if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { + if (this.state.fileInfos.length > 0 || this.state.uploadsInProgress.length > 0) { preview = ( <FilePreview - files={this.state.previews} + fileInfos={this.state.fileInfos} onRemove={this.removePreview} uploadsInProgress={this.state.uploadsInProgress} /> diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index bfacd0644..d3417e419 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -67,7 +67,7 @@ export default class CreatePost extends React.Component { channelId: ChannelStore.getCurrentId(), messageText: draft.messageText, uploadsInProgress: draft.uploadsInProgress, - previews: draft.previews, + fileInfos: draft.fileInfos, submitting: false, initialText: draft.messageText, ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), @@ -79,14 +79,14 @@ export default class CreatePost extends React.Component { getCurrentDraft() { const draft = PostStore.getCurrentDraft(); - const safeDraft = {previews: [], messageText: '', uploadsInProgress: []}; + const safeDraft = {fileInfos: [], messageText: '', uploadsInProgress: []}; if (draft) { if (draft.message) { safeDraft.messageText = draft.message; } - if (draft.previews) { - safeDraft.previews = draft.previews; + if (draft.fileInfos) { + safeDraft.fileInfos = draft.fileInfos; } if (draft.uploadsInProgress) { safeDraft.uploadsInProgress = draft.uploadsInProgress; @@ -104,10 +104,10 @@ export default class CreatePost extends React.Component { } const post = {}; - post.filenames = []; + post.file_ids = []; post.message = this.state.messageText; - if (post.message.trim().length === 0 && this.state.previews.length === 0) { + if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) { return; } @@ -122,7 +122,7 @@ export default class CreatePost extends React.Component { if (post.message.indexOf('/') === 0) { PostStore.storeDraft(this.state.channelId, null); - this.setState({messageText: '', postError: null, previews: []}); + this.setState({messageText: '', postError: null, fileInfos: []}); ChannelActions.executeCommand( this.state.channelId, @@ -153,7 +153,7 @@ export default class CreatePost extends React.Component { sendMessage(post) { post.channel_id = this.state.channelId; - post.filenames = this.state.previews; + post.file_ids = this.state.fileInfos.map((info) => info.id); const time = Utils.getTimestamp(); const userId = UserStore.getCurrentId(); @@ -163,7 +163,7 @@ export default class CreatePost extends React.Component { post.parent_id = this.state.parentId; GlobalActions.emitUserPostedEvent(post); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + this.setState({messageText: '', submitting: false, postError: null, fileInfos: [], serverError: null}); Client.createPost(post, (data) => { @@ -236,7 +236,7 @@ export default class CreatePost extends React.Component { this.focusTextbox(); } - handleFileUploadComplete(filenames, clientIds, channelId) { + handleFileUploadComplete(fileInfos, clientIds, channelId) { const draft = PostStore.getDraft(channelId); // remove each finished file from uploads @@ -248,11 +248,11 @@ export default class CreatePost extends React.Component { } } - draft.previews = draft.previews.concat(filenames); + draft.fileInfos = draft.fileInfos.concat(fileInfos); PostStore.storeDraft(channelId, draft); if (channelId === this.state.channelId) { - this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos}); } } @@ -282,11 +282,11 @@ export default class CreatePost extends React.Component { } removePreview(id) { - const previews = Object.assign([], this.state.previews); + const fileInfos = Object.assign([], this.state.fileInfos); const uploadsInProgress = this.state.uploadsInProgress; - // id can either be the path of an uploaded file or the client id of an in progress upload - let index = previews.indexOf(id); + // id can either be the id of an uploaded file or the client id of an in progress upload + let index = fileInfos.findIndex((info) => info.id === id); if (index === -1) { index = uploadsInProgress.indexOf(id); @@ -295,15 +295,15 @@ export default class CreatePost extends React.Component { this.refs.fileUpload.getWrappedInstance().cancelUpload(id); } } else { - previews.splice(index, 1); + fileInfos.splice(index, 1); } const draft = PostStore.getCurrentDraft(); - draft.previews = previews; + draft.fileInfos = fileInfos; draft.uploadsInProgress = uploadsInProgress; PostStore.storeCurrentDraft(draft); - this.setState({previews, uploadsInProgress}); + this.setState({fileInfos, uploadsInProgress}); } componentWillMount() { @@ -336,6 +336,7 @@ export default class CreatePost extends React.Component { PreferenceStore.removeChangeListener(this.onPreferenceChange); document.removeEventListener('keydown', this.showShortcuts); } + showShortcuts(e) { if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.FORWARD_SLASH) { e.preventDefault(); @@ -359,7 +360,7 @@ export default class CreatePost extends React.Component { if (this.state.channelId !== channelId) { const draft = this.getCurrentDraft(); - this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); + this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, fileInfos: draft.fileInfos, uploadsInProgress: draft.uploadsInProgress}); } } @@ -374,11 +375,11 @@ export default class CreatePost extends React.Component { getFileCount(channelId) { if (channelId === this.state.channelId) { - return this.state.previews.length + this.state.uploadsInProgress.length; + return this.state.fileInfos.length + this.state.uploadsInProgress.length; } const draft = PostStore.getDraft(channelId); - return draft.previews.length + draft.uploadsInProgress.length; + return draft.fileInfos.length + draft.uploadsInProgress.length; } handleKeyDown(e) { @@ -474,10 +475,10 @@ export default class CreatePost extends React.Component { } let preview = null; - if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { + if (this.state.fileInfos.length > 0 || this.state.uploadsInProgress.length > 0) { preview = ( <FilePreview - files={this.state.previews} + fileInfos={this.state.fileInfos} onRemove={this.removePreview} uploadsInProgress={this.state.uploadsInProgress} /> diff --git a/webapp/components/file_attachment.jsx b/webapp/components/file_attachment.jsx index cba9d8288..23d8d2446 100644 --- a/webapp/components/file_attachment.jsx +++ b/webapp/components/file_attachment.jsx @@ -1,204 +1,111 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import * as utils from 'utils/utils.jsx'; -import Client from 'client/web_client.jsx'; import Constants from 'utils/constants.jsx'; +import FileStore from 'stores/file_store.jsx'; +import * as Utils from 'utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages} from 'react-intl'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; -const holders = defineMessages({ - download: { - id: 'file_attachment.download', - defaultMessage: 'Download' - } -}); - import React from 'react'; -class FileAttachment extends React.Component { +export default class FileAttachment extends React.Component { constructor(props) { super(props); this.loadFiles = this.loadFiles.bind(this); - this.addBackgroundImage = this.addBackgroundImage.bind(this); this.onAttachmentClick = this.onAttachmentClick.bind(this); - this.canSetState = false; - this.state = {fileSize: -1}; + this.state = { + loaded: Utils.getFileType(props.fileInfo.extension) !== 'image' + }; } + componentDidMount() { this.loadFiles(); } - componentDidUpdate(prevProps) { - if (this.props.filename !== prevProps.filename) { - this.loadFiles(); - } - } - loadFiles() { - this.canSetState = true; - - var filename = this.props.filename; - - if (filename) { - var fileInfo = this.getFileInfoFromName(filename); - var type = utils.getFileType(fileInfo.ext); - - if (type === 'image') { - var self = this; // Need this reference since we use the given "this" - $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').on('load', (function loadWrapper(path, name) { - return function loader() { - $(this).remove(); - if (name in self.refs) { - var imgDiv = ReactDOM.findDOMNode(self.refs[name]); - - $(imgDiv).removeClass('post-image__load'); - $(imgDiv).addClass('post-image'); - - var width = this.width || $(this).width(); - var height = this.height || $(this).height(); - - if (width < Constants.THUMBNAIL_WIDTH && - height < Constants.THUMBNAIL_HEIGHT) { - $(imgDiv).addClass('small'); - } else { - $(imgDiv).addClass('normal'); - } - self.addBackgroundImage(name, path); - } - }; - }(fileInfo.path, filename))); - } + componentWillReceiveProps(nextProps) { + if (nextProps.fileInfo.id !== this.props.fileInfo.id) { + this.setState({ + loaded: Utils.getFileType(nextProps.fileInfo.extension) !== 'image' + }); } } - componentWillUnmount() { - // keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted - this.canSetState = false; - } - shouldComponentUpdate(nextProps, nextState) { - if (!utils.areObjectsEqual(nextProps, this.props)) { - return true; - } - - // the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering - if (nextState.fileSize !== this.state.fileSize) { - if (this.refs.fileSize) { - // update the UI element to display the file size without re-rendering the whole component - ReactDOM.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize); - return false; - } - - // we can't find the element that should hold the file size so we must not have rendered yet - return true; + componentDidUpdate(prevProps) { + if (!this.state.loaded && this.props.fileInfo.id !== prevProps.fileInfo.id) { + this.loadFiles(); } - - return true; - } - getFileInfoFromName(name) { - var fileInfo = utils.splitFileLocation(name); - - fileInfo.path = Client.getFilesRoute() + '/get' + fileInfo.path; - - return fileInfo; } - addBackgroundImage(name, path) { - var fileUrl = path; - if (name in this.refs) { - if (!path) { - fileUrl = this.getFileInfoFromName(name).path; - } + loadFiles() { + const fileInfo = this.props.fileInfo; + const fileType = Utils.getFileType(fileInfo.extension); - var imgDiv = ReactDOM.findDOMNode(this.refs[name]); - var re1 = new RegExp(' ', 'g'); - var re2 = new RegExp('\\(', 'g'); - var re3 = new RegExp('\\)', 'g'); - var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); + if (fileType === 'image') { + const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); - } - } - removeBackgroundImage(name) { - if (name in this.refs) { - $(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial'); + const img = new Image(); + img.onload = () => { + this.setState({loaded: true}); + }; + img.load(thumbnailUrl); } } + onAttachmentClick(e) { e.preventDefault(); this.props.handleImageClick(this.props.index); } + render() { - var filename = this.props.filename; + const fileInfo = this.props.fileInfo; + const fileName = fileInfo.name; + const fileUrl = FileStore.getFileUrl(fileInfo.id); - var fileInfo = utils.splitFileLocation(filename); - var fileUrl = utils.getFileUrl(filename); - var type = utils.getFileType(fileInfo.ext); + let thumbnail; + if (this.state.loaded) { + const type = Utils.getFileType(fileInfo.extension); - var thumbnail; - if (type === 'image') { - thumbnail = ( - <div - ref={filename} - className='post-image__load' - /> - ); - } else { - thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>; - } + if (type === 'image') { + let className = 'post-image'; - var fileSizeString = ''; - if (this.state.fileSize < 0) { - Client.getFileInfo( - filename, - (data) => { - if (this.canSetState) { - this.setState({fileSize: parseInt(data.size, 10)}); - } - }, - () => { - // Do nothing + if (fileInfo.width < Constants.THUMBNAIL_WIDTH && fileInfo.height < Constants.THUMBNAIL_HEIGHT) { + className += ' small'; + } else { + className += ' normal'; } - ); + + thumbnail = ( + <div + className={className} + style={{ + backgroundImage: `url(${FileStore.getFileThumbnailUrl(fileInfo.id)})` + }} + /> + ); + } else { + thumbnail = <div className={'file-icon ' + Utils.getIconClassName(type)}/>; + } } else { - fileSizeString = utils.fileSizeToString(this.state.fileSize); + thumbnail = <div className='post-image__load'/>; } - var filenameString = decodeURIComponent(utils.getFileName(filename)); - var trimmedFilename; - if (filenameString.length > 35) { - trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + '...'; + let trimmedFilename; + if (fileName.length > 35) { + trimmedFilename = fileName.substring(0, Math.min(35, fileName.length)) + '...'; } else { - trimmedFilename = filenameString; + trimmedFilename = fileName; } - var filenameOverlay = ( - <OverlayTrigger - delayShow={1000} - placement='top' - overlay={<Tooltip id='file-name__tooltip'>{this.props.intl.formatMessage(holders.download) + ' "' + filenameString + '"'}</Tooltip>} - > - <a - href={fileUrl} - download={filenameString} - className='post-image__name' - target='_blank' - rel='noopener noreferrer' - > - {trimmedFilename} - </a> - </OverlayTrigger> - ); + let filenameOverlay; if (this.props.compactDisplay) { filenameOverlay = ( <OverlayTrigger delayShow={1000} placement='top' - overlay={<Tooltip id='file-name__tooltip'>{filenameString}</Tooltip>} + overlay={<Tooltip id='file-name__tooltip'>{fileName}</Tooltip>} > <a href='#' @@ -214,13 +121,28 @@ class FileAttachment extends React.Component { </a> </OverlayTrigger> ); + } else { + filenameOverlay = ( + <OverlayTrigger + delayShow={1000} + placement='top' + overlay={<Tooltip id='file-name__tooltip'>{Utils.localizeMessage('file_attachment.download', 'Download') + ' "' + fileName + '"'}</Tooltip>} + > + <a + href={fileUrl} + download={fileName} + className='post-image__name' + target='_blank' + rel='noopener noreferrer' + > + {trimmedFilename} + </a> + </OverlayTrigger> + ); } return ( - <div - className='post-image__column' - key={filename} - > + <div className='post-image__column'> <a className='post-image__thumbnail' href='#' @@ -233,17 +155,15 @@ class FileAttachment extends React.Component { <div> <a href={fileUrl} - download={filenameString} + download={fileName} className='post-image__download' target='_blank' rel='noopener noreferrer' > - <span - className='fa fa-download' - /> + <span className='fa fa-download'/> </a> - <span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span> - <span className='post-image__size'>{fileSizeString}</span> + <span className='post-image__type'>{fileInfo.extension.toUpperCase()}</span> + <span className='post-image__size'>{Utils.fileSizeToString(fileInfo.size)}</span> </div> </div> </div> @@ -252,10 +172,7 @@ class FileAttachment extends React.Component { } FileAttachment.propTypes = { - intl: intlShape.isRequired, - - // a list of file pathes displayed by the parent FileAttachmentList - filename: React.PropTypes.string.isRequired, + fileInfo: React.PropTypes.object.isRequired, // the index of this attachment preview in the parent FileAttachmentList index: React.PropTypes.number.isRequired, @@ -264,6 +181,4 @@ FileAttachment.propTypes = { handleImageClick: React.PropTypes.func, compactDisplay: React.PropTypes.bool -}; - -export default injectIntl(FileAttachment); +};
\ No newline at end of file diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list.jsx index e4b841769..3df4684be 100644 --- a/webapp/components/file_attachment_list.jsx +++ b/webapp/components/file_attachment_list.jsx @@ -13,25 +13,34 @@ export default class FileAttachmentList extends React.Component { this.handleImageClick = this.handleImageClick.bind(this); - this.state = {showPreviewModal: false, startImgId: 0}; + this.state = {showPreviewModal: false, startImgIndex: 0}; } + handleImageClick(indexClicked) { - this.setState({showPreviewModal: true, startImgId: indexClicked}); + this.setState({showPreviewModal: true, startImgIndex: indexClicked}); } + render() { - var filenames = this.props.filenames; + const postFiles = []; + if (this.props.fileInfos && this.props.fileInfos.length > 0) { + for (let i = 0; i < Math.min(this.props.fileInfos.length, Constants.MAX_DISPLAY_FILES); i++) { + const fileInfo = this.props.fileInfos[i]; - var postFiles = []; - for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - postFiles.push( - <FileAttachment - key={'file_attachment_' + i} - filename={filenames[i]} - index={i} - handleImageClick={this.handleImageClick} - compactDisplay={this.props.compactDisplay} - /> - ); + postFiles.push( + <FileAttachment + key={fileInfo.id} + fileInfo={this.props.fileInfos[i]} + index={i} + handleImageClick={this.handleImageClick} + compactDisplay={this.props.compactDisplay} + /> + ); + } + } else if (this.props.fileCount > 0) { + for (let i = 0; i < Math.min(this.props.fileCount, Constants.MAX_DISPLAY_FILES); i++) { + // Add a placeholder to avoid pop-in once we get the file infos for this post + postFiles.push(<div className='post-image__column post-image__column--placeholder'/>); + } } return ( @@ -42,10 +51,8 @@ export default class FileAttachmentList extends React.Component { <ViewImageModal show={this.state.showPreviewModal} onModalDismissed={() => this.setState({showPreviewModal: false})} - channelId={this.props.channelId} - userId={this.props.userId} - startId={this.state.startImgId} - filenames={filenames} + startId={this.state.startImgIndex} + fileInfos={this.props.fileInfos} /> </div> ); @@ -53,15 +60,7 @@ export default class FileAttachmentList extends React.Component { } FileAttachmentList.propTypes = { - - // a list of file pathes displayed by this - filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, - - // the channel that this is part of - channelId: React.PropTypes.string, - - // the user that owns the post that this is attached to - userId: React.PropTypes.string, - + fileCount: React.PropTypes.number.isRequired, + fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, compactDisplay: React.PropTypes.bool }; diff --git a/webapp/components/file_attachment_list_container.jsx b/webapp/components/file_attachment_list_container.jsx new file mode 100644 index 000000000..f9ad3814c --- /dev/null +++ b/webapp/components/file_attachment_list_container.jsx @@ -0,0 +1,90 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import FileStore from 'stores/file_store.jsx'; + +import FileAttachmentList from './file_attachment_list.jsx'; + +export default class FileAttachmentListContainer extends React.Component { + static propTypes = { + post: React.PropTypes.object.isRequired, + compactDisplay: React.PropTypes.bool.isRequired + } + + constructor(props) { + super(props); + + this.handleFileChange = this.handleFileChange.bind(this); + + this.state = { + fileInfos: FileStore.getInfosForPost(props.post.id) + }; + } + + componentDidMount() { + FileStore.addChangeListener(this.handleFileChange); + + if (this.props.post.id && !FileStore.hasInfosForPost(this.props.post.id)) { + AsyncClient.getFileInfosForPost(this.props.post.channel_id, this.props.post.id); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.post.id !== this.props.post.id) { + this.setState({ + fileInfos: FileStore.getInfosForPost(nextProps.post.id) + }); + + if (nextProps.post.id && !FileStore.hasInfosForPost(nextProps.post.id)) { + AsyncClient.getFileInfosForPost(nextProps.post.channel_id, nextProps.post.id); + } + } + } + + shouldComponentUpdate(nextProps, nextState) { + if (this.props.post.id !== nextProps.post.id) { + return true; + } + + if (this.props.compactDisplay !== nextProps.compactDisplay) { + return true; + } + + // fileInfos are treated as immutable by the FileStore + if (nextState.fileInfos !== this.state.fileInfos) { + return true; + } + + return false; + } + + handleFileChange() { + this.setState({ + fileInfos: FileStore.getInfosForPost(this.props.post.id) + }); + } + + componentWillUnmount() { + FileStore.removeChangeListener(this.handleFileChange); + } + + render() { + let fileCount = 0; + if (this.props.post.file_ids) { + fileCount = this.props.post.file_ids.length; + } else if (this.props.post.filenames) { + fileCount = this.props.post.filenames.length; + } + + return ( + <FileAttachmentList + fileCount={fileCount} + fileInfos={this.state.fileInfos} + compactDisplay={this.props.compactDisplay} + /> + ); + } +} diff --git a/webapp/components/file_info_preview.jsx b/webapp/components/file_info_preview.jsx index b3d16b6a6..51825ce5b 100644 --- a/webapp/components/file_info_preview.jsx +++ b/webapp/components/file_info_preview.jsx @@ -1,59 +1,59 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import React from 'react'; + import * as Utils from 'utils/utils.jsx'; -import {defineMessages} from 'react-intl'; -import React from 'react'; -import {Link} from 'react-router/es6'; - -const holders = defineMessages({ - type: { - id: 'file_info_preview.type', - defaultMessage: 'File type ' - }, - size: { - id: 'file_info_preview.size', - defaultMessage: 'Size ' - } -}); +export default class FileInfoPreview extends React.Component { + shouldComponentUpdate(nextProps) { + if (nextProps.fileUrl !== this.props.fileUrl) { + return true; + } -export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMessage}) { - // non-image files include a section providing details about the file - const infoParts = []; + if (!Utils.areObjectsEqual(nextProps.fileInfo, this.props.fileInfo)) { + return true; + } - if (fileInfo.extension !== '') { - infoParts.push(formatMessage(holders.type) + fileInfo.extension.toUpperCase()); + return false; } - infoParts.push(formatMessage(holders.size) + Utils.fileSizeToString(fileInfo.size)); - - const infoString = infoParts.join(', '); - - const name = decodeURIComponent(Utils.getFileName(filename)); - - return ( - <div className='file-details__container'> - <Link - className={'file-details__preview'} - to={fileUrl} - target='_blank' - rel='noopener noreferrer' - > - <span className='file-details__preview-helper'/> - <img src={Utils.getPreviewImagePath(filename)}/> - </Link> - <div className='file-details'> - <div className='file-details__name'>{name}</div> - <div className='file-details__info'>{infoString}</div> + render() { + const fileInfo = this.props.fileInfo; + const fileUrl = this.props.fileUrl; + + // non-image files include a section providing details about the file + const infoParts = []; + + if (fileInfo.extension !== '') { + infoParts.push(Utils.localizeMessage('file_info_preview.type', 'File type ') + fileInfo.extension.toUpperCase()); + } + + infoParts.push(Utils.localizeMessage('file_info_preview.size', 'Size ') + Utils.fileSizeToString(fileInfo.size)); + + const infoString = infoParts.join(', '); + + return ( + <div className='file-details__container'> + <a + className={'file-details__preview'} + to={fileUrl} + target='_blank' + rel='noopener noreferrer' + > + <span className='file-details__preview-helper'/> + <img src={Utils.getFileIconPath(fileInfo)}/> + </a> + <div className='file-details'> + <div className='file-details__name'>{fileInfo.name}</div> + <div className='file-details__info'>{infoString}</div> + </div> </div> - </div> - ); + ); + } } FileInfoPreview.propTypes = { - filename: React.PropTypes.string.isRequired, - fileUrl: React.PropTypes.string.isRequired, fileInfo: React.PropTypes.object.isRequired, - formatMessage: React.PropTypes.func.isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx index 46ce43a6f..53cec7f7b 100644 --- a/webapp/components/file_preview.jsx +++ b/webapp/components/file_preview.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import FileStore from 'stores/file_store.jsx'; import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; @@ -21,63 +22,43 @@ export default class FilePreview extends React.Component { } } - handleRemove(e) { - var previewDiv = e.target.parentNode.parentNode; - - if (previewDiv.hasAttribute('data-filename')) { - this.props.onRemove(previewDiv.getAttribute('data-filename')); - } else if (previewDiv.hasAttribute('data-client-id')) { - this.props.onRemove(previewDiv.getAttribute('data-client-id')); - } + handleRemove(id) { + this.props.onRemove(id); } render() { var previews = []; - this.props.files.forEach((fullFilename) => { - var filename = fullFilename; - var originalFilename = filename; - var filenameSplit = filename.split('.'); - var ext = filenameSplit[filenameSplit.length - 1]; - var type = Utils.getFileType(ext); - - filename = Utils.getFileUrl(filename); + this.props.fileInfos.forEach((info) => { + const type = Utils.getFileType(info.extension); + let className = 'file-preview'; + let previewImage; if (type === 'image') { - previews.push( - <div - key={filename} - className='file-preview' - data-filename={originalFilename} - > - <img - className='file-preview__image' - src={filename} - /> - <a - className='file-preview__remove' - onClick={this.handleRemove} - > - <i className='fa fa-remove'/> - </a> - </div> + previewImage = ( + <img + className='file-preview__image' + src={FileStore.getFileUrl(info.id)} + /> ); } else { - previews.push( - <div - key={filename} - className='file-preview custom-file' - data-filename={originalFilename} - > - <div className={'file-icon ' + Utils.getIconClassName(type)}/> - <a - className='file-preview__remove' - onClick={this.handleRemove} - > - <i className='fa fa-remove'/> - </a> - </div> - ); + className += ' custom-file'; + previewImage = <div className={'file-icon ' + Utils.getIconClassName(type)}/>; } + + previews.push( + <div + key={info.id} + className={className} + > + {previewImage} + <a + className='file-preview__remove' + onClick={this.handleRemove.bind(this, info.id)} + > + <i className='fa fa-remove'/> + </a> + </div> + ); }); this.props.uploadsInProgress.forEach((clientId) => { @@ -94,7 +75,7 @@ export default class FilePreview extends React.Component { /> <a className='file-preview__remove' - onClick={this.handleRemove} + onClick={this.handleRemove.bind(this, clientId)} > <i className='fa fa-remove'/> </a> @@ -111,11 +92,11 @@ export default class FilePreview extends React.Component { } FilePreview.defaultProps = { - files: [], + fileInfos: [], uploadsInProgress: [] }; FilePreview.propTypes = { onRemove: React.PropTypes.func.isRequired, - files: React.PropTypes.array, + fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, uploadsInProgress: React.PropTypes.array }; diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx index 39abec7e4..9eff25ab5 100644 --- a/webapp/components/file_upload.jsx +++ b/webapp/components/file_upload.jsx @@ -49,13 +49,12 @@ class FileUpload extends React.Component { this.keyUpload = this.keyUpload.bind(this); this.state = { - maxFileSize: global.window.mm_config.MaxFileSize, requests: {} }; } fileUploadSuccess(channelId, data) { - this.props.onFileUpload(data.filenames, data.client_ids, channelId); + this.props.onFileUpload(data.file_infos, data.client_ids, channelId); const requests = Object.assign({}, this.state.requests); for (var j = 0; j < data.client_ids.length; j++) { @@ -81,7 +80,7 @@ class FileUpload extends React.Component { const tooLargeFiles = []; for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) { - if (files[i].size > this.state.maxFileSize) { + if (files[i].size > global.mm_config.MaxFileSize) { tooLargeFiles.push(files[i]); continue; } @@ -112,9 +111,9 @@ class FileUpload extends React.Component { } else if (tooLargeFiles.length > 1) { var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', '); - this.props.onUploadError(formatMessage(holders.filesAbove, {max: (this.state.maxFileSize / 1048576), filenames: tooLargeFilenames})); + this.props.onUploadError(formatMessage(holders.filesAbove, {max: (global.mm_config.MaxFileSize / 1048576), filenames: tooLargeFilenames})); } else if (tooLargeFiles.length > 0) { - this.props.onUploadError(formatMessage(holders.fileAbove, {max: (this.state.maxFileSize / 1048576), filename: tooLargeFiles[0].name})); + this.props.onUploadError(formatMessage(holders.fileAbove, {max: (global.mm_config.MaxFileSize / 1048576), filename: tooLargeFiles[0].name})); } } diff --git a/webapp/components/get_public_link_modal.jsx b/webapp/components/get_public_link_modal.jsx index 49fd891be..851a78f80 100644 --- a/webapp/components/get_public_link_modal.jsx +++ b/webapp/components/get_public_link_modal.jsx @@ -23,7 +23,7 @@ export default class GetPublicLinkModal extends React.Component { this.state = { show: false, - filename: '', + fileId: '', link: '' }; } @@ -34,7 +34,7 @@ export default class GetPublicLinkModal extends React.Component { componentDidUpdate(prevProps, prevState) { if (this.state.show && !prevState.show) { - AsyncClient.getPublicLink(decodeURIComponent(this.state.filename), this.handlePublicLink); + AsyncClient.getPublicLink(this.state.fileId, this.handlePublicLink); } } @@ -51,7 +51,7 @@ export default class GetPublicLinkModal extends React.Component { handleToggle(value, args) { this.setState({ show: value, - filename: args.filename, + fileId: args.fileId, link: '' }); } diff --git a/webapp/components/pdf_preview.jsx b/webapp/components/pdf_preview.jsx index 7f0f06c03..2cb0a324c 100644 --- a/webapp/components/pdf_preview.jsx +++ b/webapp/components/pdf_preview.jsx @@ -3,8 +3,6 @@ import FileInfoPreview from './file_info_preview.jsx'; -import * as Utils from 'utils/utils.jsx'; - import loadingGif from 'images/load.gif'; import React from 'react'; @@ -109,18 +107,8 @@ export default class PDFPreview extends React.Component { } } - static support(filename) { - const fileInfo = Utils.splitFileLocation(filename); - const ext = fileInfo.ext; - if (!ext) { - return false; - } - - if (ext === 'pdf') { - return true; - } - - return false; + static supports(fileInfo) { + return fileInfo.extension === 'pdf'; } render() { @@ -138,10 +126,8 @@ export default class PDFPreview extends React.Component { if (!this.state.success) { return ( <FileInfoPreview - filename={this.props.filename} - fileUrl={this.props.fileUrl} fileInfo={this.props.fileInfo} - formatMessage={this.props.formatMessage} + fileUrl={this.props.fileUrl} /> ); } @@ -185,8 +171,6 @@ export default class PDFPreview extends React.Component { } PDFPreview.propTypes = { - filename: React.PropTypes.string.isRequired, - fileUrl: React.PropTypes.string.isRequired, fileInfo: React.PropTypes.object.isRequired, - formatMessage: React.PropTypes.func.isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/components/post_view/components/commented_on_files_message_container.jsx b/webapp/components/post_view/components/commented_on_files_message_container.jsx new file mode 100644 index 000000000..5325a7644 --- /dev/null +++ b/webapp/components/post_view/components/commented_on_files_message_container.jsx @@ -0,0 +1,88 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import FileStore from 'stores/file_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class CommentedOnFilesMessageContainer extends React.Component { + static propTypes = { + parentPostChannelId: React.PropTypes.string.isRequired, + parentPostId: React.PropTypes.string.isRequired + } + + constructor(props) { + super(props); + + this.handleFileChange = this.handleFileChange.bind(this); + + this.state = { + fileInfos: FileStore.getInfosForPost(this.props.parentPostId) + }; + } + + componentDidMount() { + FileStore.addChangeListener(this.handleFileChange); + + if (!FileStore.hasInfosForPost(this.props.parentPostId)) { + AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.parentPostId !== this.props.parentPostId) { + this.setState({ + fileInfos: FileStore.getInfosForPost(this.props.parentPostId) + }); + + if (!FileStore.hasInfosForPost(this.props.parentPostId)) { + AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId); + } + } + } + + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.parentPostId !== this.props.parentPostId) { + return true; + } + + if (nextProps.parentPostChannelId !== this.props.parentPostChannelId) { + return true; + } + + // fileInfos are treated as immutable by the FileStore + if (nextState.fileInfos !== this.state.fileInfos) { + return true; + } + + return false; + } + + handleFileChange() { + this.setState({ + fileInfos: FileStore.getInfosForPost(this.props.parentPostId) + }); + } + + componentWillUnmount() { + FileStore.removeChangeListener(this.handleFileChange); + } + + render() { + let message = ' '; + + if (this.state.fileInfos && this.state.fileInfos.length > 0) { + message = this.state.fileInfos[0].name; + + if (this.state.fileInfos.length === 2) { + message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file'); + } else if (this.state.fileInfos.length > 2) { + message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.state.fileInfos.length - 1).toString()); + } + } + + return <span>{message}</span>; + } +} diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 5c02e9c40..c23939c1f 100644 --- a/webapp/components/post_view/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import FileAttachmentList from 'components/file_attachment_list.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import Constants from 'utils/constants.jsx'; +import CommentedOnFilesMessageContainer from './commented_on_files_message_container.jsx'; +import FileAttachmentListContainer from 'components/file_attachment_list_container.jsx'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; import PostMessageContainer from './post_message_container.jsx'; import PendingPostOptions from './pending_post_options.jsx'; @@ -22,6 +23,7 @@ export default class PostBody extends React.Component { this.removePost = this.removePost.bind(this); } + shouldComponentUpdate(nextProps) { if (nextProps.isCommentMention !== this.props.isCommentMention) { return true; @@ -56,7 +58,6 @@ export default class PostBody extends React.Component { render() { const post = this.props.post; - const filenames = this.props.post.filenames; const parentPost = this.props.parentPost; let comment = ''; @@ -94,14 +95,13 @@ export default class PostBody extends React.Component { let message = ''; if (parentPost.message) { message = Utils.replaceHtmlEntities(parentPost.message); - } else if (parentPost.filenames.length) { - message = parentPost.filenames[0].split('/').pop(); - - if (parentPost.filenames.length === 2) { - message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file'); - } else if (parentPost.filenames.length > 2) { - message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (parentPost.filenames.length - 1).toString()); - } + } else if (parentPost.file_ids && parentPost.file_ids.length > 0) { + message = ( + <CommentedOnFilesMessageContainer + parentPostChannelId={parentPost.channel_id} + parentPostId={parentPost.id} + /> + ); } comment = ( @@ -140,14 +140,11 @@ export default class PostBody extends React.Component { ); } - let fileAttachmentHolder = ''; - if (filenames && filenames.length > 0) { + let fileAttachmentHolder = null; + if ((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) { fileAttachmentHolder = ( - <FileAttachmentList - - filenames={filenames} - channelId={post.channel_id} - userId={post.user_id} + <FileAttachmentListContainer + post={post} compactDisplay={this.props.compactDisplay} /> ); diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index e1af1227b..18e4b4d1c 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import UserProfile from './user_profile.jsx'; -import FileAttachmentList from './file_attachment_list.jsx'; +import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx'; import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import ProfilePicture from 'components/profile_picture.jsx'; @@ -295,13 +295,11 @@ export default class RhsComment extends React.Component { var dropdown = this.createDropdown(); - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { + let fileAttachment = null; + if (post.file_ids && post.file_ids.length > 0) { fileAttachment = ( - <FileAttachmentList - filenames={post.filenames} - channelId={post.channel_id} - userId={post.user_id} + <FileAttachmentListContainer + post={post} compactDisplay={this.props.compactDisplay} /> ); diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 09ab17ba5..983469f50 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -4,7 +4,7 @@ import UserProfile from './user_profile.jsx'; import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; -import FileAttachmentList from './file_attachment_list.jsx'; +import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import ProfilePicture from 'components/profile_picture.jsx'; import ChannelStore from 'stores/channel_store.jsx'; @@ -242,13 +242,11 @@ export default class RhsRootPost extends React.Component { ); } - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { + let fileAttachment = null; + if (post.file_ids && post.file_ids.length > 0) { fileAttachment = ( - <FileAttachmentList - filenames={post.filenames} - channelId={post.channel_id} - userId={post.user_id} + <FileAttachmentListContainer + post={post} compactDisplay={this.props.compactDisplay} /> ); diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx index c9f558725..385138d54 100644 --- a/webapp/components/view_image.jsx +++ b/webapp/components/view_image.jsx @@ -11,7 +11,6 @@ import * as GlobalActions from 'actions/global_actions.jsx'; 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'; @@ -19,19 +18,11 @@ 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', - defaultMessage: 'Loading ' - } -}); - -class ViewImageModal extends React.Component { +export default class ViewImageModal extends React.Component { constructor(props) { super(props); @@ -45,18 +36,15 @@ class ViewImageModal extends React.Component { this.onModalShown = this.onModalShown.bind(this); this.onModalHidden = this.onModalHidden.bind(this); - this.onFileStoreChange = this.onFileStoreChange.bind(this); - this.handleGetPublicLink = this.handleGetPublicLink.bind(this); this.onMouseEnterImage = this.onMouseEnterImage.bind(this); this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this); this.state = { imgId: this.props.startId, - fileInfo: null, imgHeight: '100%', - loaded: Utils.fillArray(false, this.props.filenames.length), - progress: Utils.fillArray(0, this.props.filenames.length), + loaded: Utils.fillArray(false, this.props.fileInfos.length), + progress: Utils.fillArray(0, this.props.fileInfos.length), showFooter: false }; } @@ -66,7 +54,7 @@ class ViewImageModal extends React.Component { e.stopPropagation(); } let id = this.state.imgId + 1; - if (id > this.props.filenames.length - 1) { + if (id > this.props.fileInfos.length - 1) { id = 0; } this.showImage(id); @@ -78,7 +66,7 @@ class ViewImageModal extends React.Component { } let id = this.state.imgId - 1; if (id < 0) { - id = this.props.filenames.length - 1; + id = this.props.fileInfos.length - 1; } this.showImage(id); } @@ -95,8 +83,6 @@ class ViewImageModal extends React.Component { $(window).on('keyup', this.handleKeyPress); this.showImage(nextProps.startId); - - FileStore.addChangeListener(this.onFileStoreChange); } onModalHidden() { @@ -105,8 +91,6 @@ class ViewImageModal extends React.Component { if (this.refs.video) { this.refs.video.stop(); } - - FileStore.removeChangeListener(this.onFileStoreChange); } componentWillReceiveProps(nextProps) { @@ -116,64 +100,36 @@ class ViewImageModal extends React.Component { this.onModalHidden(); } - if (!Utils.areObjectsEqual(this.props.filenames, nextProps.filenames)) { + if (this.props.fileInfos !== nextProps.fileInfos) { this.setState({ - loaded: Utils.fillArray(false, nextProps.filenames.length), - progress: Utils.fillArray(0, nextProps.filenames.length) + loaded: Utils.fillArray(false, nextProps.fileInfos.length), + progress: Utils.fillArray(0, nextProps.fileInfos.length) }); } } - onFileStoreChange(filename) { - const id = this.props.filenames.indexOf(filename); - - if (id !== -1) { - if (id === this.state.imgId) { - this.setState({ - fileInfo: FileStore.getInfo(filename) - }); - } - - if (!this.state.loaded[id]) { - this.loadImage(id, filename); - } - } - } - showImage(id) { this.setState({imgId: id}); const imgHeight = $(window).height() - 100; this.setState({imgHeight}); - const filename = this.props.filenames[id]; - - if (!FileStore.hasInfo(filename)) { - // the image will actually be loaded once we know what we need to load - AsyncClient.getFileInfo(filename); - return; - } - - this.setState({ - fileInfo: FileStore.getInfo(filename) - }); - if (!this.state.loaded[id]) { - this.loadImage(id, filename); + this.loadImage(id); } } - loadImage(id, filename) { - const fileInfo = FileStore.getInfo(filename); + loadImage(index) { + const fileInfo = this.props.fileInfos[index]; const fileType = Utils.getFileType(fileInfo.extension); if (fileType === 'image') { let previewUrl; if (fileInfo.has_image_preview) { - previewUrl = Utils.getPreviewImagePath(filename); + previewUrl = FileStore.getFilePreviewUrl(fileInfo.id); } else { // some images (eg animated gifs) just show the file itself and not a preview - previewUrl = Utils.getFileUrl(filename); + previewUrl = FileStore.getFileUrl(fileInfo.id); } const img = new Image(); @@ -181,19 +137,19 @@ class ViewImageModal extends React.Component { previewUrl, () => { const progress = this.state.progress; - progress[id] = img.completedPercentage; + progress[index] = img.completedPercentage; this.setState({progress}); } ); img.onload = () => { const loaded = this.state.loaded; - loaded[id] = true; + loaded[index] = true; this.setState({loaded}); }; } else { // there's nothing to load for non-image files var loaded = this.state.loaded; - loaded[id] = true; + loaded[index] = true; this.setState({loaded}); } } @@ -201,7 +157,7 @@ class ViewImageModal extends React.Component { handleGetPublicLink() { this.props.onModalDismissed(); - GlobalActions.showGetPublicLinkModal(this.props.filenames[this.state.imgId]); + GlobalActions.showGetPublicLinkModal(this.props.fileInfos[this.state.imgId].id); } onMouseEnterImage() { @@ -213,63 +169,52 @@ class ViewImageModal extends React.Component { } render() { - if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) { - return <div/>; + if (this.props.fileInfos.length < 1 || this.props.fileInfos.length - 1 < this.state.imgId) { + return null; } - const filename = this.props.filenames[this.state.imgId]; - const fileUrl = Utils.getFileUrl(filename); + const fileInfo = this.props.fileInfos[this.state.imgId]; + const fileUrl = FileStore.getFileUrl(fileInfo.id); - var content; + let content; if (this.state.loaded[this.state.imgId]) { - // this.state.fileInfo is for the current image and we shoudl have it before we load the image - const fileInfo = this.state.fileInfo; const fileType = Utils.getFileType(fileInfo.extension); if (fileType === 'image') { content = ( <ImagePreview - filename={filename} - fileUrl={fileUrl} fileInfo={fileInfo} + fileUrl={fileUrl} maxHeight={this.state.imgHeight} /> ); } else if (fileType === 'video' || fileType === 'audio') { content = ( <AudioVideoPreview - filename={filename} + fileInfo={fileInfo} fileUrl={fileUrl} - fileInfo={this.state.fileInfo} maxHeight={this.state.imgHeight} - formatMessage={this.props.intl.formatMessage} /> ); - } else if (PDFPreview.support(filename)) { + } else if (PDFPreview.supports(fileInfo)) { content = ( <PDFPreview - filename={filename} - fileUrl={fileUrl} fileInfo={fileInfo} - formatMessage={this.props.intl.formatMessage} + fileUrl={fileUrl} /> ); - } else if (CodePreview.support(filename)) { + } else if (CodePreview.supports(fileInfo)) { content = ( <CodePreview - filename={filename} - fileUrl={fileUrl} fileInfo={fileInfo} - formatMessage={this.props.intl.formatMessage} + fileUrl={fileUrl} /> ); } else { content = ( <FileInfoPreview - filename={filename} - fileUrl={fileUrl} fileInfo={fileInfo} - formatMessage={this.props.intl.formatMessage} + fileUrl={fileUrl} /> ); } @@ -280,14 +225,14 @@ class ViewImageModal extends React.Component { content = ( <LoadingImagePreview progress={progress} - loading={this.props.intl.formatMessage(holders.loading)} + loading={Utils.localizeMessage('view_image.loading', 'Loading ')} /> ); } let leftArrow = null; let rightArrow = null; - if (this.props.filenames.length > 1) { + if (this.props.fileInfos.length > 1) { leftArrow = ( <a ref='previewArrowLeft' @@ -346,8 +291,8 @@ class ViewImageModal extends React.Component { <ViewImagePopoverBar show={this.state.showFooter} fileId={this.state.imgId} - totalFiles={this.props.filenames.length} - filename={name} + totalFiles={this.props.fileInfos.length} + filename={fileInfo.name} fileURL={fileUrl} onGetPublicLink={this.handleGetPublicLink} /> @@ -363,19 +308,13 @@ class ViewImageModal extends React.Component { ViewImageModal.defaultProps = { show: false, - filenames: [], - channelId: '', - userId: '', + fileInfos: [], startId: 0 }; ViewImageModal.propTypes = { - intl: intlShape.isRequired, show: React.PropTypes.bool.isRequired, onModalDismissed: React.PropTypes.func.isRequired, - filenames: React.PropTypes.array, - modalId: React.PropTypes.string, - channelId: React.PropTypes.string, - userId: React.PropTypes.string, + fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, startId: React.PropTypes.number }; @@ -405,10 +344,10 @@ LoadingImagePreview.propTypes = { loading: React.PropTypes.string }; -function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) { +function ImagePreview({fileInfo, fileUrl, maxHeight}) { let previewUrl; if (fileInfo.has_preview_image) { - previewUrl = Utils.getPreviewImagePath(filename); + previewUrl = FileStore.getFilePreviewUrl(fileInfo.id); } else { previewUrl = fileUrl; } @@ -429,10 +368,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) { } ImagePreview.propTypes = { - filename: React.PropTypes.string.isRequired, - fileUrl: React.PropTypes.string.isRequired, fileInfo: React.PropTypes.object.isRequired, + fileUrl: React.PropTypes.string.isRequired, maxHeight: React.PropTypes.number.isRequired }; - -export default injectIntl(ViewImageModal); diff --git a/webapp/sass/components/_files.scss b/webapp/sass/components/_files.scss index 7b7588087..e710838b1 100644 --- a/webapp/sass/components/_files.scss +++ b/webapp/sass/components/_files.scss @@ -166,6 +166,10 @@ margin: 5px 10px 5px 0; position: relative; width: 240px; + + &--placeholder { + visibility: hidden; + } } .post-image__load { diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx index 2692e6959..18a35e1fd 100644 --- a/webapp/stores/file_store.jsx +++ b/webapp/stores/file_store.jsx @@ -16,41 +16,58 @@ class FileStore extends EventEmitter { this.handleEventPayload = this.handleEventPayload.bind(this); this.dispatchToken = AppDispatcher.register(this.handleEventPayload); - this.fileInfo = new Map(); + this.setMaxListeners(600); + + this.fileInfosByPost = new Map(); } addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } - emitChange(filename) { - this.emit(CHANGE_EVENT, filename); + + emitChange() { + this.emit(CHANGE_EVENT); + } + + hasInfosForPost(postId) { + return this.fileInfosByPost.has(postId); + } + + getInfosForPost(postId) { + return this.fileInfosByPost.get(postId); + } + + saveInfos(postId, infos) { + this.fileInfosByPost.set(postId, infos); } - hasInfo(filename) { - return this.fileInfo.has(filename); + getFileUrl(fileId) { + return `/api/v3/files/${fileId}/get`; } - getInfo(filename) { - return this.fileInfo.get(filename); + getFileThumbnailUrl(fileId) { + return `/api/v3/files/${fileId}/get_thumbnail`; } - setInfo(filename, info) { - this.fileInfo.set(filename, info); + getFilePreviewUrl(fileId) { + return `/api/v3/files/${fileId}/get_preview`; } handleEventPayload(payload) { const action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_FILE_INFO: - this.setInfo(action.filename, action.info); - this.emitChange(action.filename); + case ActionTypes.RECEIVED_FILE_INFOS: + // This assumes that all received file infos are for a single post + this.saveInfos(action.postId, action.infos); + this.emitChange(action.postId); break; } } } -export default new FileStore(); +export default new FileStore();
\ No newline at end of file diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx index c5122dd7a..917b86df8 100644 --- a/webapp/stores/notification_store.jsx +++ b/webapp/stores/notification_store.jsx @@ -84,7 +84,7 @@ class NotificationStoreClass extends EventEmitter { if (msgProps.image) { body = username + Utils.localizeMessage('channel_loader.uploadedImage', ' uploaded an image'); } else if (msgProps.otherFile) { - body = Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file'); + body = username + Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file'); } else { body = username + Utils.localizeMessage('channel_loader.something', ' did something new'); } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 62283dacd..22f47fd40 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -224,7 +224,7 @@ class PostStoreClass extends EventEmitter { } else if (combinedPosts.posts.hasOwnProperty(pid)) { combinedPosts.posts[pid] = Object.assign({}, np, { state: Constants.POST_DELETED, - filenames: [] + fileIds: [] }); } } @@ -318,7 +318,7 @@ class PostStoreClass extends EventEmitter { // make sure to copy the post so that component state changes work properly postList.posts[post.id] = Object.assign({}, post, { state: Constants.POST_DELETED, - filenames: [] + fileIds: [] }); } } @@ -514,7 +514,7 @@ class PostStoreClass extends EventEmitter { } getEmptyDraft() { - return {message: '', uploadsInProgress: [], previews: []}; + return {message: '', uploadsInProgress: [], fileInfos: []}; } storeCurrentDraft(draft) { diff --git a/webapp/tests/client_file.test.jsx b/webapp/tests/client_file.test.jsx new file mode 100644 index 000000000..fac70d19c --- /dev/null +++ b/webapp/tests/client_file.test.jsx @@ -0,0 +1,248 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; +import TestHelper from './test_helper.jsx'; + +const fs = require('fs'); + +describe('Client.File', function() { + this.timeout(100000); + + before(function() { + // write a temporary file so that we have something to upload for testing + const buffer = new Buffer('R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', 'base64'); + + const testGif = fs.openSync('test.gif', 'w+'); + fs.writeFileSync(testGif, buffer); + }); + + after(function() { + fs.unlinkSync('test.gif'); + }); + + it('uploadFile', function(done) { + TestHelper.initBasic(() => { + const clientId = TestHelper.generateId(); + + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + clientId, + function(resp) { + assert.equal(resp.file_infos.length, 1); + assert.equal(resp.client_ids.length, 1); + assert.equal(resp.client_ids[0], clientId); + + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getFile', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + TestHelper.basicClient().getFile( + resp.file_infos[0].id, + function() { + done(); + }, + function(err2) { + done(new Error(err2.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getFileThumbnail', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + TestHelper.basicClient().getFileThumbnail( + resp.file_infos[0].id, + function() { + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getFilePreview', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + TestHelper.basicClient().getFilePreview( + resp.file_infos[0].id, + function() { + done(); + }, + function(err2) { + done(new Error(err2.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getFileInfo', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + const fileId = resp.file_infos[0].id; + + TestHelper.basicClient().getFileInfo( + fileId, + function(info) { + assert.equal(info.id, fileId); + assert.equal(info.name, 'test.gif'); + + done(); + }, + function(err2) { + done(new Error(err2.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getPublicLink', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + const post = TestHelper.fakePost(); + post.channel_id = TestHelper.basicChannel().id; + post.file_ids = resp.file_infos.map((info) => info.id); + + TestHelper.basicClient().createPost( + post, + function(data) { + assert.deepEqual(data.file_ids, post.file_ids); + + TestHelper.basicClient().getPublicLink( + post.file_ids[0], + function() { + done(new Error('public links should be disabled by default')); + + // request. + // get(link). + // end(TestHelper.basicChannel().handleResponse.bind( + // this, + // 'getPublicLink', + // function() { + // done(); + // }, + // function(err4) { + // done(new Error(err4.message)); + // } + // )); + }, + function() { + done(); + + // done(new Error(err3.message)); + } + ); + }, + function(err2) { + done(new Error(err2.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getFileInfosForPost', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().uploadFile( + fs.createReadStream('test.gif'), + 'test.gif', + TestHelper.basicChannel().id, + '', + function(resp) { + const post = TestHelper.fakePost(); + post.channel_id = TestHelper.basicChannel().id; + post.file_ids = resp.file_infos.map((info) => info.id); + + TestHelper.basicClient().createPost( + post, + function(data) { + assert.deepEqual(data.file_ids, post.file_ids); + + TestHelper.basicClient().getFileInfosForPost( + post.channel_id, + data.id, + function(files) { + assert.equal(files.length, 1); + assert.equal(files[0].id, resp.file_infos[0].id); + + done(); + }, + function(err3) { + done(new Error(err3.message)); + } + ); + }, + function(err2) { + done(new Error(err2.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); +}); diff --git a/webapp/tests/client_general.test.jsx b/webapp/tests/client_general.test.jsx index 61e7832da..709583c11 100644 --- a/webapp/tests/client_general.test.jsx +++ b/webapp/tests/client_general.test.jsx @@ -43,43 +43,5 @@ describe('Client.General', function() { done(); }); }); - - it('File.getFileInfo', function(done) { - TestHelper.initBasic(() => { - TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error - - TestHelper.basicClient().getFileInfo( - `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`, - function(data) { - assert.equal(data.filename, 'filename.txt'); - done(); - }, - function(err) { - done(new Error(err.message)); - } - ); - }); - }); - - it('File.getPublicLink', function(done) { - TestHelper.initBasic(() => { - TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error - var data = {}; - data.channel_id = TestHelper.basicChannel().id; - data.user_id = TestHelper.basicUser().id; - data.filename = `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`; - - TestHelper.basicClient().getPublicLink( - data, - function() { - done(new Error('not enabled')); - }, - function(err) { - assert.equal(err.id, 'api.file.get_public_link.disabled.app_error'); - done(); - } - ); - }); - }); }); diff --git a/webapp/tests/client_post.test.jsx b/webapp/tests/client_post.test.jsx index 3b9802fb4..afe10931f 100644 --- a/webapp/tests/client_post.test.jsx +++ b/webapp/tests/client_post.test.jsx @@ -230,5 +230,7 @@ describe('Client.Posts', function() { ); }); }); + + // getFileInfosForPost is tested in client_files.test.jsx }); diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 22b223bc5..5441f260c 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -715,6 +715,32 @@ export function getPostsAfter(postId, offset, numPost, isPost) { ); } +export function getFileInfosForPost(channelId, postId) { + const callName = 'getFileInfosForPost' + postId; + + if (isCallInProgress(callName)) { + return; + } + + Client.getFileInfosForPost( + channelId, + postId, + (data) => { + callTracker[callName] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_FILE_INFOS, + postId, + infos: data + }); + }, + (err) => { + callTracker[callName] = 0; + dispatchError(err, 'getPostFile'); + } + ); +} + export function getMe() { if (isCallInProgress('getMe')) { return null; @@ -923,34 +949,6 @@ export function getSuggestedCommands(command, suggestionId, component) { ); } -export function getFileInfo(filename) { - const callName = 'getFileInfo' + filename; - - if (isCallInProgress(callName)) { - return; - } - - callTracker[callName] = utils.getTimestamp(); - - Client.getFileInfo( - filename, - (data) => { - callTracker[callName] = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_FILE_INFO, - filename, - info: data - }); - }, - (err) => { - callTracker[callName] = 0; - - dispatchError(err, 'getFileInfo'); - } - ); -} - export function getStandardAnalytics(teamId) { const callName = 'getStandardAnaytics' + teamId; @@ -1432,8 +1430,8 @@ export function regenCommandToken(id) { ); } -export function getPublicLink(filename, success, error) { - const callName = 'getPublicLink' + filename; +export function getPublicLink(fileId, success, error) { + const callName = 'getPublicLink' + fileId; if (isCallInProgress(callName)) { return; @@ -1442,7 +1440,7 @@ export function getPublicLink(filename, success, error) { callTracker[callName] = utils.getTimestamp(); Client.getPublicLink( - filename, + fileId, (link) => { callTracker[callName] = 0; diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 039d48aaa..2b6e110ce 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -95,7 +95,7 @@ export const ActionTypes = keyMirror({ RECEIVED_PREFERENCE: null, RECEIVED_PREFERENCES: null, DELETED_PREFERENCES: null, - RECEIVED_FILE_INFO: null, + RECEIVED_FILE_INFOS: null, RECEIVED_ANALYTICS: null, RECEIVED_INCOMING_WEBHOOKS: null, diff --git a/webapp/utils/syntax_hightlighting.jsx b/webapp/utils/syntax_hightlighting.jsx index 4db6d11e3..47ba5bd4e 100644 --- a/webapp/utils/syntax_hightlighting.jsx +++ b/webapp/utils/syntax_hightlighting.jsx @@ -136,14 +136,9 @@ export function highlight(lang, code) { return TextFormatting.sanitizeHtml(code); } -export function getLanguageFromFilename(filename) { - const fileSplit = filename.split('.'); - - let ext = fileSplit.length > 1 ? fileSplit[fileSplit.length - 1] : ''; - ext = ext.toLowerCase(); - +export function getLanguageFromFileExtension(extension) { for (var key in HighlightedLanguages) { - if (HighlightedLanguages[key].extensions.find((x) => x === ext)) { + if (HighlightedLanguages[key].extensions.find((x) => x === extension)) { return key; } } diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 25a9dfa7d..5a47b0a63 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -413,8 +413,8 @@ export function getFileType(extin) { return 'other'; } -export function getPreviewImagePathForFileType(fileTypeIn) { - var fileType = fileTypeIn.toLowerCase(); +export function getFileIconPath(fileInfo) { + const fileType = getFileType(fileInfo.extension); var icon; if (fileType in Constants.ICON_FROM_TYPE) { @@ -451,19 +451,6 @@ export function splitFileLocation(fileLocation) { return {ext, name: filename, path: filePath}; } -export function getPreviewImagePath(filename) { - // Returns the path to a preview image that can be used to represent a file. - const fileInfo = splitFileLocation(filename); - const fileType = getFileType(fileInfo.ext); - - if (fileType === 'image') { - return getFileUrl(fileInfo.path + '_preview.jpg'); - } - - // only images have proper previews, so just use a placeholder icon for non-images - return getPreviewImagePathForFileType(fileType); -} - export function toTitleCase(str) { function doTitleCase(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); |