diff --git a/app/javascript/mastodon/features/compose/components/expires_indicator.js b/app/javascript/mastodon/features/compose/components/expires_indicator.js
new file mode 100644
index 000000000..ec296ea2a
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/components/expires_indicator.js
@@ -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 (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/compose/containers/expires_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/expires_dropdown_container.js
index 198b2efda..1b647af08 100644
--- a/app/javascript/mastodon/features/compose/containers/expires_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/expires_dropdown_container.js
@@ -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();
diff --git a/app/javascript/mastodon/features/compose/containers/expires_indicator_container.js b/app/javascript/mastodon/features/compose/containers/expires_indicator_container.js
new file mode 100644
index 000000000..2aa7b6e15
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/containers/expires_indicator_container.js
@@ -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);
diff --git a/app/javascript/mastodon/features/compose/containers/scheduled_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/scheduled_dropdown_container.js
index a45e5dc3e..f551515a6 100644
--- a/app/javascript/mastodon/features/compose/containers/scheduled_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/scheduled_dropdown_container.js
@@ -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'];
diff --git a/app/javascript/mastodon/features/ui/components/calendar_modal.js b/app/javascript/mastodon/features/ui/components/calendar_modal.js
index 8fbdf8f34..a0b1064e1 100644
--- a/app/javascript/mastodon/features/ui/components/calendar_modal.js
+++ b/app/javascript/mastodon/features/ui/components/calendar_modal.js
@@ -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' },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index df659abde..1b0878344 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -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!",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 1e85236dd..7b0564c3c 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -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": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index f81c6f2ed..38e9ca3f7 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -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');
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 438836489..651ee71b5 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -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;
diff --git a/app/javascript/styles/mastodon/datetimes.scss b/app/javascript/styles/mastodon/datetimes.scss
index 4e9f62951..29bc21067 100644
--- a/app/javascript/styles/mastodon/datetimes.scss
+++ b/app/javascript/styles/mastodon/datetimes.scss
@@ -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;
}
diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js
index 23bc2a498..1fb96eeb4 100644
--- a/config/webpack/generateLocalePacks.js
+++ b/config/webpack/generateLocalePacks.js
@@ -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});
`;