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
15 changed files with 371 additions and 96 deletions
|
|
@ -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
|
||||
| StatusVisibility
|
||||
| undefined) ?? 'public',
|
||||
const currentVisibility = useAppSelector((state) =>
|
||||
statusId
|
||||
? ((state.statuses.getIn([statusId, 'visibility'], 'public') as
|
||||
| StatusVisibility
|
||||
| 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;
|
||||
}
|
||||
dispatch(changeComposeVisibility(value));
|
||||
},
|
||||
[dispatch, statusId],
|
||||
);
|
||||
const handleQuotePolicyChange = useCallback(
|
||||
(value: string) => {
|
||||
if (isQuotePolicy(value) && !disableQuotePolicy) {
|
||||
void dispatch(setStatusQuotePolicy({ policy: value, statusId }));
|
||||
}
|
||||
},
|
||||
[disableQuotePolicy, dispatch, statusId],
|
||||
const handleVisibilityChange = useCallback((value: string) => {
|
||||
if (isStatusVisibility(value)) {
|
||||
setVisibility(value);
|
||||
}
|
||||
}, []);
|
||||
const handleQuotePolicyChange = useCallback((value: string) => {
|
||||
if (isQuotePolicy(value)) {
|
||||
setQuotePolicy(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save on close
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getCloseConfirmationMessage() {
|
||||
onChange(visibility, quotePolicy);
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
[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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue