Skip to content

Commit

Permalink
feat(widget-chat): Add file upload API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernie Zang committed Nov 16, 2016
1 parent 8e5386b commit c523442
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/widget-chat/package.json
Expand Up @@ -17,6 +17,7 @@
"babel-runtime": "^6.3.19",
"browser-saveas": "^1.0.1",
"extract-text-webpack-plugin": "^1.0.1",
"immutable": "^3.8.1",
"lodash": "^4.5.1",
"marked": "^0.3.6",
"moment": "^2.15.1",
Expand Down
74 changes: 64 additions & 10 deletions packages/widget-chat/src/actions/activity.js
@@ -1,6 +1,40 @@
import marked from 'marked';
import {filterSync} from '@ciscospark/helper-html';

import {
constructImage,
isImage,
sanitize
} from '../utils/files';

import {
storeShares,
uploadFiles
} from './share';


export const ADD_FILES_TO_ACTIVITY = `ADD_FILES_TO_ACTIVITY`;
export function addFilesToActivity(files) {
return {
type: ADD_FILES_TO_ACTIVITY,
payload: {
files
}
};
}

export const CREATE_ACTIVITY = `CREATE_ACTIVITY`;
export function createActivity(conversation, text, actor) {
return {
type: CREATE_ACTIVITY,
payload: {
actor,
conversation,
text
}
};
}

export const UPDATE_ACTIVITY_STATE = `UPDATE_ACTIVITY_STATE`;
export function updateActivityState(state) {
return {
Expand All @@ -21,30 +55,50 @@ export function updateActivityText(text) {
};
}

export const CREATE_ACTIVITY = `CREATE_ACTIVITY`;
export function createActivity(conversation, text, actor) {
return {
type: CREATE_ACTIVITY,
payload: {
actor,
conversation,
text
export function addShareFiles(conversation, activity, files, spark) {
return (dispatch) => {
const images = [];
let cleanFiles;
if (files && files.length) {
cleanFiles = files.map((file) => {
file = sanitize(file);
if (isImage(file)) {
images.push(constructImage(file));
}
return file;
});
}

Promise.all(images)
.then((localImages) => {
dispatch(updateActivityState({isUploadingShare: true}));
dispatch(addFilesToActivity(cleanFiles));
dispatch(storeShares(localImages));
dispatch(uploadFiles(conversation, activity, files, spark));
});
};
}


export function submitActivity(conversation, activity, spark) {
return (dispatch) => {
dispatch(updateActivityState({isSending: true}));
const message = _createMessageObject(activity.object.displayName);
if (!activity.files) {
if (activity.files) {
dispatch(updateActivityState({isSending: false}));
}
else {
spark.conversation.post(conversation, message)
.then(() => dispatch(createActivity(conversation, ``, conversation.participants[0])))
.then(() => dispatch(resetActivity(conversation)))
.then(() => dispatch(updateActivityState({isSending: false})));
}
};
}

export function resetActivity(conversation) {
return (dispatch) => dispatch(createActivity(conversation, ``, conversation.participants[0]));
}

function _createMessageObject(messageString) {
let content;
let markedString = marked(messageString) || ``;
Expand Down
22 changes: 22 additions & 0 deletions packages/widget-chat/src/actions/share.js
Expand Up @@ -16,6 +16,17 @@ export function receiveShare(payload) {
};
}

export const STORE_SHARES = `STORE_SHARES`;
export function storeShares(shares) {
return {
type: STORE_SHARES,
payload: {
shares
}
};
}


export function retrieveSharedFile(fileObject, spark) {
return (dispatch) => {
dispatch(fetchShare(fileObject));
Expand All @@ -26,3 +37,14 @@ export function retrieveSharedFile(fileObject, spark) {
});
};
}

export function uploadFiles(conversation, activity, files, spark) {
return (dispatch) => {
const shareActivity = spark.conversation.makeShare(conversation);
return Promise.resolve(shareActivity)
.then((share) => Promise.all(files.map((file) => share.add(file))))
.then((uploadedShares) => {
dispatch(storeShares(uploadedShares));
});
};
}
16 changes: 13 additions & 3 deletions packages/widget-chat/src/components/add-file-button/index.js
Expand Up @@ -5,16 +5,26 @@ import styles from './styles.css';

export default function AddFileButton(props) {
const {
onChange,
onClick
} = props;

return (
<button className={classNames(`add-file-button`, styles.button)} onClick={onClick}>
<span className={classNames(`add-file-icon`, styles.icon)} />
</button>
<div className={classNames(`add-file-container`, styles.container)}>
<button className={classNames(`add-file-button`, styles.button)} onClick={onClick}>
<span className={classNames(`add-file-icon`, styles.icon)} />
</button>
<input
className={classNames(`file-input`, styles.fileInput)}
multiple="multiple"
onChange={onChange}
type="file"
/>
</div>
);
}

AddFileButton.propTypes = {
onChange: PropTypes.func,
onClick: PropTypes.func
};
Empty file.
75 changes: 75 additions & 0 deletions packages/widget-chat/src/containers/file-uploader/index.js
@@ -0,0 +1,75 @@
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import classNames from 'classnames';

import spark from '../../modules/redux-spark/spark';
import {constructFile} from '../../utils/files';
import {addShareFiles} from '../../actions/activity';

import AddFileButton from '../../components/add-file-button';
import styles from './styles.css';

export class FileUploader extends Component {

constructor(props) {
super(props);
this.handleFileChange = this.handleFileChange.bind(this);
}

getFiles() {
const props = this.props;
return props.activity.files;
}

handleFileChange(e) {
e.stopPropagation();
e.preventDefault();

const props = this.props;

const {
activity,
conversation
} = props;

const files = [];

for (let i = 0; i < e.target.files.length; i++) {
files.push(constructFile(e.target.files[i]));
}
props.addShareFiles(conversation, activity, files, spark);

// Clear the value of the input so the same file can be added again.
e.target.value = ``;
}

uploadFiles(files) {
return files;
}

render() {
return (
<div className={classNames(`file-uploader-container`, styles.container)}>
<div className={classNames(`add-file-container`, styles.addFileContainer)}>
<AddFileButton onChange={this.handleFileChange} />
</div>
</div>
);
}
}

FileUploader.propTypes = {
handleSubmit: PropTypes.func
};


export default connect(
(state) => ({
activity: state.activity.activity,
conversation: state.conversation
}),
(dispatch) => bindActionCreators({
addShareFiles
}, dispatch)
)(FileUploader);
Empty file.
12 changes: 6 additions & 6 deletions packages/widget-chat/src/containers/message-composer/index.js
Expand Up @@ -8,8 +8,9 @@ import {
submitActivity,
updateActivityText
} from '../../actions/activity';
import FileUploader from '../file-uploader';
import TextArea from '../../components/textarea';
import AddFileButton from '../../components/add-file-button';


import styles from './styles.css';

Expand Down Expand Up @@ -68,9 +69,7 @@ export class MessageComposer extends Component {

return (
<div className={classNames(`message-composer`, styles.messageComposer)}>
<div className={classNames(`add-file-container`, styles.addFileContainer)}>
<AddFileButton />
</div>
<FileUploader onSubmit={this.handleSubmit} />
<div className={classNames(`textarea-container`)}>
<TextArea
onChange={this.handleChange}
Expand All @@ -93,10 +92,11 @@ MessageComposer.propTypes = {
};

function mapStateToProps(state, ownProps) {
return Object.assign({}, state.activity, {
return {
activity: state.activity.activity,
spark: ownProps.spark,
conversation: state.conversation
});
};
}

export default connect(
Expand Down
9 changes: 9 additions & 0 deletions packages/widget-chat/src/reducers/activity.js
@@ -1,5 +1,6 @@
import {constructActivity} from '../utils/activity';
import {
ADD_FILES_TO_ACTIVITY,
CREATE_ACTIVITY,
UPDATE_ACTIVITY_STATE,
UPDATE_ACTIVITY_TEXT
Expand All @@ -11,6 +12,14 @@ export default function reduceActivity(state = {
}
}, action) {
switch (action.type) {
case ADD_FILES_TO_ACTIVITY: {
const newState = Object.assign({}, state);
if (!state.activity.files) {
newState.activity.files = [];
}
newState.activity.files.concat(action.files);
return newState;
}
case CREATE_ACTIVITY: {
const {
actor,
Expand Down
14 changes: 14 additions & 0 deletions packages/widget-chat/src/utils/activity.js
@@ -1,5 +1,7 @@
import uuid from 'uuid';
import _ from 'lodash';

import {constructFile} from './files';

export function constructActivity(conversation, text, actor) {
return {
Expand All @@ -25,3 +27,15 @@ export function constructActivity(conversation, text, actor) {
_status: `pending`
};
}

export function updateActivityWithContent(activity, files) {
return _.merge({}, activity, {
object: {
objectType: `content`
},
verb: `share`,
files: {
items: files.map((file) => constructFile(file))
}
});
}
52 changes: 52 additions & 0 deletions packages/widget-chat/src/utils/files.js
@@ -1,3 +1,5 @@
import uuid from 'uuid';
import _ from 'lodash';

export function bytesToSize(bytes) {
if (!bytes || bytes === 0) {
Expand All @@ -16,3 +18,53 @@ export function bufferToBlob(buffer) {
const objectUrl = urlCreator.createObjectURL(blob);
return {blob, objectUrl};
}

export function constructFile(file) {
return _.assign(file, {
clientTempId: uuid.v4(),
displayName: file.name,
fileSize: file.size,
fileSizePretty: bytesToSize(file.size),
mimeType: file.type
});
}

export function constructImage(file) {
if (!isImage(file)) {
return Promise.resolve();
}

const objectURL = URL.createObjectURL(file);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function onload() {
resolve(img);
};
img.onerror = reject;
img.src = objectURL;
})
.then((img) => {
const dimensions = _.pick(img, `height`, `width`);
const image = {
clientTempId: file.clientTempId,
height: dimensions.height,
width: dimensions.width,
objectURL
};

return Promise.resolve(image);
});
}

export function isImage(file) {
return file.type.indexOf(`image`) !== -1;
}


export function sanitize(file) {
return _.assign(file, {
displayName: file.displayName || null,
fileSize: file.fileSize || 0,
fileSizePretty: bytesToSize(file.fileSize)
});
}

0 comments on commit c523442

Please sign in to comment.