diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 91aef4c39..6e52d8891 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -13,6 +13,7 @@ import {
selectChannel,
leaveChannel as serviceLeaveChannel} from 'service/actions/channels';
import {getPosts} from 'service/actions/posts';
+import {getFilesForPost} from 'service/actions/files';
import {savePreferences, deletePreferences} from 'service/actions/preferences';
import {getTeamMembersByIds} from 'service/actions/teams';
import {Constants, UsersTypes} from 'service/constants';
@@ -98,6 +99,18 @@ export function loadPostsIfNecessary(channel) {
};
}
+export function loadFilesForPostIfNecessary(post) {
+ return async (dispatch, getState) => {
+ const {files, teams} = getState().entities;
+ const fileIdsForPost = files.fileIdsByPostId[post.id];
+
+ if (!fileIdsForPost) {
+ const teamId = teams.currentId;
+ await getFilesForPost(teamId, post.channel_id, post.id)(dispatch, getState);
+ }
+ };
+}
+
export function selectInitialChannel(teamId) {
return async (dispatch, getState) => {
const state = getState();
diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js
new file mode 100644
index 000000000..725544b19
--- /dev/null
+++ b/app/components/file_attachment_list/file_attachment.js
@@ -0,0 +1,107 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {
+ Component,
+ PropTypes
+} from 'react';
+
+import {
+ Text,
+ View,
+ StyleSheet
+} from 'react-native';
+
+import Icon from 'react-native-vector-icons/FontAwesome';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import * as Utils from 'service/utils/file_utils.js';
+
+import FileAttachmentIcon from './file_attachment_icon';
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ downloadIcon: {
+ color: changeOpacity(theme.centerChannelColor, 0.7),
+ marginRight: 5
+ },
+ fileDownloadContainer: {
+ flexDirection: 'row',
+ marginTop: 3
+ },
+ fileInfo: {
+ marginLeft: 2,
+ fontSize: 14,
+ color: changeOpacity(theme.centerChannelColor, 0.5)
+ },
+ fileInfoContainer: {
+ flex: 1,
+ paddingHorizontal: 8,
+ paddingVertical: 5,
+ borderLeftWidth: 1,
+ borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2)
+ },
+ fileName: {
+ flexDirection: 'column',
+ flexWrap: 'wrap',
+ marginLeft: 2,
+ fontSize: 14,
+ color: theme.centerChannelColor
+ },
+ fileWrapper: {
+ flex: 1,
+ flexDirection: 'row',
+ marginTop: 10,
+ borderWidth: 1,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2)
+ }
+ });
+});
+
+export default class FileAttachment extends Component {
+ static propTypes = {
+ file: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired
+ };
+
+ renderFileInfo() {
+ const {file, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+ {file.name.trim()}
+
+
+
+
+ {`${file.extension.toUpperCase()} ${Utils.getFormattedFileSize(file)}`}
+
+
+
+ );
+ }
+
+ render() {
+ const {file, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+ {this.renderFileInfo()}
+
+ );
+ }
+}
diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js
new file mode 100644
index 000000000..86d93f6db
--- /dev/null
+++ b/app/components/file_attachment_list/file_attachment_icon.js
@@ -0,0 +1,76 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {
+ Component,
+ PropTypes
+} from 'react';
+
+import {
+ View,
+ Image,
+ StyleSheet
+} from 'react-native';
+
+import * as Utils from 'service/utils/file_utils';
+
+import audioIcon from 'assets/images/icons/audio.png';
+import codeIcon from 'assets/images/icons/code.png';
+import excelIcon from 'assets/images/icons/excel.png';
+import genericIcon from 'assets/images/icons/generic.png';
+import imageIcon from 'assets/images/icons/image.png';
+import patchIcon from 'assets/images/icons/patch.png';
+import pdfIcon from 'assets/images/icons/pdf.png';
+import pptIcon from 'assets/images/icons/ppt.png';
+import videoIcon from 'assets/images/icons/video.png';
+import wordIcon from 'assets/images/icons/word.png';
+
+const styles = StyleSheet.create({
+ fileIcon: {
+ width: 60,
+ height: 60
+ },
+ fileIconWrapper: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 100,
+ height: 100,
+ backgroundColor: '#fff'
+ }
+});
+
+const ICON_PATH_FROM_FILE_TYPE = {
+ audio: audioIcon,
+ code: codeIcon,
+ image: imageIcon,
+ other: genericIcon,
+ patch: patchIcon,
+ pdf: pdfIcon,
+ presentation: pptIcon,
+ spreadsheet: excelIcon,
+ video: videoIcon,
+ word: wordIcon
+};
+
+export default class FileAttachmentIcon extends Component {
+ static propTypes = {
+ file: PropTypes.object.isRequired
+ };
+
+ getFileIconPath(file) {
+ const fileType = Utils.getFileType(file);
+ return ICON_PATH_FROM_FILE_TYPE[fileType] || ICON_PATH_FROM_FILE_TYPE.other;
+ }
+
+ render() {
+ const {file} = this.props;
+ return (
+
+
+
+ );
+ }
+}
diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js
new file mode 100644
index 000000000..085ba6f9c
--- /dev/null
+++ b/app/components/file_attachment_list/file_attachment_list.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {
+ Component,
+ PropTypes
+} from 'react';
+
+import {
+ View
+} from 'react-native';
+
+import FileAttachment from './file_attachment';
+
+export default class FileAttachmentList extends Component {
+ static propTypes = {
+ actions: PropTypes.object.isRequired,
+ post: PropTypes.object.isRequired,
+ files: PropTypes.array.isRequired,
+ theme: PropTypes.object.isRequired
+ };
+
+ componentDidMount() {
+ const {post} = this.props;
+ this.props.actions.loadFilesForPostIfNecessary(post);
+ }
+
+ render() {
+ const fileAttachments = this.props.files.map((file) => (
+
+ ));
+
+ return (
+
+ {fileAttachments}
+
+ );
+ }
+}
diff --git a/app/components/file_attachment_list/file_attachment_list_container.js b/app/components/file_attachment_list/file_attachment_list_container.js
new file mode 100644
index 000000000..1c20bb733
--- /dev/null
+++ b/app/components/file_attachment_list/file_attachment_list_container.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {makeGetFilesForPost} from 'service/selectors/entities/files';
+import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
+import {getTheme} from 'service/selectors/entities/preferences';
+
+import FileAttachmentList from './file_attachment_list';
+
+function makeMapStateToProps() {
+ const getFilesForPost = makeGetFilesForPost();
+ return function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ files: getFilesForPost(state, ownProps),
+ theme: getTheme(state)
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ loadFilesForPostIfNecessary
+ }, dispatch)
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(FileAttachmentList);
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 273322e9a..5163c3f12 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -14,6 +14,7 @@ import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import MattermostIcon from 'app/components/mattermost_icon';
import ProfilePicture from 'app/components/profile_picture';
+import FileAttachmentList from 'app/components/file_attachment_list/file_attachment_list_container';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {isSystemMessage} from 'service/utils/post_utils.js';
@@ -166,6 +167,16 @@ export default class Post extends React.Component {
return ;
}
+ renderFileAttachments() {
+ const {post} = this.props;
+ const fileIds = post.file_ids || [];
+ let attachments;
+ if (fileIds.length > 0) {
+ attachments = ();
+ }
+ return attachments;
+ }
+
render() {
const style = getStyleSheet(this.props.theme);
const PROFILE_PICTURE_SIZE = 32;
@@ -251,6 +262,7 @@ export default class Post extends React.Component {
{this.renderReplyBar(style)}
+ {this.renderFileAttachments()}
{this.props.post.message}
@@ -275,6 +287,7 @@ export default class Post extends React.Component {
+ {this.renderFileAttachments()}
{this.props.post.message}
diff --git a/assets/base/images/icons/audio.png b/assets/base/images/icons/audio.png
new file mode 100644
index 000000000..93cf6ef33
Binary files /dev/null and b/assets/base/images/icons/audio.png differ
diff --git a/assets/base/images/icons/code.png b/assets/base/images/icons/code.png
new file mode 100644
index 000000000..94ee7bf40
Binary files /dev/null and b/assets/base/images/icons/code.png differ
diff --git a/assets/base/images/icons/excel.png b/assets/base/images/icons/excel.png
new file mode 100644
index 000000000..58a059ce2
Binary files /dev/null and b/assets/base/images/icons/excel.png differ
diff --git a/assets/base/images/icons/generic.png b/assets/base/images/icons/generic.png
new file mode 100644
index 000000000..a142d4f3d
Binary files /dev/null and b/assets/base/images/icons/generic.png differ
diff --git a/assets/base/images/icons/image.png b/assets/base/images/icons/image.png
new file mode 100644
index 000000000..39c36f30a
Binary files /dev/null and b/assets/base/images/icons/image.png differ
diff --git a/assets/base/images/icons/patch.png b/assets/base/images/icons/patch.png
new file mode 100644
index 000000000..31478971e
Binary files /dev/null and b/assets/base/images/icons/patch.png differ
diff --git a/assets/base/images/icons/pdf.png b/assets/base/images/icons/pdf.png
new file mode 100644
index 000000000..f6bec396e
Binary files /dev/null and b/assets/base/images/icons/pdf.png differ
diff --git a/assets/base/images/icons/ppt.png b/assets/base/images/icons/ppt.png
new file mode 100644
index 000000000..3a082df19
Binary files /dev/null and b/assets/base/images/icons/ppt.png differ
diff --git a/assets/base/images/icons/video.png b/assets/base/images/icons/video.png
new file mode 100644
index 000000000..d3e6c38d2
Binary files /dev/null and b/assets/base/images/icons/video.png differ
diff --git a/assets/base/images/icons/word.png b/assets/base/images/icons/word.png
new file mode 100644
index 000000000..2096dc609
Binary files /dev/null and b/assets/base/images/icons/word.png differ
diff --git a/package.json b/package.json
index fc796900a..e280c98ee 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"eslint-plugin-mocha": "4.8.0",
"eslint-plugin-react": "6.8.0",
"fetch-mock": "5.8.1",
+ "form-data": "2.1.2",
"mocha": "3.2.0",
"react-addons-test-utils": "15.4.2",
"react-dom": "15.4.2",
diff --git a/service/actions/files.js b/service/actions/files.js
new file mode 100644
index 000000000..8f03cdb2a
--- /dev/null
+++ b/service/actions/files.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {batchActions} from 'redux-batched-actions';
+
+import Client from 'service/client';
+import {FilesTypes} from 'service/constants';
+import {forceLogoutIfNecessary} from './helpers';
+
+export function getFilesForPost(teamId, channelId, postId) {
+ return async (dispatch, getState) => {
+ dispatch({type: FilesTypes.FETCH_FILES_FOR_POST_REQUEST}, getState);
+ let files;
+
+ try {
+ files = await Client.getFileInfosForPost(teamId, channelId, postId);
+ } catch (error) {
+ forceLogoutIfNecessary(error, dispatch);
+ dispatch({type: FilesTypes.FETCH_FILES_FOR_POST_FAILURE, error}, getState);
+ return;
+ }
+
+ dispatch(batchActions([
+ {
+ type: FilesTypes.RECEIVED_FILES_FOR_POST,
+ data: files,
+ postId
+ },
+ {
+ type: FilesTypes.FETCH_FILES_FOR_POST_SUCCESS
+ }
+ ]), getState);
+ };
+}
diff --git a/service/client/client.js b/service/client/client.js
index 1e7cd3413..4a29dbe91 100644
--- a/service/client/client.js
+++ b/service/client/client.js
@@ -127,9 +127,13 @@ export default class Client {
headers[HEADER_AUTH] = `${HEADER_BEARER} ${this.token}`;
}
+ if (options.headers) {
+ Object.assign(headers, options.headers);
+ }
+
return {
- headers,
- ...options
+ ...options,
+ headers
};
}
@@ -613,6 +617,26 @@ export default class Client {
);
};
+ getFileInfosForPost = async (teamId, channelId, postId) => {
+ return this.doFetch(
+ `${this.getChannelNeededRoute(teamId, channelId)}/posts/${postId}/get_file_infos`,
+ {method: 'get'}
+ );
+ };
+
+ uploadFile = async (teamId, channelId, clientId, fileFormData, formBoundary) => {
+ return this.doFetch(
+ `${this.getTeamNeededRoute(teamId)}/files/upload`,
+ {
+ method: 'post',
+ headers: {
+ 'Content-Type': `multipart/form-data; boundary=${formBoundary}`
+ },
+ body: fileFormData
+ }
+ );
+ };
+
// Preferences routes
getMyPreferences = async () => {
return this.doFetch(
diff --git a/service/constants/constants.js b/service/constants/constants.js
index f376bff73..f966a1625 100644
--- a/service/constants/constants.js
+++ b/service/constants/constants.js
@@ -37,4 +37,19 @@ const Constants = {
POST_PURPOSE_CHANGE: 'system_purpose_change'
};
-export default Constants;
+const FileConstants = {
+ AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
+ CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'txt', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'],
+ IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
+ PATCH_TYPES: ['patch'],
+ PDF_TYPES: ['pdf'],
+ PRESENTATION_TYPES: ['ppt', 'pptx'],
+ SPREADSHEET_TYPES: ['xlsx', 'csv'],
+ VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
+ WORD_TYPES: ['doc', 'docx']
+};
+
+export default {
+ ...Constants,
+ ...FileConstants
+};
diff --git a/service/constants/files.js b/service/constants/files.js
new file mode 100644
index 000000000..81c9f4c56
--- /dev/null
+++ b/service/constants/files.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import keyMirror from 'service/utils/key_mirror';
+
+const FilesTypes = keyMirror({
+ FETCH_FILES_FOR_POST_REQUEST: null,
+ FETCH_FILES_FOR_POST_SUCCESS: null,
+ FETCH_FILES_FOR_POST_FAILURE: null,
+
+ RECEIVED_FILES_FOR_POST: null
+});
+
+export default FilesTypes;
diff --git a/service/constants/index.js b/service/constants/index.js
index d83974f3d..9d07cdfb9 100644
--- a/service/constants/index.js
+++ b/service/constants/index.js
@@ -7,6 +7,7 @@ import GeneralTypes from './general';
import UsersTypes from './users';
import TeamsTypes from './teams';
import PostsTypes from './posts';
+import FilesTypes from './files';
import PreferencesTypes from './preferences';
import RequestStatus from './request_status';
import WebsocketEvents from './websocket';
@@ -23,6 +24,7 @@ export {
TeamsTypes,
ChannelTypes,
PostsTypes,
+ FilesTypes,
PreferencesTypes,
Preferences,
RequestStatus,
diff --git a/service/reducers/entities/files.js b/service/reducers/entities/files.js
new file mode 100644
index 000000000..bc9a3bad8
--- /dev/null
+++ b/service/reducers/entities/files.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {combineReducers} from 'redux';
+import {FilesTypes, UsersTypes} from 'service/constants';
+
+function files(state = {}, action) {
+ switch (action.type) {
+ case FilesTypes.RECEIVED_FILES_FOR_POST: {
+ const filesById = action.data.reduce((filesMap, file) => {
+ return {...filesMap,
+ [file.id]: file
+ };
+ }, {});
+ return {...state,
+ ...filesById
+ };
+ }
+
+ case UsersTypes.LOGOUT_SUCCESS:
+ return {};
+ default:
+ return state;
+ }
+}
+
+function fileIdsByPostId(state = {}, action) {
+ switch (action.type) {
+ case FilesTypes.RECEIVED_FILES_FOR_POST: {
+ const {data, postId} = action;
+ const filesIdsForPost = data.map((file) => file.id);
+ return {...state,
+ [postId]: filesIdsForPost
+ };
+ }
+
+ case UsersTypes.LOGOUT_SUCCESS:
+ return {};
+ default:
+ return state;
+ }
+}
+
+export default combineReducers({
+ files,
+ fileIdsByPostId
+});
diff --git a/service/reducers/entities/index.js b/service/reducers/entities/index.js
index 03753e6c9..7f90ef8ae 100644
--- a/service/reducers/entities/index.js
+++ b/service/reducers/entities/index.js
@@ -8,6 +8,7 @@ import general from './general';
import users from './users';
import teams from './teams';
import posts from './posts';
+import files from './files';
import preferences from './preferences';
export default combineReducers({
@@ -16,5 +17,6 @@ export default combineReducers({
teams,
channels,
posts,
+ files,
preferences
});
diff --git a/service/reducers/requests/files.js b/service/reducers/requests/files.js
new file mode 100644
index 000000000..044dd7b94
--- /dev/null
+++ b/service/reducers/requests/files.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {combineReducers} from 'redux';
+
+import {handleRequest, initialRequestState} from './helpers';
+import {FilesTypes} from 'service/constants';
+
+function getFilesForPost(state = initialRequestState(), action) {
+ return handleRequest(
+ FilesTypes.FETCH_FILES_FOR_POST_REQUEST,
+ FilesTypes.FETCH_FILES_FOR_POST_SUCCESS,
+ FilesTypes.FETCH_FILES_FOR_POST_FAILURE,
+ state,
+ action
+ );
+}
+
+export default combineReducers({
+ getFilesForPost
+});
diff --git a/service/reducers/requests/index.js b/service/reducers/requests/index.js
index 08bcf4103..b65caeda9 100644
--- a/service/reducers/requests/index.js
+++ b/service/reducers/requests/index.js
@@ -4,6 +4,7 @@
import {combineReducers} from 'redux';
import channels from './channels';
+import files from './files';
import general from './general';
import posts from './posts';
import teams from './teams';
@@ -12,6 +13,7 @@ import preferences from './preferences';
export default combineReducers({
channels,
+ files,
general,
posts,
teams,
diff --git a/service/selectors/entities/files.js b/service/selectors/entities/files.js
new file mode 100644
index 000000000..f0e6f448e
--- /dev/null
+++ b/service/selectors/entities/files.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {createSelector} from 'reselect';
+
+function getAllFiles(state) {
+ return state.entities.files.files;
+}
+
+function getFilesIdsForPost(state, props) {
+ return state.entities.files.fileIdsByPostId[props.post.id] || [];
+}
+
+export function makeGetFilesForPost() {
+ return createSelector(
+ [getAllFiles, getFilesIdsForPost],
+ (allFiles, fileIdsForPost) => {
+ return fileIdsForPost.map((id) => allFiles[id]);
+ }
+ );
+}
diff --git a/service/utils/file_utils.js b/service/utils/file_utils.js
new file mode 100644
index 000000000..55b73f7a7
--- /dev/null
+++ b/service/utils/file_utils.js
@@ -0,0 +1,42 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {Constants} from 'service/constants';
+
+export function getFormattedFileSize(file) {
+ const bytes = file.size;
+ const fileSizes = [
+ ['TB', 1024 * 1024 * 1024 * 1024],
+ ['GB', 1024 * 1024 * 1024],
+ ['MB', 1024 * 1024],
+ ['KB', 1024]
+ ];
+ const size = fileSizes.find((unitAndMinBytes) => {
+ const minBytes = unitAndMinBytes[1];
+ return bytes > minBytes;
+ });
+ if (size) {
+ return `${Math.floor(bytes / size[1])} ${size[0]}`;
+ }
+ return `${bytes} B`;
+}
+
+export function getFileType(file) {
+ const fileExt = file.extension.toLowerCase();
+ const fileTypes = [
+ 'image',
+ 'code',
+ 'pdf',
+ 'video',
+ 'audio',
+ 'spreadsheet',
+ 'word',
+ 'presentation',
+ 'patch'
+ ];
+ return fileTypes.find((fileType) => {
+ const constForFileTypeExtList = `${fileType}_types`.toUpperCase();
+ const fileTypeExts = Constants[constForFileTypeExtList];
+ return fileTypeExts.indexOf(fileExt) > -1;
+ }) || 'other';
+}
diff --git a/test/assets/images/test.png b/test/assets/images/test.png
new file mode 100644
index 000000000..13ede8580
Binary files /dev/null and b/test/assets/images/test.png differ
diff --git a/test/service/actions/files.test.js b/test/service/actions/files.test.js
new file mode 100644
index 000000000..3c44b7ecc
--- /dev/null
+++ b/test/service/actions/files.test.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import fs from 'fs';
+import assert from 'assert';
+
+const FormData = require('form-data');
+
+import * as Actions from 'service/actions/files';
+import Client from 'service/client';
+import configureStore from 'app/store';
+import {RequestStatus} from 'service/constants';
+import TestHelper from 'test/test_helper';
+
+describe('Actions.Files', () => {
+ let store;
+ before(async () => {
+ await TestHelper.initBasic(Client);
+ });
+
+ beforeEach(() => {
+ store = configureStore();
+ });
+
+ after(async () => {
+ await TestHelper.basicClient.logout();
+ });
+
+ it('getFilesForPost', async () => {
+ const {basicClient, basicTeam, basicChannel} = TestHelper;
+ const testFileName = 'test.png';
+ const testImageData = fs.createReadStream(`test/assets/images/${testFileName}`);
+ const clientId = TestHelper.generateId();
+
+ const imageFormData = new FormData();
+ imageFormData.append('files', testImageData);
+ imageFormData.append('channel_id', basicChannel.id);
+ imageFormData.append('client_ids', clientId);
+ const formBoundary = imageFormData.getBoundary();
+
+ const fileUploadResp = await basicClient.
+ uploadFile(basicTeam.id, basicChannel.id, clientId, imageFormData, formBoundary);
+ const fileId = fileUploadResp.file_infos[0].id;
+
+ const fakePostForFile = TestHelper.fakePost(basicChannel.id);
+ fakePostForFile.file_ids = [fileId];
+ const postForFile = await basicClient.createPost(basicTeam.id, fakePostForFile);
+
+ await Actions.getFilesForPost(
+ basicTeam.id, basicChannel.id, postForFile.id
+ )(store.dispatch, store.getState);
+
+ const filesRequest = store.getState().requests.files.getFilesForPost;
+ const {files: allFiles, fileIdsByPostId} = store.getState().entities.files;
+
+ if (filesRequest.status === RequestStatus.FAILURE) {
+ throw new Error(JSON.stringify(filesRequest.error));
+ }
+
+ assert.ok(allFiles);
+ assert.ok(allFiles[fileId]);
+ assert.equal(allFiles[fileId].id, fileId);
+ assert.equal(allFiles[fileId].name, testFileName);
+
+ assert.ok(fileIdsByPostId);
+ assert.ok(fileIdsByPostId[postForFile.id]);
+ assert.equal(fileIdsByPostId[postForFile.id][0], fileId);
+ });
+});