diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index aa1c6de20e..aa23775085 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -81,6 +81,9 @@ export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
+export const COMPOSE_CHANGE_IS_SCHEDULED = 'COMPOSE_CHANGE_IS_SCHEDULED';
+export const COMPOSE_CHANGE_SCHEDULE_TIME = 'COMPOSE_CHANGE_SCHEDULE_TIME';
+
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@@ -188,6 +191,7 @@ export function submitCompose() {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const statusId = getState().getIn(['compose', 'id'], null);
+ const is_scheduled = getState().getIn(['compose', 'is_scheduled']);
if ((!status || !status.length) && media.size === 0) {
return;
@@ -228,6 +232,7 @@ export function submitCompose() {
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
+ scheduled_at: is_scheduled ? getState().getIn(['compose', 'scheduled_at']) : null,
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -237,9 +242,22 @@ export function submitCompose() {
browserHistory.goBack();
}
+
+ if ('scheduled_at' in response.data) {
+ dispatch(showAlert({
+ message: messages.saved,
+ dismissAfter: 10000,
+ }));
+ dispatch(submitComposeSuccess({ ...response.data.params}));
+ return;
+ }
+
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
+
+
+
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = timelineId => {
@@ -835,3 +853,16 @@ export const changeMediaOrder = (a, b) => ({
a,
b,
});
+
+export function changeIsScheduled() {
+ return {
+ type: COMPOSE_CHANGE_IS_SCHEDULED,
+ };
+}
+
+export function changeScheduleTime(value) {
+ return {
+ type: COMPOSE_CHANGE_SCHEDULE_TIME,
+ value,
+ };
+}
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index b5e8dabb7b..e1e0038eb9 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -29,6 +29,9 @@ import { PollForm } from "./poll_form";
import { ReplyIndicator } from './reply_indicator';
import { UploadForm } from './upload_form';
+import ScheduleButtonContainer from '../containers/schedule_button_container';
+import { ScheduleForm } from './schedule_form';
+
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
@@ -69,6 +72,11 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
+
+ schedule_time: PropTypes.string,
+ schedule_timezone: PropTypes.string,
+ is_scheduled: PropTypes.bool.isRequired,
+ scheduled_at: PropTypes.string,
};
static defaultProps = {
@@ -295,6 +303,7 @@ class ComposeForm extends ImmutablePureComponent {
+
@@ -306,6 +315,7 @@ class ComposeForm extends ImmutablePureComponent {
/>
+
diff --git a/app/javascript/mastodon/features/compose/components/schedule_form.jsx b/app/javascript/mastodon/features/compose/components/schedule_form.jsx
new file mode 100644
index 0000000000..64dc2b0020
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/schedule_form.jsx
@@ -0,0 +1,41 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { useDispatch, useSelector} from 'react-redux';
+
+import { changeScheduleTime } from 'mastodon/actions/compose';
+
+const messages = defineMessages({
+ schedule_time: { id: 'compose_form.schedule_time', defaultMessage: 'This post is scheduled to be published at (UTC+8)' },
+});
+
+export const ScheduleForm = () => {
+ const is_scheduled = useSelector(state => state.getIn(['compose', 'is_scheduled']));
+ const schedule_time = useSelector(state => state.getIn(['compose', 'schedule_time']));
+ const dispatch = useDispatch();
+ const intl = useIntl();
+
+ const handleChange = useCallback(({ target: { value } }) => {
+ dispatch(changeScheduleTime(value));
+ }, [dispatch]);
+
+ if (!is_scheduled) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
index bda2edba60..dd7932cdbc 100644
--- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js
+++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
@@ -29,6 +29,10 @@ const mapStateToProps = state => ({
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
+ is_scheduled: state.getIn(['compose', 'is_scheduled']),
+ schedule_time: state.getIn(['compose', 'schedule_time']),
+ schedule_timezone: state.getIn(['compose', 'schedule_timezone']),
+ scheduled_at: state.getIn(['compose', 'scheduled_at']),
});
const mapDispatchToProps = (dispatch) => ({
diff --git a/app/javascript/mastodon/features/compose/containers/schedule_button_container.js b/app/javascript/mastodon/features/compose/containers/schedule_button_container.js
new file mode 100644
index 0000000000..5d92f95670
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/containers/schedule_button_container.js
@@ -0,0 +1,30 @@
+import { injectIntl, defineMessages } from "react-intl";
+
+import { connect } from 'react-redux';
+
+import ScheduleIcon from '@/material-icons/400-20px/schedule.svg?react';
+import { IconButton } from "@/mastodon/components/icon_button";
+
+import { changeIsScheduled } from '../../../actions/compose';
+
+const messages = defineMessages({
+ marked: { id: 'compose_form.schedule.marked', defaultMessage: 'This post will be published at the time chosen below'},
+ unmarked: { id: 'compose_form.schedule.unmarked', defaultMessage: 'This post will be published at once'},
+})
+
+const mapStateToProps = (state, { intl }) => ({
+ iconComponent: ScheduleIcon,
+ title: intl.formatMessage(state.getIn(['compose', 'is_scheduled']) ? messages.marked : messages.unmarked),
+ active: state.getIn(['compose', 'is_scheduled']),
+ ariaControls: 'schedule-publish',
+ size: 18,
+ inverted: true,
+});
+
+const mapDispatchToProps = dispatch => ({
+ onClick () {
+ dispatch(changeIsScheduled());
+ },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));
\ No newline at end of file
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ec3851be40..27ff191d1d 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -166,6 +166,9 @@
"compose_form.publish_form": "New post",
"compose_form.reply": "Reply",
"compose_form.save_changes": "Update",
+ "compose_form.schedule.marked": "This post will be published at the time chosen below",
+ "compose_form.schedule.unmarked": "This post will be published at once",
+ "compose_form.schedule_time": "This post is scheduled to be published at (UTC+8)",
"compose_form.spoiler.marked": "Remove content warning",
"compose_form.spoiler.unmarked": "Add content warning",
"compose_form.spoiler_placeholder": "Content warning (optional)",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 914eb30e91..4a7e083c6e 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -166,6 +166,9 @@
"compose_form.publish_form": "发嘟",
"compose_form.reply": "回复",
"compose_form.save_changes": "更改",
+ "compose_form.schedule.marked": "本文将在以下时间发布",
+ "compose_form.schedule.unmarked": "本文将立即发布",
+ "compose_form.schedule_time": "计划发文时间(北京时间)",
"compose_form.spoiler.marked": "移除内容警告",
"compose_form.spoiler.unmarked": "添加内容警告",
"compose_form.spoiler_placeholder": "内容警告 (可选)",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index bfa2ec6a06..6b9a2c3ec4 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -50,6 +50,8 @@ import {
COMPOSE_CHANGE_MEDIA_ORDER,
COMPOSE_SET_STATUS,
COMPOSE_FOCUS,
+ COMPOSE_CHANGE_IS_SCHEDULED,
+ COMPOSE_CHANGE_SCHEDULE_TIME
} from '../actions/compose';
import { REDRAFT } from '../actions/statuses';
import { STORE_HYDRATE } from '../actions/store';
@@ -94,6 +96,11 @@ const initialState = ImmutableMap({
focusY: 0,
dirty: false,
}),
+
+ schedule_time: null,
+ schedule_timezone: '+08:00',
+ is_scheduled: false,
+ scheduled_at: null,
});
const initialPoll = ImmutableMap({
@@ -127,6 +134,9 @@ function clearAll(state) {
map.update('media_attachments', list => list.clear());
map.set('poll', null);
map.set('idempotencyKey', uuid());
+ map.set('schedule_time', null);
+ map.set('is_scheduled', false);
+ map.set('scheduled_at', null);
});
}
@@ -560,6 +570,18 @@ export default function compose(state = initialState, action) {
return list.splice(indexA, 1).splice(indexB, 0, moveItem);
});
+ case COMPOSE_CHANGE_IS_SCHEDULED:
+ return state.withMutations(map => {
+ map.set('is_scheduled', !state.get('is_scheduled'));
+ map.set('scheduled_at', state.get('schedule_time') + ':00.0' + state.get('schedule_timezone'));
+ map.set('idempotencyKey', uuid());
+ });
+ case COMPOSE_CHANGE_SCHEDULE_TIME:
+ return state.withMutations(map => {
+ map.set('schedule_time', action.value);
+ map.set('scheduled_at', action.value + ':00.0' + state.get('schedule_timezone'));
+ map.set('idempotencyKey', uuid());
+ });
default:
return state;
}
diff --git a/app/javascript/material-icons/400-20px/schedule.svg b/app/javascript/material-icons/400-20px/schedule.svg
new file mode 100644
index 0000000000..350607c71e
--- /dev/null
+++ b/app/javascript/material-icons/400-20px/schedule.svg
@@ -0,0 +1,6 @@
+
+