Add schedule and expire form

This commit is contained in:
noellabo 2021-07-27 21:05:40 +09:00
parent 0b8c3df283
commit 30be912026
30 changed files with 839 additions and 32 deletions

View file

@ -9,6 +9,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
before_action :set_circle, only: [:create]
before_action :set_schedule, only: [:create]
before_action :set_expire, only: [:create]
override_rate_limit_headers :create, family: :statuses
@ -46,7 +47,7 @@ class Api::V1::StatusesController < Api::BaseController
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at],
scheduled_at: @scheduled_at,
expires_at: @expires_at,
expires_action: status_params[:expires_action],
application: doorkeeper_token.application,
@ -99,8 +100,12 @@ class Api::V1::StatusesController < Api::BaseController
render json: { error: I18n.t('statuses.errors.circle_not_found') }, status: 404
end
def set_schedule
@scheduled_at = status_params[:scheduled_at]&.to_time || (status_params[:scheduled_in].blank? ? nil : Time.now.utc + status_params[:scheduled_in].to_i.seconds)
end
def set_expire
@expires_at = status_params[:expires_at] || status_params[:expires_in].blank? ? nil : status_params[:expires_in].to_i.seconds.from_now
@expires_at = status_params[:expires_at] || (status_params[:expires_in].blank? ? nil : (@scheduled_at || Time.now.utc) + status_params[:expires_in].to_i.seconds)
end
def status_params
@ -111,6 +116,7 @@ class Api::V1::StatusesController < Api::BaseController
:sensitive,
:spoiler_text,
:visibility,
:scheduled_in,
:scheduled_at,
:quote_id,
:expires_in,

View file

@ -12,6 +12,7 @@ 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 } from 'date-fns';
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
@ -37,9 +38,9 @@ export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL';
export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
@ -47,19 +48,19 @@ export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
@ -73,6 +74,12 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_DATETIME_FORM_OPEN = 'COMPOSE_DATETIME_FORM_OPEN';
export const COMPOSE_DATETIME_FORM_CLOSE = 'COMPOSE_DATETIME_FORM_CLOSE';
export const COMPOSE_SCHEDULED_CHANGE = 'COMPOSE_SCHEDULED_CHANGE';
export const COMPOSE_EXPIRES_CHANGE = 'COMPOSE_EXPIRES_CHANGE';
export const COMPOSE_EXPIRES_ACTION_CHANGE = 'COMPOSE_EXPIRES_ACTION_CHANGE';
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.' },
@ -155,12 +162,56 @@ export function directCompose(account, routerHistory) {
};
};
const parseSimpleDurationFormat = (value, origin = new Date()) => {
if (!value || typeof value !== 'string') {
return null;
}
const [_, year = 0, month = 0, day = 0, hour = 0, minite = 0] = value.match(/^(?:(\d+)y)?(?:(\d+)m(?=[\do])o?)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/) ?? [];
const duration = millisecondsToSeconds(addMinutes(addHours(addDays(addMonths(addYears(origin, year), month), day), hour), minite) - origin);
return duration == 0 ? null : duration;
};
export const getDateTimeFromText = (value, origin = new Date()) => {
const duration = parseSimpleDurationFormat(value, origin);
const datetime = (() => {
if (duration) {
return addSeconds(origin, duration);
}
if (typeof value !== 'string') {
return value;
}
if (value.length >= 7) {
const isoDateTime = parseISO(value);
if (isoDateTime.toString() === "Invalid Date") {
return null;
} else {
return isoDateTime;
}
}
return null
})();
return {
in: duration,
at: datetime,
};
};
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const homeVisibilities = getHomeVisibilities(getState());
const limitedVisibilities = getLimitedVisibilities(getState());
const { in: scheduled_in = null, at: scheduled_at = null } = getDateTimeFromText(getState().getIn(['compose', 'scheduled']), new Date());
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']);
if ((!status || !status.length) && media.size === 0) {
return;
@ -178,12 +229,20 @@ export function submitCompose(routerHistory) {
circle_id: getState().getIn(['compose', 'circle_id']),
poll: getState().getIn(['compose', 'poll'], null),
quote_id: getState().getIn(['compose', 'quote_from'], null),
scheduled_at: !scheduled_in && scheduled_at ? formatISO(set(scheduled_at, { seconds: 0 })) : null,
scheduled_in: scheduled_in,
expires_at: !expires_in && expires_at ? formatISO(set(expires_at, { seconds: 59 })) : null,
expires_in: expires_in,
expires_action: expires_action,
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
if (response.data.scheduled_at !== null && response.data.scheduled_at !== undefined) {
dispatch(submitComposeSuccess({ ...response.data }));
return;
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}
@ -718,3 +777,36 @@ export function changePollSettings(expiresIn, isMultiple) {
isMultiple,
};
};
export function addDateTime() {
return {
type: COMPOSE_DATETIME_FORM_OPEN,
};
};
export function removeDateTime() {
return {
type: COMPOSE_DATETIME_FORM_CLOSE,
};
};
export function changeScheduled(value) {
return {
type: COMPOSE_SCHEDULED_CHANGE,
value: value,
};
};
export function changeExpires(value) {
return {
type: COMPOSE_EXPIRES_CHANGE,
value: value,
};
};
export function changeExpiresAction(value) {
return {
type: COMPOSE_EXPIRES_ACTION_CHANGE,
value: value,
};
};

View file

@ -8,11 +8,13 @@ import QuoteIndicatorContainer from '../containers/quote_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestInput from '../../../components/autosuggest_input';
import PollButtonContainer from '../containers/poll_button_container';
import DateTimeButtonContainer from '../containers/datetime_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl } from 'react-intl';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import CircleDropdownContainer from '../containers/circle_dropdown_container';
import DateTimeFormContainer from '../containers/datetime_form_container';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../containers/poll_form_container';
import UploadFormContainer from '../containers/upload_form_container';
@ -251,6 +253,7 @@ class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__modifiers'>
<UploadFormContainer />
<PollFormContainer />
<DateTimeFormContainer />
</div>
</AutosuggestTextarea>
@ -260,6 +263,7 @@ class ComposeForm extends ImmutablePureComponent {
<PollButtonContainer />
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
<DateTimeButtonContainer />
</div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
</div>

View file

@ -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_datetime: { id: 'datetime_button.add_datetime', defaultMessage: 'Add datetime' },
remove_datetime: { id: 'datetime_button.remove_datetime', defaultMessage: 'Remove datetime' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
export default
@injectIntl
class DateTimeButton 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__datetime-button'>
<IconButton
icon='calendar'
title={intl.formatMessage(active ? messages.remove_datetime : messages.add_datetime)}
disabled={disabled}
onClick={this.handleClick}
className={`compose-form__datetime-button-icon ${active ? 'active' : ''}`}
size={18}
inverted
style={iconStyle}
/>
</div>
);
}
}

View file

@ -0,0 +1,168 @@
import React, { forwardRef } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import { getDateTimeFromText } from 'mastodon/actions/compose';
import { layoutFromWindow } from 'mastodon/is_mobile';
import { openModal } from '../../../actions/modal';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import classNames from 'classnames'
import { format, minTime, maxTime, max } from 'date-fns'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
const messages = defineMessages({
datetime_open_calendar: { id: 'datetime.open_calendar', defaultMessage: 'Open calendar' },
datetime_unset: { id: 'datetime.unset', defaultMessage: '(Unset)' },
datetime_select: { id: 'datetime.select', defaultMessage: 'Select datetime' },
datetime_time_subheading: { id: 'datetime.time_subheading', defaultMessage: 'Time' },
datetime_placeholder: { id: 'datetime.placeholder', defaultMessage: 'Enter the date or duration' },
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
months: { id: 'intervals.full.months', defaultMessage: '{number, plural, one {# month} other {# months}}' },
years: { id: 'intervals.full.years', defaultMessage: '{number, plural, one {# year} other {# years}}' },
years_months: { id: 'intervals.full.years_months', defaultMessage: '{year, plural, one {# year} other {# years}} and {month, plural, one {# month} other {# months}}' },
});
const mapStateToProps = (state, { value, origin, minDate, maxDate }) => {
const { at: valueAt, in: valueIn } = getDateTimeFromText(value, origin);
const dateValue = typeof value === 'string' ? valueAt : value;
const stringValue = typeof value === 'string' ? value : format(value, 'yyyy-MM-dd HH:mm');
const invalid = !!value && (!valueIn && !valueAt || dateValue < (minDate ?? minTime) || (maxDate ?? maxTime) < dateValue);
return {
datetimePresets: state.get('datetimePresets'),
dateValue: dateValue,
stringValue: stringValue,
invalid: invalid,
}
};
export default @connect(mapStateToProps)
@injectIntl
class DateTimeDropdown extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
presets: ImmutablePropTypes.list,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(Date),
]).isRequired,
stringValue: PropTypes.string,
dateValue: PropTypes.instanceOf(Date),
invalid: PropTypes.bool.isRequired,
origin: PropTypes.instanceOf(Date),
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
openToDate: PropTypes.instanceOf(Date),
placeholder: PropTypes.string,
className: PropTypes.string,
id: PropTypes.string,
valueKey: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
origin: new Date(),
};
handleChange = e => {
this.props.onChange(e.target.value);
this.input.focus();
};
setInput = (c) => {
this.input = c;
}
getDateTimePresets = () => {
const { datetimePresets, intl } = this.props;
if (!datetimePresets) {
return ImmutableList([
ImmutableMap({ id: '5m', title: intl.formatMessage(messages.minutes, { number: 5 }) }),
ImmutableMap({ id: '30m', title: intl.formatMessage(messages.minutes, { number: 30 }) }),
ImmutableMap({ id: '1h', title: intl.formatMessage(messages.hours, { number: 1 }) }),
ImmutableMap({ id: '6h', title: intl.formatMessage(messages.hours, { number: 6 }) }),
ImmutableMap({ id: '1d', title: intl.formatMessage(messages.days, { number: 1 }) }),
ImmutableMap({ id: '3d', title: intl.formatMessage(messages.days, { number: 3 }) }),
ImmutableMap({ id: '7d', title: intl.formatMessage(messages.days, { number: 7 }) }),
ImmutableMap({ id: '1mo', title: intl.formatMessage(messages.months, { number: 1 }) }),
ImmutableMap({ id: '6mo', title: intl.formatMessage(messages.months, { number: 6 }) }),
ImmutableMap({ id: '1y', title: intl.formatMessage(messages.years, { number: 1 }) }),
ImmutableMap({ id: '1y1mo', title: intl.formatMessage(messages.years_months, { year: 1, month: 1 }) }),
]);
}
return datetimePresets.toList();
};
onOpenCalendar = () => {
const { valueKey, onChange, minDate, maxDate, openToDate, dispatch } = this.props
dispatch(openModal('CALENDAR', {
valueKey: valueKey,
onChange: onChange,
minDate: minDate,
maxDate: maxDate,
openToDate: openToDate,
}));
};
render () {
const { dateValue, stringValue, invalid, minDate, maxDate, openToDate, placeholder, id, className, onChange, intl } = this.props;
const layout = layoutFromWindow();
const CalendarIconButton = forwardRef(({ value, onClick }, ref) => (
<IconButton icon='calendar' className='datetime-dropdown__calendar-icon' title={intl.formatMessage(messages.datetime_open_calendar)} style={{ width: 'auto', height: 'auto' }} onClick={onClick} ref={ref} />
));
return (
<div className='datetime-dropdown'>
{layout === 'mobile' ?
<IconButton icon='calendar' className='datetime-dropdown__calendar-icon' title={intl.formatMessage(messages.datetime_open_calendar)} style={{ width: 'auto', height: 'auto' }} onClick={this.onOpenCalendar} />
:
<DatePicker
selected={max([dateValue ?? openToDate, minDate ?? minTime])}
onChange={onChange}
customInput={<CalendarIconButton />}
minDate={minDate}
maxDate={maxDate}
timeInputLabel={intl.formatMessage(messages.datetime_time_subheading)}
showTimeInput
portalId='modal-root'
/>
}
<input
type='text'
ref={this.setInput}
placeholder={placeholder ?? intl.formatMessage(messages.datetime_placeholder)}
value={stringValue}
onChange={this.handleChange}
dir='auto'
aria-autocomplete='list'
id={id}
className={classNames('datetime-dropdown__input', className, { 'datetime-dropdown__input-invalid': invalid })}
/>
<select className='datetime-dropdown__menu' title={intl.formatMessage(messages.datetime_select)} value={stringValue} onChange={this.handleChange}>
<option value={stringValue} key='default'></option>
<option value='' key='unset'>{intl.formatMessage(messages.datetime_unset)}</option>
{this.getDateTimePresets().map(item =>
<option value={item.get('id')} key={item.get('id')}>{item.get('title')}</option>,
)}
</select>
</div>
);
}
}

View file

@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage } from 'react-intl';
import ScheduledDropDownContainer from '../containers/scheduled_dropdown_container';
import ExpiresDropDownContainer from '../containers/expires_dropdown_container';
import ExpiresActionContainer from '../containers/expires_action_container';
export default
@injectIntl
class DateTimeForm extends ImmutablePureComponent {
static propTypes = {
form_enable: PropTypes.bool.isRequired,
};
render () {
const { form_enable } = this.props;
if (!form_enable) {
return null;
}
return (
<div className='compose-form__datetime-wrapper'>
<div className='datetime__schedule'>
<div className='datetime__category'><FormattedMessage id='datetime.scheduled' defaultMessage='Scheduled' /></div>
<ScheduledDropDownContainer />
</div>
<div className='datetime__expire'>
<div className='datetime__category'><FormattedMessage id='datetime.expires' defaultMessage='Expires' /></div>
<ExpiresDropDownContainer />
<ExpiresActionContainer />
</div>
</div>
);
}
}

View file

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import RadioButton from 'mastodon/components/radio_button';
const messages = defineMessages({
expires_mark: { id: 'datetime.expires_action.mark', defaultMessage: 'Mark as expired' },
expires_delete: { id: 'datetime.expires_action.delete', defaultMessage: 'Delete' },
});
export default @injectIntl
class ExpiresAction extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleChange = e => {
this.props.onChange(e.target.value);
};
render () {
const { value, intl } = this.props;
return (
<div className='datetime-action' role='group'>
<RadioButton name='expires_action' value='mark' label={intl.formatMessage(messages.expires_mark)} checked={value === 'mark'} onChange={this.handleChange} />
<RadioButton name='expires_action' value='delete' label={intl.formatMessage(messages.expires_delete)} checked={value === 'delete'} onChange={this.handleChange} />
</div>
);
}
}

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import DateTimeButton from '../components/datetime_button';
import { addDateTime, removeDateTime } from '../../../actions/compose';
const mapStateToProps = state => ({
active: state.getIn(['compose', 'datetime_form']) !== null,
});
const mapDispatchToProps = dispatch => ({
onClick () {
dispatch((_, getState) => {
if (getState().getIn(['compose', 'datetime_form'])) {
dispatch(removeDateTime());
} else {
dispatch(addDateTime());
}
});
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DateTimeButton);

View file

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import DateTimeForm from '../components/datetime_form';
const mapStateToProps = state => ({
form_enable: !!state.getIn(['compose', 'datetime_form']),
});
export default connect(mapStateToProps)(DateTimeForm);

View file

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import ExpiresAction from '../components/expires_action';
import { changeExpiresAction } from '../../../actions/compose';
const mapStateToProps = state => ({
value: state.getIn(['compose', 'expires_action']) ?? '',
});
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeExpiresAction(value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ExpiresAction);

View file

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import DateTimeDropdown from '../components/datetime_dropdown';
import { changeExpires } from '../../../actions/compose';
import { getDateTimeFromText } from '../../../actions/compose';
import { addDays, addSeconds, set } from 'date-fns'
const mapStateToProps = (state, { intl }) => {
const valueKey = ['compose', 'expires'];
const value = state.getIn(valueKey) ?? '';
const scheduledAt = getDateTimeFromText(state.getIn(['compose', 'scheduled']), new Date()).at ?? new Date();
return {
value: value,
valueKey: valueKey,
origin: scheduledAt,
minDate: addSeconds(scheduledAt, 60),
maxDate: addSeconds(scheduledAt, 37152000),
openToDate: set(addDays(scheduledAt, 1), { minutes: 0, seconds: 0 }),
};
};
const mapDispatchToProps = (dispatch) => ({
onChange (value) {
dispatch(changeExpires(value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DateTimeDropdown);

View file

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import DateTimeDropdown from '../components/datetime_dropdown';
import { changeScheduled } from '../../../actions/compose';
import { addDays, addSeconds, set } from 'date-fns'
const mapStateToProps = state => {
const valueKey = ['compose', 'scheduled'];
const value = state.getIn(valueKey) ?? '';
return {
value: value,
valueKey: valueKey,
minDate: addSeconds(new Date(), 300),
openToDate: set(addDays(new Date(), 1), { minutes: 0, seconds: 0 }),
};
};
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeScheduled(value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DateTimeDropdown);

View file

@ -0,0 +1,68 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages } from 'react-intl';
import { getDateTimeFromText } from 'mastodon/actions/compose';
import { minTime, max } from 'date-fns'
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
const messages = defineMessages({
datetime_time_subheading: { id: 'datetime.time_subheading', defaultMessage: 'Time' },
});
const mapStateToProps = (state, { valueKey, origin, openToDate, minDate }) => {
const value = state.getIn(valueKey);
const dateValue = typeof value === 'string' ? getDateTimeFromText(value, origin).at : value;
return {
// selected: dateValue ?? openToDate,
selected: max([dateValue ?? openToDate, minDate ?? minTime]),
}
};
export default @connect(mapStateToProps, null, null, { forwardRef: true })
@injectIntl
class CalendarModal extends ImmutablePureComponent {
static propTypes = {
selected: PropTypes.instanceOf(Date),
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
handleChange = (value) => {
this.props.onChange(value);
};
handleSelect = () => {
this.props.onClose();
};
render () {
const { selected, minDate, maxDate, intl } = this.props;
return (
<div className='modal-root__modal calendar-modal'>
<div className='calendar-modal__container'>
<DatePicker
selected={selected}
onChange={this.handleChange}
onSelect={this.handleSelect}
minDate={minDate}
maxDate={maxDate}
timeInputLabel={intl.formatMessage(messages.datetime_time_subheading)}
showTimeInput
inline
/>
</div>
</div>
);
}
}

View file

@ -12,6 +12,7 @@ import BoostModal from './boost_modal';
import AudioModal from './audio_modal';
import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal';
import CalendarModal from './calendar_modal';
import {
MuteModal,
BlockModal,
@ -39,6 +40,7 @@ const MODAL_COMPONENTS = {
'LIST_ADDER': ListAdder,
'CIRCLE_EDITOR': CircleEditor,
'CIRCLE_ADDER': CircleAdder,
'CALENDAR': () => Promise.resolve({ default: CalendarModal }),
};
export default class ModalRoot extends React.PureComponent {
@ -72,7 +74,7 @@ export default class ModalRoot extends React.PureComponent {
}
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS', 'CALENDAR'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
}
renderError = (props) => {

View file

@ -167,6 +167,17 @@
"conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation",
"conversation.with": "With {names}",
"datetime_button.add_datetime": "Add datetime",
"datetime_button.remove_datetime": "Remove datetime",
"datetime.expires": "Expires",
"datetime.expires_action.mark": "Mark as expired",
"datetime.expires_action.delete": "Delete",
"datetime.open_calendar": "Open calendar",
"datetime.placeholder": "Enter the date or duration",
"datetime.scheduled": "Scheduled",
"datetime.select": "Select datetime",
"datetime.time_subheading": "Time",
"datetime.unset": "(Unset)",
"directory.federated": "From known fediverse",
"directory.local": "From {domain} only",
"directory.new_arrivals": "New arrivals",
@ -263,6 +274,9 @@
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
"intervals.full.months": "{number, plural, one {# month} other {# months}}",
"intervals.full.years": "{number, plural, one {# year} other {# years}}",
"intervals.full.years_months": "{year, plural, one {# year} other {# years}} and {month, plural, one {# month} other {# months}}",
"keyboard_shortcuts.back": "Navigate back",
"keyboard_shortcuts.blocked": "Open blocked users list",
"keyboard_shortcuts.boost": "Boost post",

View file

@ -167,6 +167,17 @@
"conversation.mark_as_read": "既読にする",
"conversation.open": "会話を表示",
"conversation.with": "{names}",
"datetime_button.add_datetime": "日時を追加",
"datetime_button.remove_datetime": "日時を削除",
"datetime.expires": "終了日時",
"datetime.expires_action.mark": "保持",
"datetime.expires_action.delete": "削除",
"datetime.open_calendar": "カレンダーを開く",
"datetime.placeholder": "日付か期間を入力",
"datetime.scheduled": "公開日時",
"datetime.select": "日時を選択",
"datetime.time_subheading": "時間",
"datetime.unset": "(解除)",
"directory.federated": "既知の連合より",
"directory.local": "{domain} のみ",
"directory.new_arrivals": "新着順",
@ -264,6 +275,9 @@
"intervals.full.days": "{number}日",
"intervals.full.hours": "{number}時間",
"intervals.full.minutes": "{number}分",
"intervals.full.months": "{number}か月",
"intervals.full.years": "{number}年",
"intervals.full.years_months": "{year}年{month}か月",
"keyboard_shortcuts.back": "戻る",
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
"keyboard_shortcuts.boost": "ブースト",

View file

@ -45,6 +45,11 @@ import {
INIT_MEDIA_EDIT_MODAL,
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
COMPOSE_CHANGE_MEDIA_FOCUS,
COMPOSE_DATETIME_FORM_OPEN,
COMPOSE_DATETIME_FORM_CLOSE,
COMPOSE_SCHEDULED_CHANGE,
COMPOSE_EXPIRES_CHANGE,
COMPOSE_EXPIRES_ACTION_CHANGE,
} from '../actions/compose';
import { TIMELINE_DELETE, TIMELINE_EXPIRE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
@ -53,6 +58,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde
import uuid from '../uuid';
import { me } from '../initial_state';
import { unescapeHTML } from '../utils/html';
import { parseISO, format } from 'date-fns';
const initialState = ImmutableMap({
mounted: 0,
@ -93,6 +99,10 @@ const initialState = ImmutableMap({
focusY: 0,
dirty: false,
}),
datetime_form: null,
scheduled: null,
expires: null,
expires_action: 'mark',
});
const initialPoll = ImmutableMap({
@ -141,7 +151,11 @@ const clearAll = state => {
map.update('media_attachments', list => list.clear());
map.set('poll', null);
map.set('idempotencyKey', uuid());
});
map.set('datetime_form', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');
});
};
const appendMedia = (state, media, file) => {
@ -356,7 +370,11 @@ export default function compose(state = initialState, action) {
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.set('datetime_form', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
@ -375,7 +393,11 @@ export default function compose(state = initialState, action) {
map.set('focusDate', new Date());
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.set('datetime_form', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
@ -399,6 +421,10 @@ export default function compose(state = initialState, action) {
map.set('circle_id', null);
map.set('poll', null);
map.set('idempotencyKey', uuid());
map.set('datetime_form', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');
});
case COMPOSE_SUBMIT_REQUEST:
return state.set('is_submitting', true);
@ -509,7 +535,11 @@ export default function compose(state = initialState, action) {
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('sensitive', action.status.get('sensitive'));
map.set('datetime_form', !!action.status.get('scheduled_at') || !!action.status.get('expires_at') ? true : null);
map.set('scheduled', action.status.get('scheduled_at'));
map.set('expires', action.status.get('expires_at') ? format(parseISO(action.status.get('expires_at')), 'yyyy-MM-dd HH:mm') : null);
map.set('expires_action', action.status.get('expires_action') ?? 'mark');
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
@ -538,6 +568,21 @@ export default function compose(state = initialState, action) {
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
case COMPOSE_POLL_SETTINGS_CHANGE:
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
case COMPOSE_DATETIME_FORM_OPEN:
return state.set('datetime_form', true);
case COMPOSE_DATETIME_FORM_CLOSE:
return state.withMutations(map => {
map.set('datetime_form', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');
});
case COMPOSE_SCHEDULED_CHANGE:
return state.set('scheduled', action.value);
case COMPOSE_EXPIRES_CHANGE:
return state.set('expires', action.value);
case COMPOSE_EXPIRES_ACTION_CHANGE:
return state.set('expires_action', action.value);
default:
return state;
}

View file

@ -17,6 +17,7 @@
@import 'mastodon/boost';
@import 'mastodon/components';
@import 'mastodon/polls';
@import 'mastodon/datetimes';
@import 'mastodon/introduction';
@import 'mastodon/modal';
@import 'mastodon/emoji_picker';

View file

@ -148,6 +148,8 @@ html {
.compose-form__autosuggest-wrapper,
.poll__option input[type="text"],
.compose-form .spoiler-input__input,
.compose-form .compose-form__datetime-wrapper select,
.compose-form .compose-form__datetime-wrapper input,
.compose-form__poll-wrapper select,
.circle-dropdown,
.search__input,
@ -173,11 +175,19 @@ html {
border-bottom: 0;
}
.compose-form .compose-form__datetime-wrapper .datetime__schedule .datetime-dropdown .datetime-dropdown__input-invalid,
.compose-form .compose-form__datetime-wrapper .datetime__expire .datetime-dropdown .datetime-dropdown__input-invalid {
background-color: darken($error-value-color, 35%);
}
.compose-form .compose-form__datetime-wrapper select,
.compose-form__poll-wrapper select,
.circle-dropdown__menu {
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
}
.compose-form .compose-form__datetime-wrapper .datetime__schedule,
.compose-form .compose-form__datetime-wrapper .datetime__expire,
.compose-form__poll-wrapper,
.compose-form__poll-wrapper .poll__footer {
border-top-color: lighten($ui-base-color, 8%);

View file

@ -1206,7 +1206,7 @@
}
.status-expired .status__expiration-time {
color: red;
color: $warning-red;
}
.status__display-name {

View file

@ -0,0 +1,90 @@
.compose-form {
.compose-form__datetime-wrapper {
.datetime__schedule,
.datetime__expire {
border-top: 1px solid darken($simple-background-color, 8%);
padding: 10px;
.datetime-dropdown {
display: flex;
.datetime-dropdown__input {
flex: 1 1 auto;
}
.datetime-dropdown__input-invalid {
background-color: lighten($error-value-color, 35%);
}
.datetime-dropdown__calendar-icon,
select {
flex: 0 0 auto;
&:focus {
border-color: $highlight-text-color;
}
}
.react-datepicker-wrapper,
.react-datepicker__input-container {
display: flex;
}
.react-datepicker-wrapper {
padding: 0 4px 0 0;
}
}
}
.datetime__category {
margin-bottom: 4px;
}
.datetime-action {
display: flex;
.radio-button {
flex: 1 1 auto;
}
}
input {
box-sizing: border-box;
font-size: 14px;
color: $inverted-text-color;
border: 1px solid darken($simple-background-color, 14%);
padding: 6px 10px;
border-radius: 4px 0 0 4px;
min-width: 0;
margin-left: 0;
margin-right: 0;
}
select {
appearance: none;
box-sizing: border-box;
font-size: 14px;
color: $inverted-text-color;
display: inline-block;
width: 30px;
outline: 0;
font-family: inherit;
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px;
border: 1px solid darken($simple-background-color, 14%);
border-radius: 0 4px 4px 0;
padding: 6px 10px;
text-indent: 100%;
margin-left: 0;
margin-right: 0;
}
.icon-button.disabled {
color: darken($simple-background-color, 14%);
}
}
}
.react-datepicker__navigation-icon--previous,
.react-datepicker__navigation-icon--next {
width: 0;
}

View file

@ -60,7 +60,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
end
def process_expiry_params
expiry = @object['expiry'].to_time rescue nil
expiry = @object['expiry']&.to_time
if expiry.nil?
@params

View file

@ -122,7 +122,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_expiry_params
expiry = @object['expiry'].to_time rescue nil
expiry = @object['expiry']&.to_time
if expiry.nil?
@params

View file

@ -406,7 +406,7 @@ class Account < ApplicationRecord
account: account,
name: attributes['name'].strip[0, string_limit],
value: attributes['value'].strip[0, string_limit],
verified_at: attributes['verified_at']&.to_datetime,
verified_at: attributes['verified_at']&.to_time,
)
end

View file

@ -26,7 +26,7 @@ class ScheduledStatus < ApplicationRecord
private
def validate_future_date
errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET
errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET - 20.seconds
end
def validate_total_limit

View file

@ -20,6 +20,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
attribute :quote_id, if: :quote?
attribute :expires_at, if: :has_expires?
attribute :expires_action, if: :has_expires?
attribute :visibility_ex, if: :visibility_ex?
belongs_to :reblog, serializer: REST::StatusSerializer
@ -74,6 +75,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object&.status_expire&.expires_at || object.expired_at
end
def expires_action
object&.status_expire&.action || 'mark'
end
def visibility_ex?
object.limited_visibility?
end

View file

@ -75,7 +75,7 @@ class PostStatusService < BaseService
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@visibility = :limited if @circle.present?
@visibility = :limited if @visibility&.to_sym != :direct && @in_reply_to&.limited_visibility?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = @options[:scheduled_at].is_a?(Time) ? @options[:scheduled_at] : @options[:scheduled_at]&.to_time
@scheduled_at = nil if scheduled_in_the_past?
if @quote_id.nil? && md = @text.match(/QT:\s*\[\s*(https:\/\/.+?)\s*\]/)
@quote_id = quote_from_url(md[1])&.id
@ -149,10 +149,10 @@ class PostStatusService < BaseService
def validate_expires!
return if @options[:expires_at].blank?
@expires_at = @options[:expires_at].is_a?(Time) ? @options[:expires_at] : @options[:expires_at].to_time rescue nil
@expires_at = @options[:expires_at].is_a?(Time) ? @options[:expires_at] : @options[:expires_at]&.to_time
raise Mastodon::ValidationError, I18n.t('status_expire.validations.invalid_expire_at') if @expires_at.nil?
raise Mastodon::ValidationError, I18n.t('status_expire.validations.expire_in_the_past') if @expires_at <= Time.now.utc + MIN_EXPIRE_OFFSET
raise Mastodon::ValidationError, I18n.t('status_expire.validations.expire_in_the_past') if @expires_at <= (@options[:scheduled_at]&.to_time || Time.now.utc) + MIN_EXPIRE_OFFSET
@expires_action = begin
case @options[:expires_action]&.to_sym
@ -195,7 +195,7 @@ class PostStatusService < BaseService
end
def scheduled_in_the_past?
@scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET
@scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET - 20.seconds
end
def bump_potential_friendship!

View file

@ -33,7 +33,16 @@ locales.forEach(locale => {
'../../node_modules/react-intl/locale-data/en.js',
].filter(filename => fs.existsSync(path.join(outPath, filename)))
.map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
const dateFnsLocaleDataPath = [
// first try date-fns
`../../node_modules/date-fns/locale/${locale}/index.js`,
// first try date-fns
`../../node_modules/date-fns/locale/${baseLocale}/index.js`,
// fall back to English (this is what date-fns does anyway)
`../../node_modules/date-fns/locale/en-US/index.js`,
].filter(filename => fs.existsSync(path.join(outPath, filename)))
.map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
const localeContent = `//
// locale_${locale}.js
// automatically generated by generateLocalePacks.js
@ -41,6 +50,10 @@ locales.forEach(locale => {
import messages from '../../app/javascript/mastodon/locales/${locale}.json';
import localeData from ${JSON.stringify(localeDataPath)};
import { setLocale } from '../../app/javascript/mastodon/locales';
import dateFnsLocaleDate from ${JSON.stringify(dateFnsLocaleDataPath)};
import { registerLocale, setDefaultLocale } from 'react-datepicker';
registerLocale('${locale}', dateFnsLocaleDate)
setDefaultLocale('${locale}');
setLocale({messages, localeData});
`;
fs.writeFileSync(localePath, localeContent, 'utf8');

View file

@ -86,6 +86,7 @@
"cross-env": "^7.0.3",
"css-loader": "^5.2.7",
"cssnano": "^4.1.11",
"date-fns": "^2.29.3",
"detect-passive-events": "^2.0.3",
"dotenv": "^10.0.0",
"emoji-mart": "^3.0.1",
@ -124,6 +125,7 @@
"prop-types": "^15.5.10",
"punycode": "^2.1.0",
"react": "^16.14.0",
"react-datepicker": "^4.1.1",
"react-dom": "^16.14.0",
"react-hotkeys": "^1.1.4",
"react-immutable-proptypes": "^2.2.0",

View file

@ -1454,6 +1454,11 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
"@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==
"@rails/ujs@^6.1.4":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.4.tgz#093d5341595a02089ed309dec40f3c37da7b1b10"
@ -3087,7 +3092,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5, classnames@^2.3.1:
classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@ -3752,6 +3757,16 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.0.1:
version "2.23.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"
integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -9055,6 +9070,18 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
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.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
@ -9074,6 +9101,11 @@ react-event-listener@^0.6.0:
prop-types "^15.6.0"
warning "^4.0.1"
react-fast-compare@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-hotkeys@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72"
@ -9172,6 +9204,11 @@ react-notification@^6.8.5:
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.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.9.3.tgz#5bac8c1e9e7e057a125181dee2d784864dd62902"
@ -9184,6 +9221,14 @@ react-overlays@^0.9.3:
react-transition-group "^2.2.1"
warning "^3.0.0"
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@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-4.0.8.tgz#e84d59d1517b79f53b0f39c8ddb40682af648c1b"
@ -11450,7 +11495,7 @@ warning@^3.0.0:
dependencies:
loose-envify "^1.0.0"
warning@^4.0.0, warning@^4.0.1:
warning@^4.0.0, warning@^4.0.1, warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==