summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-09-30 11:06:30 -0400
committerGitHub <noreply@github.com>2016-09-30 11:06:30 -0400
commit8a0e649f989a824bb3bbfd1900a5b8e5383b47e1 (patch)
tree4b424929fe13ebec438d2f41a2729e37e5160720 /webapp
parenta2deeed597dea15d9b7ca237be71988469f58cdd (diff)
downloadchat-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')
-rw-r--r--webapp/actions/global_actions.jsx5
-rw-r--r--webapp/client/client.jsx71
-rw-r--r--webapp/components/audio_video_preview.jsx12
-rw-r--r--webapp/components/code_preview.jsx18
-rw-r--r--webapp/components/create_comment.jsx36
-rw-r--r--webapp/components/create_post.jsx47
-rw-r--r--webapp/components/file_attachment.jsx251
-rw-r--r--webapp/components/file_attachment_list.jsx55
-rw-r--r--webapp/components/file_attachment_list_container.jsx90
-rw-r--r--webapp/components/file_info_preview.jsx88
-rw-r--r--webapp/components/file_preview.jsx83
-rw-r--r--webapp/components/file_upload.jsx9
-rw-r--r--webapp/components/get_public_link_modal.jsx6
-rw-r--r--webapp/components/pdf_preview.jsx24
-rw-r--r--webapp/components/post_view/components/commented_on_files_message_container.jsx88
-rw-r--r--webapp/components/post_view/components/post_body.jsx31
-rw-r--r--webapp/components/rhs_comment.jsx12
-rw-r--r--webapp/components/rhs_root_post.jsx12
-rw-r--r--webapp/components/view_image.jsx140
-rw-r--r--webapp/sass/components/_files.scss4
-rw-r--r--webapp/stores/file_store.jsx43
-rw-r--r--webapp/stores/notification_store.jsx2
-rw-r--r--webapp/stores/post_store.jsx6
-rw-r--r--webapp/tests/client_file.test.jsx248
-rw-r--r--webapp/tests/client_general.test.jsx38
-rw-r--r--webapp/tests/client_post.test.jsx2
-rw-r--r--webapp/utils/async_client.jsx60
-rw-r--r--webapp/utils/constants.jsx2
-rw-r--r--webapp/utils/syntax_hightlighting.jsx9
-rw-r--r--webapp/utils/utils.jsx17
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();