Schedule posts
This commit is contained in:
parent
969910481f
commit
3a209e2fea
|
@ -63,6 +63,10 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
|||
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
||||
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||
|
||||
export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
||||
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
||||
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
||||
|
||||
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.' },
|
||||
|
@ -133,7 +137,7 @@ export function directCompose(account, routerHistory) {
|
|||
export function handleComposeSubmit(dispatch, getState, response, status) {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
dispatch(insertIntoTagHistory(response.data.tags || [], status));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
// To make the app more responsive, immediately push the status into the columns
|
||||
|
@ -179,7 +183,7 @@ export function submitCompose(routerHistory, group) {
|
|||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
group_id: group ? group.get('id') : null,
|
||||
scheduled_at: getState().getIn(['compose', 'schedule'], null),
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
|
@ -533,6 +537,25 @@ export function removePoll() {
|
|||
};
|
||||
};
|
||||
|
||||
export function addSchedule() {
|
||||
return {
|
||||
type: COMPOSE_SCHEDULE_ADD,
|
||||
};
|
||||
};
|
||||
|
||||
export function setSchedule(date) {
|
||||
return {
|
||||
type: COMPOSE_SCHEDULE_SET,
|
||||
date: date,
|
||||
};
|
||||
};
|
||||
|
||||
export function removeSchedule() {
|
||||
return {
|
||||
type: COMPOSE_SCHEDULE_REMOVE,
|
||||
};
|
||||
};
|
||||
|
||||
export function addPollOption(title) {
|
||||
return {
|
||||
type: COMPOSE_POLL_OPTION_ADD,
|
||||
|
|
|
@ -12,6 +12,8 @@ import UploadButtonContainer from '../containers/upload_button_container';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||
import ScheduleFormContainer from '../containers/schedule_form_container';
|
||||
import ScheduleButtonContainer from '../containers/schedule_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
|
@ -71,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
group: ImmutablePropTypes.map,
|
||||
isModalOpen: PropTypes.bool,
|
||||
clickableAreaRef: PropTypes.object,
|
||||
scheduledAt: PropTypes.instanceOf(Date),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -310,6 +313,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
<ScheduleFormContainer />
|
||||
</div>
|
||||
}
|
||||
</AutosuggestTextarea>
|
||||
|
@ -321,6 +325,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
<ScheduleButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<MarkdownButtonContainer />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
add_schedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' },
|
||||
remove_schedule: { id: 'schedule_button.remove_schedule', defaultMessage: 'Post immediately' },
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
};
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class ScheduleButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, active, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__schedule-button'>
|
||||
<IconButton
|
||||
icon='calendar'
|
||||
title={intl.formatMessage(active ? messages.remove_schedule : messages.add_schedule)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
className={`compose-form__schedule-button-icon ${active ? 'active' : ''}`}
|
||||
size={18}
|
||||
inverted
|
||||
style={iconStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const messages = defineMessages({
|
||||
schedule: { id: 'schedule.post_time', defaultMessage: 'Post Date/Time' },
|
||||
});
|
||||
|
||||
class ScheduleForm extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
schedule: PropTypes.instanceOf(Date),
|
||||
intl: PropTypes.object.isRequired,
|
||||
onSchedule: PropTypes.func.isRequired,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
setSchedule(date)
|
||||
{
|
||||
this.setState({ schedule: date });
|
||||
this.props.onSchedule(date);
|
||||
}
|
||||
|
||||
openDatePicker(datePicker)
|
||||
{
|
||||
if (!datePicker)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
datePicker.setOpen(true);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this.setState({ schedule: this.props.schedule });
|
||||
}
|
||||
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.setSchedule = this.setSchedule.bind(this);
|
||||
}
|
||||
|
||||
isCurrentOrFutureDate(date)
|
||||
{
|
||||
return date && new Date().setHours(0, 0, 0, 0) <= date.setHours(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
isFiveMinutesFromNow(time)
|
||||
{
|
||||
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); // now, plus five minutes (Pleroma won't schedule posts )
|
||||
const selectedDate = new Date(time);
|
||||
|
||||
return fiveMinutesFromNow.getTime() < selectedDate.getTime();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.active || !this.state)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
const { schedule } = this.state;
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
selected={schedule}
|
||||
showTimeSelect
|
||||
dateFormat='MMMM d, yyyy h:mm aa'
|
||||
timeIntervals={15}
|
||||
timeFormat='HH:mm'
|
||||
timeInputLabel='Time:'
|
||||
wrapperClassName='react-datepicker-wrapper'
|
||||
onChange={this.setSchedule}
|
||||
placeholderText={this.props.intl.formatMessage(messages.schedule)}
|
||||
filterDate={this.isCurrentOrFutureDate}
|
||||
filterTime={this.isFiveMinutesFromNow}
|
||||
ref={this.isCurrentOrFutureDate(schedule) ? null : this.openDatePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
schedule: state.getIn(['compose', 'schedule']),
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(ScheduleForm));
|
|
@ -26,6 +26,7 @@ const mapStateToProps = state => ({
|
|||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isModalOpen: state.get('modal').modalType === 'COMPOSE',
|
||||
maxTootChars: state.getIn(['instance', 'max_toot_chars']),
|
||||
schedule: state.getIn(['instance', 'schedule']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ScheduleButton from '../components/schedule_button';
|
||||
import { addSchedule, removeSchedule } from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'schedule']) ? true : false,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'schedule'])) {
|
||||
dispatch(removeSchedule());
|
||||
} else {
|
||||
dispatch(addSchedule());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleButton);
|
|
@ -0,0 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ScheduleForm from '../components/schedule_form';
|
||||
import { setSchedule } from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
schedule: state.getIn(['compose', 'schedule']),
|
||||
active: state.getIn(['compose', 'schedule']) ? true : false,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSchedule(date) {
|
||||
dispatch(setSchedule(date));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleForm);
|
|
@ -189,6 +189,9 @@
|
|||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"schedule_button.add_schedule": "Schedule post for later",
|
||||
"schedule_button.remove_schedule": "Post immediately",
|
||||
"schedule.post_time": "Post Date/Time",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
|
||||
"confirmations.admin.deactivate_user.message": "You are about to deactivate @{acct}. Deactivating a user is a reversible action.",
|
||||
|
|
|
@ -32,6 +32,9 @@ import {
|
|||
COMPOSE_RESET,
|
||||
COMPOSE_POLL_ADD,
|
||||
COMPOSE_POLL_REMOVE,
|
||||
COMPOSE_SCHEDULE_ADD,
|
||||
COMPOSE_SCHEDULE_SET,
|
||||
COMPOSE_SCHEDULE_REMOVE,
|
||||
COMPOSE_POLL_OPTION_ADD,
|
||||
COMPOSE_POLL_OPTION_CHANGE,
|
||||
COMPOSE_POLL_OPTION_REMOVE,
|
||||
|
@ -81,6 +84,10 @@ const initialPoll = ImmutableMap({
|
|||
multiple: false,
|
||||
});
|
||||
|
||||
const initialSchedule = new Date();
|
||||
initialSchedule.setDate(initialSchedule.getDate() - 1);
|
||||
|
||||
|
||||
function statusToTextMentions(state, status, account) {
|
||||
const author = status.getIn(['account', 'acct']);
|
||||
const mentions = status.get('mentions', []).map(m => m.get('acct'));
|
||||
|
@ -107,6 +114,7 @@ function clearAll(state) {
|
|||
map.set('media_attachments', ImmutableList());
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('schedule', null);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -398,6 +406,12 @@ export default function compose(state = initialState, action) {
|
|||
return state.set('poll', initialPoll);
|
||||
case COMPOSE_POLL_REMOVE:
|
||||
return state.set('poll', null);
|
||||
case COMPOSE_SCHEDULE_ADD:
|
||||
return state.set('schedule', initialSchedule);
|
||||
case COMPOSE_SCHEDULE_SET:
|
||||
return state.set('schedule', action.date);
|
||||
case COMPOSE_SCHEDULE_REMOVE:
|
||||
return state.set('schedule', null);
|
||||
case COMPOSE_POLL_OPTION_ADD:
|
||||
return state.updateIn(['poll', 'options'], options => options.push(action.title));
|
||||
case COMPOSE_POLL_OPTION_CHANGE:
|
||||
|
|
|
@ -90,7 +90,8 @@
|
|||
}
|
||||
|
||||
.autosuggest-textarea__textarea,
|
||||
.spoiler-input__input {
|
||||
.spoiler-input__input,
|
||||
.react-datepicker__input-container input {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
@ -376,6 +377,16 @@
|
|||
}
|
||||
} // end .compose-form
|
||||
|
||||
.react-datepicker-wrapper {
|
||||
margin-left: 10px;
|
||||
background: var(--background-color);
|
||||
border: 2px solid var(--brand-color);
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
background: var(--background-color) !important;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
align-items: center;
|
||||
background: rgba($base-overlay-background, 0.8);
|
||||
|
|
|
@ -129,6 +129,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.ellipsis::after { content: "…"; }
|
||||
|
||||
.timeline-compose-block {
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"qrcode.react": "^1.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-color": "^2.18.1",
|
||||
"react-datepicker": "^4.1.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^6.0.0",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
|
|
40
yarn.lock
40
yarn.lock
|
@ -1690,6 +1690,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
|
||||
integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
|
||||
|
||||
"@popperjs/core@^2.9.2":
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
||||
|
@ -3415,6 +3420,11 @@ classnames@^2.2.5:
|
|||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
||||
classnames@^2.2.6:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
clean-css@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
|
||||
|
@ -4135,6 +4145,11 @@ data-urls@^2.0.0:
|
|||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@^2.0.1:
|
||||
version "2.22.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
|
||||
integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
|
||||
|
||||
date-now@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||
|
@ -9983,6 +9998,18 @@ react-color@^2.18.1:
|
|||
reactcss "^1.2.0"
|
||||
tinycolor2 "^1.4.1"
|
||||
|
||||
react-datepicker@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.1.1.tgz#5ecef49c672b2250fca26327c988464e6ba52b62"
|
||||
integrity sha512-vtZIA7MbUrffRw1CHiyOGtmTO/tTdZGr5BYaiRucHMTb6rCqA8TkaQhzX6tTwMwP8vV38Khv4UWohrJbiX1rMw==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.0.1"
|
||||
prop-types "^15.7.2"
|
||||
react-onclickoutside "^6.10.0"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
react-dom@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
|
||||
|
@ -10102,6 +10129,11 @@ react-notification@^6.8.4:
|
|||
dependencies:
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-onclickoutside@^6.10.0:
|
||||
version "6.11.2"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz#790e2100b9a3589eefca1404ecbf0476b81b7928"
|
||||
integrity sha512-640486eSwU/t5iD6yeTlefma8dI3bxPXD93hM9JGKyYITAd0P1JFkkcDeyHZRqNpY/fv1YW0Fad9BXr44OY8wQ==
|
||||
|
||||
react-overlays@^0.9.0:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.9.2.tgz#51ab1c62ded5af4d279bd3b943999531bbd648da"
|
||||
|
@ -10122,6 +10154,14 @@ react-popper@^2.2.3:
|
|||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-popper@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
|
||||
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-redux-loading-bar@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-5.0.0.tgz#fffbc2b893c556b7b4c577743427507ee6dbc1f3"
|
||||
|
|
Loading…
Reference in New Issue