diff --git a/app/soapbox/actions/scheduled_statuses.js b/app/soapbox/actions/scheduled_statuses.js index 4e54846b8..5d5b493f1 100644 --- a/app/soapbox/actions/scheduled_statuses.js +++ b/app/soapbox/actions/scheduled_statuses.js @@ -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_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() { return (dispatch, getState) => { 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() { return { type: SCHEDULED_STATUSES_FETCH_REQUEST, diff --git a/app/soapbox/features/scheduled_statuses/builder.js b/app/soapbox/features/scheduled_statuses/builder.js new file mode 100644 index 000000000..fd0772fe3 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/builder.js @@ -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); +}; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js new file mode 100644 index 000000000..c0fd0ffd8 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js @@ -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 ( +
+
+
+
+
+ + + + + {favicon && +
+ + + +
} + +
+
+ + + +
+ + + +
+
+ + + + + + {status.get('poll') && } + + {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( + + )} + + +
+
+
+ ); + } + +}; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js new file mode 100644 index 000000000..509f01520 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js @@ -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 ( +
+
+ +
+
+ ); + } + +} diff --git a/app/soapbox/features/scheduled_statuses/index.js b/app/soapbox/features/scheduled_statuses/index.js index ace5684bd..0d293e7e0 100644 --- a/app/soapbox/features/scheduled_statuses/index.js +++ b/app/soapbox/features/scheduled_statuses/index.js @@ -5,8 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; 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 ScheduledStatus from './components/scheduled_status'; import { debounce } from 'lodash'; const messages = defineMessages({ @@ -29,11 +30,8 @@ class ScheduledStatuses extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, - shouldUpdateScroll: PropTypes.func, statusIds: ImmutablePropTypes.list.isRequired, intl: PropTypes.object.isRequired, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, hasMore: PropTypes.bool, isLoading: PropTypes.bool, }; @@ -49,24 +47,19 @@ class ScheduledStatuses extends ImmutablePureComponent { render() { - const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; - const pinned = !!columnId; - + const { intl, statusIds, hasMore, isLoading } = this.props; const emptyMessage = ; return ( - + isLoading={isLoading} + hasMore={hasMore} + > + {statusIds.map(id => )} + ); } diff --git a/app/soapbox/reducers/scheduled_statuses.js b/app/soapbox/reducers/scheduled_statuses.js index e2ab142e4..2e304a8eb 100644 --- a/app/soapbox/reducers/scheduled_statuses.js +++ b/app/soapbox/reducers/scheduled_statuses.js @@ -1,4 +1,9 @@ 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 { Map as ImmutableMap, fromJS } from 'immutable'; @@ -10,6 +15,8 @@ const importStatus = (state, status) => { const importStatuses = (state, statuses) => state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); +const deleteStatus = (state, id) => state.delete(id); + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -18,7 +25,11 @@ export default function statuses(state = initialState, action) { case STATUS_CREATE_SUCCESS: return importStatus(state, action.status); case STATUSES_IMPORT: + case SCHEDULED_STATUSES_FETCH_SUCCESS: return importStatuses(state, action.statuses); + case SCHEDULED_STATUS_CANCEL_REQUEST: + case SCHEDULED_STATUS_CANCEL_SUCCESS: + return deleteStatus(state, action.id); default: return state; } diff --git a/app/soapbox/reducers/status_lists.js b/app/soapbox/reducers/status_lists.js index b5a96abe5..a9e5079b2 100644 --- a/app/soapbox/reducers/status_lists.js +++ b/app/soapbox/reducers/status_lists.js @@ -33,6 +33,8 @@ import { SCHEDULED_STATUSES_EXPAND_REQUEST, SCHEDULED_STATUSES_EXPAND_SUCCESS, SCHEDULED_STATUSES_EXPAND_FAIL, + SCHEDULED_STATUS_CANCEL_REQUEST, + SCHEDULED_STATUS_CANCEL_SUCCESS, } from '../actions/scheduled_statuses'; 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 => { - 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); case SCHEDULED_STATUSES_EXPAND_SUCCESS: 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: return state; } diff --git a/app/styles/components/detailed-status.scss b/app/styles/components/detailed-status.scss index 5bf63db28..4cbaad0ed 100644 --- a/app/styles/components/detailed-status.scss +++ b/app/styles/components/detailed-status.scss @@ -73,7 +73,10 @@ .detailed-status__button { padding: 10px 0; +} +.status__button, +.detailed-status__button { .icon-button { display: inline-flex; align-items: center;