Add indicator when default expires is set

This commit is contained in:
noellabo 2022-10-11 11:47:03 +09:00
parent 206b5dbf04
commit a97312571c
15 changed files with 168 additions and 40 deletions

View file

@ -12,7 +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';
import { addYears, addMonths, addDays, addHours, addMinutes, addSeconds, millisecondsToSeconds, set, parseISO, formatISO, format } from 'date-fns';
import { Set as ImmutableSet } from 'immutable';
import { postReferenceModal } from '../initial_state';
@ -187,10 +187,10 @@ const parseSimpleDurationFormat = (value, origin = new Date()) => {
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);
const [, year = 0, month = 0, day = 0, hour = 0, minute = 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), minute) - origin);
return duration == 0 ? null : duration;
return duration === 0 ? null : duration;
};
export const getDateTimeFromText = (value, origin = new Date()) => {
@ -843,16 +843,20 @@ export function removeDateTime() {
};
export function changeScheduled(value) {
const date = value instanceof Date ? format(value, 'yyyy-MM-dd HH:mm') : value;
return {
type: COMPOSE_SCHEDULED_CHANGE,
value: value,
value: date,
};
};
export function changeExpires(value) {
const date = value instanceof Date ? format(value, 'yyyy-MM-dd HH:mm') : value;
return {
type: COMPOSE_EXPIRES_CHANGE,
value: value,
value: date,
};
};

View file

@ -16,6 +16,7 @@ import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container';
import CircleDropdownContainer from '../containers/circle_dropdown_container';
import DateTimeFormContainer from '../containers/datetime_form_container';
import ExpiresIndicatorContainer from '../containers/expires_indicator_container';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../containers/poll_form_container';
import UploadFormContainer from '../containers/upload_form_container';
@ -261,6 +262,7 @@ class ComposeForm extends ImmutablePureComponent {
<UploadFormContainer />
<PollFormContainer />
<DateTimeFormContainer />
<ExpiresIndicatorContainer />
</div>
</AutosuggestTextarea>

View file

@ -8,10 +8,10 @@ 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";
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' },
@ -50,7 +50,6 @@ class DateTimeDropdown extends React.PureComponent {
};
static propTypes = {
presets: ImmutablePropTypes.list,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(Date),
@ -67,7 +66,9 @@ class DateTimeDropdown extends React.PureComponent {
id: PropTypes.string,
valueKey: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired,
datetimePresets: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
};
static defaultProps = {
@ -85,7 +86,7 @@ class DateTimeDropdown extends React.PureComponent {
getDateTimePresets = () => {
const { datetimePresets, intl } = this.props;
if (!datetimePresets) {
return ImmutableList([
ImmutableMap({ id: '5m', title: intl.formatMessage(messages.minutes, { number: 5 }) }),
@ -101,12 +102,12 @@ class DateTimeDropdown extends React.PureComponent {
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
const { valueKey, onChange, minDate, maxDate, openToDate, dispatch } = this.props;
dispatch(openModal('CALENDAR', {
valueKey: valueKey,
@ -121,15 +122,13 @@ class DateTimeDropdown extends React.PureComponent {
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} />
const CalendarIconButton = forwardRef((props, ref) => (
<IconButton icon='calendar' className='datetime-dropdown__calendar-icon' title={intl.formatMessage(messages.datetime_open_calendar)} style={{ width: 'auto', height: 'auto' }} onClick={props.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} />
:
{layout === 'mobile' ? <CalendarIconButton onClick={this.onOpenCalendar} /> : (
<DatePicker
selected={max([dateValue ?? openToDate, minDate ?? minTime])}
onChange={onChange}
@ -140,7 +139,7 @@ class DateTimeDropdown extends React.PureComponent {
showTimeInput
portalId='modal-root'
/>
}
)}
<input
type='text'
@ -155,7 +154,7 @@ class DateTimeDropdown extends React.PureComponent {
/>
<select className='datetime-dropdown__menu' title={intl.formatMessage(messages.datetime_select)} value={stringValue} onChange={this.handleChange}>
<option value={stringValue} key='default'></option>
<option value={stringValue} key='default' />
<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>,

View file

@ -24,7 +24,7 @@ class DateTimeForm extends ImmutablePureComponent {
return (
<div className='compose-form__datetime-wrapper'>
<div className='datetime__schedule'>
<div className='datetime__category'><FormattedMessage id='datetime.scheduled' defaultMessage='Scheduled' /></div>
<div className='datetime__category'><FormattedMessage id='datetime.scheduled' defaultMessage='Scheduled' /></div>
<ScheduledDropDownContainer />
</div>
<div className='datetime__expire'>

View file

@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import IconButton from '../../../components/icon_button';
import { formatDuration } from 'date-fns';
const messages = defineMessages({
cancel: { id: 'expires_indicator.cancel', defaultMessage: 'Cancel' },
expires_mark: { id: 'datetime.expires_action.mark', defaultMessage: 'Mark as expired' },
expires_delete: { id: 'datetime.expires_action.delete', defaultMessage: 'Delete' },
});
export default @injectIntl
class ExpiresIndicator extends ImmutablePureComponent {
static propTypes = {
default_expires: PropTypes.bool,
expires: PropTypes.string,
expires_action: PropTypes.string,
onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onCancel();
}
render () {
const { default_expires, expires, expires_action, intl } = this.props;
if (!default_expires) {
return null;
}
const [, years = 0, months = 0, days = 0, hours = 0, minutes = 0] = expires.match(/^(?:(\d+)y)?(?:(\d+)m(?=[\do])o?)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/) ?? [];
const duration_message = formatDuration({ years: Number(years), months: Number(months), days: Number(days), hours: Number(hours), minutes: Number(minutes) });
const expires_action_message = (expires_action => {
switch (expires_action) {
case 'mark':
return intl.formatMessage(messages.expires_mark);
case 'delete':
return intl.formatMessage(messages.expires_delete);
default:
return '';
}
})(expires_action);
return (
<div className='compose-form__datetime-wrapper'>
<div className='datetime__expires-indicator'>
<div className='datetime__expires-indicator__icon'>
<Icon id='calendar' fixedWidth />
</div>
<div className='datetime__expires-indicator__message'>
<div className='expires-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
<FormattedMessage id='expires_indicator.message' defaultMessage='{action} after {duration}' values={{ action: expires_action_message, duration: duration_message }} />
</div>
</div>
</div>
);
}
}

View file

@ -2,9 +2,9 @@ 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'
import { addDays, addSeconds, set } from 'date-fns';
const mapStateToProps = (state, { intl }) => {
const mapStateToProps = (state) => {
const valueKey = ['compose', 'expires'];
const value = state.getIn(valueKey) ?? '';
const scheduledAt = getDateTimeFromText(state.getIn(['compose', 'scheduled']), new Date()).at ?? new Date();

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import ExpiresIndicator from '../components/expires_indicator';
import { removeDateTime } from '../../../actions/compose';
const mapStateToProps = state => ({
default_expires: state.getIn(['compose', 'default_expires']),
expires: state.getIn(['compose', 'expires']),
expires_action: state.getIn(['compose', 'expires_action']),
});
const mapDispatchToProps = dispatch => ({
onCancel () {
dispatch(removeDateTime());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ExpiresIndicator);

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import DateTimeDropdown from '../components/datetime_dropdown';
import { changeScheduled } from '../../../actions/compose';
import { addDays, addSeconds, set } from 'date-fns'
import { addDays, addSeconds, set } from 'date-fns';
const mapStateToProps = state => {
const valueKey = ['compose', 'scheduled'];

View file

@ -4,11 +4,9 @@ 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";
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' },

View file

@ -276,6 +276,8 @@
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"expires_indicator.cancel": "Cancel",
"expires_indicator.message": "{action} after {duration}",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",

View file

@ -276,6 +276,8 @@
"error.unexpected_crash.next_steps_addons": "それらを無効化してからリロードをお試しください。それでも解決しない場合、他のブラウザやアプリで Mastodon をお試しください。",
"errors.unexpected_crash.copy_stacktrace": "スタックトレースをクリップボードにコピー",
"errors.unexpected_crash.report_issue": "問題を報告",
"expires_indicator.cancel": "キャンセル",
"expires_indicator.message": "{duration}後に{action}する",
"follow_recommendations.done": "完了",
"follow_recommendations.heading": "投稿を見たい人をフォローしてください!ここにおすすめがあります。",
"follow_recommendations.lead": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",

View file

@ -106,6 +106,7 @@ const initialState = ImmutableMap({
dirty: false,
}),
datetime_form: null,
default_expires: null,
scheduled: null,
expires: null,
expires_action: 'mark',
@ -162,7 +163,8 @@ const clearAll = state => {
map.update('media_attachments', list => list.clear());
map.set('poll', null);
map.set('idempotencyKey', uuid());
map.set('datetime_form', state.get('default_expires_in') ? true : null);
map.set('datetime_form', null);
map.set('default_expires', state.get('default_expires_in') ? true : null);
map.set('scheduled', null);
map.set('expires', state.get('default_expires_in', null));
map.set('expires_action', state.get('default_expires_action', 'mark'));
@ -420,7 +422,8 @@ export default function compose(state = initialState, action) {
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.set('datetime_form', state.get('default_expires_in') ? true : null);
map.set('datetime_form', null);
map.set('default_expires', state.get('default_expires_in') ? true : null);
map.set('scheduled', null);
map.set('expires', state.get('default_expires_in', null));
map.set('expires_action', state.get('default_expires_action', 'mark'));
@ -448,7 +451,8 @@ 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', state.get('default_expires_in') ? true : null);
map.set('datetime_form', null);
map.set('default_expires', state.get('default_expires_in') ? true : null);
map.set('scheduled', null);
map.set('expires', state.get('default_expires_in', null));
map.set('expires_action', state.get('default_expires_action', 'mark'));
@ -478,7 +482,8 @@ export default function compose(state = initialState, action) {
map.set('circle_id', null);
map.set('poll', null);
map.set('idempotencyKey', uuid());
map.set('datetime_form', state.get('default_expires_in') ? true : null);
map.set('datetime_form', null);
map.set('default_expires', state.get('default_expires_in') ? true : null);
map.set('scheduled', null);
map.set('expires', state.get('default_expires_in', null));
map.set('expires_action', state.get('default_expires_action', 'mark'));
@ -585,7 +590,7 @@ export default function compose(state = initialState, action) {
}));
case REDRAFT:
return state.withMutations(map => {
const default_expires_in_exist = state.get('default_expires_in') ? true : null;
const datetime_form = !!action.status.get('scheduled_at') || !!action.status.get('expires_at') ? true : null;
map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status))));
map.set('in_reply_to', action.status.get('in_reply_to_id'));
@ -600,7 +605,8 @@ 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 : default_expires_in_exist);
map.set('datetime_form', datetime_form);
map.set('default_expires', !datetime_form && state.get('default_expires_in') ? 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') : state.get('default_expires_in', null));
map.set('expires_action', action.status.get('expires_action') ?? state.get('default_expires_action', 'mark'));
@ -636,10 +642,14 @@ export default function compose(state = initialState, action) {
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);
return state.withMutations(map => {
map.set('datetime_form', true);
map.set('default_expires', null);
});
case COMPOSE_DATETIME_FORM_CLOSE:
return state.withMutations(map => {
map.set('datetime_form', null);
map.set('default_expires', null);
map.set('scheduled', null);
map.set('expires', null);
map.set('expires_action', 'mark');

View file

@ -813,11 +813,16 @@
}
.reply-indicator__cancel,
.quote-indicator__cancel {
.quote-indicator__cancel,
.expires-indicator__cancel {
float: right;
line-height: 24px;
}
.expires-indicator__cancel {
margin-top: -4px;
}
.reply-indicator__display-name,
.quote-indicator__display-name {
color: $inverted-text-color;

View file

@ -36,6 +36,25 @@
}
}
.datetime__expires-indicator {
color: $inverted-text-color;
background: lighten($ui-primary-color, 8%);
border-top: 1px solid darken($simple-background-color, 8%);
padding: 10px;
font-size: 13px;
font-weight: 400;
display: flex;
column-gap: 4px;
.datetime__expires-indicator__icon {
flex: 0 0 auto;
}
.datetime__expires-indicator__message {
flex: 1 1 auto;
}
}
.datetime__category {
margin-bottom: 4px;
}

View file

@ -42,7 +42,7 @@ locales.forEach(locale => {
`../../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
@ -52,7 +52,9 @@ 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)
import setDefaultOptions from 'date-fns/setDefaultOptions';
setDefaultOptions({ locale: dateFnsLocaleDate });
registerLocale('${locale}', dateFnsLocaleDate);
setDefaultLocale('${locale}');
setLocale({messages, localeData});
`;