mirror of
https://github.com/mastodon/mastodon.git
synced 2026-10-27 01:15:25 -05:00
Composer redesign: Settings menu (#40798)
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
&:hover:not(
|
||||
:active,
|
||||
:disabled,
|
||||
[aria-disabled='true'],
|
||||
[aria-pressed='true'],
|
||||
[aria-expanded='true']
|
||||
) {
|
||||
@@ -40,7 +41,8 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ interface ForwardedReferenceProps {
|
||||
onClick: React.MouseEventHandler;
|
||||
onMouseEnter: React.MouseEventHandler;
|
||||
onMouseLeave: React.MouseEventHandler;
|
||||
onFocus: React.KeyboardEventHandler;
|
||||
onBlur: React.KeyboardEventHandler;
|
||||
onFocus: React.FocusEventHandler;
|
||||
onBlur: React.FocusEventHandler;
|
||||
}
|
||||
|
||||
type GetTooltipProps = <T extends Record<string, unknown>>(
|
||||
@@ -96,7 +96,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
setIsOpen(false);
|
||||
}, [clearOpenTimeout, isOpen]);
|
||||
|
||||
const handleFocus = useCallback<React.KeyboardEventHandler>(
|
||||
const handleFocus: React.FocusEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (
|
||||
e.target instanceof HTMLElement &&
|
||||
|
||||
@@ -9,8 +9,8 @@ import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
||||
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
|
||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Remove content warning' },
|
||||
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Add content warning' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FlagIcon, LockSimpleOpenIcon } from '@phosphor-icons/react';
|
||||
|
||||
import {
|
||||
changeComposeSpoilerness,
|
||||
insertEmojiCompose,
|
||||
} from '@/mastodon/actions/compose';
|
||||
import { ToggleButton } from '@/mastodon/components/button/redesign';
|
||||
import { insertEmojiCompose } from '@/mastodon/actions/compose';
|
||||
import { normalizeKey } from '@/mastodon/components/hotkeys/utils';
|
||||
import { Icon, useIconWeight } from '@/mastodon/components/icon';
|
||||
import {
|
||||
closeComposer,
|
||||
getComposerTextarea,
|
||||
@@ -28,8 +19,9 @@ import { ComposeFormHeader } from './header';
|
||||
import { ComposeHints } from './hints';
|
||||
import { LanguageButton } from './language';
|
||||
import { ComposeReply } from './reply';
|
||||
import { selectComposeSensitive, selectComposeType } from './selectors';
|
||||
import { selectComposeType } from './selectors';
|
||||
import { ComposeSensitiveField } from './sensitive';
|
||||
import { ComposeSettingsMenu } from './settings';
|
||||
import classes from './styles.module.scss';
|
||||
import { ComposeTextarea } from './textarea';
|
||||
import { ComposeVisibility } from './visibility';
|
||||
@@ -45,15 +37,11 @@ export const RedesignComposeForm: React.FC<
|
||||
RedesignComposeFormProps & React.ComponentPropsWithRef<'form'>
|
||||
> = ({ autoFocus, className, noMinimize, redirectOnSuccess, ...props }) => {
|
||||
const type = useAppSelector(selectComposeType);
|
||||
const { sensitive } = useAppSelector(selectComposeSensitive);
|
||||
|
||||
const { onSensitiveChange, onEmojiPick, onSubmit } =
|
||||
useComposeHandlers(redirectOnSuccess);
|
||||
const { onEmojiPick, onSubmit } = useComposeHandlers(redirectOnSuccess);
|
||||
|
||||
const titleId = useId();
|
||||
|
||||
const sensitiveIcon = useIconWeight(FlagIcon, sensitive && 'fill');
|
||||
|
||||
return (
|
||||
<form
|
||||
{...props}
|
||||
@@ -75,27 +63,9 @@ export const RedesignComposeForm: React.FC<
|
||||
|
||||
<LanguageButton />
|
||||
|
||||
<ToggleButton
|
||||
size='sm'
|
||||
active={sensitive}
|
||||
onClick={onSensitiveChange}
|
||||
leadingIcon={sensitiveIcon}
|
||||
>
|
||||
<FormattedMessage id='compose.sensitive' defaultMessage='Sensitive' />
|
||||
</ToggleButton>
|
||||
<ComposeSettingsMenu />
|
||||
</div>
|
||||
|
||||
{type === 'message' && (
|
||||
<p className={classes.toolbarMessage}>
|
||||
<Icon id='lock-open' icon={LockSimpleOpenIcon} />
|
||||
<FormattedMessage
|
||||
id='compose.message.notice'
|
||||
defaultMessage='Messages are not end-to-end encrypted'
|
||||
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ComposeSensitiveField />
|
||||
|
||||
<ComposeTextarea
|
||||
@@ -140,10 +110,6 @@ function useComposeHandlers(redirectOnSuccess?: boolean) {
|
||||
};
|
||||
}, [dispatch, isModalOpen]);
|
||||
|
||||
const onSensitiveChange = useCallback(() => {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
}, [dispatch]);
|
||||
|
||||
const onEmojiPick: OnEmojiPick = useCallback(
|
||||
(emoji) => {
|
||||
const position = getComposerTextarea()?.selectionStart ?? 0;
|
||||
@@ -179,6 +145,5 @@ function useComposeHandlers(redirectOnSuccess?: boolean) {
|
||||
return {
|
||||
onSubmit,
|
||||
onEmojiPick,
|
||||
onSensitiveChange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
|
||||
import { MagnifyingGlassIcon, TranslateIcon } from '@phosphor-icons/react';
|
||||
|
||||
import { changeComposeLanguage } from '@/mastodon/actions/compose';
|
||||
import { CaretIcon } from '@/mastodon/components/button/redesign';
|
||||
import { TextInput } from '@/mastodon/components/form_fields/redesign';
|
||||
import {
|
||||
Menu,
|
||||
@@ -33,7 +32,7 @@ export const LanguageButton: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger size='sm' trailingIcon={CaretIcon}>
|
||||
<MenuTrigger size='sm' leadingIcon={TranslateIcon}>
|
||||
{langCode.toLocaleUpperCase()}
|
||||
</MenuTrigger>
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { changeComposeSpoilerness } from '@/mastodon/actions/compose';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button/redesign';
|
||||
import {
|
||||
ModalShell,
|
||||
ModalActions,
|
||||
ModalTitle,
|
||||
} from '@/mastodon/components/modal_shell/redesign';
|
||||
import {
|
||||
requestComposerFocus,
|
||||
submitComposer,
|
||||
} from '@/mastodon/reducers/slices/composer';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
const ComposerModalSensitive: React.FC<{ redirectOnSuccess?: boolean }> = ({
|
||||
redirectOnSuccess,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const onCancel = useCallback(() => {
|
||||
dispatch(requestComposerFocus());
|
||||
dispatch(
|
||||
closeModal({
|
||||
modalType: 'COMPOSER_ADD_CONTENT_WARNING',
|
||||
ignoreFocus: false,
|
||||
}),
|
||||
);
|
||||
}, [dispatch]);
|
||||
const onContinue = useCallback(() => {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
dispatch(submitComposer({ redirectOnSuccess }));
|
||||
dispatch(
|
||||
closeModal({
|
||||
modalType: 'COMPOSER_ADD_CONTENT_WARNING',
|
||||
ignoreFocus: false,
|
||||
}),
|
||||
);
|
||||
}, [dispatch, redirectOnSuccess]);
|
||||
|
||||
return (
|
||||
<ModalShell>
|
||||
<ModalTitle>
|
||||
<FormattedMessage
|
||||
id='compose.sensitive_modal.title'
|
||||
defaultMessage='Publish without content warning?'
|
||||
/>
|
||||
</ModalTitle>
|
||||
|
||||
<FormattedMessage
|
||||
id='compose.sensitive_modal.body'
|
||||
defaultMessage="You haven't added content warning text. If you publish now, your post will not be hidden behind a content warning."
|
||||
/>
|
||||
|
||||
<ModalActions>
|
||||
<Button variant='solid' color='destructive' onClick={onCancel}>
|
||||
<FormattedMessage
|
||||
id='compose.sensitive_modal.cancel'
|
||||
defaultMessage='Continue editing'
|
||||
/>
|
||||
</Button>
|
||||
<Button variant='solid' onClick={onContinue}>
|
||||
<FormattedMessage
|
||||
id='compose.sensitive_modal.continue'
|
||||
defaultMessage='Publish without warning'
|
||||
/>
|
||||
</Button>
|
||||
</ModalActions>
|
||||
</ModalShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComposerModalSensitive;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { length } from 'stringz';
|
||||
|
||||
import type { ApiMediaAttachmentJSON } from '@/mastodon/api_types/media_attachments';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import { immutableListToSuggestions } from '@/mastodon/components/autosuggest/utils';
|
||||
import type { StatusVisibility } from '@/mastodon/models/status';
|
||||
import type { ComposeType } from '@/mastodon/reducers/slices/composer';
|
||||
@@ -17,6 +18,20 @@ export const selectComposePrivacy = createAppSelector(
|
||||
(privacy, defaultPrivacy) => privacy ?? defaultPrivacy,
|
||||
);
|
||||
|
||||
export const selectComposeQuotePolicy = createAppSelector(
|
||||
[
|
||||
(state) => state.compose.get('quote_policy') as ApiQuotePolicy | undefined,
|
||||
(state) => state.compose.get('default_quote_policy') as ApiQuotePolicy,
|
||||
selectComposePrivacy,
|
||||
],
|
||||
(quotePolicy, defaultQuotePolicy, privacy) => {
|
||||
if (privacy === 'private' || privacy === 'direct') {
|
||||
return 'nobody';
|
||||
}
|
||||
return quotePolicy ?? defaultQuotePolicy;
|
||||
},
|
||||
);
|
||||
|
||||
export const selectComposeType = createAppSelector(
|
||||
[
|
||||
(state) => state.compose.get('in_reply_to') as string | null,
|
||||
@@ -131,10 +146,12 @@ export const selectComposeSensitive = createAppSelector(
|
||||
[
|
||||
(state) => !!state.compose.get('spoiler'),
|
||||
(state) => state.compose.get('spoiler_text'),
|
||||
(state) => !!state.compose.get('sensitive'),
|
||||
],
|
||||
(sensitive, text) => ({
|
||||
sensitive,
|
||||
(spoiler, text, sensitive) => ({
|
||||
sensitive: spoiler,
|
||||
sensitiveText: typeof text === 'string' ? text : '',
|
||||
mediaSensitive: sensitive || spoiler,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
183
app/javascript/mastodon/features/compose/redesign/settings.tsx
Normal file
183
app/javascript/mastodon/features/compose/redesign/settings.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { EyeSlashIcon, GearIcon, WarningIcon } from '@phosphor-icons/react';
|
||||
|
||||
import {
|
||||
changeComposeSensitivity,
|
||||
changeComposeSpoilerness,
|
||||
} from '@/mastodon/actions/compose';
|
||||
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import { IconButton } from '@/mastodon/components/button/redesign';
|
||||
import {
|
||||
Menu,
|
||||
MenuItemCheckbox,
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuItemRadio,
|
||||
MenuList,
|
||||
MenuTrigger,
|
||||
} from '@/mastodon/components/menu';
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
|
||||
import {
|
||||
selectComposePrivacy,
|
||||
selectComposeQuotePolicy,
|
||||
selectComposeSensitive,
|
||||
} from './selectors';
|
||||
|
||||
export const ComposeSettingsMenu: React.FC = () => {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger as={IconButton} icon={GearIcon} size='sm'>
|
||||
<FormattedMessage id='compose.settings' defaultMessage='Settings' />
|
||||
</MenuTrigger>
|
||||
|
||||
<MenuList maxWidth={280} placement='top-end'>
|
||||
<ComposeSettingsInnerMenu />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const ComposeSettingsInnerMenu: React.FC = () => {
|
||||
// Quote policy
|
||||
const quotePolicy = useAppSelector(selectComposeQuotePolicy);
|
||||
const privacy = useAppSelector(selectComposePrivacy);
|
||||
const disablePublicQuotes = privacy === 'private';
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const onQuotePolicyChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
let newQuotePolicy: ApiQuotePolicy = 'nobody';
|
||||
switch (value) {
|
||||
case 'public':
|
||||
newQuotePolicy = 'public';
|
||||
break;
|
||||
case 'followers':
|
||||
newQuotePolicy = 'followers';
|
||||
break;
|
||||
}
|
||||
dispatch(setComposeQuotePolicy(newQuotePolicy));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Sensitive content
|
||||
const { sensitive, mediaSensitive } = useAppSelector(selectComposeSensitive);
|
||||
|
||||
const onSensitiveChange = useCallback(() => {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
}, [dispatch]);
|
||||
const onMediaSensitiveChange = useCallback(() => {
|
||||
dispatch(changeComposeSensitivity());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{privacy !== 'direct' && (
|
||||
<MenuItemGroup
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy'
|
||||
defaultMessage='Who can quote'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItemRadio
|
||||
name='quote_policy'
|
||||
value='public'
|
||||
checked={quotePolicy === 'public'}
|
||||
onChange={onQuotePolicyChange}
|
||||
disabled={disablePublicQuotes}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='visibility_modal.quote_public'
|
||||
defaultMessage='Anyone'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
|
||||
<MenuItemRadio
|
||||
name='quote_policy'
|
||||
value='followers'
|
||||
checked={quotePolicy === 'followers'}
|
||||
onChange={onQuotePolicyChange}
|
||||
disabled={disablePublicQuotes}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
|
||||
<MenuItemRadio
|
||||
name='quote_policy'
|
||||
value='nobody'
|
||||
checked={quotePolicy === 'nobody'}
|
||||
onChange={onQuotePolicyChange}
|
||||
description={
|
||||
disablePublicQuotes && (
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy.only_me_hint'
|
||||
defaultMessage="When your post's visibility is set to Followers, it can only be quoted by you."
|
||||
/>
|
||||
)
|
||||
}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='visibility_modal.quote_nobody'
|
||||
defaultMessage='Just me'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
</MenuItemGroup>
|
||||
)}
|
||||
|
||||
{privacy !== 'direct' && <MenuItemDivider />}
|
||||
|
||||
<MenuItemGroup
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='compose.sensitive.label'
|
||||
defaultMessage='Sensitive content'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItemCheckbox
|
||||
name='content_warning'
|
||||
value='on'
|
||||
checked={sensitive}
|
||||
onChange={onSensitiveChange}
|
||||
keepMenuOpenOnClick
|
||||
icon={WarningIcon}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose_form.spoiler.unmarked'
|
||||
defaultMessage='Add content warning'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
<MenuItemCheckbox
|
||||
name='media_spoiler'
|
||||
value='on'
|
||||
checked={mediaSensitive}
|
||||
disabled={sensitive}
|
||||
onChange={onMediaSensitiveChange}
|
||||
keepMenuOpenOnClick
|
||||
icon={EyeSlashIcon}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.blur_media'
|
||||
defaultMessage='Blur media'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
</MenuItemGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -119,7 +119,7 @@
|
||||
.textareaMirror {
|
||||
@include mixins.type-body;
|
||||
|
||||
padding: var(--space-2);
|
||||
padding: var(--space-1);
|
||||
|
||||
// Prevent automatic page zoom in Safari when focusing
|
||||
// text fields with font-sizes below 16px
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import {
|
||||
ChatCircleDotsIcon,
|
||||
MagnifyingGlassIcon,
|
||||
NewspaperIcon,
|
||||
QuotesIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
changeComposeVisibility,
|
||||
setComposeQuotePolicy,
|
||||
} from '@/mastodon/actions/compose_typed';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import { ChatCircleDotsIcon, EyeIcon } from '@phosphor-icons/react';
|
||||
|
||||
import { changeComposeVisibility } from '@/mastodon/actions/compose_typed';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import { Button, CaretIcon } from '@/mastodon/components/button/redesign';
|
||||
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import {
|
||||
Menu,
|
||||
MenuList,
|
||||
@@ -28,10 +20,11 @@ import {
|
||||
MenuItemRadio,
|
||||
MenuItemCheckbox,
|
||||
} from '@/mastodon/components/menu';
|
||||
import { selectPlainAccount } from '@/mastodon/selectors/accounts';
|
||||
import { Tooltip } from '@/mastodon/components/tooltip';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import { selectComposeMentions, selectComposePrivacy } from './selectors';
|
||||
import { selectComposePrivacy } from './selectors';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const ComposeVisibility: React.FC<{ className?: string }> = ({
|
||||
className,
|
||||
@@ -39,28 +32,53 @@ export const ComposeVisibility: React.FC<{ className?: string }> = ({
|
||||
const privacy = useAppSelector(selectComposePrivacy);
|
||||
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
||||
|
||||
if (privacy === 'direct') {
|
||||
return (
|
||||
<div className={classNames(className, classes.toolbarMessage)}>
|
||||
<Icon icon={EyeIcon} />
|
||||
<FormattedMessage
|
||||
id='compose.privacy.direct.hint'
|
||||
defaultMessage='Visible to everyone mentioned. Not encrypted.'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Tooltip
|
||||
renderTextWhenClosed
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='compose.privacy.editing'
|
||||
defaultMessage='Visibility can’t be edited after a post has been published.'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{({ getTooltipProps, tooltipId }) => (
|
||||
<Button
|
||||
{...getTooltipProps()}
|
||||
size='sm'
|
||||
aria-disabled
|
||||
aria-describedby={tooltipId}
|
||||
>
|
||||
<ComposeVisibilityButtonText privacy={privacy} />
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<FormattedMessage
|
||||
id='compose.post.to'
|
||||
defaultMessage='To:'
|
||||
description='Before button that indicates who a post is for (Public, Followers, mentioned people)'
|
||||
/>
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
as={Button}
|
||||
size='sm'
|
||||
trailingIcon={CaretIcon}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<MenuTrigger as={Button} size='sm' trailingIcon={CaretIcon}>
|
||||
<ComposeVisibilityButtonText privacy={privacy} />
|
||||
</MenuTrigger>
|
||||
|
||||
{privacy !== 'direct' ? (
|
||||
<ComposeVisibilityMenu />
|
||||
) : (
|
||||
<ComposeDirectMenu />
|
||||
)}
|
||||
<ComposeVisibilityMenu />
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
@@ -69,34 +87,22 @@ export const ComposeVisibility: React.FC<{ className?: string }> = ({
|
||||
const ComposeVisibilityButtonText: React.FC<{
|
||||
privacy: StatusVisibility;
|
||||
}> = ({ privacy }) => {
|
||||
const mentions = useAppSelector(selectComposeMentions);
|
||||
const firstMentionedAccount = useAppSelector((state) =>
|
||||
selectPlainAccount(state, mentions.at(0)),
|
||||
);
|
||||
|
||||
if (privacy === 'public' || privacy === 'unlisted') {
|
||||
if (privacy === 'public') {
|
||||
return (
|
||||
<FormattedMessage id='privacy.public.short' defaultMessage='Public' />
|
||||
);
|
||||
} else if (privacy === 'unlisted') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='compose.privacy.unlisted'
|
||||
defaultMessage='Public, hidden from search'
|
||||
/>
|
||||
);
|
||||
} else if (privacy === 'private') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='compose.post.privacy.followers'
|
||||
defaultMessage='Followers {count, plural, =0 {} one {+ # other} other {+ # others}}'
|
||||
description='Count is # of other people mentioned in the post. If zero, just output "Followers".'
|
||||
values={{ count: mentions.length }}
|
||||
/>
|
||||
);
|
||||
} else if (mentions.length > 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='compose.message.direct.followers'
|
||||
defaultMessage='{name} {count, plural, =0 {} one {+ # other} other {+ # others}}'
|
||||
description='Name is the primary display name, count is # of other people mentioned in the post'
|
||||
values={{
|
||||
name: <DisplayNameSimple account={firstMentionedAccount} />,
|
||||
count: mentions.length - 1,
|
||||
}}
|
||||
id='compose.privacy.followers'
|
||||
defaultMessage='Followers (+ mentions)'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -109,18 +115,6 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
const defaultPrivacy = useAppSelector(
|
||||
(state) => state.compose.get('default_privacy') as StatusVisibility,
|
||||
);
|
||||
const currentQuotePolicy = useAppSelector(
|
||||
(state) => state.compose.get('quote_policy') as ApiQuotePolicy | undefined,
|
||||
);
|
||||
const defaultQuotePolicy = useAppSelector(
|
||||
(state) => state.compose.get('default_quote_policy') as ApiQuotePolicy,
|
||||
);
|
||||
|
||||
// Track the last public quote policy, so the picker remembers what was last used before quoting was disabled.
|
||||
const [lastQuotePolicy, setLastQuotePolicy] = useState(
|
||||
defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public',
|
||||
);
|
||||
const quotePolicy = currentQuotePolicy ?? defaultQuotePolicy;
|
||||
|
||||
const isReply = useAppSelector((state) => !!state.compose.get('in_reply_to'));
|
||||
|
||||
@@ -144,31 +138,6 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
[defaultPrivacy, dispatch, privacy],
|
||||
);
|
||||
|
||||
const handleQuotePolicyChange = useCallback(
|
||||
({ value, checked }: { value: string; checked?: boolean }) => {
|
||||
let newQuotePolicy: ApiQuotePolicy = 'nobody';
|
||||
switch (value) {
|
||||
case 'public':
|
||||
newQuotePolicy = 'public';
|
||||
setLastQuotePolicy(newQuotePolicy);
|
||||
break;
|
||||
case 'followers':
|
||||
newQuotePolicy = 'followers';
|
||||
setLastQuotePolicy(newQuotePolicy);
|
||||
break;
|
||||
case 'others':
|
||||
// If it's not checked, then it's nobody.
|
||||
if (checked) {
|
||||
// Only use the default if it's not nobody, as then it'll never be enabled.
|
||||
newQuotePolicy = lastQuotePolicy;
|
||||
}
|
||||
break;
|
||||
}
|
||||
dispatch(setComposeQuotePolicy(newQuotePolicy));
|
||||
},
|
||||
[dispatch, lastQuotePolicy],
|
||||
);
|
||||
|
||||
const handleSwitchToMessage: React.MouseEventHandler<HTMLButtonElement> =
|
||||
useCallback(() => {
|
||||
dispatch(changeComposeVisibility('direct'));
|
||||
@@ -202,8 +171,8 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='privacy.private.short'
|
||||
defaultMessage='Followers'
|
||||
id='compose.privacy.followers'
|
||||
defaultMessage='Followers (+ mentions)'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
|
||||
@@ -212,68 +181,22 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
<MenuItemCheckbox
|
||||
value='unlisted'
|
||||
disabled={privacy === 'private'}
|
||||
checked={privacy === 'public'}
|
||||
checked={privacy === 'unlisted' || privacy === 'private'}
|
||||
onChange={handlePrivacyChange}
|
||||
icon={MagnifyingGlassIcon}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.discoverable'
|
||||
defaultMessage='Discoverable in public feeds & search results'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
|
||||
<MenuItemCheckbox
|
||||
value='others'
|
||||
disabled={privacy === 'private'}
|
||||
checked={quotePolicy !== 'nobody' && privacy !== 'private'}
|
||||
onChange={handleQuotePolicyChange}
|
||||
icon={QuotesIcon}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.quotable'
|
||||
defaultMessage='Allow others to quote'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
</MenuItemGroup>
|
||||
|
||||
{quotePolicy !== 'nobody' && privacy !== 'private' && (
|
||||
<MenuItemGroup
|
||||
label={
|
||||
description={
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy'
|
||||
defaultMessage='Who can quote'
|
||||
id='compose.discoverable.hint'
|
||||
defaultMessage='Also applies to discovery feeds'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItemRadio
|
||||
name='quote_policy'
|
||||
value='public'
|
||||
checked={quotePolicy === 'public'}
|
||||
onChange={handleQuotePolicyChange}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy.anyone'
|
||||
defaultMessage='Anyone'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
|
||||
<MenuItemRadio
|
||||
name='quote_policy'
|
||||
value='followers'
|
||||
checked={quotePolicy === 'followers'}
|
||||
onChange={handleQuotePolicyChange}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='compose.visibility.quote_policy.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
</MenuItemGroup>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id='compose.discoverable'
|
||||
defaultMessage='Hide from search results'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
</MenuItemGroup>
|
||||
|
||||
<MenuItemDivider />
|
||||
|
||||
@@ -286,7 +209,7 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='compose.post.to_message'
|
||||
defaultMessage='Compose a message instead'
|
||||
defaultMessage='Convert to private message'
|
||||
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
|
||||
/>
|
||||
)}
|
||||
@@ -294,51 +217,3 @@ const ComposeVisibilityMenu: React.FC = () => {
|
||||
</MenuList>
|
||||
);
|
||||
};
|
||||
|
||||
const ComposeDirectMenu: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSwitchToPost: React.MouseEventHandler<HTMLButtonElement> =
|
||||
useCallback(() => {
|
||||
dispatch(
|
||||
openModal({ modalType: 'COMPOSER_SWITCH_TO_POST', modalProps: {} }),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const isReply = useAppSelector((state) => !!state.compose.get('in_reply_to'));
|
||||
|
||||
return (
|
||||
<MenuList placement='bottom-start' offset={4} maxWidth={280}>
|
||||
<MenuItemGroup
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='compose.visibility.title'
|
||||
defaultMessage='Visibility'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItemRadio value='direct' disabled checked>
|
||||
<FormattedMessage
|
||||
id='compose.visibility.direct_note'
|
||||
defaultMessage='Everyone mentioned'
|
||||
/>
|
||||
</MenuItemRadio>
|
||||
</MenuItemGroup>
|
||||
|
||||
<MenuItemDivider />
|
||||
|
||||
<MenuItem icon={NewspaperIcon} onClick={handleSwitchToPost}>
|
||||
{isReply ? (
|
||||
<FormattedMessage
|
||||
id='compose.visibility.to_reply'
|
||||
defaultMessage='Reply publicly instead'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='compose.visibility.to_post'
|
||||
defaultMessage='Compose a post instead'
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,6 +110,7 @@ export const MODAL_COMPONENTS = {
|
||||
'COMPOSER_REARRANGE': () => import('@/mastodon/features/compose/redesign/modal_rearrange'),
|
||||
'COMPOSER_SWITCH_TO_POST': () => import('@/mastodon/features/compose/redesign/modal_switch'),
|
||||
'COMPOSER_ADD_QUOTE': () => import('@/mastodon/features/compose/redesign/modal_quote'),
|
||||
'COMPOSER_ADD_CONTENT_WARNING': () => import('@/mastodon/features/compose/redesign/modal_sensitive'),
|
||||
'NOTIFICATION_SETTINGS': () => import('@/mastodon/features/notifications_v2/components/notification_settings_modal'),
|
||||
'NOTIFICATION_REQUESTS_SETTINGS': () => import('@/mastodon/features/notifications/components/notification_requests_settings').then(module => ({default: module.NotificationRequestsSettingsModal})),
|
||||
'HASHTAG_SETTINGS': () => import('@/mastodon/features/hashtag_timeline/components/column_settings_modal'),
|
||||
|
||||
@@ -507,6 +507,7 @@
|
||||
"community.column_settings.local_only": "Local only",
|
||||
"community.column_settings.media_only": "Media Only",
|
||||
"community.column_settings.remote_only": "Remote only",
|
||||
"compose.blur_media": "Blur media",
|
||||
"compose.cancel_modal.body": "You have a draft already in progress. What would you like to do?",
|
||||
"compose.cancel_modal.continue": "Continue draft",
|
||||
"compose.cancel_modal.delete": "Delete draft",
|
||||
@@ -516,7 +517,8 @@
|
||||
"compose.cancel_modal.edit.title": "Unsaved changes",
|
||||
"compose.cancel_modal.title": "Discard draft",
|
||||
"compose.counter": "{current, number}/{max, number}",
|
||||
"compose.discoverable": "Discoverable in public feeds & search results",
|
||||
"compose.discoverable": "Hide from search results",
|
||||
"compose.discoverable.hint": "Also applies to discovery feeds",
|
||||
"compose.error.blank_post": "Post can't be blank.",
|
||||
"compose.expand": "Show composer",
|
||||
"compose.hints.empty": "Post can't be blank",
|
||||
@@ -530,8 +532,6 @@
|
||||
"compose.language.change": "Change language",
|
||||
"compose.language.not-found": "No language found",
|
||||
"compose.language.search": "Search languages...",
|
||||
"compose.message.direct.followers": "{name} {count, plural, =0 {} one {+ # other} other {+ # others}}",
|
||||
"compose.message.notice": "Messages are not end-to-end encrypted",
|
||||
"compose.message.placeholder": "Add your recipients and your message.",
|
||||
"compose.message.publish": "Send",
|
||||
"compose.message.published.body": "Message sent",
|
||||
@@ -542,16 +542,17 @@
|
||||
"compose.poll.duration": "Duration:",
|
||||
"compose.poll.multiple": "Allow multiple selections",
|
||||
"compose.post.placeholder": "What would you like to say?",
|
||||
"compose.post.privacy.followers": "Followers {count, plural, =0 {} one {+ # other} other {+ # others}}",
|
||||
"compose.post.title.edit": "Edit post",
|
||||
"compose.post.title.new": "New post",
|
||||
"compose.post.to": "To:",
|
||||
"compose.post.to_message": "Compose a message instead",
|
||||
"compose.post.to_message": "Convert to private message",
|
||||
"compose.post.to_private_reply": "Reply privately instead",
|
||||
"compose.privacy.direct.hint": "Visible to everyone mentioned. Not encrypted.",
|
||||
"compose.privacy.editing": "Visibility can’t be edited after a post has been published.",
|
||||
"compose.privacy.followers": "Followers (+ mentions)",
|
||||
"compose.privacy.unlisted": "Public, hidden from search",
|
||||
"compose.publish": "Publish",
|
||||
"compose.published.body": "Post published.",
|
||||
"compose.published.open": "Open",
|
||||
"compose.quotable": "Allow others to quote",
|
||||
"compose.quote.audio": "Audio file {duration}",
|
||||
"compose.quote.poll": "Poll {sep} {isOpen, select, open {Accepting responses} other {Closed}}",
|
||||
"compose.quote.spoiler": "Content:",
|
||||
@@ -574,8 +575,13 @@
|
||||
"compose.reply_private.title.edit": "Edit private reply",
|
||||
"compose.reply_private.title.new": "New private reply",
|
||||
"compose.saved.body": "Post saved.",
|
||||
"compose.sensitive": "Sensitive",
|
||||
"compose.sensitive.label": "Sensitive content",
|
||||
"compose.sensitive.text": "Content warning",
|
||||
"compose.sensitive_modal.body": "You haven't added content warning text. If you publish now, your post will not be hidden behind a content warning.",
|
||||
"compose.sensitive_modal.cancel": "Continue editing",
|
||||
"compose.sensitive_modal.continue": "Publish without warning",
|
||||
"compose.sensitive_modal.title": "Publish without content warning?",
|
||||
"compose.settings": "Settings",
|
||||
"compose.switch_modal.back": "Back",
|
||||
"compose.switch_modal.body": "Your message has limited visibility. If you convert to a post, it will switch to your default post visibility.",
|
||||
"compose.switch_modal.continue": "Continue",
|
||||
@@ -587,13 +593,10 @@
|
||||
"compose.upload.menu.delete": "Remove image",
|
||||
"compose.upload.menu.edit_alt": "Edit alt text",
|
||||
"compose.upload.menu.rearrange": "Rearrange…",
|
||||
"compose.visibility.direct_note": "Everyone mentioned",
|
||||
"compose.visibility.quote_policy": "Who can quote",
|
||||
"compose.visibility.quote_policy.anyone": "Anyone",
|
||||
"compose.visibility.quote_policy.followers": "Followers",
|
||||
"compose.visibility.quote_policy.only_me_hint": "When your post's visibility is set to Followers, it can only be quoted by you.",
|
||||
"compose.visibility.title": "Visibility",
|
||||
"compose.visibility.to_post": "Compose a post instead",
|
||||
"compose.visibility.to_reply": "Reply publicly instead",
|
||||
"compose_form.direct_message_warning_learn_more": "Learn more",
|
||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
||||
"compose_form.encryption_warning_messages": "Messages on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||
|
||||
@@ -314,6 +314,15 @@ export const submitComposer = createAppThunk(
|
||||
modalProps: {},
|
||||
}),
|
||||
);
|
||||
} else if (!!compose.get('spoiler') && !compose.get('spoiler_text')) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'COMPOSER_ADD_CONTENT_WARNING',
|
||||
modalProps: {
|
||||
redirectOnSuccess,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
submitCompose((status: ApiStatusJSON) => {
|
||||
|
||||
Reference in New Issue
Block a user