1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
|
@@ -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.' }, open: { id: 'compose.published.open', defaultMessage: 'Open' }, @@ -187,6 +190,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 return; @@ -227,6 +231,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']), @@ -236,9 +241,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 => { @@ -829,3 +847,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
@@ -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 { <PollButtonContainer /> <SpoilerButtonContainer /> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> + <ScheduleButtonContainer /> <CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} /> </div>
@@ -306,6 +315,7 @@ class ComposeForm extends ImmutablePureComponent { /> </div> </div> + <ScheduleForm /> </div> </div> </form>
new file mode 100644
@@ -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: '计划发文时间(北京时间)' }, +}); + +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 ( + <div> + <label>{intl.formatMessage(messages.schedule_time)}</label> + <input + className='search__input' + type='datetime-local' + value={schedule_time} + onChange={handleChange} + /> + </div> + ); +} \ No newline at end of file
@@ -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'], 5000), + 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) => ({
new file mode 100644
@@ -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: '本文将在以下时间发布'}, + unmarked: { id: 'compose_form.schedule.unmarked', defaultMessage: '文本将立即发布'}, +}) + +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
@@ -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; }
new file mode 100644
@@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 20 20" version="1.1"> +<g id="surface1"> +<path style=" stroke:none;fill-rule:nonzero;fill-opacity:1;" d="M 13.476562 8.167969 C 15.507812 8.167969 17.382812 9.214844 18.398438 10.917969 C 19.417969 12.609375 19.417969 14.722656 18.398438 16.417969 C 17.382812 18.117188 15.507812 19.167969 13.476562 19.167969 C 10.332031 19.167969 7.785156 16.699219 7.785156 13.667969 C 7.785156 10.632812 10.332031 8.167969 13.476562 8.167969 Z M 7.160156 1.25 L 7.160156 2.058594 L 12.214844 2.058594 L 12.214844 1.25 C 12.214844 1.023438 12.398438 0.832031 12.632812 0.832031 L 13.066406 0.832031 C 13.292969 0.832031 13.484375 1.015625 13.484375 1.25 L 13.484375 2.058594 L 18.125 2.058594 C 18.359375 2.058594 18.542969 2.25 18.542969 2.476562 L 18.542969 8.367188 C 18.542969 8.589844 18.359375 8.785156 18.125 8.785156 L 17.691406 8.785156 C 17.457031 8.785156 17.273438 8.601562 17.273438 8.367188 L 17.273438 6.949219 L 2.101562 6.949219 L 2.101562 17.332031 L 7.375 17.332031 C 7.609375 17.332031 7.792969 17.515625 7.792969 17.75 L 7.792969 18.140625 C 7.792969 18.375 7.609375 18.558594 7.375 18.558594 L 0.832031 18.558594 L 0.832031 2.476562 C 0.832031 2.242188 1.015625 2.058594 1.25 2.058594 L 5.890625 2.058594 L 5.890625 1.25 C 5.890625 1.023438 6.074219 0.832031 6.308594 0.832031 L 6.742188 0.832031 C 6.964844 0.832031 7.160156 1.015625 7.160156 1.25 Z M 13.476562 9.390625 C 11.890625 9.390625 10.433594 10.207031 9.640625 11.535156 C 8.851562 12.859375 8.851562 14.492188 9.640625 15.808594 C 10.433594 17.125 11.890625 17.949219 13.476562 17.949219 C 15.917969 17.949219 17.898438 16.035156 17.898438 13.675781 C 17.898438 11.316406 15.917969 9.390625 13.476562 9.390625 Z M 12.523438 11.226562 L 13.375 11.226562 C 13.609375 11.226562 13.792969 11.410156 13.792969 11.640625 L 13.792969 13.941406 C 13.792969 14.089844 13.875 14.234375 14.007812 14.308594 L 14.976562 14.851562 C 15.074219 14.902344 15.144531 14.996094 15.175781 15.105469 C 15.207031 15.210938 15.191406 15.328125 15.132812 15.425781 L 14.726562 16.117188 L 12.75 15.015625 C 12.617188 14.941406 12.535156 14.800781 12.535156 14.648438 L 12.535156 11.226562 Z M 5.890625 3.273438 L 2.101562 3.273438 L 2.101562 5.714844 L 17.273438 5.714844 L 17.273438 3.273438 L 13.484375 3.273438 L 13.484375 4.082031 C 13.484375 4.316406 13.300781 4.5 13.066406 4.5 L 12.632812 4.5 C 12.398438 4.5 12.214844 4.316406 12.214844 4.082031 L 12.214844 3.273438 L 7.160156 3.273438 L 7.160156 4.082031 C 7.160156 4.316406 6.976562 4.5 6.742188 4.5 L 6.308594 4.5 C 6.074219 4.5 5.890625 4.316406 5.890625 4.082031 Z M 5.890625 3.273438 "/> +</g> +</svg> (END)
|