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
|
|
@ -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();
|
||||
|
|
|
|||
107
app/components/file_attachment_list/file_attachment.js
Normal file
|
|
@ -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 (
|
||||
<View style={style.fileInfoContainer}>
|
||||
<Text
|
||||
numberOfLines={4}
|
||||
style={style.fileName}
|
||||
>
|
||||
{file.name.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Icon
|
||||
name='download'
|
||||
size={16}
|
||||
style={style.downloadIcon}
|
||||
/>
|
||||
<Text style={style.fileInfo}>
|
||||
{`${file.extension.toUpperCase()} ${Utils.getFormattedFileSize(file)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {file, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.fileWrapper}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
{this.renderFileInfo()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
76
app/components/file_attachment_list/file_attachment_icon.js
Normal file
|
|
@ -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 (
|
||||
<View style={styles.fileIconWrapper}>
|
||||
<Image
|
||||
style={styles.fileIcon}
|
||||
source={this.getFileIconPath(file)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
43
app/components/file_attachment_list/file_attachment_list.js
Normal file
|
|
@ -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) => (
|
||||
<FileAttachment
|
||||
key={file.id}
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<View>
|
||||
{fileAttachments}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 <View style={replyBarStyle}/>;
|
||||
}
|
||||
|
||||
renderFileAttachments() {
|
||||
const {post} = this.props;
|
||||
const fileIds = post.file_ids || [];
|
||||
let attachments;
|
||||
if (fileIds.length > 0) {
|
||||
attachments = (<FileAttachmentList post={post}/>);
|
||||
}
|
||||
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 {
|
|||
</View>
|
||||
<View style={style.messageContainerWithReplyBar}>
|
||||
{this.renderReplyBar(style)}
|
||||
{this.renderFileAttachments()}
|
||||
<Text style={messageStyle}>
|
||||
{this.props.post.message}
|
||||
</Text>
|
||||
|
|
@ -275,6 +287,7 @@ export default class Post extends React.Component {
|
|||
</Text>
|
||||
</View>
|
||||
<View style={style.messageContainer}>
|
||||
{this.renderFileAttachments()}
|
||||
<Text style={messageStyle}>
|
||||
{this.props.post.message}
|
||||
</Text>
|
||||
|
|
|
|||
BIN
assets/base/images/icons/audio.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/base/images/icons/code.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/base/images/icons/excel.png
Normal file
|
After Width: | Height: | Size: 477 B |
BIN
assets/base/images/icons/generic.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/base/images/icons/image.png
Normal file
|
After Width: | Height: | Size: 1,012 B |
BIN
assets/base/images/icons/patch.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/base/images/icons/pdf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/base/images/icons/ppt.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
assets/base/images/icons/video.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
assets/base/images/icons/word.png
Normal file
|
After Width: | Height: | Size: 617 B |
|
|
@ -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",
|
||||
|
|
|
|||
34
service/actions/files.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
14
service/constants/files.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
47
service/reducers/entities/files.js
Normal file
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
21
service/reducers/requests/files.js
Normal file
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
21
service/selectors/entities/files.js
Normal file
|
|
@ -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]);
|
||||
}
|
||||
);
|
||||
}
|
||||
42
service/utils/file_utils.js
Normal file
|
|
@ -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';
|
||||
}
|
||||
BIN
test/assets/images/test.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
69
test/service/actions/files.test.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||