Skip to content
Snippets Groups Projects
Commit 499beb44 authored by Eugen Rochko's avatar Eugen Rochko
Browse files

UI for uploading media attachments (and cancelling them)

Mostly resolves #8, though attachments are still not displayed in public view
parent 1efa8e48
No related branches found
No related tags found
No related merge requests found
Showing
with 222 additions and 18 deletions
app/assets/images/void.png

180 B

import api from '../api'
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT = 'COMPOSE_SUBMIT';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT = 'COMPOSE_SUBMIT';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_UPLOAD = 'COMPOSE_UPLOAD';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export function changeCompose(text) {
return {
......@@ -34,7 +40,8 @@ export function submitCompose() {
api(getState).post('/api/statuses', {
status: getState().getIn(['compose', 'text'], ''),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null)
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id'))
}).then(function (response) {
dispatch(submitComposeSuccess(response.data));
}).catch(function (error) {
......@@ -63,3 +70,56 @@ export function submitComposeFail(error) {
};
}
export function uploadCompose(files) {
return function (dispatch, getState) {
dispatch(uploadComposeRequest());
let data = new FormData();
data.append('file', files[0]);
api(getState).post('/api/media', data, {
onUploadProgress: function (e) {
dispatch(uploadComposeProgress(e.loaded, e.total));
}
}).then(function (response) {
dispatch(uploadComposeSuccess(response.data));
}).catch(function (error) {
dispatch(uploadComposeFail(error));
});
};
}
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST
};
}
export function uploadComposeProgress(loaded, total) {
return {
type: COMPOSE_UPLOAD_PROGRESS,
loaded: loaded,
total: total
};
}
export function uploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_SUCCESS,
media: media
};
}
export function uploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_FAIL,
error: error
};
}
export function undoUploadCompose(media_id) {
return {
type: COMPOSE_UPLOAD_UNDO,
media_id: media_id
};
}
......@@ -3,9 +3,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
const Button = React.createClass({
propTypes: {
text: React.PropTypes.string.isRequired,
text: React.PropTypes.string,
onClick: React.PropTypes.func,
disabled: React.PropTypes.bool
disabled: React.PropTypes.bool,
block: React.PropTypes.bool,
secondary: React.PropTypes.bool
},
mixins: [PureRenderMixin],
......@@ -18,8 +20,8 @@ const Button = React.createClass({
render () {
return (
<button className='button' disabled={this.props.disabled} onClick={this.handleClick} style={{ fontFamily: 'Roboto', display: 'inline-block', position: 'relative', boxSizing: 'border-box', textAlign: 'center', border: '10px none', color: '#fff', fontSize: '14px', fontWeight: '500', letterSpacing: '0', textTransform: 'uppercase', padding: '0 16px', height: '36px', cursor: 'pointer', lineHeight: '36px', borderRadius: '4px', textDecoration: 'none' }}>
{this.props.text}
<button className={`button ${this.props.secondary ? 'button-secondary' : ''}`} disabled={this.props.disabled} onClick={this.handleClick} style={{ fontFamily: 'Roboto', display: this.props.block ? 'block' : 'inline-block', width: this.props.block ? '100%' : 'auto', position: 'relative', boxSizing: 'border-box', textAlign: 'center', border: '10px none', color: '#fff', fontSize: '14px', fontWeight: '500', letterSpacing: '0', textTransform: 'uppercase', padding: '0 16px', height: '36px', cursor: 'pointer', lineHeight: '36px', borderRadius: '4px', textDecoration: 'none' }}>
{this.props.text || this.props.children}
</button>
);
}
......
......@@ -3,6 +3,7 @@ import Button from './button';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ReplyIndicator from './reply_indicator';
import UploadButton from './upload_button';
const ComposeForm = React.createClass({
......@@ -39,7 +40,7 @@ const ComposeForm = React.createClass({
}
return (
<div style={{ marginBottom: '30px', padding: '10px' }}>
<div style={{ padding: '10px' }}>
{replyArea}
<textarea disabled={this.props.is_submitting} placeholder='What is on your mind?' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='compose-form__textarea' style={{ display: 'block', boxSizing: 'border-box', width: '100%', height: '100px', resize: 'none', border: 'none', color: '#282c37', padding: '10px', fontFamily: 'Roboto', fontSize: '14px', margin: '0' }} />
......
......@@ -2,6 +2,7 @@ import ColumnsArea from './columns_area';
import Drawer from './drawer';
import ComposeFormContainer from '../containers/compose_form_container';
import FollowFormContainer from '../containers/follow_form_container';
import UploadFormContainer from '../containers/upload_form_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
const Frontend = React.createClass({
......@@ -14,6 +15,7 @@ const Frontend = React.createClass({
<Drawer>
<div style={{ flex: '1 1 auto' }}>
<ComposeFormContainer />
<UploadFormContainer />
</div>
<FollowFormContainer />
......
import PureRenderMixin from 'react-addons-pure-render-mixin';
import Button from './button';
const UploadButton = React.createClass({
propTypes: {
disabled: React.PropTypes.bool,
onSelectFile: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
handleChange (e) {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
},
handleClick () {
this.refs.fileElement.click();
},
render () {
return (
<div>
<Button disabled={this.props.disabled} onClick={this.handleClick} block={true}>
<i className='fa fa-fw fa-photo' /> Add images
</Button>
<input ref='fileElement' type='file' onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} />
</div>
);
}
});
export default UploadButton;
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import UploadButton from './upload_button';
import IconButton from './icon_button';
const UploadForm = React.createClass({
propTypes: {
media: ImmutablePropTypes.list.isRequired,
is_uploading: React.PropTypes.bool,
onSelectFile: React.PropTypes.func.isRequired,
onRemoveFile: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
render () {
let uploads = this.props.media.map(function (attachment) {
return (
<div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'>
<div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
<IconButton icon='times' title='Undo' size={36} onClick={() => this.props.onRemoveFile(attachment.get('id'))} />
</div>
</div>
);
}.bind(this));
return (
<div style={{ marginBottom: '20px', padding: '10px', paddingTop: '0' }}>
<UploadButton onSelectFile={this.props.onSelectFile} disabled={this.props.is_uploading || this.props.media.size > 3} />
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
{uploads}
</div>
</div>
);
}
});
export default UploadForm;
import { connect } from 'react-redux';
import UploadForm from '../components/upload_form';
import { uploadCompose, undoUploadCompose } from '../actions/compose';
const mapStateToProps = function (state, props) {
return {
media: state.getIn(['compose', 'media_attachments']),
progress: state.getIn(['compose', 'progress']),
is_uploading: state.getIn(['compose', 'is_uploading'])
};
};
const mapDispatchToProps = function (dispatch) {
return {
onSelectFile: function (files) {
dispatch(uploadCompose(files));
},
onRemoveFile: function (media_id) {
dispatch(undoUploadCompose(media_id));
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(UploadForm);
......@@ -5,7 +5,10 @@ import Immutable from 'immutable';
const initialState = Immutable.Map({
text: '',
in_reply_to: null,
is_submitting: false
is_submitting: false,
is_uploading: false,
progress: 0,
media_attachments: Immutable.List([])
});
export default function compose(state = initialState, action) {
......@@ -19,16 +22,33 @@ export default function compose(state = initialState, action) {
});
case constants.COMPOSE_REPLY_CANCEL:
return state.withMutations(map => {
map.set('in_reply_to', null).set('text', '');
map.set('in_reply_to', null);
map.set('text', '');
});
case constants.COMPOSE_SUBMIT_REQUEST:
return state.set('is_submitting', true);
case constants.COMPOSE_SUBMIT_SUCCESS:
return state.withMutations(map => {
map.set('text', '').set('is_submitting', false).set('in_reply_to', null);
map.set('text', '');
map.set('is_submitting', false);
map.set('in_reply_to', null);
map.update('media_attachments', list => list.clear());
});
case constants.COMPOSE_SUBMIT_FAIL:
return state.set('is_submitting', false);
case constants.COMPOSE_UPLOAD_REQUEST:
return state.set('is_uploading', true);
case constants.COMPOSE_UPLOAD_SUCCESS:
return state.withMutations(map => {
map.update('media_attachments', list => list.push(Immutable.fromJS(action.media)));
map.set('is_uploading', false);
});
case constants.COMPOSE_UPLOAD_FAIL:
return state.set('is_uploading', false);
case constants.COMPOSE_UPLOAD_UNDO:
return state.update('media_attachments', list => list.filterNot(item => item.get('id') === action.media_id));
case constants.COMPOSE_UPLOAD_PROGRESS:
return state.set('progress', Math.round((action.loaded / action.total) * 100));
case TIMELINE_DELETE:
if (action.id === state.get('in_reply_to')) {
return state.set('in_reply_to', null);
......
......@@ -8,6 +8,18 @@
&:disabled {
background-color: #9baec8;
}
&.button-secondary {
background-color: #282c37;
&:hover {
background-color: #282c37;
}
&:disabled {
background-color: #9baec8;
}
}
}
.icon-button {
......@@ -39,7 +51,6 @@
.status__content, .reply-indicator__content {
font-size: 15px;
line-height: 20px;
white-space: pre-wrap;
word-wrap: break-word;
font-weight: 300;
......@@ -95,3 +106,7 @@
text-decoration: underline;
}
}
.transparent-background {
background: image-url('void.png');
}
......@@ -19,7 +19,7 @@ class PostStatusService < BaseService
def attach_media(status, media_ids)
return if media_ids.nil? || !media_ids.is_a?(Enumerable)
media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(2).map { |id| id.to_i })
media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map { |id| id.to_i })
media.update(status_id: status.id)
end
......
object @media
attribute :id
node(:url) { |media| full_asset_url(media.file.url) }
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment