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
Этот коммит содержится в:
Harrison Healey
2016-09-30 11:06:30 -04:00
коммит произвёл GitHub
родитель a2deeed597
Коммит 8a0e649f98
60 изменённых файлов: 3454 добавлений и 2062 удалений

Просмотреть файл

@@ -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: {}
};

Просмотреть файл

@@ -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,34 +1524,20 @@ 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) {
request.
get(`${this.getFilesRoute()}/get_info${filename}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getFileInfo', success, error));
}
getPublicLink(filename, success, error) {
const data = {
filename
};
request.
post(`${this.getFilesRoute()}/get_public_link`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send(data).
end(this.handleResponse.bind(this, 'getPublicLink', success, error));
}
uploadFile(file, filename, channelId, clientId, success, error) {
return request.
post(`${this.getFilesRoute()}/upload`).
post(`${this.getTeamFilesRoute()}/upload`).
set(this.defaultHeaders).
attach('files', file, filename).
field('channel_id', channelId).
@@ -1556,6 +1546,51 @@ export default class Client {
end(this.handleResponse.bind(this, 'uploadFile', success, error));
}
getFile(fileId, success, error) {
request.
get(`${this.getFileRoute(fileId)}/get`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getFile', success, error));
}
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.
get(`${this.getFileRoute(fileId)}/get`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getFilePreview', success, error));
}
getFileInfo(fileId, success, error) {
request.
get(`${this.getFileRoute(fileId)}/get_info`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
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
registerOAuthApp(app, success, error) {

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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}
/>

Просмотреть файл

@@ -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}
/>

Просмотреть файл

@@ -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();
}
componentWillReceiveProps(nextProps) {
if (nextProps.fileInfo.id !== this.props.fileInfo.id) {
this.setState({
loaded: Utils.getFileType(nextProps.fileInfo.extension) !== 'image'
});
}
}
componentDidUpdate(prevProps) {
if (this.props.filename !== prevProps.filename) {
if (!this.state.loaded && this.props.fileInfo.id !== prevProps.fileInfo.id) {
this.loadFiles();
}
}
loadFiles() {
this.canSetState = true;
const fileInfo = this.props.fileInfo;
const fileType = Utils.getFileType(fileInfo.extension);
var filename = this.props.filename;
if (fileType === 'image') {
const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id);
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)));
}
const img = new Image();
img.onload = () => {
this.setState({loaded: true});
};
img.load(thumbnailUrl);
}
}
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;
}
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;
}
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');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
}
}
removeBackgroundImage(name) {
if (name in this.refs) {
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
}
}
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);
};

Просмотреть файл

@@ -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});
}
render() {
var filenames = this.props.filenames;
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}
/>
);
handleImageClick(indexClicked) {
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
}
render() {
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];
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
};

Просмотреть файл

@@ -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}
/>
);
}
}

Просмотреть файл

@@ -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';
export default class FileInfoPreview extends React.Component {
shouldComponentUpdate(nextProps) {
if (nextProps.fileUrl !== this.props.fileUrl) {
return true;
}
const holders = defineMessages({
type: {
id: 'file_info_preview.type',
defaultMessage: 'File type '
},
size: {
id: 'file_info_preview.size',
defaultMessage: 'Size '
}
});
if (!Utils.areObjectsEqual(nextProps.fileInfo, this.props.fileInfo)) {
return true;
}
export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMessage}) {
// non-image files include a section providing details about the file
const infoParts = [];
if (fileInfo.extension !== '') {
infoParts.push(formatMessage(holders.type) + fileInfo.extension.toUpperCase());
return false;
}
infoParts.push(formatMessage(holders.size) + Utils.fileSizeToString(fileInfo.size));
render() {
const fileInfo = this.props.fileInfo;
const fileUrl = this.props.fileUrl;
const infoString = infoParts.join(', ');
// non-image files include a section providing details about the file
const infoParts = [];
const name = decodeURIComponent(Utils.getFileName(filename));
if (fileInfo.extension !== '') {
infoParts.push(Utils.localizeMessage('file_info_preview.type', 'File type ') + fileInfo.extension.toUpperCase());
}
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>
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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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}));
}
}

Просмотреть файл

@@ -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: ''
});
}

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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>;
}
}

Просмотреть файл

@@ -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}
/>
);

Просмотреть файл

@@ -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}
/>
);

Просмотреть файл

@@ -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}
/>
);

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -166,6 +166,10 @@
margin: 5px 10px 5px 0;
position: relative;
width: 240px;
&--placeholder {
visibility: hidden;
}
}
.post-image__load {

Просмотреть файл

@@ -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);
}
hasInfo(filename) {
return this.fileInfo.has(filename);
hasInfosForPost(postId) {
return this.fileInfosByPost.has(postId);
}
getInfo(filename) {
return this.fileInfo.get(filename);
getInfosForPost(postId) {
return this.fileInfosByPost.get(postId);
}
setInfo(filename, info) {
this.fileInfo.set(filename, info);
saveInfos(postId, infos) {
this.fileInfosByPost.set(postId, infos);
}
getFileUrl(fileId) {
return `/api/v3/files/${fileId}/get`;
}
getFileThumbnailUrl(fileId) {
return `/api/v3/files/${fileId}/get_thumbnail`;
}
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();

Просмотреть файл

@@ -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');
}

Просмотреть файл

@@ -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) {

248
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));
}
);
});
});
});

Просмотреть файл

@@ -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();
}
);
});
});
});

Просмотреть файл

@@ -230,5 +230,7 @@ describe('Client.Posts', function() {
);
});
});
// getFileInfosForPost is tested in client_files.test.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;

Просмотреть файл

@@ -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,

Просмотреть файл

@@ -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;
}
}

Просмотреть файл

@@ -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();