Add scheduled statuses column
This commit is contained in:
parent
5e92944842
commit
304af2abcb
56 changed files with 1053 additions and 123 deletions
|
@ -90,6 +90,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_post_reference_modal,
|
||||
:setting_add_reference_modal,
|
||||
:setting_unselect_reference_modal,
|
||||
:setting_delete_scheduled_status_modal,
|
||||
:setting_enable_empty_column,
|
||||
:setting_content_font_size,
|
||||
:setting_info_font_size,
|
||||
|
|
|
@ -12,9 +12,10 @@ 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, format } from 'date-fns';
|
||||
import { addYears, addMonths, addDays, addHours, addMinutes, addSeconds, millisecondsToSeconds, set, formatISO, format } from 'date-fns';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
import { postReferenceModal, enableFederatedTimeline } from '../initial_state';
|
||||
import { deleteScheduledStatus } from './scheduled_statuses';
|
||||
|
||||
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
|
||||
|
||||
|
@ -35,6 +36,10 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
|||
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
|
||||
export const COMPOSE_SCHEDULED_EDIT_CANCEL = 'COMPOSE_SCHEDULED_EDIT_CANCEL';
|
||||
|
||||
export const SCHEDULED_STATUS_SUBMIT_SUCCESS = 'SCHEDULED_STATUS_SUBMIT_SUCCESS';
|
||||
|
||||
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
|
||||
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
|
||||
export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL';
|
||||
|
@ -182,6 +187,12 @@ export function directCompose(account, routerHistory) {
|
|||
};
|
||||
};
|
||||
|
||||
export function cancelScheduledStatusCompose() {
|
||||
return {
|
||||
type: COMPOSE_SCHEDULED_EDIT_CANCEL,
|
||||
};
|
||||
};
|
||||
|
||||
const parseSimpleDurationFormat = (value, origin = new Date()) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return null;
|
||||
|
@ -205,7 +216,7 @@ export const getDateTimeFromText = (value, origin = new Date()) => {
|
|||
}
|
||||
|
||||
if (value.length >= 7) {
|
||||
const isoDateTime = parseISO(value);
|
||||
const isoDateTime = new Date(value);
|
||||
|
||||
if (isoDateTime.toString() === 'Invalid Date') {
|
||||
return null;
|
||||
|
@ -255,6 +266,7 @@ export function submitCompose(routerHistory) {
|
|||
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']);
|
||||
const statusReferenceIds = getState().getIn(['compose', 'references']);
|
||||
const scheduled_status_id = getState().getIn(['compose', 'scheduled_status_id']);
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
return;
|
||||
|
@ -285,7 +297,11 @@ export function submitCompose(routerHistory) {
|
|||
},
|
||||
}).then(function (response) {
|
||||
if (response.data.scheduled_at !== null && response.data.scheduled_at !== undefined) {
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
dispatch(submitScheduledStatusSuccess({ ...response.data }));
|
||||
if (scheduled_status_id) {
|
||||
dispatch(deleteScheduledStatus(scheduled_status_id));
|
||||
}
|
||||
routerHistory.push('/scheduled_statuses');
|
||||
return;
|
||||
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
|
||||
routerHistory.goBack();
|
||||
|
@ -348,6 +364,13 @@ export function submitComposeFail(error) {
|
|||
};
|
||||
};
|
||||
|
||||
export function submitScheduledStatusSuccess(status) {
|
||||
return {
|
||||
type: SCHEDULED_STATUS_SUBMIT_SUCCESS,
|
||||
scheduled_status: status,
|
||||
};
|
||||
};
|
||||
|
||||
export function uploadCompose(files) {
|
||||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
|
@ -898,7 +921,7 @@ export function removeReference(id) {
|
|||
};
|
||||
};
|
||||
|
||||
export function resetReference() {
|
||||
export function resetReference() {
|
||||
return {
|
||||
type: COMPOSE_REFERENCE_RESET,
|
||||
};
|
||||
|
|
|
@ -66,7 +66,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
|
||||
let filtered = false;
|
||||
|
||||
if (['mention', 'status', 'status_reference'].includes(notification.type)) {
|
||||
if (['mention', 'status', 'scheduled_status', 'status_reference'].includes(notification.type)) {
|
||||
const dropRegex = filters[0];
|
||||
const regex = filters[1];
|
||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||
|
|
147
app/javascript/mastodon/actions/scheduled_statuses.js
Normal file
147
app/javascript/mastodon/actions/scheduled_statuses.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
import api, { getLinks } from '../api';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
import { fetchStatuses, redraft } from './statuses';
|
||||
import { uniqCompact } from '../utils/uniq';
|
||||
|
||||
export const SCHEDULED_STATUSES_FETCH_REQUEST = 'SCHEDULED_STATUSES_FETCH_REQUEST';
|
||||
export const SCHEDULED_STATUSES_FETCH_SUCCESS = 'SCHEDULED_STATUSES_FETCH_SUCCESS';
|
||||
export const SCHEDULED_STATUSES_FETCH_FAIL = 'SCHEDULED_STATUSES_FETCH_FAIL';
|
||||
|
||||
export const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQUEST';
|
||||
export const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS';
|
||||
export const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export const SCHEDULED_STATUS_DELETE_REQUEST = 'SCHEDULED_STATUS_DELETE_REQUEST';
|
||||
export const SCHEDULED_STATUS_DELETE_SUCCESS = 'SCHEDULED_STATUS_DELETE_SUCCESS';
|
||||
export const SCHEDULED_STATUS_DELETE_FAIL = 'SCHEDULED_STATUS_DELETE_FAIL';
|
||||
|
||||
export function fetchScheduledStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['scheduled_statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchScheduledStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/scheduled_statuses').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchScheduledStatusesFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchScheduledStatusesRequest() {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_FETCH_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchScheduledStatusesSuccess(scheduled_statuses, next) {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_FETCH_SUCCESS,
|
||||
scheduled_statuses,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchScheduledStatusesFail(error) {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandScheduledStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['scheduled_statuses', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['scheduled_statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandScheduledStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandScheduledStatusesFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandScheduledStatusesRequest() {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_EXPAND_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandScheduledStatusesSuccess(scheduled_statuses, next) {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_EXPAND_SUCCESS,
|
||||
scheduled_statuses,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandScheduledStatusesFail(error) {
|
||||
return {
|
||||
type: SCHEDULED_STATUSES_EXPAND_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteScheduledStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(deleteScheduledStatusRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(() => {
|
||||
dispatch(deleteScheduledStatusSuccess(id));
|
||||
}).catch(error => {
|
||||
dispatch(deleteScheduledStatusFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteScheduledStatusRequest(id) {
|
||||
return {
|
||||
type: SCHEDULED_STATUS_DELETE_REQUEST,
|
||||
id: id,
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteScheduledStatusSuccess(id) {
|
||||
return {
|
||||
type: SCHEDULED_STATUS_DELETE_SUCCESS,
|
||||
id: id,
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteScheduledStatusFail(id, error) {
|
||||
return {
|
||||
type: SCHEDULED_STATUS_DELETE_FAIL,
|
||||
id: id,
|
||||
error: error,
|
||||
};
|
||||
};
|
||||
|
||||
export function redraftScheduledStatus(scheduled_status, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
const status = ImmutableMap({
|
||||
...scheduled_status.get('params').toObject(),
|
||||
...scheduled_status.delete('params').toObject(),
|
||||
scheduled_status_id: scheduled_status.get('id'),
|
||||
quote: scheduled_status.getIn(['params', 'quote_id']) ? ImmutableMap({ id: scheduled_status.getIn(['params', 'quote_id']), url: '' }) : null,
|
||||
});
|
||||
|
||||
dispatch(fetchStatuses(uniqCompact([status.get('in_reply_to_id'), status.getIn(['quote', 'id']), ...status.get('status_reference_ids').toArray()])));
|
||||
|
||||
const replyStatus = getState().getIn(['statuses', status.get('in_reply_to_id')], null)?.update('account', account => getState().getIn(['accounts', account])) || null;
|
||||
|
||||
dispatch(redraft(getState, status, replyStatus, status.get('text')));
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
};
|
|
@ -112,7 +112,7 @@ export function fetchStatuses(ids) {
|
|||
dispatch(fetchAccountsFromStatuses(statuses));
|
||||
dispatch(fetchStatusesSuccess());
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusesFail(id, error));
|
||||
dispatch(fetchStatusesFail(newStatusIds, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -123,10 +123,10 @@ export function fetchStatusesSuccess() {
|
|||
};
|
||||
};
|
||||
|
||||
export function fetchStatusesFail(id, error) {
|
||||
export function fetchStatusesFail(ids, error) {
|
||||
return {
|
||||
type: STATUSES_FETCH_FAIL,
|
||||
id,
|
||||
ids,
|
||||
error,
|
||||
skipAlert: true,
|
||||
};
|
||||
|
@ -144,12 +144,8 @@ export function redraft(getState, status, replyStatus, raw_text) {
|
|||
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
const replyStatus = status.get('in_reply_to_id') ? getState().getIn(['statuses', status.get('in_reply_to_id')]) : null;
|
||||
|
||||
if (status.get('poll')) {
|
||||
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||
}
|
||||
const status = getState().getIn(['statuses', id]).update('poll', poll => poll ? getState().getIn(['polls', poll]) : null);
|
||||
const replyStatus = status.get('in_reply_to_id') ? getState().getIn(['statuses', status.get('in_reply_to_id')]).update('account', account => getState().getIn(['accounts', account])) : null;
|
||||
|
||||
dispatch(deleteStatusRequest(id));
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from './announcements';
|
||||
import { fetchFilters } from './filters';
|
||||
import { getLocale } from '../locales';
|
||||
import { deleteScheduledStatusSuccess } from './scheduled_statuses';
|
||||
|
||||
const { messages } = getLocale();
|
||||
|
||||
|
@ -85,6 +86,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'expire':
|
||||
dispatch(expireFromTimelines(data.payload));
|
||||
break;
|
||||
case 'scheduled_status':
|
||||
dispatch(deleteScheduledStatusSuccess(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||
break;
|
||||
|
|
|
@ -711,9 +711,9 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const expires_at = status.get('expires_at')
|
||||
const expires_date = expires_at && new Date(expires_at)
|
||||
const expired = expires_date && expires_date.getTime() < intl.now()
|
||||
const expires_at = status.get('expires_at');
|
||||
const expires_date = expires_at && new Date(expires_at);
|
||||
const expired = expires_date && expires_date.getTime() < intl.now();
|
||||
|
||||
const ancestorCount = showThread && show_reply_tree_button && status.get('in_reply_to_id', 0) > 0 ? 1 : 0;
|
||||
const descendantCount = showThread && show_reply_tree_button ? status.get('replies_count', 0) : 0;
|
||||
|
@ -732,30 +732,34 @@ class Status extends ImmutablePureComponent {
|
|||
threadMarkTitle = intl.formatMessage(messages.mark_descendant);
|
||||
}
|
||||
|
||||
const threadMark = threadCount > 0 ? <span className={classNames('status__thread_mark', {
|
||||
'status__thread_mark-ancenstor': (ancestorCount + referenceCount) > 0,
|
||||
'status__thread_mark-descendant': descendantCount > 0,
|
||||
})} title={threadMarkTitle}>+</span> : null;
|
||||
const threadMark = threadCount > 0 ? (<span
|
||||
className={classNames('status__thread_mark', {
|
||||
'status__thread_mark-ancenstor': (ancestorCount + referenceCount) > 0,
|
||||
'status__thread_mark-descendant': descendantCount > 0,
|
||||
})} title={threadMarkTitle}
|
||||
>+</span>) : null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, {
|
||||
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
||||
unread,
|
||||
focusable: !this.props.muted,
|
||||
'status__wrapper-with-expiration': expires_date,
|
||||
'status__wrapper-expired': expired,
|
||||
'status__wrapper-referenced': referenced,
|
||||
'status__wrapper-context-referenced': contextReferenced,
|
||||
'status__wrapper-reference': referenceCount > 0,
|
||||
})} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
<div
|
||||
className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, {
|
||||
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
||||
unread,
|
||||
focusable: !this.props.muted,
|
||||
'status__wrapper-with-expiration': expires_date,
|
||||
'status__wrapper-expired': expired,
|
||||
'status__wrapper-referenced': referenced,
|
||||
'status__wrapper-context-referenced': contextReferenced,
|
||||
'status__wrapper-reference': referenceCount > 0,
|
||||
})} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}
|
||||
>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, 'status-with-expiration': expires_date, 'status-expired': expired, referenced, 'context-referenced': contextReferenced })} data-id={status.get('id')}>
|
||||
<AccountActionBar account={status.get('account')} {...other} />
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
{status.get('expires_at') && <span className='status__expiration-time'><time dateTime={expires_at} title={intl.formatDate(expires_date, dateFormatOptions)}><i className="fa fa-clock-o" aria-hidden="true"></i></time></span>}
|
||||
{status.get('expires_at') && <span className='status__expiration-time'><time dateTime={expires_at} title={intl.formatDate(expires_date, dateFormatOptions)}><i className='fa fa-clock-o' aria-hidden='true' /></time></span>}
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
{threadMark}
|
||||
{disableRelativeTime ? <AbsoluteTimestamp timestamp={status.get('created_at')} /> : <RelativeTimestamp timestamp={status.get('created_at')} /> }
|
||||
|
@ -776,7 +780,7 @@ class Status extends ImmutablePureComponent {
|
|||
{quote}
|
||||
{media}
|
||||
|
||||
{enableReaction && (contextType == 'thread' || !compactReaction) && <EmojiReactionsBar
|
||||
{enableReaction && (contextType === 'thread' || !compactReaction) && <EmojiReactionsBar
|
||||
status={status}
|
||||
addEmojiReaction={this.props.addEmojiReaction}
|
||||
removeEmojiReaction={this.props.removeEmojiReaction}
|
||||
|
|
|
@ -84,8 +84,8 @@ class StatusItem extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('mini-status__wrapper', `mini-status__wrapper-${status.get('visibility')}`, { 'mini-status__wrapper-reply': !!status.get('in_reply_to_id') })} tabIndex={0} ref={this.handleRef}>
|
||||
<div className={classNames('mini-status', `mini-status-${status.get('visibility')}`, { 'mini-status-reply': !!status.get('in_reply_to_id') })} onClick={this.handleClick} data-id={status.get('id')}>
|
||||
<div className={classNames('mini-status__wrapper', `mini-status__wrapper-${status.get('visibility')}`, { 'mini-status__wrapper-reply': !!status.get('in_reply_to_id') })} ref={this.handleRef}>
|
||||
<div className={classNames('mini-status', `mini-status-${status.get('visibility')}`, { 'mini-status-reply': !!status.get('in_reply_to_id') })} onClick={this.handleClick} role='button' tabIndex={0} data-id={status.get('id')}>
|
||||
<div className='mini-status__account'>
|
||||
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} data-group={status.getIn(['account', 'group'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='mini-status__avatar'>
|
||||
|
@ -95,7 +95,7 @@ class StatusItem extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='mini-status__content'>
|
||||
<div className='mini-status__content__text translate' dangerouslySetInnerHTML={{__html: status.get('shortHtml')}} />
|
||||
<div className='mini-status__content__text translate' dangerouslySetInnerHTML={{ __html: status.get('shortHtml') }} />
|
||||
<Bundle fetchComponent={ThumbnailGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
<Component
|
||||
|
|
|
@ -102,7 +102,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0 && state.getIn(['compose', 'dirty'])) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
|
@ -134,7 +134,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0 && state.getIn(['compose', 'dirty'])) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.quoteMessage),
|
||||
confirm: intl.formatMessage(messages.quoteConfirm),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import CharacterCounter from './character_counter';
|
||||
import Button from '../../../components/button';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -36,6 +36,9 @@ const messages = defineMessages({
|
|||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
update: { id: 'compose_form.update_scheduled_status', defaultMessage: 'Update scheduled post' },
|
||||
delete: { id: 'compose_form.delete_scheduled_status', defaultMessage: 'Delete scheduled post' },
|
||||
and: { id: 'compose_form.and', defaultMessage: ' + ' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -61,6 +64,8 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
isCircleUnselected: PropTypes.bool,
|
||||
prohibitedVisibilities: ImmutablePropTypes.set,
|
||||
prohibitedWords: ImmutablePropTypes.set,
|
||||
isScheduled: PropTypes.bool,
|
||||
isScheduledStatusEditting: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
|
@ -217,6 +222,14 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
if (this.props.isScheduledStatusEditting) {
|
||||
if (this.props.isScheduled ) {
|
||||
publishText = <span className='compose-form__update'>{intl.formatMessage(messages.update)}</span>;
|
||||
} else {
|
||||
publishText = <Fragment><span className='compose-form__delete'>{intl.formatMessage(messages.delete)}</span>{intl.formatMessage(messages.and)}{publishText}</Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form'>
|
||||
<WarningContainer />
|
||||
|
|
|
@ -38,7 +38,7 @@ const mapStateToProps = (state, { value, origin, minDate, maxDate }) => {
|
|||
dateValue: dateValue,
|
||||
stringValue: stringValue,
|
||||
invalid: invalid,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
|
@ -21,6 +21,7 @@ class QuoteIndicator extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
isScheduledStatusEditting: PropTypes.bool,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -37,7 +38,7 @@ class QuoteIndicator extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const { status, isScheduledStatusEditting, intl } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
|
@ -48,7 +49,7 @@ class QuoteIndicator extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='quote-indicator'>
|
||||
<div className='quote-indicator__header'>
|
||||
<div className='quote-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||
{!isScheduledStatusEditting && <div className='quote-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>}
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='quote-indicator__display-name'>
|
||||
<div className='quote-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
|
|
|
@ -21,6 +21,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
isScheduledStatusEditting: PropTypes.bool,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -42,7 +43,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const { status, isScheduledStatusEditting, intl } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
|
@ -53,7 +54,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||
{!isScheduledStatusEditting && <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>}
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
|
|
|
@ -29,6 +29,8 @@ const mapStateToProps = state => ({
|
|||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']),
|
||||
prohibitedWords: state.getIn(['compose', 'prohibited_words']),
|
||||
isScheduled: !!state.getIn(['compose', 'scheduled']),
|
||||
isScheduledStatusEditting: !!state.getIn(['compose', 'scheduled_status_id']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
|
|
@ -8,6 +8,7 @@ const makeMapStateToProps = () => {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
status: getStatus(state, { id: state.getIn(['compose', 'quote_from']) }),
|
||||
isScheduledStatusEditting: !!state.getIn(['compose', 'scheduled_status_id']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -8,6 +8,7 @@ const makeMapStateToProps = () => {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
|
||||
isScheduledStatusEditting: !!state.getIn(['compose', 'scheduled_status_id']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Warning from '../components/warning';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from '../../../initial_state';
|
||||
import { cancelScheduledStatusCompose } from '../../../actions/compose';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
|
||||
const buildHashtagRE = () => {
|
||||
try {
|
||||
|
@ -37,9 +40,38 @@ const mapStateToProps = state => ({
|
|||
limitedMessageWarning: state.getIn(['compose', 'privacy']) === 'limited',
|
||||
mutualMessageWarning: state.getIn(['compose', 'privacy']) === 'mutual',
|
||||
personalMessageWarning: state.getIn(['compose', 'privacy']) === 'personal',
|
||||
isScheduledStatusEditting: !!state.getIn(['compose', 'scheduled_status_id']),
|
||||
});
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, limitedMessageWarning, mutualMessageWarning, personalMessageWarning }) => {
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel () {
|
||||
dispatch(cancelScheduledStatusCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const ScheduledStatusWarningWrapper = ({ isScheduledStatusEditting, onCancel }) => {
|
||||
if (!isScheduledStatusEditting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='scheduled-status-warning-indicator'>
|
||||
<div className='scheduled-status-warning-indicator__cancel'><IconButton title='Cancel' icon='times' onClick={onCancel} inverted /></div>
|
||||
<div className='scheduled-status-warning-indicator__content translate'>
|
||||
<Icon id='clock-o' fixedWidth /><FormattedMessage id='compose_form.scheduled_status_warning' defaultMessage='Scheduled post editing in progress.' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ScheduledStatusWarningWrapper.propTypes = {
|
||||
isScheduledStatusEditting: PropTypes.bool,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const PrivacyWarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, limitedMessageWarning, mutualMessageWarning, personalMessageWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
|
@ -73,7 +105,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
|||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
PrivacyWarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
|
@ -82,4 +114,24 @@ WarningWrapper.propTypes = {
|
|||
personalMessageWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
||||
const WarningWrapper = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<ScheduledStatusWarningWrapper {...props} />
|
||||
<PrivacyWarningWrapper {...props} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
limitedMessageWarning: PropTypes.bool,
|
||||
mutualMessageWarning: PropTypes.bool,
|
||||
personalMessageWarning: PropTypes.bool,
|
||||
isScheduledStatusEditting: PropTypes.bool,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(WarningWrapper);
|
||||
|
|
|
@ -37,7 +37,7 @@ const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
|
|||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0 && state.getIn(['compose', 'dirty'])) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
|
|
|
@ -11,6 +11,7 @@ import { me, profile_directory, showTrends, enableLimitedTimeline, enablePersona
|
|||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { fetchFavouriteDomains } from 'mastodon/actions/favourite_domains';
|
||||
import { fetchFavouriteTags } from 'mastodon/actions/favourite_tags';
|
||||
import { fetchScheduledStatuses } from 'mastodon/actions/scheduled_statuses';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import NavigationContainer from '../compose/containers/navigation_container';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
@ -31,6 +32,7 @@ const messages = defineMessages({
|
|||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
scheduled_statuses: { id: 'navigation_bar.scheduled_statuses', defaultMessage: 'Scheduled Posts' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Emoji reactions' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
|
@ -60,6 +62,7 @@ const mapStateToProps = state => ({
|
|||
myAccount: state.getIn(['accounts', me]),
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||
scheduledStatusesCount: state.getIn(['scheduled_statuses', 'items'], ImmutableList()).size,
|
||||
lists: getOrderedLists(state),
|
||||
favourite_domains: getOrderedDomains(state),
|
||||
favourite_tags: getOrderedTags(state),
|
||||
|
@ -70,6 +73,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
|
||||
fetchFavouriteDomains: () => dispatch(fetchFavouriteDomains()),
|
||||
fetchFavouriteTags: () => dispatch(fetchFavouriteTags()),
|
||||
fetchScheduledStatuses: () => dispatch(fetchScheduledStatuses()),
|
||||
});
|
||||
|
||||
const badgeDisplay = (number, limit) => {
|
||||
|
@ -101,7 +105,9 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
fetchFollowRequests: PropTypes.func.isRequired,
|
||||
fetchFavouriteDomains: PropTypes.func.isRequired,
|
||||
fetchFavouriteTags: PropTypes.func.isRequired,
|
||||
fetchScheduledStatuses: PropTypes.func.isRequired,
|
||||
unreadFollowRequests: PropTypes.number,
|
||||
scheduledStatusesCount: PropTypes.number,
|
||||
unreadNotifications: PropTypes.number,
|
||||
lists: ImmutablePropTypes.list,
|
||||
favourite_domains: ImmutablePropTypes.list,
|
||||
|
@ -109,7 +115,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { fetchFollowRequests, fetchFavouriteDomains, fetchFavouriteTags, multiColumn } = this.props;
|
||||
const { fetchFollowRequests, fetchFavouriteDomains, fetchFavouriteTags, fetchScheduledStatuses, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
|
@ -119,10 +125,11 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
fetchFollowRequests();
|
||||
fetchFavouriteDomains();
|
||||
fetchFavouriteTags();
|
||||
fetchScheduledStatuses();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, lists, favourite_domains, favourite_tags, columnWidth } = this.props;
|
||||
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, scheduledStatusesCount, lists, favourite_domains, favourite_tags, columnWidth } = this.props;
|
||||
|
||||
const navItems = [];
|
||||
let height = (multiColumn) ? 0 : 60;
|
||||
|
@ -245,14 +252,24 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
height += 48*6;
|
||||
|
||||
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
||||
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
if (scheduledStatusesCount > 0) {
|
||||
navItems.push(<ColumnLink key='scheduled_statuses' icon='clock-o' text={intl.formatMessage(messages.scheduled_statuses)} to='/scheduled_statuses' />);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
if (lists && !lists.isEmpty()) {
|
||||
navItems.push(<ColumnSubheading key='header-lists' text={intl.formatMessage(messages.lists_subheading)} />);
|
||||
height += 34;
|
||||
|
||||
lists.map(list => {
|
||||
navItems.push(<ColumnLink key={`list:${list.get('id')}`} icon='list-ul' text={list.get('title')} to={`/timelines/list/${list.get('id')}`} />);
|
||||
height += 48
|
||||
})
|
||||
height += 48;
|
||||
});
|
||||
}
|
||||
|
||||
if (favourite_domains && !favourite_domains.isEmpty()) {
|
||||
|
@ -261,8 +278,8 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
favourite_domains.map(favourite_domain => {
|
||||
navItems.push(<ColumnLink key={`favourite_domain:${favourite_domain.get('id')}`} icon='users' text={favourite_domain.get('name')} to={`/timelines/public/domain/${favourite_domain.get('name')}`} />);
|
||||
height += 48
|
||||
})
|
||||
height += 48;
|
||||
});
|
||||
}
|
||||
|
||||
if (favourite_tags && !favourite_tags.isEmpty()) {
|
||||
|
@ -271,13 +288,8 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
favourite_tags.map(favourite_tag => {
|
||||
navItems.push(<ColumnLink key={`favourite_tag:${favourite_tag.get('id')}`} icon='hashtag' text={favourite_tag.get('name')} to={`/timelines/tag/${favourite_tag.get('name')}`} />);
|
||||
height += 48
|
||||
})
|
||||
}
|
||||
|
||||
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
||||
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
height += 48;
|
||||
height += 48;
|
||||
});
|
||||
}
|
||||
|
||||
if (!multiColumn) {
|
||||
|
|
|
@ -178,6 +178,16 @@ export default class ColumnSettings extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
<div role='group' aria-labelledby='notifications-scheduled-status'>
|
||||
<span id='notifications-scheduled-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.scheduled_statuses' defaultMessage='Scheduled posts:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'scheduled_status']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'scheduled_status']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'scheduled_status']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'scheduled_status']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ const tooltips = defineMessages({
|
|||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
reactions: { id: 'notifications.filter.emoji_reactions', defaultMessage: 'Reactions' },
|
||||
reference: { id: 'notifications.filter.status_references', defaultMessage: 'Status references' },
|
||||
scheduled_statuses: { id: 'notifications.filter.scheduled_statuses', defaultMessage: 'Scheduled statuses' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -98,6 +99,13 @@ class FilterBar extends React.PureComponent {
|
|||
>
|
||||
<Icon id='home' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'scheduled_status' ? 'active' : ''}
|
||||
onClick={this.onClick('scheduled_status')}
|
||||
title={intl.formatMessage(tooltips.scheduled_statuses)}
|
||||
>
|
||||
<Icon id='clock-o' fixedWidth />
|
||||
</button>
|
||||
{enableReaction &&
|
||||
<button
|
||||
className={selectedFilter === 'emoji_reaction' ? 'active' : ''}
|
||||
|
|
|
@ -22,6 +22,7 @@ const messages = defineMessages({
|
|||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||
emoji_reaction: { id: 'notification.emoji_reaction', defaultMessage: '{name} reactioned your post' },
|
||||
status_reference: { id: 'notification.status_reference', defaultMessage: '{name} referenced your post' },
|
||||
scheduled_status: { id: 'notification.scheduled_status', defaultMessage: 'Your scheduled post has been posted' },
|
||||
});
|
||||
|
||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||
|
@ -277,6 +278,38 @@ class Notification extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderScheduledStatus (notification) {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-scheduled-status focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.scheduled_status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='clock-o' fixedWidth />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.scheduled_status' defaultMessage='Your scheduled post has been posted' />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
getScrollPosition={this.props.getScrollPosition}
|
||||
updateScrollBottom={this.props.updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
renderPoll (notification, account) {
|
||||
const { intl, unread } = this.props;
|
||||
const ownPoll = me === account.get('id');
|
||||
|
@ -402,6 +435,8 @@ class Notification extends ImmutablePureComponent {
|
|||
return this.renderReblog(notification, link);
|
||||
case 'status':
|
||||
return this.renderStatus(notification, link);
|
||||
case 'scheduled_status':
|
||||
return this.renderScheduledStatus(notification, link);
|
||||
case 'poll':
|
||||
return this.renderPoll(notification, account);
|
||||
case 'emoji_reaction':
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {Fragment} from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -20,20 +20,12 @@ const messages = defineMessages({
|
|||
clearConfirm: { id: 'confirmations.clear.confirm', defaultMessage: 'Clear' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const statusIds = state.getIn(['compose', 'references']).toList().sort((a, b) => b - a);
|
||||
|
||||
return {
|
||||
statusIds,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
const mapStateToProps = (state) => ({
|
||||
statusIds: state.getIn(['compose', 'references']).toList().sort((a, b) => b - a),
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@connect(makeMapStateToProps)
|
||||
@connect(mapStateToProps)
|
||||
class ReferenceStack extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -68,7 +60,7 @@ class ReferenceStack extends ImmutablePureComponent {
|
|||
const { statusIds, intl } = this.props;
|
||||
|
||||
if (!enableStatusReference || statusIds.isEmpty()) {
|
||||
return <Fragment></Fragment>;
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
const title = (
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ThumbnailGallery } from 'mastodon/features/ui/util/async-components';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
import Bundle from 'mastodon/features/ui/components/bundle';
|
||||
|
||||
const messages = defineMessages({
|
||||
unselect: { id: 'reference_stack.unselect', defaultMessage: 'Unselecting a post' },
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
class ScheduledStatus extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
scheduledStatus: ImmutablePropTypes.map,
|
||||
onDelete: PropTypes.func,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
onDeleteScheduledStatus: PropTypes.func,
|
||||
onRedraftScheduledStatus: PropTypes.func,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
this.props.onMoveUp(this.props.scheduledStatus.get('id'));
|
||||
}
|
||||
|
||||
handleHotkeyMoveDown = () => {
|
||||
this.props.onMoveDown(this.props.scheduledStatus.get('id'));
|
||||
}
|
||||
|
||||
handleDeleteClick = (e) => {
|
||||
const { scheduledStatus, onDeleteScheduledStatus } = this.props;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
onDeleteScheduledStatus(scheduledStatus.get('id'), e);
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const { scheduledStatus, onRedraftScheduledStatus } = this.props;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
onRedraftScheduledStatus(scheduledStatus, this.context.router.history, e);
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, scheduledStatus } = this.props;
|
||||
|
||||
if (scheduledStatus === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('mini-status__wrapper', `mini-status__wrapper-${scheduledStatus.getIn(['params', 'visibility'])}`, 'focusable', { 'mini-status__wrapper-reply': !!scheduledStatus.getIn(['params', 'in_reply_to_id']) })} role='button' tabIndex={0} onClick={this.handleClick} ref={this.handleRef}>
|
||||
<div className={classNames('mini-status', `mini-status-${scheduledStatus.getIn(['params', 'visibility'])}`, { 'mini-status-reply': !!scheduledStatus.getIn(['params', 'in_reply_to_id']) })} data-id={scheduledStatus.get('id')}>
|
||||
<div className='mini-status__content'>
|
||||
<div className='mini-status__content__text translate' dangerouslySetInnerHTML={{ __html: scheduledStatus.getIn(['params', 'text']) }} />
|
||||
<Bundle fetchComponent={ThumbnailGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => <Component media={scheduledStatus.get('media_attachments')} />}
|
||||
</Bundle>
|
||||
<div className='mini-status__scheduled-time'>
|
||||
<Icon id='clock-o' fixedWidth />
|
||||
{intl.formatDate(scheduledStatus.get('scheduled_at'), dateFormatOptions)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mini-status__unselect'><IconButton title={intl.formatMessage(messages.unselect)} icon='times' onClick={this.handleDeleteClick} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ScheduledStatusContainer from '../containers/scheduled_status_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import LoadGap from 'mastodon/components/load_gap';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
export default class ScheduledStatusList extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
scheduledStatuses: ImmutablePropTypes.list.isRequired,
|
||||
onLoadMore: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
trackScroll: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
prepend: PropTypes.node,
|
||||
emptyMessage: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
trackScroll: true,
|
||||
};
|
||||
|
||||
getCurrentStatusIndex = id => {
|
||||
return this.props.scheduledStatuses.findIndex(v => v.get('id') === id);
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
this.props.onLoadMore(this.props.scheduledStatuses.size > 0 ? this.props.scheduledStatuses.last().get('id') : undefined);
|
||||
}, 300, { leading: true })
|
||||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { scheduledStatuses, onLoadMore, timelineId, ...other } = this.props;
|
||||
const { isLoading } = other;
|
||||
|
||||
let scrollableContent = (isLoading || scheduledStatuses.size > 0) ? (
|
||||
scheduledStatuses.map((scheduledStatus, index) => scheduledStatus === null ? (
|
||||
<LoadGap
|
||||
key={'gap:' + scheduledStatuses.getIn([index + 1, 'id'])}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? scheduledStatuses.get([index - 1, 'id']) : null}
|
||||
onClick={onLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<ScheduledStatusContainer
|
||||
key={scheduledStatus.get('id')}
|
||||
scheduledStatus={scheduledStatus}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
/>
|
||||
))
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} showLoading={isLoading && scheduledStatuses.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ScheduledStatus from '../components/scheduled_status';
|
||||
import { deleteScheduledStatus, redraftScheduledStatus } from 'mastodon/actions/scheduled_statuses';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { deleteScheduledStatusModal } from 'mastodon/initial_state';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft_scheduled_status.confirm', defaultMessage: 'View & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft_scheduled_status.message', defaultMessage: 'Redraft now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
emojiMap: customEmojiMap(state),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onDeleteScheduledStatus (id, e) {
|
||||
if (e.shiftKey ^ !deleteScheduledStatusModal) {
|
||||
dispatch(deleteScheduledStatus(id));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteScheduledStatus(id)),
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
onRedraftScheduledStatus (scheduledStatus, history) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0 && state.getIn(['compose', 'dirty'])) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.redraftMessage),
|
||||
confirm: intl.formatMessage(messages.redraftConfirm),
|
||||
onConfirm: () => dispatch(redraftScheduledStatus(scheduledStatus, history)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(redraftScheduledStatus(scheduledStatus, history));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(ScheduledStatus));
|
132
app/javascript/mastodon/features/scheduled_statuses/index.js
Normal file
132
app/javascript/mastodon/features/scheduled_statuses/index.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import ScheduledStatusList from './components/scheduled_status_list';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import { defaultColumnWidth } from 'mastodon/initial_state';
|
||||
import { changeSetting } from '../../actions/settings';
|
||||
import { changeColumnParams } from '../../actions/columns';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = (state, { columnId }) => {
|
||||
const uuid = columnId;
|
||||
const columns = state.getIn(['settings', 'columns']);
|
||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
||||
const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'scheduled_statuses', 'columnWidth']);
|
||||
|
||||
return {
|
||||
scheduledStatuses: state.getIn(['scheduled_statuses', 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['scheduled_statuses', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['scheduled_statuses', 'next']),
|
||||
columnWidth: columnWidth ?? defaultColumnWidth,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps)
|
||||
@injectIntl
|
||||
class ScheduledStatus extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
scheduledStatuses: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
columnWidth: PropTypes.string,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchScheduledStatuses());
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('SCHEDULED_STATUS', {}));
|
||||
}
|
||||
}
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandScheduledStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
handleWidthChange = (value) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, 'columnWidth', value));
|
||||
} else {
|
||||
dispatch(changeSetting(['scheduled_statuses', 'columnWidth'], value));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, scheduledStatuses, columnId, multiColumn, hasMore, isLoading, columnWidth } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.scheduled_statuses' defaultMessage='There are no scheduled posts. Posts with a scheduled publication date and time will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)} columnWidth={columnWidth}>
|
||||
<ColumnHeader
|
||||
icon='clock-o'
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
columnWidth={columnWidth}
|
||||
onWidthChange={this.handleWidthChange}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<ScheduledStatusList
|
||||
trackScroll={!pinned}
|
||||
scheduledStatuses={scheduledStatuses}
|
||||
scrollKey={`scheduled_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onDeleteScheduledStatus={this.handleDeleteScheduledStatus}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -66,7 +66,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
onReply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0 && state.getIn(['compose', 'dirty'])) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
Directory,
|
||||
Trends,
|
||||
Suggestions,
|
||||
ScheduledStatuses,
|
||||
} from '../../ui/util/async-components';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import ComposePanel from './compose_panel';
|
||||
|
@ -65,6 +66,7 @@ const componentMap = {
|
|||
'DIRECTORY': Directory,
|
||||
'TRENDS': Trends,
|
||||
'SUGGESTIONS': Suggestions,
|
||||
'SCHEDULED_STATUS': ScheduledStatuses,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -5,6 +5,7 @@ import Icon from 'mastodon/components/icon';
|
|||
import { profile_directory, showTrends, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline, enablePersonalTimeline } from 'mastodon/initial_state';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
import ScheduledStatusesNavLink from './scheduled_statuses_nav_link';
|
||||
import ListPanel from './list_panel';
|
||||
import FavouriteDomainPanel from './favourite_domain_panel';
|
||||
import FavouriteTagPanel from './favourite_tag_panel';
|
||||
|
@ -17,6 +18,7 @@ const NavigationPanel = () => (
|
|||
{enablePersonalTimeline && <NavLink className='column-link column-link--transparent' to='/timelines/personal' data-preview-title-id='column.personal' data-preview-icon='book' ><Icon className='column-link__icon' id='book' fixedWidth /><FormattedMessage id='navigation_bar.personal_timeline' defaultMessage='Personal' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
||||
<FollowRequestsNavLink />
|
||||
<ScheduledStatusesNavLink />
|
||||
{enableLocalTimeline && <NavLink className='column-link column-link--transparent' exact to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>}
|
||||
{enableFederatedTimeline && <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/accounts/2'><Icon className='column-link__icon' id='info-circle' fixedWidth /><FormattedMessage id='navigation_bar.information_acct' defaultMessage='Fedibird info' /></NavLink>
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchScheduledStatuses } from 'mastodon/actions/scheduled_statuses';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
count: state.getIn(['scheduled_statuses', 'items'], ImmutableList()).size,
|
||||
});
|
||||
|
||||
export default @withRouter
|
||||
@connect(mapStateToProps)
|
||||
class ScheduledStatusesNavLink extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(fetchScheduledStatuses());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { count } = this.props;
|
||||
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavLink className='column-link column-link--transparent' to='/scheduled_statuses'><Icon className='column-link__icon' id='clock-o' fixedWidth /><FormattedMessage id='navigation_bar.scheduled_statuses' defaultMessage='Scheduled Posts' /></NavLink>;
|
||||
}
|
||||
|
||||
}
|
|
@ -69,6 +69,7 @@ import {
|
|||
Trends,
|
||||
Suggestions,
|
||||
EmptyColumn,
|
||||
ScheduledStatuses,
|
||||
} from './util/async-components';
|
||||
import { me, enableEmptyColumn } from '../../initial_state';
|
||||
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||
|
@ -225,6 +226,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
<WrappedRoute path='/circles' component={Circles} content={children} />
|
||||
<WrappedRoute path='/scheduled_statuses' component={ScheduledStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/empty' component={EmptyColumn} content={children} />
|
||||
|
||||
|
|
|
@ -233,3 +233,7 @@ export function Suggestions () {
|
|||
export function EmptyColumn () {
|
||||
return import(/* webpackChunkName: "features/empty" */'../../empty');
|
||||
}
|
||||
|
||||
export function ScheduledStatuses () {
|
||||
return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses');
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export const deleteModal = getMeta('delete_modal');
|
|||
export const postReferenceModal = getMeta('post_reference_modal');
|
||||
export const addReferenceModal = getMeta('add_reference_modal');
|
||||
export const unselectReferenceModal = getMeta('unselect_reference_modal');
|
||||
export const deleteScheduledStatusModal = getMeta('delete_scheduled_status_modal');
|
||||
export const me = getMeta('me');
|
||||
export const searchEnabled = getMeta('search_enabled');
|
||||
export const invitesEnabled = getMeta('invites_enabled');
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
"column.personal": "Personal home",
|
||||
"column.pins": "Pinned posts",
|
||||
"column.public": "Federated timeline",
|
||||
"column.scheduled_statuses": "Scheduled Posts",
|
||||
"column_back_button.label": "Back",
|
||||
"column_back_button_to_pots.label": "Back to post detail",
|
||||
"column_close_button.label": "Close",
|
||||
|
@ -151,6 +152,8 @@
|
|||
"community.column_settings.remote_only": "Remote only",
|
||||
"community.column_settings.without_bot": "Without bot",
|
||||
"community.column_settings.without_media": "Without media",
|
||||
"compose_form.and": " + ",
|
||||
"compose_form.delete_scheduled_status": "Delete scheduled post",
|
||||
"compose_form.direct_message_warning": "This post will only be sent to the mentioned users.",
|
||||
"compose_form.direct_message_warning_learn_more": "Learn more",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
|
@ -168,12 +171,14 @@
|
|||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.scheduled_status_warning": "Scheduled post editing in progress.",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
|
||||
"compose_form.spoiler.marked": "Remove content warning",
|
||||
"compose_form.spoiler.unmarked": "Add content warning",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.update_scheduled_status": "Update scheduled post",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.block.block_and_report": "Block & Report",
|
||||
"confirmations.block.confirm": "Block",
|
||||
|
@ -201,6 +206,8 @@
|
|||
"confirmations.quote.message": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.redraft.confirm": "Delete & redraft",
|
||||
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
||||
"confirmations.redraft_scheduled_status.confirm": "View & redraft",
|
||||
"confirmations.redraft_scheduled_status.message": "Redraft now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.reply.confirm": "Reply",
|
||||
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
|
@ -268,6 +275,7 @@
|
|||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||
"empty_column.mutes": "You haven't muted any users yet.",
|
||||
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
|
||||
"empty_column.scheduled_statuses": "There are no scheduled posts. Posts with a scheduled publication date and time will show up here.",
|
||||
"empty_column.personal": "Personal posts unavailable",
|
||||
"empty_column.pinned_unavailable": "Pinned posts unavailable",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
|
||||
|
@ -428,6 +436,7 @@
|
|||
"navigation_bar.pins": "Pinned posts",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"navigation_bar.scheduled_statuses": "Scheduled Posts",
|
||||
"navigation_bar.security": "Security",
|
||||
"navigation_bar.short.community_timeline": "LTL",
|
||||
"navigation_bar.short.getting_started": "Started",
|
||||
|
@ -450,6 +459,7 @@
|
|||
"notification.poll": "A poll you have voted in has ended",
|
||||
"notification.emoji_reaction": "{name} reactioned your post",
|
||||
"notification.reblog": "{name} boosted your post",
|
||||
"notification.scheduled_status": "Your scheduled post has been posted",
|
||||
"notification.status": "{name} just posted",
|
||||
"notification.status_reference": "{name} referenced your post",
|
||||
"notifications.clear": "Clear notifications",
|
||||
|
@ -466,6 +476,7 @@
|
|||
"notifications.column_settings.push": "Push notifications",
|
||||
"notifications.column_settings.emoji_reaction": "Reactions:",
|
||||
"notifications.column_settings.reblog": "Boosts:",
|
||||
"notifications.column_settings.scheduled_statuses": "Scheduled posts:",
|
||||
"notifications.column_settings.show": "Show in column",
|
||||
"notifications.column_settings.sound": "Play sound",
|
||||
"notifications.column_settings.status": "New posts:",
|
||||
|
@ -478,6 +489,7 @@
|
|||
"notifications.filter.mentions": "Mentions",
|
||||
"notifications.filter.polls": "Poll results",
|
||||
"notifications.filter.emoji_reactions": "Reactions",
|
||||
"notifications.filter.scheduled_statuses": "Scheduled posts",
|
||||
"notifications.filter.statuses": "Updates from people you follow",
|
||||
"notifications.filter.status_references": "Status references",
|
||||
"notifications.grant_permission": "Grant permission.",
|
||||
|
@ -536,6 +548,7 @@
|
|||
"report.placeholder": "Additional comments",
|
||||
"report.submit": "Submit",
|
||||
"report.target": "Reporting {target}",
|
||||
"scheduled-status-warning-indicator.cancel": "Cancel",
|
||||
"search.placeholder": "Search",
|
||||
"searchability.change": "Adjust status searchability",
|
||||
"searchability.direct.short": "Reacted-users-only (Search)",
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
"column.personal": "自分限定ホーム",
|
||||
"column.pins": "固定された投稿",
|
||||
"column.public": "連合タイムライン",
|
||||
"column.scheduled_statuses": "予約投稿",
|
||||
"column_back_button.label": "戻る",
|
||||
"column_back_button_to_pots.label": "投稿の詳細に戻る",
|
||||
"column_close_button.label": "閉じる",
|
||||
|
@ -151,6 +152,8 @@
|
|||
"community.column_settings.remote_only": "リモートのみ表示",
|
||||
"community.column_settings.without_bot": "Botを除外",
|
||||
"community.column_settings.without_media": "メディアを除外",
|
||||
"compose_form.and": " + ",
|
||||
"compose_form.delete_scheduled_status": "予約投稿の削除",
|
||||
"compose_form.direct_message_warning": "この投稿はメンションされた人にのみ送信されます。",
|
||||
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
|
||||
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
|
||||
|
@ -168,12 +171,14 @@
|
|||
"compose_form.poll.switch_to_single": "単一選択に変更",
|
||||
"compose_form.publish": "トゥート",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.scheduled_status_warning": "予約投稿の編集中",
|
||||
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
|
||||
"compose_form.sensitive.marked": "メディアに閲覧注意が設定されています",
|
||||
"compose_form.sensitive.unmarked": "メディアに閲覧注意が設定されていません",
|
||||
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
|
||||
"compose_form.spoiler.unmarked": "本文は隠されていません",
|
||||
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
|
||||
"compose_form.update_scheduled_status": "予約投稿の更新",
|
||||
"confirmation_modal.cancel": "キャンセル",
|
||||
"confirmations.block.block_and_report": "ブロックし通報",
|
||||
"confirmations.block.confirm": "ブロック",
|
||||
|
@ -201,6 +206,8 @@
|
|||
"confirmations.quote.message": "今引用すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.redraft.confirm": "削除して下書きに戻す",
|
||||
"confirmations.redraft.message": "本当にこの投稿を削除して下書きに戻しますか? この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。",
|
||||
"confirmations.redraft_scheduled_status.confirm": "閲覧・下書きに戻す",
|
||||
"confirmations.redraft_scheduled_status.message": "今下書きに戻すと現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.reply.confirm": "返信",
|
||||
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.unfollow.confirm": "フォロー解除",
|
||||
|
@ -271,6 +278,7 @@
|
|||
"empty_column.personal": "自分限定投稿はありません。",
|
||||
"empty_column.pinned_unavailable": "固定された投稿はありません。",
|
||||
"empty_column.referred_by_statuses": "まだ、参照している投稿はありません。誰かが投稿を参照すると、ここに表示されます。",
|
||||
"empty_column.scheduled_statuses": "予約されている投稿はありません。公開日時を指定した投稿がここに表示されます。",
|
||||
"empty_column.suggestions": "まだおすすめできるユーザーがいません。",
|
||||
"empty_column.trends": "まだ何もトレンドがありません。",
|
||||
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
||||
|
@ -428,6 +436,7 @@
|
|||
"navigation_bar.pins": "固定した投稿",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.public_timeline": "連合タイムライン",
|
||||
"navigation_bar.scheduled_statuses": "予約投稿",
|
||||
"navigation_bar.security": "セキュリティ",
|
||||
"navigation_bar.short.community_timeline": "ローカル",
|
||||
"navigation_bar.short.getting_started": "スタート",
|
||||
|
@ -450,6 +459,7 @@
|
|||
"notification.poll": "アンケートが終了しました",
|
||||
"notification.emoji_reaction": "{name}さんがあなたの投稿にリアクションしました",
|
||||
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||
"notification.scheduled_status": "予約投稿が投稿されました",
|
||||
"notification.status": "{name}さんが投稿しました",
|
||||
"notification.status_reference": "{name}さんがあなたの投稿を参照しました",
|
||||
"notifications.clear": "通知を消去",
|
||||
|
@ -466,6 +476,7 @@
|
|||
"notifications.column_settings.push": "プッシュ通知",
|
||||
"notifications.column_settings.emoji_reaction": "リアクション:",
|
||||
"notifications.column_settings.reblog": "ブースト:",
|
||||
"notifications.column_settings.scheduled_statuses": "予約投稿:",
|
||||
"notifications.column_settings.show": "カラムに表示",
|
||||
"notifications.column_settings.sound": "通知音を再生",
|
||||
"notifications.column_settings.status": "新しい投稿:",
|
||||
|
@ -478,6 +489,7 @@
|
|||
"notifications.filter.mentions": "返信",
|
||||
"notifications.filter.polls": "アンケート結果",
|
||||
"notifications.filter.emoji_reactions": "リアクション",
|
||||
"notifications.filter.scheduled_statuses": "予約投稿",
|
||||
"notifications.filter.statuses": "フォローしている人の新着情報",
|
||||
"notifications.filter.status_references": "投稿の参照",
|
||||
"notifications.grant_permission": "権限の付与",
|
||||
|
@ -536,6 +548,7 @@
|
|||
"report.placeholder": "追加コメント",
|
||||
"report.submit": "通報する",
|
||||
"report.target": "{target}さんを通報する",
|
||||
"scheduled-status-warning-indicator.cancel": "キャンセル",
|
||||
"search.placeholder": "検索",
|
||||
"searchability.change": "検索範囲を変更",
|
||||
"searchability.direct.short": "リアクション限定(検索)",
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
COMPOSE_UPLOAD_FAIL,
|
||||
COMPOSE_UPLOAD_UNDO,
|
||||
COMPOSE_UPLOAD_PROGRESS,
|
||||
SCHEDULED_STATUS_SUBMIT_SUCCESS,
|
||||
THUMBNAIL_UPLOAD_REQUEST,
|
||||
THUMBNAIL_UPLOAD_SUCCESS,
|
||||
THUMBNAIL_UPLOAD_FAIL,
|
||||
|
@ -54,6 +55,7 @@ import {
|
|||
COMPOSE_REFERENCE_ADD,
|
||||
COMPOSE_REFERENCE_REMOVE,
|
||||
COMPOSE_REFERENCE_RESET,
|
||||
COMPOSE_SCHEDULED_EDIT_CANCEL,
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE } from '../actions/timelines';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
|
@ -62,7 +64,7 @@ import { Map as ImmutableMap, List as ImmutableList, Set as ImmutableSet, Ordere
|
|||
import uuid from '../uuid';
|
||||
import { me } from '../initial_state';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
import { parseISO, format } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
mounted: 0,
|
||||
|
@ -84,6 +86,7 @@ const initialState = ImmutableMap({
|
|||
is_submitting: false,
|
||||
is_changing_upload: false,
|
||||
is_uploading: false,
|
||||
dirty: false,
|
||||
progress: 0,
|
||||
isUploadingThumbnail: false,
|
||||
thumbnailProgress: 0,
|
||||
|
@ -114,6 +117,7 @@ const initialState = ImmutableMap({
|
|||
context_references: ImmutableSet(),
|
||||
prohibited_visibilities: ImmutableSet(),
|
||||
prohibited_words: ImmutableSet(),
|
||||
scheduled_status_id: null,
|
||||
});
|
||||
|
||||
const initialPoll = ImmutableMap({
|
||||
|
@ -122,18 +126,18 @@ const initialPoll = ImmutableMap({
|
|||
multiple: false,
|
||||
});
|
||||
|
||||
const statusToTextMentions = (text, privacy, status) => {
|
||||
if(status === null) {
|
||||
const statusToTextMentions = (text, privacy, replyStatus) => {
|
||||
if(replyStatus === null) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let mentions = ImmutableOrderedSet();
|
||||
|
||||
if (status.getIn(['account', 'id']) !== me) {
|
||||
mentions = mentions.add(`@${status.getIn(['account', 'acct'])} `);
|
||||
if (replyStatus.getIn(['account', 'id']) !== me) {
|
||||
mentions = mentions.add(`@${replyStatus.getIn(['account', 'acct'])} `);
|
||||
}
|
||||
|
||||
mentions = mentions.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `));
|
||||
mentions = mentions.union(replyStatus.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `));
|
||||
|
||||
const match = /^(\s*(?:(?:@\S+)\s*)*)([\s\S]*)/.exec(text);
|
||||
const extrctMentions = ImmutableOrderedSet(match[1].trim().split(/\s+/).filter(Boolean).map(mention => `${mention} `));
|
||||
|
@ -163,6 +167,7 @@ const clearAll = state => {
|
|||
map.update('media_attachments', list => list.clear());
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', false);
|
||||
map.set('datetime_form', null);
|
||||
map.set('default_expires', state.get('default_expires_in') ? true : null);
|
||||
map.set('scheduled', null);
|
||||
|
@ -170,7 +175,8 @@ const clearAll = state => {
|
|||
map.set('expires_action', state.get('default_expires_action', 'mark'));
|
||||
map.update('references', set => set.clear());
|
||||
map.update('context_references', set => set.clear());
|
||||
});
|
||||
map.set('scheduled_status_id', null);
|
||||
});
|
||||
};
|
||||
|
||||
const appendMedia = (state, media, file) => {
|
||||
|
@ -284,9 +290,9 @@ const hydrate = (state, hydratedState) => {
|
|||
const domParser = new DOMParser();
|
||||
|
||||
const expandMentions = status => {
|
||||
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
|
||||
const fragment = domParser.parseFromString(status.get('content', ''), 'text/html').documentElement;
|
||||
|
||||
status.get('mentions').forEach(mention => {
|
||||
status.get('mentions', ImmutableList()).forEach(mention => {
|
||||
fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`;
|
||||
});
|
||||
|
||||
|
@ -357,11 +363,13 @@ export default function compose(state = initialState, action) {
|
|||
}
|
||||
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', true);
|
||||
});
|
||||
case COMPOSE_SPOILERNESS_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('spoiler', !state.get('spoiler'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', true);
|
||||
|
||||
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
|
||||
map.set('sensitive', true);
|
||||
|
@ -371,7 +379,8 @@ export default function compose(state = initialState, action) {
|
|||
if (!state.get('spoiler')) return state;
|
||||
return state
|
||||
.set('spoiler_text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid())
|
||||
.set('dirty', true);
|
||||
case COMPOSE_VISIBILITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
const searchability = searchabilityCap(action.value, state.get('searchability'));
|
||||
|
@ -380,12 +389,14 @@ export default function compose(state = initialState, action) {
|
|||
map.set('privacy', action.value);
|
||||
map.set('searchability', searchability);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', true);
|
||||
map.set('circle_id', null);
|
||||
});
|
||||
case COMPOSE_SEARCHABILITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('searchability', action.value);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', true);
|
||||
|
||||
const privacy = privacyExpand(action.value, state.get('privacy'));
|
||||
|
||||
|
@ -398,11 +409,13 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_CIRCLE_CHANGE:
|
||||
return state
|
||||
.set('circle_id', action.value)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid())
|
||||
.set('dirty', true);
|
||||
case COMPOSE_CHANGE:
|
||||
return state
|
||||
.set('text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid())
|
||||
.set('dirty', true);
|
||||
case COMPOSE_COMPOSING_CHANGE:
|
||||
return state.set('is_composing', action.value);
|
||||
case COMPOSE_REPLY:
|
||||
|
@ -422,12 +435,14 @@ export default function compose(state = initialState, action) {
|
|||
map.set('caretPosition', null);
|
||||
map.set('preselectDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', false);
|
||||
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'));
|
||||
map.update('context_references', set => set.clear().concat(action.context_references));
|
||||
map.set('scheduled_status_id', null);
|
||||
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
@ -451,12 +466,14 @@ export default function compose(state = initialState, action) {
|
|||
map.set('focusDate', new Date());
|
||||
map.set('preselectDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', false);
|
||||
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'));
|
||||
map.update('context_references', set => set.clear().add(action.status.get('id')));
|
||||
map.set('scheduled_status_id', null);
|
||||
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
@ -468,6 +485,7 @@ export default function compose(state = initialState, action) {
|
|||
});
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
case COMPOSE_QUOTE_CANCEL:
|
||||
case COMPOSE_SCHEDULED_EDIT_CANCEL:
|
||||
case COMPOSE_RESET:
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', null);
|
||||
|
@ -482,21 +500,24 @@ export default function compose(state = initialState, action) {
|
|||
map.set('circle_id', null);
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('dirty', false);
|
||||
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'));
|
||||
map.update('context_references', set => set.clear());
|
||||
if (action.type == COMPOSE_RESET) {
|
||||
if (action.type === COMPOSE_RESET || action.type === COMPOSE_SCHEDULED_EDIT_CANCEL) {
|
||||
map.update('references', set => set.clear());
|
||||
}
|
||||
map.set('scheduled_status_id', null);
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_changing_upload', true);
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
case SCHEDULED_STATUS_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
return state.set('is_submitting', false);
|
||||
|
@ -547,6 +568,8 @@ export default function compose(state = initialState, action) {
|
|||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('scheduled_status_id', null);
|
||||
map.set('dirty', false);
|
||||
});
|
||||
case COMPOSE_DIRECT:
|
||||
return state.withMutations(map => {
|
||||
|
@ -557,6 +580,8 @@ export default function compose(state = initialState, action) {
|
|||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('scheduled_status_id', null);
|
||||
map.set('dirty', false);
|
||||
});
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
|
||||
|
@ -593,27 +618,30 @@ export default function compose(state = initialState, action) {
|
|||
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'));
|
||||
map.set('quote_from', action.status.getIn(['quote', 'id']));
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id', null));
|
||||
map.set('quote_from', action.status.getIn(['quote', 'id'], null));
|
||||
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
|
||||
map.set('reply_status', action.replyStatus);
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
map.set('searchability', action.status.get('searchability'));
|
||||
map.set('circle_id', action.status.get('circle_id'));
|
||||
map.set('media_attachments', action.status.get('media_attachments'));
|
||||
map.set('privacy', action.status.get('visibility', state.get('default_privacy')));
|
||||
map.set('searchability', action.status.get('searchability', state.get('default_searchability')));
|
||||
map.set('circle_id', action.status.get('circle_id', null));
|
||||
map.set('media_attachments', action.status.get('media_attachments', ImmutableList()));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('sensitive', action.status.get('sensitive'));
|
||||
map.set('dirty', false);
|
||||
map.set('poll', action.status.get('poll', null));
|
||||
map.set('sensitive', action.status.get('sensitive', false));
|
||||
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('scheduled', action.status.get('scheduled_at') ? format(new Date(action.status.get('scheduled_at')), 'yyyy-MM-dd HH:mm') : null);
|
||||
map.set('expires', action.status.get('expires_at') ? format(new Date(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'));
|
||||
map.update('references', set => set.clear().concat(action.status.get('status_reference_ids')).delete(action.status.getIn(['quote', 'id'])));
|
||||
map.update('references', set => set.clear().concat(action.status.get('status_reference_ids', ImmutableList())).delete(action.status.getIn(['quote', 'id'], ImmutableList())));
|
||||
map.update('context_references', set => set.clear().concat(action.context_references));
|
||||
map.set('scheduled_status_id', action.status.get('scheduled_status_id', null));
|
||||
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
if (action.status.get('spoiler_text', '').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', action.status.get('spoiler_text'));
|
||||
} else {
|
||||
|
@ -623,7 +651,7 @@ export default function compose(state = initialState, action) {
|
|||
|
||||
if (action.status.get('poll')) {
|
||||
map.set('poll', ImmutableMap({
|
||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||
options: action.status.getIn(['poll', 'options']).map(x => typeof x === 'string' ? x : x.get('title')),
|
||||
multiple: action.status.getIn(['poll', 'multiple']),
|
||||
expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
|
||||
}));
|
||||
|
@ -653,13 +681,14 @@ export default function compose(state = initialState, action) {
|
|||
map.set('scheduled', null);
|
||||
map.set('expires', null);
|
||||
map.set('expires_action', 'mark');
|
||||
map.set('dirty', true);
|
||||
});
|
||||
case COMPOSE_SCHEDULED_CHANGE:
|
||||
return state.set('scheduled', action.value);
|
||||
return state.set('scheduled', action.value).set('dirty', true);;
|
||||
case COMPOSE_EXPIRES_CHANGE:
|
||||
return state.set('expires', action.value);
|
||||
return state.set('expires', action.value).set('dirty', true);;
|
||||
case COMPOSE_EXPIRES_ACTION_CHANGE:
|
||||
return state.set('expires_action', action.value);
|
||||
return state.set('expires_action', action.value).set('dirty', true);;
|
||||
case COMPOSE_REFERENCE_ADD:
|
||||
return state.update('references', set => set.add(action.id));
|
||||
case COMPOSE_REFERENCE_REMOVE:
|
||||
|
|
|
@ -11,6 +11,7 @@ import status_status_lists from './status_status_lists';
|
|||
import accounts from './accounts';
|
||||
import accounts_counters from './accounts_counters';
|
||||
import statuses from './statuses';
|
||||
import scheduled_statuses from './scheduled_statuses';
|
||||
import relationships from './relationships';
|
||||
import settings from './settings';
|
||||
import push_notifications from './push_notifications';
|
||||
|
@ -61,6 +62,7 @@ const reducers = {
|
|||
accounts,
|
||||
accounts_counters,
|
||||
statuses,
|
||||
scheduled_statuses,
|
||||
relationships,
|
||||
settings,
|
||||
push_notifications,
|
||||
|
|
70
app/javascript/mastodon/reducers/scheduled_statuses.js
Normal file
70
app/javascript/mastodon/reducers/scheduled_statuses.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
SCHEDULED_STATUSES_FETCH_REQUEST,
|
||||
SCHEDULED_STATUSES_FETCH_SUCCESS,
|
||||
SCHEDULED_STATUSES_FETCH_FAIL,
|
||||
SCHEDULED_STATUSES_EXPAND_REQUEST,
|
||||
SCHEDULED_STATUSES_EXPAND_SUCCESS,
|
||||
SCHEDULED_STATUSES_EXPAND_FAIL,
|
||||
SCHEDULED_STATUS_DELETE_SUCCESS,
|
||||
} from '../actions/scheduled_statuses';
|
||||
import {
|
||||
SCHEDULED_STATUS_SUBMIT_SUCCESS,
|
||||
} from '../actions/compose';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
items: ImmutableList(),
|
||||
});
|
||||
|
||||
const normalizeList = (state, scheduled_statuses, next) => {
|
||||
return state.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('loaded', true);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', fromJS(scheduled_statuses));
|
||||
});
|
||||
};
|
||||
|
||||
const appendToList = (state, scheduled_statuses, next) => {
|
||||
return state.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', map.get('items').concat(fromJS(scheduled_statuses)));
|
||||
});
|
||||
};
|
||||
|
||||
const prependOneToList = (state, scheduled_status) => {
|
||||
return state.withMutations(map => {
|
||||
map.set('items', map.get('items').unshift(fromJS(scheduled_status)));
|
||||
});
|
||||
};
|
||||
|
||||
const removeOneFromList = (state, id) => {
|
||||
return state.withMutations(map => {
|
||||
map.set('items', map.get('items').filter(item => item.get('id') !== id));
|
||||
});
|
||||
};
|
||||
|
||||
export default function statusLists(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case SCHEDULED_STATUSES_FETCH_REQUEST:
|
||||
case SCHEDULED_STATUSES_EXPAND_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case SCHEDULED_STATUSES_FETCH_FAIL:
|
||||
case SCHEDULED_STATUSES_EXPAND_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case SCHEDULED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, action.scheduled_statuses, action.next);
|
||||
case SCHEDULED_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToList(state, action.scheduled_statuses, action.next);
|
||||
case SCHEDULED_STATUS_SUBMIT_SUCCESS:
|
||||
return prependOneToList(state, action.scheduled_status);
|
||||
case SCHEDULED_STATUS_DELETE_SUCCESS:
|
||||
return removeOneFromList(state, action.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -78,6 +78,7 @@ const initialState = ImmutableMap({
|
|||
status: false,
|
||||
emoji_reaction: false,
|
||||
status_reference: false,
|
||||
scheduled_status: false,
|
||||
}),
|
||||
|
||||
quickFilter: ImmutableMap({
|
||||
|
@ -99,6 +100,7 @@ const initialState = ImmutableMap({
|
|||
status: true,
|
||||
emoji_reaction: true,
|
||||
status_reference: true,
|
||||
scheduled_status: true,
|
||||
}),
|
||||
|
||||
sounds: ImmutableMap({
|
||||
|
@ -111,6 +113,7 @@ const initialState = ImmutableMap({
|
|||
status: true,
|
||||
emoji_reaction: true,
|
||||
status_reference: true,
|
||||
scheduled_status: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
export const uniq = array => {
|
||||
return array.filter((x, i, self) => self.indexOf(x) === i)
|
||||
return array.filter((x, i, self) => self.indexOf(x) === i);
|
||||
};
|
||||
|
||||
export const uniqCompact = array => {
|
||||
return array.filter((x, i, self) => x && self.indexOf(x) === i)
|
||||
return array.filter((x, i, self) => x && self.indexOf(x) === i);
|
||||
};
|
||||
|
||||
export const uniqWithoutNull = array => {
|
||||
return array.filter((x, i, self) => !x || self.indexOf(x) === i)
|
||||
return array.filter((x, i, self) => !x || self.indexOf(x) === i);
|
||||
};
|
||||
|
|
|
@ -742,7 +742,8 @@ html {
|
|||
}
|
||||
|
||||
.status__content,
|
||||
.reply-indicator__content {
|
||||
.reply-indicator__content,
|
||||
.scheduled-status-warning-indicator__content {
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
|
|
@ -796,8 +796,14 @@
|
|||
background: $success-green;
|
||||
}
|
||||
|
||||
.scheduled-status-warning-indicator {
|
||||
color: $inverted-text-color;
|
||||
background: $ui-primary-color;
|
||||
}
|
||||
|
||||
.reply-indicator,
|
||||
.quote-indicator {
|
||||
.quote-indicator,
|
||||
.scheduled-status-warning-indicator {
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
|
@ -807,13 +813,15 @@
|
|||
}
|
||||
|
||||
.reply-indicator__header,
|
||||
.quote-indicator__header {
|
||||
.quote-indicator__header,
|
||||
.scheduled-status-warning-indicator__header {
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reply-indicator__cancel,
|
||||
.quote-indicator__cancel,
|
||||
.scheduled-status-warning-indicator__cancel,
|
||||
.expires-indicator__cancel {
|
||||
float: right;
|
||||
line-height: 24px;
|
||||
|
@ -973,6 +981,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.scheduled-status-warning-indicator__content {
|
||||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.announcements__item__content {
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
|
@ -1511,7 +1524,8 @@
|
|||
}
|
||||
|
||||
.reply-indicator__content,
|
||||
.quote-indicator__content {
|
||||
.quote-indicator__content,
|
||||
.scheduled-status-warning-indicator__content {
|
||||
color: $inverted-text-color;
|
||||
font-size: 14px;
|
||||
|
||||
|
@ -8526,6 +8540,12 @@ noscript {
|
|||
flex: 0 0 auto;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__scheduled-time {
|
||||
color: $light-text-color;
|
||||
margin-top: 4px;
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-gallery {
|
||||
|
|
|
@ -38,6 +38,7 @@ class UserSettingsDecorator
|
|||
post_reference_modal
|
||||
add_reference_modal
|
||||
unselect_reference_modal
|
||||
delete_scheduled_status_modal
|
||||
auto_play_gif
|
||||
expand_spoiers
|
||||
reduce_motion
|
||||
|
|
|
@ -27,6 +27,7 @@ class Notification < ApplicationRecord
|
|||
'Poll' => :poll,
|
||||
'EmojiReaction' => :emoji_reaction,
|
||||
'StatusReference' => :status_reference,
|
||||
'ScheduledStatus' => :scheduled_status,
|
||||
}.freeze
|
||||
|
||||
TYPES = %i(
|
||||
|
@ -39,6 +40,7 @@ class Notification < ApplicationRecord
|
|||
poll
|
||||
emoji_reaction
|
||||
status_reference
|
||||
scheduled_status
|
||||
).freeze
|
||||
|
||||
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
||||
|
@ -99,6 +101,8 @@ class Notification < ApplicationRecord
|
|||
emoji_reaction&.status
|
||||
when :status_reference
|
||||
status_reference&.status
|
||||
when :scheduled_status
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -141,6 +145,8 @@ class Notification < ApplicationRecord
|
|||
notification.emoji_reaction.status = cached_status
|
||||
when :status_reference
|
||||
notification.status_reference.status = cached_status
|
||||
when :scheduled_status
|
||||
notification.status = cached_status
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -157,7 +163,7 @@ class Notification < ApplicationRecord
|
|||
return unless new_record?
|
||||
|
||||
case activity_type
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'EmojiReaction'
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'EmojiReaction', 'ScheduledStatus'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'Mention', 'StatusReference'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
|
|
|
@ -137,7 +137,7 @@ class User < ApplicationRecord
|
|||
:new_features_policy,
|
||||
:theme_instance_ticker, :theme_public,
|
||||
:enable_status_reference, :match_visibility_of_references,
|
||||
:post_reference_modal, :add_reference_modal, :unselect_reference_modal,
|
||||
:post_reference_modal, :add_reference_modal, :unselect_reference_modal, :delete_scheduled_status_modal,
|
||||
:hexagon_avatar, :enable_empty_column,
|
||||
:content_font_size, :info_font_size, :content_emoji_reaction_size, :emoji_scale, :picker_emoji_size,
|
||||
:hide_bot_on_public_timeline, :confirm_follow_from_bot,
|
||||
|
|
|
@ -69,6 +69,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store[:post_reference_modal] = object.current_account.user.setting_post_reference_modal
|
||||
store[:add_reference_modal] = object.current_account.user.setting_add_reference_modal
|
||||
store[:unselect_reference_modal] = object.current_account.user.setting_unselect_reference_modal
|
||||
store[:delete_scheduled_status_modal] = object.current_account.user.setting_delete_scheduled_status_modal
|
||||
store[:enable_empty_column] = object.current_account.user.setting_enable_empty_column
|
||||
store[:content_font_size] = object.current_account.user.setting_content_font_size
|
||||
store[:info_font_size] = object.current_account.user.setting_info_font_size
|
||||
|
|
|
@ -13,7 +13,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def status_type?
|
||||
[:favourite, :reblog, :status, :mention, :poll, :emoji_reaction, :status_reference].include?(object.type)
|
||||
[:favourite, :reblog, :status, :mention, :poll, :emoji_reaction, :status_reference, :scheduled_status].include?(object.type)
|
||||
end
|
||||
|
||||
def reblog?
|
||||
|
|
|
@ -10,6 +10,6 @@ class REST::ScheduledStatusSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def params
|
||||
object.params.without(:application_id)
|
||||
object.params.without('application_id')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,6 +54,10 @@ class NotifyService < BaseService
|
|||
FeedManager.instance.filter?(:status_references, @notification.status_reference.status, @recipient)
|
||||
end
|
||||
|
||||
def blocked_scheduled_status?
|
||||
false
|
||||
end
|
||||
|
||||
def following_sender?
|
||||
return @following_sender if defined?(@following_sender)
|
||||
@following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
|
||||
|
@ -160,7 +164,7 @@ class NotifyService < BaseService
|
|||
|
||||
def blocked?
|
||||
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway
|
||||
blocked ||= from_self? && @notification.type != :poll # Skip for interactions with self
|
||||
blocked ||= from_self? && !%i(poll scheduled_status).include?(@notification.type) # Skip for interactions with self
|
||||
|
||||
return blocked if message? && from_staff?
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ class PostStatusService < BaseService
|
|||
# @option [String] :idempotency Optional idempotency key
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @option [String] :searchability
|
||||
# @option [Boolean] :notify Optional notification of completion of schedule post
|
||||
# @return [Status]
|
||||
def call(account, options = {})
|
||||
@account = account
|
||||
|
@ -53,11 +54,17 @@ class PostStatusService < BaseService
|
|||
|
||||
redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
|
||||
|
||||
create_notification!
|
||||
|
||||
@status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_notification!
|
||||
NotifyService.new.call(@status.account, :scheduled_status, @status) if @options[:notify] && @status.account.local?
|
||||
end
|
||||
|
||||
def status_from_uri(uri)
|
||||
ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||
end
|
||||
|
@ -81,7 +88,6 @@ class PostStatusService < BaseService
|
|||
@visibility = :limited if @visibility&.to_sym != :direct && @in_reply_to&.limited_visibility?
|
||||
@searchability = searchability
|
||||
@scheduled_at = @options[:scheduled_at].is_a?(Time) ? @options[:scheduled_at] : @options[:scheduled_at]&.to_datetime&.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
|
||||
@text.sub!(/QT:\s*\[.*?\]/, '')
|
||||
|
@ -273,11 +279,12 @@ class PostStatusService < BaseService
|
|||
|
||||
def scheduled_options
|
||||
@options.tap do |options_hash|
|
||||
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
|
||||
options_hash[:application_id] = options_hash.delete(:application)&.id
|
||||
options_hash[:scheduled_at] = nil
|
||||
options_hash[:idempotency] = nil
|
||||
options_hash[:with_rate_limit] = false
|
||||
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id&.to_s
|
||||
options_hash[:application_id] = options_hash.delete(:application)&.id
|
||||
options_hash[:scheduled_at] = nil
|
||||
options_hash[:idempotency] = nil
|
||||
options_hash[:status_reference_ids] = options_hash[:status_reference_ids]&.map(&:to_s)&.filter{ |id| id != options_hash[:quote_id] }
|
||||
options_hash[:with_rate_limit] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
= f.input :setting_post_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
|
||||
= f.input :setting_add_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
|
||||
= f.input :setting_unselect_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
|
||||
= f.input :setting_delete_scheduled_status_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
|
||||
|
||||
%h4= t 'appearance.sensitive_content'
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ class PublishScheduledStatusWorker
|
|||
scheduled_status.account,
|
||||
options_with_objects(scheduled_status.params.with_indifferent_access)
|
||||
)
|
||||
|
||||
remove_scheduled_status(scheduled_status)
|
||||
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
|
||||
true
|
||||
end
|
||||
|
@ -21,6 +23,11 @@ class PublishScheduledStatusWorker
|
|||
options.tap do |options_hash|
|
||||
options_hash[:application] = Doorkeeper::Application.find(options_hash.delete(:application_id)) if options[:application_id]
|
||||
options_hash[:thread] = Status.find(options_hash.delete(:in_reply_to_id)) if options_hash[:in_reply_to_id]
|
||||
options_hash[:notify] = true
|
||||
end
|
||||
end
|
||||
|
||||
def remove_scheduled_status(scheduled_status)
|
||||
Redis.current.publish("timeline:#{scheduled_status.account.id}", Oj.dump(event: :scheduled_status, payload: scheduled_status.id.to_s))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -259,6 +259,7 @@ en:
|
|||
setting_default_search_searchability: Search range
|
||||
setting_default_sensitive: Always mark media as sensitive
|
||||
setting_delete_modal: Show confirmation dialog before deleting a post
|
||||
setting_delete_scheduled_status_modal: Show confirmation dialog before removing a scheduled status
|
||||
setting_disable_account_delete: Disable account delete
|
||||
setting_disable_block: Disable block
|
||||
setting_disable_clear_all_notifications: Disable clear all notifications
|
||||
|
|
|
@ -255,6 +255,7 @@ ja:
|
|||
setting_default_search_searchability: 検索の対象とする範囲
|
||||
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
||||
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
|
||||
setting_delete_scheduled_status_modal: 予約投稿を削除する前に確認ダイアログを表示する
|
||||
setting_disable_account_delete: アカウント削除を無効にする
|
||||
setting_disable_block: ブロックを無効にする
|
||||
setting_disable_clear_all_notifications: 通知の全消去を無効にする
|
||||
|
|
|
@ -111,6 +111,7 @@ defaults: &defaults
|
|||
post_reference_modal: false
|
||||
add_reference_modal: true
|
||||
unselect_reference_modal: false
|
||||
delete_scheduled_status_modal: true
|
||||
hexagon_avatar: false
|
||||
enable_empty_column: false
|
||||
hide_bot_on_public_timeline: false
|
||||
|
|
Loading…
Reference in a new issue