Add schedule and expire form
This commit is contained in:
parent
0b8c3df283
commit
30be912026
30 changed files with 839 additions and 32 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ブースト",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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%);
|
||||
|
|
|
@ -1206,7 +1206,7 @@
|
|||
}
|
||||
|
||||
.status-expired .status__expiration-time {
|
||||
color: red;
|
||||
color: $warning-red;
|
||||
}
|
||||
|
||||
.status__display-name {
|
||||
|
|
90
app/javascript/styles/mastodon/datetimes.scss
Normal file
90
app/javascript/styles/mastodon/datetimes.scss
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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",
|
||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Reference in a new issue