mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 10:00:50 +00:00
Add privacy presets in composer
This commit is contained in:
parent
441eb89537
commit
70e4e032d3
|
|
@ -27,6 +27,7 @@ interface Props {
|
|||
classNamePrefix?: string;
|
||||
style?: React.CSSProperties;
|
||||
items: SelectItem[];
|
||||
header?: React.ReactNode;
|
||||
onChange: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
|
@ -35,6 +36,7 @@ export const DropdownSelector: React.FC<Props> = ({
|
|||
style,
|
||||
items,
|
||||
value,
|
||||
header,
|
||||
classNamePrefix = 'privacy-dropdown',
|
||||
onClose,
|
||||
onChange,
|
||||
|
|
@ -142,42 +144,45 @@ export const DropdownSelector: React.FC<Props> = ({
|
|||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<ul style={style} role='listbox' ref={listRef}>
|
||||
{items.map((item) => (
|
||||
<li
|
||||
role='option'
|
||||
tabIndex={0}
|
||||
key={item.value}
|
||||
data-index={item.value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
className={classNames(`${classNamePrefix}__option`, {
|
||||
active: item.value === currentValue,
|
||||
})}
|
||||
aria-selected={item.value === currentValue}
|
||||
ref={item.value === currentValue ? focusedItemRef : null}
|
||||
>
|
||||
{item.icon && item.iconComponent && (
|
||||
<div className={`${classNamePrefix}__option__icon`}>
|
||||
<Icon id={item.icon} icon={item.iconComponent} />
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
{header}
|
||||
<ul style={style} role='listbox' ref={listRef}>
|
||||
{items.map((item) => (
|
||||
<li
|
||||
role='option'
|
||||
tabIndex={0}
|
||||
key={item.value}
|
||||
data-index={item.value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
className={classNames(`${classNamePrefix}__option`, {
|
||||
active: item.value === currentValue,
|
||||
})}
|
||||
aria-selected={item.value === currentValue}
|
||||
ref={item.value === currentValue ? focusedItemRef : null}
|
||||
>
|
||||
{item.icon && item.iconComponent && (
|
||||
<div className={`${classNamePrefix}__option__icon`}>
|
||||
<Icon id={item.icon} icon={item.iconComponent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${classNamePrefix}__option__content`}>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
|
||||
{item.extra && (
|
||||
<div
|
||||
className={`${classNamePrefix}__option__additional`}
|
||||
title={item.extra}
|
||||
>
|
||||
<Icon id='info-circle' icon={InfoIcon} />
|
||||
<div className={`${classNamePrefix}__option__content`}>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{item.extra && (
|
||||
<div
|
||||
className={`${classNamePrefix}__option__additional`}
|
||||
title={item.extra}
|
||||
>
|
||||
<Icon id='info-circle' icon={InfoIcon} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Overlay from 'react-overlays/esm/Overlay';
|
||||
import type { Placement } from 'react-overlays/esm/usePopper';
|
||||
|
||||
import {
|
||||
changeComposeVisibility,
|
||||
setComposeQuotePolicy,
|
||||
|
|
@ -12,9 +16,11 @@ import {
|
|||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import { DropdownSelector } from '@/mastodon/components/dropdown_selector';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import ArrowDropDown from '@/material-icons/400-24px/arrow_drop_down.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';
|
||||
|
|
@ -73,6 +79,175 @@ const visibilityOptions = {
|
|||
},
|
||||
};
|
||||
|
||||
const shortMessageFromSettings = (
|
||||
intl: IntlShape,
|
||||
visibility: StatusVisibility,
|
||||
quotePolicy: ApiQuotePolicy,
|
||||
) => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
const PrivacyPresetsDropdown: FC<PrivacyDropdownProps> = ({
|
||||
disabled = false,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const targetRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [placement, setPlacement] = useState<Placement>('bottom');
|
||||
|
||||
const visibility = useAppSelector(
|
||||
(state) => state.compose.get('privacy') as StatusVisibility,
|
||||
);
|
||||
const defaultQuotePolicy = useAppSelector(
|
||||
(state) => state.compose.get('default_quote_policy') as ApiQuotePolicy,
|
||||
);
|
||||
|
||||
const handleOverlayEnter = useCallback(
|
||||
({ placement }: { placement?: Placement }) => {
|
||||
if (placement) setPlacement(placement);
|
||||
},
|
||||
[setPlacement],
|
||||
);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setOpen(!open);
|
||||
}, [open, setOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const handlePresetChange = useCallback(
|
||||
(value: string) => {
|
||||
switch (value) {
|
||||
case 'public':
|
||||
dispatch(changeComposeVisibility('public'));
|
||||
dispatch(setComposeQuotePolicy(defaultQuotePolicy));
|
||||
break;
|
||||
case 'unlisted':
|
||||
dispatch(changeComposeVisibility('unlisted'));
|
||||
dispatch(
|
||||
setComposeQuotePolicy(
|
||||
defaultQuotePolicy === 'nobody' ? 'nobody' : 'followers',
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'private':
|
||||
dispatch(changeComposeVisibility('private'));
|
||||
dispatch(setComposeQuotePolicy('nobody'));
|
||||
break;
|
||||
case 'direct':
|
||||
dispatch(changeComposeVisibility('direct'));
|
||||
dispatch(setComposeQuotePolicy('nobody'));
|
||||
break;
|
||||
}
|
||||
},
|
||||
[dispatch, defaultQuotePolicy],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: 'globe',
|
||||
iconComponent: PublicIcon,
|
||||
value: 'public',
|
||||
text: shortMessageFromSettings(intl, 'public', defaultQuotePolicy),
|
||||
meta: 'Anyone and on off Mastodon',
|
||||
},
|
||||
{
|
||||
icon: 'unlock',
|
||||
iconComponent: QuietTimeIcon,
|
||||
value: 'unlisted',
|
||||
text: shortMessageFromSettings(
|
||||
intl,
|
||||
'unlisted',
|
||||
defaultQuotePolicy === 'nobody' ? 'nobody' : 'followers',
|
||||
),
|
||||
meta: 'Hidden from Mastodon search results, trending, and public timelines',
|
||||
},
|
||||
{
|
||||
icon: 'lock',
|
||||
iconComponent: LockIcon,
|
||||
value: 'private',
|
||||
text: shortMessageFromSettings(intl, 'private', 'nobody'),
|
||||
meta: 'Only your followers',
|
||||
},
|
||||
{
|
||||
icon: 'at',
|
||||
iconComponent: AlternateEmailIcon,
|
||||
value: 'direct',
|
||||
text: shortMessageFromSettings(intl, 'direct', 'nobody'),
|
||||
meta: 'Everyone mentioned in the post',
|
||||
},
|
||||
],
|
||||
[intl, defaultQuotePolicy],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
onClick={handleToggle}
|
||||
disabled={disabled}
|
||||
ref={targetRef}
|
||||
>
|
||||
<Icon id='down-arrow' icon={ArrowDropDown} />
|
||||
</button>
|
||||
<Overlay
|
||||
show={open}
|
||||
offset={[5, 5]}
|
||||
placement={placement}
|
||||
flip
|
||||
target={targetRef}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div
|
||||
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||||
>
|
||||
<DropdownSelector
|
||||
header={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id='privacy.quick_settings_presets'
|
||||
defaultMessage='Quick settings presets'
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
items={options}
|
||||
value={visibility}
|
||||
onClose={handleClose}
|
||||
onChange={handlePresetChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -88,25 +263,7 @@ const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
|||
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,
|
||||
});
|
||||
return shortMessageFromSettings(intl, visibility, quotePolicy);
|
||||
}, [quotePolicy, visibility, intl]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
@ -133,15 +290,18 @@ const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
|||
}, [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>
|
||||
<div className='split-button'>
|
||||
<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>
|
||||
<PrivacyPresetsDropdown disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -755,6 +755,7 @@
|
|||
"privacy.private.short": "Followers",
|
||||
"privacy.public.long": "Anyone on and off Mastodon",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.quick_settings_presets": "Quick settings presets",
|
||||
"privacy.quote.anyone": "{visibility}, anyone can quote",
|
||||
"privacy.quote.disabled": "{visibility}, quotes disabled",
|
||||
"privacy.quote.limited": "{visibility}, quotes limited",
|
||||
|
|
|
|||
|
|
@ -1074,6 +1074,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
.split-button .dropdown-button {
|
||||
&:first-child {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
border-inline-end: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
padding: 0;
|
||||
|
||||
.icon {
|
||||
width: unset;
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
cursor: default;
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
|
|
@ -5603,6 +5622,17 @@ a.status-card {
|
|||
z-index: 9999;
|
||||
}
|
||||
|
||||
.privacy-dropdown__dropdown {
|
||||
h4 {
|
||||
--dropdown-text-color: $primary-text-color;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 12px;
|
||||
color: var(--dropdown-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.privacy-dropdown__option,
|
||||
.visibility-dropdown__option {
|
||||
--dropdown-text-color: $primary-text-color;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user