diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 5e7295dd4..43704d3dc 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -90,6 +90,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_post_reference_modal, :setting_add_reference_modal, :setting_unselect_reference_modal, + :setting_delete_scheduled_status_modal, :setting_enable_empty_column, :setting_content_font_size, :setting_info_font_size, diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 382552eeb..166193842 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -12,9 +12,10 @@ import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; import { openModal } from './modal'; import { defineMessages } from 'react-intl'; -import { addYears, addMonths, addDays, addHours, addMinutes, addSeconds, millisecondsToSeconds, set, parseISO, formatISO, format } from 'date-fns'; +import { addYears, addMonths, addDays, addHours, addMinutes, addSeconds, millisecondsToSeconds, set, formatISO, format } from 'date-fns'; import { Set as ImmutableSet } from 'immutable'; import { postReferenceModal, enableFederatedTimeline } from '../initial_state'; +import { deleteScheduledStatus } from './scheduled_statuses'; let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; @@ -35,6 +36,10 @@ 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 const COMPOSE_SCHEDULED_EDIT_CANCEL = 'COMPOSE_SCHEDULED_EDIT_CANCEL'; + +export const SCHEDULED_STATUS_SUBMIT_SUCCESS = 'SCHEDULED_STATUS_SUBMIT_SUCCESS'; + export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST'; export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS'; export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL'; @@ -182,6 +187,12 @@ export function directCompose(account, routerHistory) { }; }; +export function cancelScheduledStatusCompose() { + return { + type: COMPOSE_SCHEDULED_EDIT_CANCEL, + }; +}; + const parseSimpleDurationFormat = (value, origin = new Date()) => { if (!value || typeof value !== 'string') { return null; @@ -205,7 +216,7 @@ export const getDateTimeFromText = (value, origin = new Date()) => { } if (value.length >= 7) { - const isoDateTime = parseISO(value); + const isoDateTime = new Date(value); if (isoDateTime.toString() === 'Invalid Date') { return null; @@ -255,6 +266,7 @@ export function submitCompose(routerHistory) { const { in: expires_in = null, at: expires_at = null } = getDateTimeFromText(getState().getIn(['compose', 'expires']), scheduled_at ?? new Date()); const expires_action = getState().getIn(['compose', 'expires_action']); const statusReferenceIds = getState().getIn(['compose', 'references']); + const scheduled_status_id = getState().getIn(['compose', 'scheduled_status_id']); if ((!status || !status.length) && media.size === 0) { return; @@ -285,7 +297,11 @@ export function submitCompose(routerHistory) { }, }).then(function (response) { if (response.data.scheduled_at !== null && response.data.scheduled_at !== undefined) { - dispatch(submitComposeSuccess({ ...response.data })); + dispatch(submitScheduledStatusSuccess({ ...response.data })); + if (scheduled_status_id) { + dispatch(deleteScheduledStatus(scheduled_status_id)); + } + routerHistory.push('/scheduled_statuses'); return; } else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) { routerHistory.goBack(); @@ -348,6 +364,13 @@ export function submitComposeFail(error) { }; }; +export function submitScheduledStatusSuccess(status) { + return { + type: SCHEDULED_STATUS_SUBMIT_SUCCESS, + scheduled_status: status, + }; +}; + export function uploadCompose(files) { return function (dispatch, getState) { const uploadLimit = 4; @@ -898,7 +921,7 @@ export function removeReference(id) { }; }; -export function resetReference() { +export function resetReference() { return { type: COMPOSE_REFERENCE_RESET, }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index a5c281da1..171943e8d 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -66,7 +66,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (['mention', 'status', 'status_reference'].includes(notification.type)) { + if (['mention', 'status', 'scheduled_status', 'status_reference'].includes(notification.type)) { const dropRegex = filters[0]; const regex = filters[1]; const searchIndex = searchTextFromRawStatus(notification.status); diff --git a/app/javascript/mastodon/actions/scheduled_statuses.js b/app/javascript/mastodon/actions/scheduled_statuses.js new file mode 100644 index 000000000..c5f73e83a --- /dev/null +++ b/app/javascript/mastodon/actions/scheduled_statuses.js @@ -0,0 +1,147 @@ +import api, { getLinks } from '../api'; +import { Map as ImmutableMap } from 'immutable'; +import { ensureComposeIsVisible } from './compose'; +import { fetchStatuses, redraft } from './statuses'; +import { uniqCompact } from '../utils/uniq'; + +export const SCHEDULED_STATUSES_FETCH_REQUEST = 'SCHEDULED_STATUSES_FETCH_REQUEST'; +export const SCHEDULED_STATUSES_FETCH_SUCCESS = 'SCHEDULED_STATUSES_FETCH_SUCCESS'; +export const SCHEDULED_STATUSES_FETCH_FAIL = 'SCHEDULED_STATUSES_FETCH_FAIL'; + +export const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQUEST'; +export const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS'; +export const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL'; + +export const SCHEDULED_STATUS_DELETE_REQUEST = 'SCHEDULED_STATUS_DELETE_REQUEST'; +export const SCHEDULED_STATUS_DELETE_SUCCESS = 'SCHEDULED_STATUS_DELETE_SUCCESS'; +export const SCHEDULED_STATUS_DELETE_FAIL = 'SCHEDULED_STATUS_DELETE_FAIL'; + +export function fetchScheduledStatuses() { + return (dispatch, getState) => { + if (getState().getIn(['scheduled_statuses', 'isLoading'])) { + return; + } + + dispatch(fetchScheduledStatusesRequest()); + + api(getState).get('/api/v1/scheduled_statuses').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchScheduledStatusesFail(error)); + }); + }; +}; + +export function fetchScheduledStatusesRequest() { + return { + type: SCHEDULED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchScheduledStatusesSuccess(scheduled_statuses, next) { + return { + type: SCHEDULED_STATUSES_FETCH_SUCCESS, + scheduled_statuses, + next, + }; +}; + +export function fetchScheduledStatusesFail(error) { + return { + type: SCHEDULED_STATUSES_FETCH_FAIL, + error, + }; +}; + +export function expandScheduledStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['scheduled_statuses', 'next'], null); + + if (url === null || getState().getIn(['scheduled_statuses', 'isLoading'])) { + return; + } + + dispatch(expandScheduledStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandScheduledStatusesFail(error)); + }); + }; +}; + +export function expandScheduledStatusesRequest() { + return { + type: SCHEDULED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandScheduledStatusesSuccess(scheduled_statuses, next) { + return { + type: SCHEDULED_STATUSES_EXPAND_SUCCESS, + scheduled_statuses, + next, + }; +}; + +export function expandScheduledStatusesFail(error) { + return { + type: SCHEDULED_STATUSES_EXPAND_FAIL, + error, + }; +}; + +export function deleteScheduledStatus(id) { + return (dispatch, getState) => { + dispatch(deleteScheduledStatusRequest(id)); + + api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(() => { + dispatch(deleteScheduledStatusSuccess(id)); + }).catch(error => { + dispatch(deleteScheduledStatusFail(id, error)); + }); + }; +}; + +export function deleteScheduledStatusRequest(id) { + return { + type: SCHEDULED_STATUS_DELETE_REQUEST, + id: id, + }; +}; + +export function deleteScheduledStatusSuccess(id) { + return { + type: SCHEDULED_STATUS_DELETE_SUCCESS, + id: id, + }; +}; + +export function deleteScheduledStatusFail(id, error) { + return { + type: SCHEDULED_STATUS_DELETE_FAIL, + id: id, + error: error, + }; +}; + +export function redraftScheduledStatus(scheduled_status, routerHistory) { + return (dispatch, getState) => { + const status = ImmutableMap({ + ...scheduled_status.get('params').toObject(), + ...scheduled_status.delete('params').toObject(), + scheduled_status_id: scheduled_status.get('id'), + quote: scheduled_status.getIn(['params', 'quote_id']) ? ImmutableMap({ id: scheduled_status.getIn(['params', 'quote_id']), url: '' }) : null, + }); + + dispatch(fetchStatuses(uniqCompact([status.get('in_reply_to_id'), status.getIn(['quote', 'id']), ...status.get('status_reference_ids').toArray()]))); + + const replyStatus = getState().getIn(['statuses', status.get('in_reply_to_id')], null)?.update('account', account => getState().getIn(['accounts', account])) || null; + + dispatch(redraft(getState, status, replyStatus, status.get('text'))); + ensureComposeIsVisible(getState, routerHistory); + }; +}; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 087715b48..de20ff861 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -112,7 +112,7 @@ export function fetchStatuses(ids) { dispatch(fetchAccountsFromStatuses(statuses)); dispatch(fetchStatusesSuccess()); }).catch(error => { - dispatch(fetchStatusesFail(id, error)); + dispatch(fetchStatusesFail(newStatusIds, error)); }); }; }; @@ -123,10 +123,10 @@ export function fetchStatusesSuccess() { }; }; -export function fetchStatusesFail(id, error) { +export function fetchStatusesFail(ids, error) { return { type: STATUSES_FETCH_FAIL, - id, + ids, error, skipAlert: true, }; @@ -144,12 +144,8 @@ export function redraft(getState, status, replyStatus, raw_text) { export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { - let status = getState().getIn(['statuses', id]); - const replyStatus = status.get('in_reply_to_id') ? getState().getIn(['statuses', status.get('in_reply_to_id')]) : null; - - if (status.get('poll')) { - status = status.set('poll', getState().getIn(['polls', status.get('poll')])); - } + const status = getState().getIn(['statuses', id]).update('poll', poll => poll ? getState().getIn(['polls', poll]) : null); + const replyStatus = status.get('in_reply_to_id') ? getState().getIn(['statuses', status.get('in_reply_to_id')]).update('account', account => getState().getIn(['accounts', account])) : null; dispatch(deleteStatusRequest(id)); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 0c0b29b73..5dde26fe9 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -22,6 +22,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; +import { deleteScheduledStatusSuccess } from './scheduled_statuses'; const { messages } = getLocale(); @@ -85,6 +86,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'expire': dispatch(expireFromTimelines(data.payload)); break; + case 'scheduled_status': + dispatch(deleteScheduledStatusSuccess(data.payload)); + break; case 'notification': dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index a8bac7a34..b5aac3405 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -711,9 +711,9 @@ class Status extends ImmutablePureComponent { ); } - const expires_at = status.get('expires_at') - const expires_date = expires_at && new Date(expires_at) - const expired = expires_date && expires_date.getTime() < intl.now() + const expires_at = status.get('expires_at'); + const expires_date = expires_at && new Date(expires_at); + const expired = expires_date && expires_date.getTime() < intl.now(); const ancestorCount = showThread && show_reply_tree_button && status.get('in_reply_to_id', 0) > 0 ? 1 : 0; const descendantCount = showThread && show_reply_tree_button ? status.get('replies_count', 0) : 0; @@ -732,30 +732,34 @@ class Status extends ImmutablePureComponent { threadMarkTitle = intl.formatMessage(messages.mark_descendant); } - const threadMark = threadCount > 0 ? 0, - 'status__thread_mark-descendant': descendantCount > 0, - })} title={threadMarkTitle}>+ : null; + const threadMark = threadCount > 0 ? ( 0, + 'status__thread_mark-descendant': descendantCount > 0, + })} title={threadMarkTitle} + >+) : null; return ( -
0, - })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> +
0, + })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} + > {prepend}
- {status.get('expires_at') && } + {status.get('expires_at') &&