From 3b7a6df617a4fd5e48951002e01322cedd9d2036 Mon Sep 17 00:00:00 2001 From: Chris Duarte Date: Fri, 17 Feb 2017 14:40:40 -0800 Subject: [PATCH] PLT-5530 Render placeholder icons + metadata for file attachments in posts list (#249) * Add icons for file types * Compress image files for file type icons * Add constants for file types * Add utils for file metadata * Add actions & reducers for fetching file attachments * Render file attachments in posts * Add reducers for handling files requests * Refactor getFileType in file utils * Refactor getFileIconPath in file utils * Refactor getFormattedFileSize in file utils * Trim trailing whitespace in getTruncatedFilename * Style file attachment metadata * Remove entity store reducer for files fetch failure * Change filesForPost to fileIdsForPost in files store * Use a selector for getFilesForPost * Memoize getFilesForPost selector * Dispatch postId with getFilesForPost * Add test for getFilesForPost * Upload seed file in getFilesForPost test * Display correct icon for image attachments * Fix reducers for receiving files for post Expect response data to be array not object * Style attachment post * Merge headers (defaults + opts) in Client.doFetch * Upload file as FormData * Use form-data lib instead of built-in polyfill * Associate file with post in getFilesForPost test * Improve assertions in getFilesForPost test * Check for createPost failure in getFilesForPost test * Use correct post id in getFilesForPost test * Use client not action to create post in getFilesForPost test * Fix file upload in getFilesForPost test * Add assertion for name of uploaded file * Update attachment post style * Fix spelling of loadFilesForPostIfNecessary * Remove eslint-disable no-magic-numbers from file_utils.js * Remove unused index prop from FileAttachment * Clarify code for merging headers in Client.getOptions * Dynamically truncate file attachment names * Extract component for FileAttachmentIcon * Use white bg for FileAttachmentIcon to match webapp styles * Match webapp styles for file attachment borders * Move file icon path lookup to FileAttachmentIcon * Use more logical ordering for file type lookup --- app/actions/views/channel.js | 13 +++ .../file_attachment_list/file_attachment.js | 107 ++++++++++++++++++ .../file_attachment_icon.js | 76 +++++++++++++ .../file_attachment_list.js | 43 +++++++ .../file_attachment_list_container.js | 32 ++++++ app/components/post/post.js | 13 +++ assets/base/images/icons/audio.png | Bin 0 -> 1691 bytes assets/base/images/icons/code.png | Bin 0 -> 1393 bytes assets/base/images/icons/excel.png | Bin 0 -> 477 bytes assets/base/images/icons/generic.png | Bin 0 -> 2262 bytes assets/base/images/icons/image.png | Bin 0 -> 1012 bytes assets/base/images/icons/patch.png | Bin 0 -> 1506 bytes assets/base/images/icons/pdf.png | Bin 0 -> 2301 bytes assets/base/images/icons/ppt.png | Bin 0 -> 2018 bytes assets/base/images/icons/video.png | Bin 0 -> 662 bytes assets/base/images/icons/word.png | Bin 0 -> 617 bytes package.json | 1 + service/actions/files.js | 34 ++++++ service/client/client.js | 28 ++++- service/constants/constants.js | 17 ++- service/constants/files.js | 14 +++ service/constants/index.js | 2 + service/reducers/entities/files.js | 47 ++++++++ service/reducers/entities/index.js | 2 + service/reducers/requests/files.js | 21 ++++ service/reducers/requests/index.js | 2 + service/selectors/entities/files.js | 21 ++++ service/utils/file_utils.js | 42 +++++++ test/assets/images/test.png | Bin 0 -> 5054 bytes test/service/actions/files.test.js | 69 +++++++++++ 30 files changed, 581 insertions(+), 3 deletions(-) create mode 100644 app/components/file_attachment_list/file_attachment.js create mode 100644 app/components/file_attachment_list/file_attachment_icon.js create mode 100644 app/components/file_attachment_list/file_attachment_list.js create mode 100644 app/components/file_attachment_list/file_attachment_list_container.js create mode 100644 assets/base/images/icons/audio.png create mode 100644 assets/base/images/icons/code.png create mode 100644 assets/base/images/icons/excel.png create mode 100644 assets/base/images/icons/generic.png create mode 100644 assets/base/images/icons/image.png create mode 100644 assets/base/images/icons/patch.png create mode 100644 assets/base/images/icons/pdf.png create mode 100644 assets/base/images/icons/ppt.png create mode 100644 assets/base/images/icons/video.png create mode 100644 assets/base/images/icons/word.png create mode 100644 service/actions/files.js create mode 100644 service/constants/files.js create mode 100644 service/reducers/entities/files.js create mode 100644 service/reducers/requests/files.js create mode 100644 service/selectors/entities/files.js create mode 100644 service/utils/file_utils.js create mode 100644 test/assets/images/test.png create mode 100644 test/service/actions/files.test.js 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 0000000000000000000000000000000000000000..93cf6ef33ba799f82c80e69c6b17e8b91da33a03 GIT binary patch literal 1691 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 literal 0 HcmV?d00001 diff --git a/assets/base/images/icons/excel.png b/assets/base/images/icons/excel.png new file mode 100644 index 0000000000000000000000000000000000000000..58a059ce225ff93438ef7127c735024ec5855063 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P3?%t>9eV(z%mREuT!FOCq!?0>*(>8IK%GJ* zL4Lsu4hjYT@1K7!u)jWFegAxe1mAqeC(9Wa7+pMF978G?k6dHTJ8B@%@^GW9*}eap zo>pyHHNRr{euFtRijVhmZ{~=8B4jaT+x@Z|efL?Pv-ZEMeDeILWag*;%5V2hTM(TcHB%ENuPi2D&yQzJ58wJxa=&^ zx@8waFMKo1vq-rhwwrSy?>3=ZN!kl_w+YRrr`>_A*2&Yum)sPey<5LKxaf%A`TY7ax%rXx z`P=)}GBh+OaAi6EKbytKU_SZ%@0V%}917;r@m&4wt*zggKRC3YQ|GmqGM@zB-Om&b P3~dHaS3j3^P6?hAII@o+q=iMZQHi(b8g$VZQHhO+cwtToqc|fb<#WkOgc?E$#s3-wn;zBnY824 za+qO;8D^Mah8bpHGq|^- zlfr29)}yQaw?cz^ii#wmqxS!`ZE$a+<4WxudeBH{h)d`!y3>eA#+CFtHyxcqB|aYK z2)e?^$icnU2i!fVDS8Se{LjBvm!o&lP?Rrl>~$zIP*LtiozP6w3*GBy+~w#k6wG;^ z%TP~LN<2^RK!XiK9gz2bE<_RfMgaGOz>5=~`!N5f(J|sV-whPpa5T~4LX@C;IgfRV z0PbXzB4Hhuq0Pj9pW*@mcO3mkniO0ws)vFkt|y6z+}PkEG+Q!mDf-@~bzMRDah;A5 z-HJPw0Mg|GxW|dO-`c#srwD&|Rd?ebR1{nwcYcj^lL34Hd+V zkc9iagKJOZoac6f3tV(m0bJ7Hd?zbiSKQG=+!+kq8wGHGP~nTQesD?X~h&#su_Ye{Hcg1m^Mumhmbyf{`2oZNK1NU0naOrP`{EpV3lV~n_TJTs; zq8!4S_Wk=w00#FD8`hMBmXQ9hW*_SblucMuF1k-8+`&ZLc^0?_iMZu#SW^O8d4Y}g z57~s9hh9<%cMuVGJ_Gj}MR4OeAKx&96E{l?*Wi}76PKeU7dY1A0=O@$ggcOkyTAhX z01>x>O}I-5^XM-TH(xc}0Yuz|4BV>~!Hpy0deNg6xS0l5L0m$eB;vlJ5^jIug;*pJ zx1vqBONeV4flb z+{MIs^kd*Ysyl8!;)PgjfxAxu+!zOO3su4ml8Aey0B&W4aDy3psR9l+RuXO%n{XFN z!hKi(_cfJp`w}n25)0hDn&S31xbktU+OVc*iMS65;J&UBZXW^MIL2$sI^%{&!hJ&} z+}^|sv6O*(13F4PRyCV&7ZT?&fPwoU0YgnOR=?z^gesz^R{4;9C8U*>%bgUfK=Q|&=x z>j(?jj1oAHU5oRW??}d-O~f5)fqPB_S0&)`#Low~Lh?PXB`DTG++h-N-&YMc4W$zn z@EGy%woQtqyqA%f?mkVr4A8CPmMtxkuAUsvBmV|SC zLb!NcQ7FgrFw%**qZqh%3*dgJI&KK(`TNq6vh42qx0b0 zzWTZ!=ko&EBZxa5UBTX}t3NJzaK07xjsv?&zDFB4pY}F{@gPoNT*5|s<$-r_Echn} z=XVSG1NB4`&*3*Nwm_oG<);tI2w96~O({D3gLq_&3k>$lnm}=uh(V<`S5%8Me6;%;Hwxm|!SMuYLHU?i2JZJp zjgDNN7n#u+G#hn8%~4&{8Vy9N2sfS^TpN*pLM5n7!Mt*|_8R1OP@ZDAMdo*|T;%)E zAw_Ucpr?$AJ-a|AIgC3Nr5X);p}$4v{S3RuzYo$j$f>9YDzq8)II4`IJZ3odesXtI zAQ^Tm`W;28vs&}}xguJDN(E7mp!VoK^;c}(U;YraMB~sFbQ)#-qqAr`nuWTe@6gS< kuiFeW%rL_YGt4mKFJZOs(9mQujQ{`u07*qoM6N<$g0~!AJOBUy literal 0 HcmV?d00001 diff --git a/assets/base/images/icons/image.png b/assets/base/images/icons/image.png new file mode 100644 index 0000000000000000000000000000000000000000..39c36f30a0860be7ea6c20c48c40a352df820d9f GIT binary patch literal 1012 zcmVo;#{ouB0Ty^Ja80VdHg z3$OqSFkrxd0Rt9b0Ty5Z1`HVRVc#BrBuSz;jK`d9+qP}nf%9GPY}>YN+cwv>y<>ak zzu#KD)m`X_%E}iJzui&3=$vnyRrwU*0UnL_FdB6jjrVc1<#RYNvsS`4P=If-l4W!( zFmqPH0H^>1u!^O0EAWR<2p?KXM*=frS9C%lbYfQr-q|}^;D;u^3`Ouv%Htl7eirzl z$%CN?hNd{~iWm=J7vBT7KoPX2IPj~`3{UtTxE_k2KBaMwgyv9+MeR#;c!N?HcX^D3 z=5c%m0^1K+Jf@eR96t6r@Sacvd!#JxVFADHwO}Qm|IqO<6vD?Ti@PjFLjf-IIq-@h zz84S1iYW{H9F)g@z8^NMg5RJ3zhTvs#XUI0U*fj#`nZsJ3*l6JgNf+CM0|r&u~5q5 zE`t$Jf$nxLu;+i+e+mj=3>LD$#o74b*df7r`=JvG>0k?-{HB+}NR(o`1m-P?Ay5Rb zTj1n3eIGPKDYlL|?_nsSdMsmsliTptXoco58C%8p!%RCA!&w$Mna!5qcPNL6*dpY- zB`^?*=_d=E%x2Gp0*uFIA?Do=C7=WAS>SjZT^XfN9^6#z1-89dMZ7viq|h__(mwHu3-2P)3Z$!dVXdH59_* zD1|cQ@T^`Jy#YEP&%Kq^!1q9Aas!sj+Pz7OVInk*IWKF0PlXJC-?A3?C&&Qk#Dc4V0HT z&DFq9L3x?4oeRvGrBMy#W!kX1Yk|+VaG_l8THvl0EtFn%XTy~C=zs(O002PH|JUsZ z3|gQ70|pEjFkk@|U;!3jz<>dJHedq=Y`_L=zy@r<1`ODM4cLGU*nkaqFkk~VV88}! izy@r<25i89$4Ls1Z=mA}VT&+Y#SEeq+8wJ1ub!!&b<@m^cpdIc#-23MrMjAJu$|+ZHn* z2{GTJr7v;&VhyBR%u4hKd)z50eM&aj7$4TS`ym&S5|fMF!}v+L0Fo9aANz$R?!Fc` zBl57j>$ubGWjUjAF~(Kgfq|T}HF0zgI&SMgzY3X^XdP7C8ct60nIBL!h`^6Sj1v!J zI_?n=|tI6@zByFU$HKPcxnzRMCIB0Z3>$fBofbJ&wteTI?2EXK|J<(q6-}Y zvA|yHm&ih&vB2JXq^w0Yz)lP7t!=1N+9FSeSYWRmUD_hQvcTT^sH6q9LZJorUOt)> zx4;u27FdO2id*12u)sRJ{NKe@!!}r89pb3+&&7>^zyj+rp!gr!EwC16{c~||s|##_ zU-{?a)~X9^ftUQbuqF_|0xMF4x-kx8Ab}2F32Fmd;3HxjZiG0t95d47kt738 z!i+RofLw@USH?KJ=y;@G52dp?>*7+!TXLU_arnaFzU3+rn67(>(=$tsc25?fQ-nq+ ze+qG6MvTKcNWf-CZ20*}&b|wCh(iMQPQ(K525}%O7RX6{3p^F#Ks?4l)CRV|dHMre zV0($y{t_LB1nkp@1wIhsK$feoNpVz-P~f{A!r`E+19*K5T*UlI{@>7WnAm z7T8kp5EdK4^2Z{-w7^QoD5-gww!lZ}9%X2w3~QIQ(5Ec0w;oPpp@S^2xB5hNAh-<# zFXp!G;;GqAE)a8XxANYUVYhJ5k1|(4CJ5&wo!sgak2?>N* ziPk}1z1|NyAQ@%i=e}elN^}C^HAsWA^y#{dqThrQ`$D)RNz4Ep~rAhHVf}Y{P@-YXuVzT@T0L zlaQXN%Et#d3bn0RYO$N(7`%cw#F5Q-0Y{**mB~!(${2{V@EVq8@T<)dyo56_5S6Te zyO;B!I09GTNqm4=SdJ}!$Org0upBe+0iMJaI1GbPH>Tk63$8J(4Rr7=XaE2J07*qo IM6N<$g8mWGBme*a literal 0 HcmV?d00001 diff --git a/assets/base/images/icons/pdf.png b/assets/base/images/icons/pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bec396e65697e19d9dde0606ad3e084550514d GIT binary patch literal 2301 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 X!Fh;2Ql@Z}00000NkvXXu0mjfDO6VW literal 0 HcmV?d00001 diff --git a/assets/base/images/icons/ppt.png b/assets/base/images/icons/ppt.png new file mode 100644 index 0000000000000000000000000000000000000000..3a082df1965ec7c9f179b03ab0e7e3c58e6b6120 GIT binary patch literal 2018 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 literal 0 HcmV?d00001 diff --git a/assets/base/images/icons/word.png b/assets/base/images/icons/word.png new file mode 100644 index 0000000000000000000000000000000000000000..2096dc60936271bd3c6f07b096afa0a709584437 GIT binary patch literal 617 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P3?%t>9eV(z%mREuT!C~%(jHRLHmCe+K%GJ* zL4Lsu>;DHB)Zc$!(7#{7VgC690bhA>-75?Xj2Apz978G?lNK0Durgdbr4a2Tz}ED( zgRAMlU;p3cM*Hv0*4aHRQo_@*>i3>EOqrql6F;wwYdcoCRU`4M*oxJ=B&F219gq=Y z`mL2FX|Y)Bdiuox-PJQv9yoIMDNeX^|CS~@v+e>51B39l6Pr(o1?=Gv;g5W=KcA&G z@j=8=P4b6duoIW?Y zRK5Ob$r%-W%=uew&)2774PV_K1;Vb;O^~qh)PL?G7w>ttR#C)(nAl|$60jFcp@&63*CVU!gPhQ@g zzCh=sgr4Q&sYBJCdWIsIx@F@C{tbi$ka(+k^`!#kxjOp@&Um%+11U(ebydHMt$AueE { + 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 0000000000000000000000000000000000000000..13ede8580481b96b347c2d70075dac61988fbb78 GIT binary patch literal 5054 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 literal 0 HcmV?d00001 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); + }); +});