mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 18:10:58 +00:00
Add privacy presets in composer
This commit is contained in:
parent
441eb89537
commit
70e4e032d3
|
|
@ -27,6 +27,7 @@ interface Props {
|
||||||
classNamePrefix?: string;
|
classNamePrefix?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
items: SelectItem[];
|
items: SelectItem[];
|
||||||
|
header?: React.ReactNode;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +36,7 @@ export const DropdownSelector: React.FC<Props> = ({
|
||||||
style,
|
style,
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
|
header,
|
||||||
classNamePrefix = 'privacy-dropdown',
|
classNamePrefix = 'privacy-dropdown',
|
||||||
onClose,
|
onClose,
|
||||||
onChange,
|
onChange,
|
||||||
|
|
@ -142,42 +144,45 @@ export const DropdownSelector: React.FC<Props> = ({
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul style={style} role='listbox' ref={listRef}>
|
<>
|
||||||
{items.map((item) => (
|
{header}
|
||||||
<li
|
<ul style={style} role='listbox' ref={listRef}>
|
||||||
role='option'
|
{items.map((item) => (
|
||||||
tabIndex={0}
|
<li
|
||||||
key={item.value}
|
role='option'
|
||||||
data-index={item.value}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown}
|
key={item.value}
|
||||||
onClick={handleClick}
|
data-index={item.value}
|
||||||
className={classNames(`${classNamePrefix}__option`, {
|
onKeyDown={handleKeyDown}
|
||||||
active: item.value === currentValue,
|
onClick={handleClick}
|
||||||
})}
|
className={classNames(`${classNamePrefix}__option`, {
|
||||||
aria-selected={item.value === currentValue}
|
active: item.value === currentValue,
|
||||||
ref={item.value === currentValue ? focusedItemRef : null}
|
})}
|
||||||
>
|
aria-selected={item.value === currentValue}
|
||||||
{item.icon && item.iconComponent && (
|
ref={item.value === currentValue ? focusedItemRef : null}
|
||||||
<div className={`${classNamePrefix}__option__icon`}>
|
>
|
||||||
<Icon id={item.icon} icon={item.iconComponent} />
|
{item.icon && item.iconComponent && (
|
||||||
</div>
|
<div className={`${classNamePrefix}__option__icon`}>
|
||||||
)}
|
<Icon id={item.icon} icon={item.iconComponent} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`${classNamePrefix}__option__content`}>
|
<div className={`${classNamePrefix}__option__content`}>
|
||||||
<strong>{item.text}</strong>
|
<strong>{item.text}</strong>
|
||||||
{item.meta}
|
{item.meta}
|
||||||
</div>
|
|
||||||
|
|
||||||
{item.extra && (
|
|
||||||
<div
|
|
||||||
className={`${classNamePrefix}__option__additional`}
|
|
||||||
title={item.extra}
|
|
||||||
>
|
|
||||||
<Icon id='info-circle' icon={InfoIcon} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</li>
|
{item.extra && (
|
||||||
))}
|
<div
|
||||||
</ul>
|
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 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 classNames from 'classnames';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/esm/Overlay';
|
||||||
|
import type { Placement } from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
changeComposeVisibility,
|
changeComposeVisibility,
|
||||||
setComposeQuotePolicy,
|
setComposeQuotePolicy,
|
||||||
|
|
@ -12,9 +16,11 @@ import {
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||||
|
import { DropdownSelector } from '@/mastodon/components/dropdown_selector';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
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 LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.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 PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
@ -88,25 +263,7 @@ const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
||||||
return { icon: option.icon, iconComponent: option.iconComponent };
|
return { icon: option.icon, iconComponent: option.iconComponent };
|
||||||
}, [visibility]);
|
}, [visibility]);
|
||||||
const text = useMemo(() => {
|
const text = useMemo(() => {
|
||||||
const visibilityText = intl.formatMessage(
|
return shortMessageFromSettings(intl, visibility, quotePolicy);
|
||||||
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]);
|
}, [quotePolicy, visibility, intl]);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
@ -133,15 +290,18 @@ const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
|
||||||
}, [dispatch, handleChange]);
|
}, [dispatch, handleChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className='split-button'>
|
||||||
type='button'
|
<button
|
||||||
title={intl.formatMessage(privacyMessages.change_privacy)}
|
type='button'
|
||||||
onClick={handleOpen}
|
title={intl.formatMessage(privacyMessages.change_privacy)}
|
||||||
disabled={disabled}
|
onClick={handleOpen}
|
||||||
className={classNames('dropdown-button')}
|
disabled={disabled}
|
||||||
>
|
className={classNames('dropdown-button')}
|
||||||
<Icon id={icon} icon={iconComponent} />
|
>
|
||||||
<span className='dropdown-button__label'>{text}</span>
|
<Icon id={icon} icon={iconComponent} />
|
||||||
</button>
|
<span className='dropdown-button__label'>{text}</span>
|
||||||
|
</button>
|
||||||
|
<PrivacyPresetsDropdown disabled={disabled} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -755,6 +755,7 @@
|
||||||
"privacy.private.short": "Followers",
|
"privacy.private.short": "Followers",
|
||||||
"privacy.public.long": "Anyone on and off Mastodon",
|
"privacy.public.long": "Anyone on and off Mastodon",
|
||||||
"privacy.public.short": "Public",
|
"privacy.public.short": "Public",
|
||||||
|
"privacy.quick_settings_presets": "Quick settings presets",
|
||||||
"privacy.quote.anyone": "{visibility}, anyone can quote",
|
"privacy.quote.anyone": "{visibility}, anyone can quote",
|
||||||
"privacy.quote.disabled": "{visibility}, quotes disabled",
|
"privacy.quote.disabled": "{visibility}, quotes disabled",
|
||||||
"privacy.quote.limited": "{visibility}, quotes limited",
|
"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 {
|
.character-counter {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-family: $font-sans-serif, sans-serif;
|
font-family: $font-sans-serif, sans-serif;
|
||||||
|
|
@ -5603,6 +5622,17 @@ a.status-card {
|
||||||
z-index: 9999;
|
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,
|
.privacy-dropdown__option,
|
||||||
.visibility-dropdown__option {
|
.visibility-dropdown__option {
|
||||||
--dropdown-text-color: $primary-text-color;
|
--dropdown-text-color: $primary-text-color;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user