forked from treehouse/mastodon
Deleting statuses from UI
parent
a41c3487bd
commit
ef2b50c9ac
|
@ -5,6 +5,10 @@ export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
|||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
|
||||
|
||||
export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
|
||||
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
|
||||
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
|
||||
|
||||
export function fetchStatusRequest(id) {
|
||||
return {
|
||||
type: STATUS_FETCH_REQUEST,
|
||||
|
@ -41,3 +45,37 @@ export function fetchStatusFail(id, error) {
|
|||
error: error
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(deleteStatusRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
}).catch(error => {
|
||||
dispatch(deleteStatusFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatusRequest(id) {
|
||||
return {
|
||||
type: STATUS_DELETE_REQUEST,
|
||||
id: id
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatusSuccess(id) {
|
||||
return {
|
||||
type: STATUS_DELETE_SUCCESS,
|
||||
id: id
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatusFail(id, error) {
|
||||
return {
|
||||
type: STATUS_DELETE_FAIL,
|
||||
id: id,
|
||||
error: error
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,8 +26,16 @@ const IconButton = React.createClass({
|
|||
},
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
display: 'inline-block',
|
||||
fontSize: `${this.props.size}px`,
|
||||
width: `${this.props.size}px`,
|
||||
height: `${this.props.size}px`,
|
||||
lineHeight: `${this.props.size}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<a href='#' title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''}`} onClick={this.handleClick} style={{ display: 'inline-block', fontSize: `${this.props.size}px`, width: `${this.props.size}px`, height: `${this.props.size}px`, lineHeight: `${this.props.size}px`}}>
|
||||
<a href='#' title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''}`} onClick={this.handleClick} style={style}>
|
||||
<i className={`fa fa-fw fa-${this.props.icon}`}></i>
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -2,11 +2,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import Avatar from './avatar';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import IconButton from './icon_button';
|
||||
import DisplayName from './display_name';
|
||||
import MediaGallery from './media_gallery';
|
||||
import VideoPlayer from './video_player';
|
||||
import StatusContent from './status_content';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
|
||||
const Status = React.createClass({
|
||||
|
||||
|
@ -19,23 +19,13 @@ const Status = React.createClass({
|
|||
wrapped: React.PropTypes.bool,
|
||||
onReply: React.PropTypes.func,
|
||||
onFavourite: React.PropTypes.func,
|
||||
onReblog: React.PropTypes.func
|
||||
onReblog: React.PropTypes.func,
|
||||
onDelete: React.PropTypes.func,
|
||||
me: React.PropTypes.number
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleReplyClick () {
|
||||
this.props.onReply(this.props.status);
|
||||
},
|
||||
|
||||
handleFavouriteClick () {
|
||||
this.props.onFavourite(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
},
|
||||
|
||||
handleClick () {
|
||||
const { status } = this.props;
|
||||
this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
|
@ -96,11 +86,7 @@ const Status = React.createClass({
|
|||
|
||||
{media}
|
||||
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px'}}><IconButton title='Reply' icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px'}}><IconButton active={status.get('reblogged')} title='Reblog' icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left'}}><IconButton active={status.get('favourited')} title='Favourite' icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
</div>
|
||||
<StatusActionBar {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import IconButton from './icon_button';
|
||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||
|
||||
const StatusActionBar = React.createClass({
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReply: React.PropTypes.func,
|
||||
onFavourite: React.PropTypes.func,
|
||||
onReblog: React.PropTypes.func,
|
||||
onDelete: React.PropTypes.func
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleReplyClick () {
|
||||
this.props.onReply(this.props.status);
|
||||
},
|
||||
|
||||
handleFavouriteClick () {
|
||||
this.props.onFavourite(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
},
|
||||
|
||||
handleDeleteClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.onDelete(this.props.status);
|
||||
},
|
||||
|
||||
render () {
|
||||
const { status, me } = this.props;
|
||||
let menu = '';
|
||||
|
||||
if (status.getIn(['account', 'id']) === me) {
|
||||
menu = (
|
||||
<ul>
|
||||
<li><a href='#' onClick={this.handleDeleteClick}>Delete</a></li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title='Reply' icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('reblogged')} title='Reblog' icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title='Favourite' icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
|
||||
<div onClick={e => e.stopPropagation()} style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||
<Dropdown>
|
||||
<DropdownTrigger className='icon-button' style={{ fontSize: '18px', lineHeight: '18px', width: '18px', height: '18px' }}>
|
||||
<i className='fa fa-fw fa-ellipsis-h' />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent>{menu}</DropdownContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default StatusActionBar;
|
|
@ -9,7 +9,9 @@ const StatusList = React.createClass({
|
|||
onReply: React.PropTypes.func,
|
||||
onReblog: React.PropTypes.func,
|
||||
onFavourite: React.PropTypes.func,
|
||||
onScrollToBottom: React.PropTypes.func
|
||||
onDelete: React.PropTypes.func,
|
||||
onScrollToBottom: React.PropTypes.func,
|
||||
me: React.PropTypes.number
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
@ -23,11 +25,13 @@ const StatusList = React.createClass({
|
|||
},
|
||||
|
||||
render () {
|
||||
const { statuses, onScrollToBottom, ...other } = this.props;
|
||||
|
||||
return (
|
||||
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable' onScroll={this.handleScroll}>
|
||||
<div>
|
||||
{this.props.statuses.map((status) => {
|
||||
return <Status key={status.get('id')} status={status} onReply={this.props.onReply} onReblog={this.props.onReblog} onFavourite={this.props.onFavourite} />;
|
||||
{statuses.map((status) => {
|
||||
return <Status key={status.get('id')} {...other} status={status} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
fetchAccountTimeline,
|
||||
expandAccountTimeline
|
||||
} from '../../actions/accounts';
|
||||
import { deleteStatus } from '../../actions/statuses';
|
||||
import { replyCompose } from '../../actions/compose';
|
||||
import { favourite, reblog } from '../../actions/interactions';
|
||||
import Header from './components/header';
|
||||
|
@ -72,6 +73,10 @@ const Account = React.createClass({
|
|||
this.props.dispatch(favourite(status));
|
||||
},
|
||||
|
||||
handleDelete (status) {
|
||||
this.props.dispatch(deleteStatus(status.get('id')));
|
||||
},
|
||||
|
||||
handleScrollToBottom () {
|
||||
this.props.dispatch(expandAccountTimeline(this.props.account.get('id')));
|
||||
},
|
||||
|
@ -87,7 +92,7 @@ const Account = React.createClass({
|
|||
<div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
|
||||
<Header account={account} />
|
||||
<ActionBar account={account} me={me} onFollow={this.handleFollow} onUnfollow={this.handleUnfollow} />
|
||||
<StatusList statuses={statuses} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} />
|
||||
<StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,29 +4,35 @@ import { replyCompose } from '../../../actions/compose';
|
|||
import { reblog, favourite } from '../../../actions/interactions';
|
||||
import { expandTimeline } from '../../../actions/timelines';
|
||||
import { selectStatus } from '../../../reducers/timelines';
|
||||
import { deleteStatus } from '../../../actions/statuses';
|
||||
|
||||
const mapStateToProps = function (state, props) {
|
||||
return {
|
||||
statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id))
|
||||
statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)),
|
||||
me: state.getIn(['timelines', 'me'])
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = function (dispatch, props) {
|
||||
return {
|
||||
onReply: function (status) {
|
||||
onReply (status) {
|
||||
dispatch(replyCompose(status));
|
||||
},
|
||||
|
||||
onFavourite: function (status) {
|
||||
onFavourite (status) {
|
||||
dispatch(favourite(status));
|
||||
},
|
||||
|
||||
onReblog: function (status) {
|
||||
onReblog (status) {
|
||||
dispatch(reblog(status));
|
||||
},
|
||||
|
||||
onScrollToBottom: function () {
|
||||
onScrollToBottom () {
|
||||
dispatch(expandTimeline(props.type));
|
||||
},
|
||||
|
||||
onDelete (status) {
|
||||
dispatch(deleteStatus(status.get('id')));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,7 +13,10 @@ import {
|
|||
ACCOUNT_TIMELINE_FETCH_FAIL,
|
||||
ACCOUNT_TIMELINE_EXPAND_FAIL
|
||||
} from '../actions/accounts';
|
||||
import { STATUS_FETCH_FAIL } from '../actions/statuses';
|
||||
import {
|
||||
STATUS_FETCH_FAIL,
|
||||
STATUS_DELETE_FAIL
|
||||
} from '../actions/statuses';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.List();
|
||||
|
@ -51,6 +54,7 @@ export default function notifications(state = initialState, action) {
|
|||
case ACCOUNT_TIMELINE_FETCH_FAIL:
|
||||
case ACCOUNT_TIMELINE_EXPAND_FAIL:
|
||||
case STATUS_FETCH_FAIL:
|
||||
case STATUS_DELETE_FAIL:
|
||||
return notificationFromError(state, action.error);
|
||||
case NOTIFICATION_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.notification.key);
|
||||
|
|
|
@ -16,7 +16,10 @@ import {
|
|||
ACCOUNT_TIMELINE_FETCH_SUCCESS,
|
||||
ACCOUNT_TIMELINE_EXPAND_SUCCESS
|
||||
} from '../actions/accounts';
|
||||
import { STATUS_FETCH_SUCCESS } from '../actions/statuses';
|
||||
import {
|
||||
STATUS_FETCH_SUCCESS,
|
||||
STATUS_DELETE_SUCCESS
|
||||
} from '../actions/statuses';
|
||||
import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
|
@ -142,10 +145,28 @@ function updateTimeline(state, timeline, status) {
|
|||
};
|
||||
|
||||
function deleteStatus(state, id) {
|
||||
const status = state.getIn(['statuses', id]);
|
||||
|
||||
if (!status) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Remove references from timelines
|
||||
['home', 'mentions'].forEach(function (timeline) {
|
||||
state = state.update(timeline, list => list.filterNot(item => item === id));
|
||||
});
|
||||
|
||||
// Remove references from account timelines
|
||||
state = state.updateIn(['accounts_timelines', status.get('account')], Immutable.List(), list => list.filterNot(item => item === id));
|
||||
|
||||
// Remove reblogs of deleted status
|
||||
const references = state.get('statuses').filter(item => item.get('reblog') === id);
|
||||
|
||||
references.forEach(referencingId => {
|
||||
state = deleteStatus(state, referencingId);
|
||||
});
|
||||
|
||||
// Remove normalized status
|
||||
return state.deleteIn(['statuses', id]);
|
||||
};
|
||||
|
||||
|
@ -194,6 +215,7 @@ export default function timelines(state = initialState, action) {
|
|||
case TIMELINE_UPDATE:
|
||||
return updateTimeline(state, action.timeline, Immutable.fromJS(action.status));
|
||||
case TIMELINE_DELETE:
|
||||
case STATUS_DELETE_SUCCESS:
|
||||
return deleteStatus(state, action.id);
|
||||
case REBLOG_SUCCESS:
|
||||
case FAVOURITE_SUCCESS:
|
||||
|
|
|
@ -156,3 +156,64 @@
|
|||
.transparent-background {
|
||||
background: image-url('void.png');
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown__content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dropdown--active .dropdown__content {
|
||||
display: block;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 4.5px 7.8px 4.5px;
|
||||
border-color: transparent transparent #d9e1e8 transparent;
|
||||
top: -7px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
&:first-child a {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
&:last-child a {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
&:first-child:last-child a {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
padding: 6px 16px;
|
||||
width: 120px;
|
||||
text-decoration: none;
|
||||
background: #d9e1e8;
|
||||
color: #282c37;
|
||||
|
||||
&:hover {
|
||||
background: #2b90d9;
|
||||
color: #d9e1e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::AppsController < ApplicationController
|
||||
class Api::V1::AppsController < ApiController
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
!!! 5
|
||||
%html
|
||||
%head
|
||||
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
|
||||
%meta{:content => 'text/html; charset=UTF-8', 'http-equiv' => 'Content-Type'}/
|
||||
%meta{:charset => 'utf-8'}/
|
||||
%meta{:name => 'viewport', :content => 'width=device-width, initial-scale=1'}/
|
||||
%meta{'http-equiv' => 'X-UA-Compatible', :content => 'IE=edge'}/
|
||||
|
||||
%title
|
||||
= "#{yield(:page_title)} - " if content_for?(:page_title)
|
||||
Mastodon
|
||||
|
||||
= stylesheet_link_tag 'application', media: 'all'
|
||||
= csrf_meta_tags
|
||||
|
||||
= yield :header_tags
|
||||
|
||||
%body{ class: @body_classes }
|
||||
= content_for?(:content) ? yield(:content) : yield
|
||||
|
|
Loading…
Reference in New Issue