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