mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 09:21:11 +00:00
Composer quote improvements (#35835)
Co-authored-by: Claire <claire.github-309c@sitedethib.com> Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
parent
e770303968
commit
f85f0eee1b
|
@ -84,6 +84,7 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
|||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
uploadQuote: { id: 'upload_error.quote', defaultMessage: 'File upload not allowed with quotes.' },
|
||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||
|
@ -146,7 +147,7 @@ export function resetCompose() {
|
|||
};
|
||||
}
|
||||
|
||||
export const focusCompose = (defaultText) => (dispatch, getState) => {
|
||||
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
|
@ -303,6 +304,11 @@ export function submitComposeFail(error) {
|
|||
|
||||
export function uploadCompose(files) {
|
||||
return function (dispatch, getState) {
|
||||
// Exit if there's a quote.
|
||||
if (getState().compose.get('quoted_status_id')) {
|
||||
dispatch(showAlert({ message: messages.uploadQuote }));
|
||||
return;
|
||||
}
|
||||
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
|
@ -12,7 +14,27 @@ import {
|
|||
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||
import type { Status } from '../models/status';
|
||||
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
import { showAlert } from './alerts';
|
||||
import { focusCompose } from './compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
quoteErrorUpload: {
|
||||
id: 'quote_error.upload',
|
||||
defaultMessage: 'Quoting is not allowed with media attachments.',
|
||||
},
|
||||
quoteErrorPoll: {
|
||||
id: 'quote_error.poll',
|
||||
defaultMessage: 'Quoting is not allowed with polls.',
|
||||
},
|
||||
quoteErrorQuote: {
|
||||
id: 'quote_error.quote',
|
||||
defaultMessage: 'Only one quote at a time is allowed.',
|
||||
},
|
||||
quoteErrorUnauthorized: {
|
||||
id: 'quote_error.unauthorized',
|
||||
defaultMessage: 'You are not authorized to quote this post.',
|
||||
},
|
||||
});
|
||||
|
||||
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||
unattached?: boolean;
|
||||
|
@ -78,14 +100,43 @@ export const changeUploadCompose = createDataLoadingThunk(
|
|||
},
|
||||
);
|
||||
|
||||
export const quoteComposeByStatus = createAppThunk(
|
||||
export const quoteCompose = createAppThunk(
|
||||
'compose/quoteComposeStatus',
|
||||
(status: Status, { getState }) => {
|
||||
ensureComposeIsVisible(getState);
|
||||
(status: Status, { dispatch }) => {
|
||||
dispatch(focusCompose());
|
||||
return status;
|
||||
},
|
||||
);
|
||||
|
||||
export const quoteComposeByStatus = createAppThunk(
|
||||
(status: Status, { dispatch, getState }) => {
|
||||
const composeState = getState().compose;
|
||||
const mediaAttachments = composeState.get('media_attachments');
|
||||
|
||||
if (composeState.get('poll')) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
||||
} else if (
|
||||
composeState.get('is_uploading') ||
|
||||
(mediaAttachments &&
|
||||
typeof mediaAttachments !== 'string' &&
|
||||
typeof mediaAttachments !== 'number' &&
|
||||
typeof mediaAttachments !== 'boolean' &&
|
||||
mediaAttachments.size !== 0)
|
||||
) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorUpload }));
|
||||
} else if (composeState.get('quoted_status_id')) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorQuote }));
|
||||
} else if (
|
||||
status.getIn(['quote_approval', 'current_user']) !== 'automatic' &&
|
||||
status.getIn(['quote_approval', 'current_user']) !== 'manual'
|
||||
) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorUnauthorized }));
|
||||
} else {
|
||||
dispatch(quoteCompose(status));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const quoteComposeById = createAppThunk(
|
||||
(statusId: string, { dispatch, getState }) => {
|
||||
const status = getState().statuses.get(statusId);
|
||||
|
@ -97,6 +148,6 @@ export const quoteComposeById = createAppThunk(
|
|||
|
||||
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||
|
||||
export const setQuotePolicy = createAction<ApiQuotePolicy>(
|
||||
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
|
||||
'compose/setQuotePolicy',
|
||||
);
|
||||
|
|
|
@ -133,3 +133,9 @@ export interface ApiStatusSourceJSON {
|
|||
text: string;
|
||||
spoiler_text: string;
|
||||
}
|
||||
|
||||
export function isStatusVisibility(
|
||||
visibility: string,
|
||||
): visibility is StatusVisibility {
|
||||
return ['public', 'unlisted', 'private', 'direct'].includes(visibility);
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ class Status extends ImmutablePureComponent {
|
|||
onToggleCollapsed: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
onQuoteCancel: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
|
@ -583,7 +584,7 @@ class Status extends ImmutablePureComponent {
|
|||
<DisplayName account={status.get('account')} />
|
||||
</Link>
|
||||
|
||||
{this.props.contextType === 'compose' && isQuotedPost && (
|
||||
{isQuotedPost && !!this.props.onQuoteCancel && (
|
||||
<IconButton
|
||||
onClick={this.handleQuoteCancel}
|
||||
className='status__quote-cancel'
|
||||
|
|
|
@ -63,12 +63,21 @@ type GetStatusSelector = (
|
|||
props: { id?: string | null; contextType?: string },
|
||||
) => Status | null;
|
||||
|
||||
export const QuotedStatus: React.FC<{
|
||||
interface QuotedStatusProps {
|
||||
quote: QuoteMap;
|
||||
contextType?: string;
|
||||
variant?: 'full' | 'link';
|
||||
nestingLevel?: number;
|
||||
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
|
||||
onQuoteCancel?: () => void; // Used for composer.
|
||||
}
|
||||
|
||||
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
quote,
|
||||
contextType,
|
||||
nestingLevel = 1,
|
||||
variant = 'full',
|
||||
onQuoteCancel,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const quotedStatusId = quote.get('quoted_status');
|
||||
const quoteState = quote.get('state');
|
||||
|
@ -160,6 +169,7 @@ export const QuotedStatus: React.FC<{
|
|||
id={quotedStatusId}
|
||||
contextType={contextType}
|
||||
avatarSize={32}
|
||||
onQuoteCancel={onQuoteCancel}
|
||||
>
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
|
|
|
@ -41,10 +41,10 @@ import {
|
|||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from '../actions/statuses';
|
||||
import { setStatusQuotePolicy } from '../actions/statuses_typed';
|
||||
import Status from '../components/status';
|
||||
import { deleteModal } from '../initial_state';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||
import { quoteComposeCancel } from '../actions/compose_typed';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
@ -112,18 +112,18 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onQuoteCancel() {
|
||||
if (contextType === 'compose') {
|
||||
dispatch(quoteComposeCancel());
|
||||
}
|
||||
},
|
||||
|
||||
onRevokeQuote (status) {
|
||||
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
|
||||
},
|
||||
|
||||
onQuotePolicyChange(status) {
|
||||
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
|
||||
const statusId = status.get('id');
|
||||
const handleChange = (_, quotePolicy) => {
|
||||
dispatch(
|
||||
setStatusQuotePolicy({ policy: quotePolicy, statusId }),
|
||||
);
|
||||
}
|
||||
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
|
||||
},
|
||||
|
||||
onEdit (status) {
|
||||
|
|
|
@ -15,10 +15,8 @@ import { missingAltTextModal } from 'mastodon/initial_state';
|
|||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import AutosuggestTextarea from 'mastodon/components/autosuggest_textarea';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { countableText } from '../util/counter';
|
||||
|
@ -32,6 +30,7 @@ import { ReplyIndicator } from './reply_indicator';
|
|||
import { UploadForm } from './upload_form';
|
||||
import { Warning } from './warning';
|
||||
import { ComposeQuotedStatus } from './quoted_post';
|
||||
import { VisibilityButton } from './visibility_button';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
|
||||
|
@ -260,7 +259,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<EditIndicator />
|
||||
|
||||
<div className='compose-form__dropdowns'>
|
||||
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||
<VisibilityButton disabled={this.props.isEditing} />
|
||||
<LanguageDropdown />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Map } from 'immutable';
|
||||
|
||||
import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
|
||||
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
export const ComposeQuotedStatus: FC = () => {
|
||||
const quotedStatusId = useAppSelector(
|
||||
(state) => state.compose.get('quoted_status_id') as string | null,
|
||||
);
|
||||
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
||||
const quote = useMemo(
|
||||
() =>
|
||||
quotedStatusId
|
||||
|
@ -20,8 +22,17 @@ export const ComposeQuotedStatus: FC = () => {
|
|||
: null,
|
||||
[quotedStatusId],
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const handleQuoteCancel = useCallback(() => {
|
||||
dispatch(quoteComposeCancel());
|
||||
}, [dispatch]);
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
return <QuotedStatus quote={quote} contextType='compose' />;
|
||||
return (
|
||||
<QuotedStatus
|
||||
quote={quote}
|
||||
onQuoteCancel={!isEditing ? handleQuoteCancel : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { changeComposeVisibility } from '@/mastodon/actions/compose';
|
||||
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||
import { isFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
|
||||
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
|
||||
import { messages as privacyMessages } from './privacy_dropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
anyone_quote: {
|
||||
id: 'privacy.quote.anyone',
|
||||
defaultMessage: '{visibility}, anyone can quote',
|
||||
},
|
||||
limited_quote: {
|
||||
id: 'privacy.quote.limited',
|
||||
defaultMessage: '{visibility}, quotes limited',
|
||||
},
|
||||
disabled_quote: {
|
||||
id: 'privacy.quote.disabled',
|
||||
defaultMessage: '{visibility}, quotes disabled',
|
||||
},
|
||||
});
|
||||
|
||||
interface PrivacyDropdownProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
|
||||
if (!isFeatureEnabled('outgoing_quotes')) {
|
||||
return <PrivacyDropdownContainer {...props} />;
|
||||
}
|
||||
return <PrivacyModalButton {...props} />;
|
||||
};
|
||||
|
||||
const visibilityOptions = {
|
||||
public: {
|
||||
icon: 'globe',
|
||||
iconComponent: PublicIcon,
|
||||
value: 'public',
|
||||
text: privacyMessages.public_short,
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
iconComponent: QuietTimeIcon,
|
||||
value: 'unlisted',
|
||||
text: privacyMessages.unlisted_short,
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
iconComponent: LockIcon,
|
||||
value: 'private',
|
||||
text: privacyMessages.private_short,
|
||||
},
|
||||
direct: {
|
||||
icon: 'at',
|
||||
iconComponent: AlternateEmailIcon,
|
||||
value: 'direct',
|
||||
text: privacyMessages.direct_short,
|
||||
},
|
||||
};
|
||||
|
||||
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { visibility, quotePolicy } = useAppSelector((state) => ({
|
||||
visibility: state.compose.get('privacy') as StatusVisibility,
|
||||
quotePolicy: state.compose.get('quote_policy') as ApiQuotePolicy,
|
||||
}));
|
||||
|
||||
const { icon, iconComponent } = useMemo(() => {
|
||||
const option = visibilityOptions[visibility];
|
||||
return { icon: option.icon, iconComponent: option.iconComponent };
|
||||
}, [visibility]);
|
||||
const text = useMemo(() => {
|
||||
const visibilityText = intl.formatMessage(
|
||||
visibilityOptions[visibility].text,
|
||||
);
|
||||
if (visibility === 'private' || visibility === 'direct') {
|
||||
return visibilityText;
|
||||
}
|
||||
if (quotePolicy === 'nobody') {
|
||||
return intl.formatMessage(messages.disabled_quote, {
|
||||
visibility: visibilityText,
|
||||
});
|
||||
}
|
||||
if (quotePolicy !== 'public') {
|
||||
return intl.formatMessage(messages.limited_quote, {
|
||||
visibility: visibilityText,
|
||||
});
|
||||
}
|
||||
return intl.formatMessage(messages.anyone_quote, {
|
||||
visibility: visibilityText,
|
||||
});
|
||||
}, [quotePolicy, visibility, intl]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChange: VisibilityModalCallback = useCallback(
|
||||
(newVisibility, newQuotePolicy) => {
|
||||
if (newVisibility !== visibility) {
|
||||
dispatch(changeComposeVisibility(newVisibility));
|
||||
}
|
||||
if (newQuotePolicy !== quotePolicy) {
|
||||
dispatch(setComposeQuotePolicy(newQuotePolicy));
|
||||
}
|
||||
},
|
||||
[dispatch, quotePolicy, visibility],
|
||||
);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'COMPOSE_PRIVACY',
|
||||
modalProps: { onChange: handleChange },
|
||||
}),
|
||||
);
|
||||
}, [dispatch, handleChange]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(privacyMessages.change_privacy)}
|
||||
onClick={handleOpen}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button')}
|
||||
>
|
||||
<Icon id={icon} icon={iconComponent} />
|
||||
<span className='dropdown-button__label'>{text}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
@ -15,7 +15,6 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
|
|||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
|
@ -57,6 +56,7 @@ import {
|
|||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from '../../actions/statuses';
|
||||
import { setStatusQuotePolicy } from '../../actions/statuses_typed';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
|
||||
import { StatusQuoteManager } from '../../components/status_quoted';
|
||||
|
@ -266,8 +266,14 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleQuotePolicyChange = (status) => {
|
||||
const statusId = status.get('id');
|
||||
const { dispatch } = this.props;
|
||||
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
|
||||
const handleChange = (_, quotePolicy) => {
|
||||
dispatch(
|
||||
setStatusQuotePolicy({ policy: quotePolicy, statusId }),
|
||||
);
|
||||
}
|
||||
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
|
||||
};
|
||||
|
||||
handleEditClick = (status) => {
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
import { forwardRef, useCallback, useId, useMemo } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { changeComposeVisibility } from '@/mastodon/actions/compose';
|
||||
import { setStatusQuotePolicy } from '@/mastodon/actions/statuses_typed';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import { isQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import { isStatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import { Dropdown } from '@/mastodon/components/dropdown';
|
||||
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { messages as privacyMessages } from '@/mastodon/features/compose/components/privacy_dropdown';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store';
|
||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
|
||||
import type { BaseConfirmationModalProps } from './confirmation_modals/confirmation_modal';
|
||||
|
||||
|
@ -43,13 +49,26 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
export type VisibilityModalCallback = (
|
||||
visibility: StatusVisibility,
|
||||
quotePolicy: ApiQuotePolicy,
|
||||
) => void;
|
||||
|
||||
interface VisibilityModalProps extends BaseConfirmationModalProps {
|
||||
statusId: string;
|
||||
statusId?: string;
|
||||
onChange: VisibilityModalCallback;
|
||||
}
|
||||
|
||||
const selectStatusPolicy = createAppSelector(
|
||||
[(state) => state.statuses, (_state, statusId: string) => statusId],
|
||||
(statuses, statusId) => {
|
||||
[
|
||||
(state) => state.statuses,
|
||||
(_state, statusId?: string) => statusId,
|
||||
(state) => state.compose.get('quote_policy') as ApiQuotePolicy,
|
||||
],
|
||||
(statuses, statusId, composeQuotePolicy) => {
|
||||
if (!statusId) {
|
||||
return composeQuotePolicy;
|
||||
}
|
||||
const status = statuses.get(statusId);
|
||||
if (!status) {
|
||||
return 'public';
|
||||
|
@ -77,24 +96,25 @@ const selectStatusPolicy = createAppSelector(
|
|||
);
|
||||
|
||||
export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ onClose, statusId }, ref) => {
|
||||
({ onClose, onChange, statusId }, ref) => {
|
||||
const intl = useIntl();
|
||||
const currentVisibility = useAppSelector(
|
||||
(state) =>
|
||||
(state.statuses.getIn([statusId, 'visibility'], 'public') as
|
||||
const currentVisibility = useAppSelector((state) =>
|
||||
statusId
|
||||
? ((state.statuses.getIn([statusId, 'visibility'], 'public') as
|
||||
| StatusVisibility
|
||||
| undefined) ?? 'public',
|
||||
| undefined) ?? 'public')
|
||||
: (state.compose.get('privacy') as StatusVisibility),
|
||||
);
|
||||
const currentQuotePolicy = useAppSelector((state) =>
|
||||
selectStatusPolicy(state, statusId),
|
||||
);
|
||||
|
||||
const [visibility, setVisibility] = useState(currentVisibility);
|
||||
const [quotePolicy, setQuotePolicy] = useState(currentQuotePolicy);
|
||||
|
||||
const disableVisibility = !!statusId;
|
||||
const disableQuotePolicy =
|
||||
currentVisibility === 'private' || currentVisibility === 'direct';
|
||||
const isSaving = useAppSelector(
|
||||
(state) =>
|
||||
state.statuses.getIn([statusId, 'isSavingQuotePolicy']) === true,
|
||||
);
|
||||
visibility === 'private' || visibility === 'direct';
|
||||
|
||||
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(
|
||||
() => [
|
||||
|
@ -102,21 +122,30 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
value: 'public',
|
||||
text: intl.formatMessage(privacyMessages.public_short),
|
||||
meta: intl.formatMessage(privacyMessages.public_long),
|
||||
icon: 'globe',
|
||||
iconComponent: PublicIcon,
|
||||
},
|
||||
{
|
||||
value: 'unlisted',
|
||||
text: intl.formatMessage(privacyMessages.unlisted_short),
|
||||
meta: intl.formatMessage(privacyMessages.unlisted_long),
|
||||
extra: intl.formatMessage(privacyMessages.unlisted_extra),
|
||||
icon: 'unlock',
|
||||
iconComponent: QuietTimeIcon,
|
||||
},
|
||||
{
|
||||
value: 'private',
|
||||
text: intl.formatMessage(privacyMessages.private_short),
|
||||
meta: intl.formatMessage(privacyMessages.private_long),
|
||||
icon: 'lock',
|
||||
iconComponent: LockIcon,
|
||||
},
|
||||
{
|
||||
value: 'direct',
|
||||
text: intl.formatMessage(privacyMessages.direct_short),
|
||||
meta: intl.formatMessage(privacyMessages.direct_long),
|
||||
icon: 'at',
|
||||
iconComponent: AlternateEmailIcon,
|
||||
},
|
||||
],
|
||||
[intl],
|
||||
|
@ -133,24 +162,27 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
[intl],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleVisibilityChange = useCallback(
|
||||
(value: string) => {
|
||||
// Published statuses cannot change visibility.
|
||||
if (statusId) {
|
||||
return;
|
||||
const handleVisibilityChange = useCallback((value: string) => {
|
||||
if (isStatusVisibility(value)) {
|
||||
setVisibility(value);
|
||||
}
|
||||
dispatch(changeComposeVisibility(value));
|
||||
},
|
||||
[dispatch, statusId],
|
||||
);
|
||||
const handleQuotePolicyChange = useCallback(
|
||||
(value: string) => {
|
||||
if (isQuotePolicy(value) && !disableQuotePolicy) {
|
||||
void dispatch(setStatusQuotePolicy({ policy: value, statusId }));
|
||||
}, []);
|
||||
const handleQuotePolicyChange = useCallback((value: string) => {
|
||||
if (isQuotePolicy(value)) {
|
||||
setQuotePolicy(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save on close
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getCloseConfirmationMessage() {
|
||||
onChange(visibility, quotePolicy);
|
||||
return null;
|
||||
},
|
||||
[disableQuotePolicy, dispatch, statusId],
|
||||
}),
|
||||
[onChange, quotePolicy, visibility],
|
||||
);
|
||||
|
||||
const privacyDropdownId = useId();
|
||||
|
@ -192,7 +224,7 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
<label
|
||||
htmlFor={privacyDropdownId}
|
||||
className={classNames('visibility-dropdown__label', {
|
||||
disabled: isSaving || !!statusId,
|
||||
disabled: disableVisibility,
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -203,10 +235,10 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
<Dropdown
|
||||
items={visibilityItems}
|
||||
classPrefix='visibility-dropdown'
|
||||
current={currentVisibility}
|
||||
current={visibility}
|
||||
onChange={handleVisibilityChange}
|
||||
title={intl.formatMessage(privacyMessages.change_privacy)}
|
||||
disabled={isSaving || !!statusId}
|
||||
disabled={disableVisibility}
|
||||
id={privacyDropdownId}
|
||||
/>
|
||||
{!!statusId && (
|
||||
|
@ -222,7 +254,7 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
<label
|
||||
htmlFor={quoteDropdownId}
|
||||
className={classNames('visibility-dropdown__label', {
|
||||
disabled: disableQuotePolicy || isSaving,
|
||||
disabled: disableQuotePolicy,
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -234,15 +266,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||
items={quoteItems}
|
||||
onChange={handleQuotePolicyChange}
|
||||
classPrefix='visibility-dropdown'
|
||||
current={currentQuotePolicy}
|
||||
current={quotePolicy}
|
||||
title={intl.formatMessage(messages.buttonTitle)}
|
||||
disabled={disableQuotePolicy || isSaving}
|
||||
disabled={disableQuotePolicy}
|
||||
id={quoteDropdownId}
|
||||
/>
|
||||
<QuotePolicyHelper
|
||||
policy={currentQuotePolicy}
|
||||
visibility={currentVisibility}
|
||||
/>
|
||||
<QuotePolicyHelper policy={quotePolicy} visibility={visibility} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -738,11 +738,18 @@
|
|||
"privacy.private.short": "Followers",
|
||||
"privacy.public.long": "Anyone on and off Mastodon",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.quote.anyone": "{visibility}, anyone can quote",
|
||||
"privacy.quote.disabled": "{visibility}, quotes disabled",
|
||||
"privacy.quote.limited": "{visibility}, quotes limited",
|
||||
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
|
||||
"privacy.unlisted.long": "Fewer algorithmic fanfares",
|
||||
"privacy.unlisted.short": "Quiet public",
|
||||
"privacy_policy.last_updated": "Last updated {date}",
|
||||
"privacy_policy.title": "Privacy Policy",
|
||||
"quote_error.poll": "Quoting is not allowed with polls.",
|
||||
"quote_error.quote": "Only one quote at a time is allowed.",
|
||||
"quote_error.unauthorized": "You are not authorized to quote this post.",
|
||||
"quote_error.upload": "Quoting is not allowed with media attachments.",
|
||||
"recommended": "Recommended",
|
||||
"refresh": "Refresh",
|
||||
"regeneration_indicator.please_stand_by": "Please stand by.",
|
||||
|
@ -944,6 +951,7 @@
|
|||
"upload_button.label": "Add images, a video or an audio file",
|
||||
"upload_error.limit": "File upload limit exceeded.",
|
||||
"upload_error.poll": "File upload not allowed with polls.",
|
||||
"upload_error.quote": "File upload not allowed with quotes.",
|
||||
"upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
|
||||
"upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
|
||||
"upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
|
||||
|
|
|
@ -2,9 +2,9 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde
|
|||
|
||||
import {
|
||||
changeUploadCompose,
|
||||
quoteComposeByStatus,
|
||||
quoteCompose,
|
||||
quoteComposeCancel,
|
||||
setQuotePolicy,
|
||||
setComposeQuotePolicy,
|
||||
} from 'mastodon/actions/compose_typed';
|
||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||
|
||||
|
@ -329,22 +329,15 @@ export const composeReducer = (state = initialState, action) => {
|
|||
return state.set('is_changing_upload', true);
|
||||
} else if (changeUploadCompose.rejected.match(action)) {
|
||||
return state.set('is_changing_upload', false);
|
||||
} else if (quoteComposeByStatus.match(action)) {
|
||||
} else if (quoteCompose.match(action)) {
|
||||
const status = action.payload;
|
||||
if (
|
||||
status.getIn(['quote_approval', 'current_user']) === 'automatic' &&
|
||||
state.get('media_attachments').size === 0 &&
|
||||
!state.get('is_uploading') &&
|
||||
!state.get('poll')
|
||||
) {
|
||||
return state
|
||||
.set('quoted_status_id', status.get('id'))
|
||||
.set('spoiler', status.get('sensitive'))
|
||||
.set('spoiler_text', status.get('spoiler_text'));
|
||||
}
|
||||
} else if (quoteComposeCancel.match(action)) {
|
||||
return state.set('quoted_status_id', null);
|
||||
} else if (setQuotePolicy.match(action)) {
|
||||
} else if (setComposeQuotePolicy.match(action)) {
|
||||
return state.set('quote_policy', action.payload);
|
||||
}
|
||||
|
||||
|
@ -520,6 +513,7 @@ export const composeReducer = (state = initialState, action) => {
|
|||
map.set('sensitive', action.status.get('sensitive'));
|
||||
map.set('language', action.status.get('language'));
|
||||
map.set('id', null);
|
||||
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status']));
|
||||
// Mastodon-authored posts can be expected to have at most one automatic approval policy
|
||||
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');
|
||||
|
||||
|
@ -551,6 +545,7 @@ export const composeReducer = (state = initialState, action) => {
|
|||
map.set('idempotencyKey', uuid());
|
||||
map.set('sensitive', action.status.get('sensitive'));
|
||||
map.set('language', action.status.get('language'));
|
||||
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status']));
|
||||
// Mastodon-authored posts can be expected to have at most one automatic approval policy
|
||||
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export const getFilters = createSelector(
|
|||
(_, { contextType }: { contextType: string }) => contextType,
|
||||
],
|
||||
(filters, contextType) => {
|
||||
if (!contextType || contextType === 'compose') {
|
||||
if (!contextType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -5512,7 +5512,10 @@ a.status-card {
|
|||
|
||||
.privacy-dropdown__option__content,
|
||||
.privacy-dropdown__option__content strong,
|
||||
.privacy-dropdown__option__additional {
|
||||
.privacy-dropdown__option__additional,
|
||||
.visibility-dropdown__option__content,
|
||||
.visibility-dropdown__option__content strong,
|
||||
.visibility-dropdown__option__additional {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
@ -5526,13 +5529,15 @@ a.status-card {
|
|||
}
|
||||
}
|
||||
|
||||
.privacy-dropdown__option__icon {
|
||||
.privacy-dropdown__option__icon,
|
||||
.visibility-dropdown__option__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.privacy-dropdown__option__content {
|
||||
.privacy-dropdown__option__content,
|
||||
.visibility-dropdown__option__content {
|
||||
flex: 1 1 auto;
|
||||
color: $darker-text-color;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user