ScheduledStatuses: ability to view and cancel scheduled statuses
This commit is contained in:
parent
840706a500
commit
97f89acff9
|
@ -8,6 +8,10 @@ export const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQU
|
||||||
export const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS';
|
export const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS';
|
||||||
export const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL';
|
export const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const SCHEDULED_STATUS_CANCEL_REQUEST = 'SCHEDULED_STATUS_CANCEL_REQUEST';
|
||||||
|
export const SCHEDULED_STATUS_CANCEL_SUCCESS = 'SCHEDULED_STATUS_CANCEL_SUCCESS';
|
||||||
|
export const SCHEDULED_STATUS_CANCEL_FAIL = 'SCHEDULED_STATUS_CANCEL_FAIL';
|
||||||
|
|
||||||
export function fetchScheduledStatuses() {
|
export function fetchScheduledStatuses() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (getState().getIn(['status_lists', 'scheduled_statuses', 'isLoading'])) {
|
if (getState().getIn(['status_lists', 'scheduled_statuses', 'isLoading'])) {
|
||||||
|
@ -25,6 +29,17 @@ export function fetchScheduledStatuses() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function cancelScheduledStatus(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id });
|
||||||
|
api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(({ data }) => {
|
||||||
|
dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchScheduledStatusesRequest() {
|
export function fetchScheduledStatusesRequest() {
|
||||||
return {
|
return {
|
||||||
type: SCHEDULED_STATUSES_FETCH_REQUEST,
|
type: SCHEDULED_STATUSES_FETCH_REQUEST,
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
import { normalizeStatus } from 'soapbox/actions/importer/normalizer';
|
||||||
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
export const buildStatus = (state, scheduledStatus) => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const me = state.get('me');
|
||||||
|
const params = scheduledStatus.get('params');
|
||||||
|
const account = getAccount(state, me);
|
||||||
|
|
||||||
|
const status = normalizeStatus({
|
||||||
|
account,
|
||||||
|
application: null,
|
||||||
|
bookmarked: false,
|
||||||
|
card: null,
|
||||||
|
content: params.get('text'),
|
||||||
|
created_at: params.get('scheduled_at'),
|
||||||
|
emojis: [],
|
||||||
|
favourited: false,
|
||||||
|
favourites_count: 0,
|
||||||
|
id: scheduledStatus.get('id'),
|
||||||
|
in_reply_to_account_id: null,
|
||||||
|
in_reply_to_id: params.get('in_reply_to_id'),
|
||||||
|
language: null,
|
||||||
|
media_attachments: scheduledStatus.get('media_attachments'),
|
||||||
|
mentions: [],
|
||||||
|
muted: false,
|
||||||
|
pinned: false,
|
||||||
|
poll: params.get('poll'),
|
||||||
|
reblog: null,
|
||||||
|
reblogged: false,
|
||||||
|
reblogs_count: 0,
|
||||||
|
replies_count: 0,
|
||||||
|
sensitive: params.get('sensitive'),
|
||||||
|
spoiler_text: '',
|
||||||
|
tags: [],
|
||||||
|
text: null,
|
||||||
|
uri: `/scheduled_statuses/${scheduledStatus.get('id')}`,
|
||||||
|
url: `/scheduled_statuses/${scheduledStatus.get('id')}`,
|
||||||
|
visibility: params.get('visibility'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return fromJS(status).set('account', account);
|
||||||
|
};
|
|
@ -0,0 +1,90 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
|
import { buildStatus } from '../builder';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||||
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
|
import Avatar from 'soapbox/components/avatar';
|
||||||
|
import DisplayName from 'soapbox/components/display_name';
|
||||||
|
import AttachmentList from 'soapbox/components/attachment_list';
|
||||||
|
import PollContainer from 'soapbox/containers/poll_container';
|
||||||
|
import ScheduledStatusActionBar from './scheduled_status_action_bar';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const scheduledStatus = state.getIn(['scheduled_statuses', props.statusId]);
|
||||||
|
return {
|
||||||
|
status: buildStatus(state, scheduledStatus),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class ScheduledStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, showThread, account, ...other } = this.props;
|
||||||
|
if (!status.get('account')) return null;
|
||||||
|
|
||||||
|
const statusUrl = `/scheduled_statuses/${status.get('id')}`;
|
||||||
|
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||||
|
const domain = getDomain(status.get('account'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='scheduled-status'>
|
||||||
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id') })} tabIndex={this.props.muted ? null : 0}>
|
||||||
|
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
|
||||||
|
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||||
|
<div className='status__info'>
|
||||||
|
<NavLink to={statusUrl} className='status__relative-time'>
|
||||||
|
<RelativeTimestamp timestamp={status.get('created_at')} futureDate />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
{favicon &&
|
||||||
|
<div className='status__favicon'>
|
||||||
|
<Link to={`/timeline/${domain}`}>
|
||||||
|
<img src={favicon} alt='' title={domain} />
|
||||||
|
</Link>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div className='status__profile'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])}>
|
||||||
|
<Avatar account={status.get('account')} size={48} />
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||||
|
<DisplayName account={status.get('account')} />
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
expanded
|
||||||
|
collapsable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AttachmentList
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||||
|
|
||||||
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
|
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||||
|
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScheduledStatusActionBar status={status} account={account} {...other} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
cancel: { id: 'schedled_status.cancel', defaultMessage: 'Cancel' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const me = state.get('me');
|
||||||
|
return {
|
||||||
|
me,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, null, null, { forwardRef: true })
|
||||||
|
@injectIntl
|
||||||
|
class ScheduledStatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
me: SoapboxPropTypes.me,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancelClick = e => {
|
||||||
|
const { status, dispatch } = this.props;
|
||||||
|
dispatch(cancelScheduledStatus(status.get('id')));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status__action-bar'>
|
||||||
|
<div className='status__button'>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.cancel)}
|
||||||
|
text={intl.formatMessage(messages.cancel)}
|
||||||
|
icon='close'
|
||||||
|
onClick={this.handleCancelClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,8 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import StatusList from '../../components/status_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses';
|
import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses';
|
||||||
|
import ScheduledStatus from './components/scheduled_status';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -29,11 +30,8 @@ class ScheduledStatuses extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
|
||||||
statusIds: ImmutablePropTypes.list.isRequired,
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -49,24 +47,19 @@ class ScheduledStatuses extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
const { intl, statusIds, hasMore, isLoading } = this.props;
|
||||||
const pinned = !!columnId;
|
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.scheduled_statuses' defaultMessage="You don't have any scheduled statuses yet. When you add one, it will show up here." />;
|
const emptyMessage = <FormattedMessage id='empty_column.scheduled_statuses' defaultMessage="You don't have any scheduled statuses yet. When you add one, it will show up here." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column icon='edit' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
<Column icon='edit' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
<StatusList
|
<ScrollableList
|
||||||
trackScroll={!pinned}
|
scrollKey='scheduled_statuses'
|
||||||
statusIds={statusIds}
|
|
||||||
scrollKey={`scheduled_statuses-${columnId}`}
|
|
||||||
hasMore={hasMore}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onLoadMore={this.handleLoadMore}
|
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
isLoading={isLoading}
|
||||||
/>
|
hasMore={hasMore}
|
||||||
|
>
|
||||||
|
{statusIds.map(id => <ScheduledStatus key={id} statusId={id} />)}
|
||||||
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { STATUS_CREATE_SUCCESS } from 'soapbox/actions/statuses';
|
import { STATUS_CREATE_SUCCESS } from 'soapbox/actions/statuses';
|
||||||
|
import {
|
||||||
|
SCHEDULED_STATUSES_FETCH_SUCCESS,
|
||||||
|
SCHEDULED_STATUS_CANCEL_REQUEST,
|
||||||
|
SCHEDULED_STATUS_CANCEL_SUCCESS,
|
||||||
|
} from 'soapbox/actions/scheduled_statuses';
|
||||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
@ -10,6 +15,8 @@ const importStatus = (state, status) => {
|
||||||
const importStatuses = (state, statuses) =>
|
const importStatuses = (state, statuses) =>
|
||||||
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
|
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
|
||||||
|
|
||||||
|
const deleteStatus = (state, id) => state.delete(id);
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
export default function statuses(state = initialState, action) {
|
||||||
|
@ -18,7 +25,11 @@ export default function statuses(state = initialState, action) {
|
||||||
case STATUS_CREATE_SUCCESS:
|
case STATUS_CREATE_SUCCESS:
|
||||||
return importStatus(state, action.status);
|
return importStatus(state, action.status);
|
||||||
case STATUSES_IMPORT:
|
case STATUSES_IMPORT:
|
||||||
|
case SCHEDULED_STATUSES_FETCH_SUCCESS:
|
||||||
return importStatuses(state, action.statuses);
|
return importStatuses(state, action.statuses);
|
||||||
|
case SCHEDULED_STATUS_CANCEL_REQUEST:
|
||||||
|
case SCHEDULED_STATUS_CANCEL_SUCCESS:
|
||||||
|
return deleteStatus(state, action.id);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ import {
|
||||||
SCHEDULED_STATUSES_EXPAND_REQUEST,
|
SCHEDULED_STATUSES_EXPAND_REQUEST,
|
||||||
SCHEDULED_STATUSES_EXPAND_SUCCESS,
|
SCHEDULED_STATUSES_EXPAND_SUCCESS,
|
||||||
SCHEDULED_STATUSES_EXPAND_FAIL,
|
SCHEDULED_STATUSES_EXPAND_FAIL,
|
||||||
|
SCHEDULED_STATUS_CANCEL_REQUEST,
|
||||||
|
SCHEDULED_STATUS_CANCEL_SUCCESS,
|
||||||
} from '../actions/scheduled_statuses';
|
} from '../actions/scheduled_statuses';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
@ -81,9 +83,9 @@ const prependOneToList = (state, listType, status) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeOneFromList = (state, listType, status) => {
|
const removeOneFromList = (state, listType, statusId) => {
|
||||||
return state.update(listType, listMap => listMap.withMutations(map => {
|
return state.update(listType, listMap => listMap.withMutations(map => {
|
||||||
map.set('items', map.get('items').filter(item => item !== status.get('id')));
|
map.set('items', map.get('items').filter(item => item !== statusId));
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,6 +135,9 @@ export default function statusLists(state = initialState, action) {
|
||||||
return normalizeList(state, 'scheduled_statuses', action.statuses, action.next);
|
return normalizeList(state, 'scheduled_statuses', action.statuses, action.next);
|
||||||
case SCHEDULED_STATUSES_EXPAND_SUCCESS:
|
case SCHEDULED_STATUSES_EXPAND_SUCCESS:
|
||||||
return appendToList(state, 'scheduled_statuses', action.statuses, action.next);
|
return appendToList(state, 'scheduled_statuses', action.statuses, action.next);
|
||||||
|
case SCHEDULED_STATUS_CANCEL_REQUEST:
|
||||||
|
case SCHEDULED_STATUS_CANCEL_SUCCESS:
|
||||||
|
return removeOneFromList(state, 'scheduled_statuses', action.id || action.status.get('id'));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,10 @@
|
||||||
|
|
||||||
.detailed-status__button {
|
.detailed-status__button {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__button,
|
||||||
|
.detailed-status__button {
|
||||||
.icon-button {
|
.icon-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
Loading…
Reference in New Issue