mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 16:59:41 +00:00
[Glitch] Refactor PrivacyDropdown to TypeScript
Port 8a235dd187 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -1,158 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import Overlay from 'react-overlays/Overlay';
|
|
||||||
|
|
||||||
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 { DropdownSelector } from 'flavours/glitch/components/dropdown_selector';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
|
|
||||||
export const messages = defineMessages({
|
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
|
||||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
|
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
|
||||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Hidden from Mastodon search results, trending, and public timelines' },
|
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
|
||||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' },
|
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
|
||||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' },
|
|
||||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Change post privacy' },
|
|
||||||
unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: '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.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class PrivacyDropdown extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
noDirect: PropTypes.bool,
|
|
||||||
container: PropTypes.func,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
open: false,
|
|
||||||
placement: 'bottom',
|
|
||||||
};
|
|
||||||
|
|
||||||
handleToggle = () => {
|
|
||||||
if (this.state.open && this.activeElement) {
|
|
||||||
this.activeElement.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ open: !this.state.open });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = e => {
|
|
||||||
switch(e.key) {
|
|
||||||
case 'Escape':
|
|
||||||
this.handleClose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown = () => {
|
|
||||||
if (!this.state.open) {
|
|
||||||
this.activeElement = document.activeElement;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleButtonKeyDown = (e) => {
|
|
||||||
switch(e.key) {
|
|
||||||
case ' ':
|
|
||||||
case 'Enter':
|
|
||||||
this.handleMouseDown();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClose = () => {
|
|
||||||
if (this.state.open && this.activeElement) {
|
|
||||||
this.activeElement.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
this.setState({ open: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = value => {
|
|
||||||
this.props.onChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
|
||||||
const { intl: { formatMessage } } = this.props;
|
|
||||||
|
|
||||||
this.options = [
|
|
||||||
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
|
||||||
{ icon: 'unlock', iconComponent: QuietTimeIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long), extra: formatMessage(messages.unlisted_extra) },
|
|
||||||
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!this.props.noDirect) {
|
|
||||||
this.options.push(
|
|
||||||
{ icon: 'at', iconComponent: AlternateEmailIcon, value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetRef = c => {
|
|
||||||
this.target = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
findTarget = () => {
|
|
||||||
return this.target;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOverlayEnter = (state) => {
|
|
||||||
this.setState({ placement: state.placement });
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, container, disabled, intl } = this.props;
|
|
||||||
const { open, placement } = this.state;
|
|
||||||
|
|
||||||
const valueOption = this.options.find(item => item.value === value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
title={intl.formatMessage(messages.change_privacy)}
|
|
||||||
aria-expanded={open}
|
|
||||||
onClick={this.handleToggle}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
onKeyDown={this.handleButtonKeyDown}
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames('dropdown-button', { active: open })}
|
|
||||||
>
|
|
||||||
<Icon id={valueOption.icon} icon={valueOption.iconComponent} />
|
|
||||||
<span className='dropdown-button__label'>{valueOption.text}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
|
||||||
{({ props, placement }) => (
|
|
||||||
<div {...props}>
|
|
||||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
|
||||||
<DropdownSelector
|
|
||||||
items={this.options}
|
|
||||||
value={value}
|
|
||||||
onClose={this.handleClose}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Overlay>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(PrivacyDropdown);
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { OverlayProps } from 'react-overlays/Overlay';
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses';
|
||||||
|
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 { DropdownSelector } from 'flavours/glitch/components/dropdown_selector';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
|
public_long: {
|
||||||
|
id: 'privacy.public.long',
|
||||||
|
defaultMessage: 'Anyone on and off Mastodon',
|
||||||
|
},
|
||||||
|
unlisted_short: {
|
||||||
|
id: 'privacy.unlisted.short',
|
||||||
|
defaultMessage: 'Quiet public',
|
||||||
|
},
|
||||||
|
unlisted_long: {
|
||||||
|
id: 'privacy.unlisted.long',
|
||||||
|
defaultMessage:
|
||||||
|
'Hidden from Mastodon search results, trending, and public timelines',
|
||||||
|
},
|
||||||
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||||
|
private_long: {
|
||||||
|
id: 'privacy.private.long',
|
||||||
|
defaultMessage: 'Only your followers',
|
||||||
|
},
|
||||||
|
direct_short: {
|
||||||
|
id: 'privacy.direct.short',
|
||||||
|
defaultMessage: 'Specific people',
|
||||||
|
},
|
||||||
|
direct_long: {
|
||||||
|
id: 'privacy.direct.long',
|
||||||
|
defaultMessage: 'Everyone mentioned in the post',
|
||||||
|
},
|
||||||
|
change_privacy: {
|
||||||
|
id: 'privacy.change',
|
||||||
|
defaultMessage: 'Change post privacy',
|
||||||
|
},
|
||||||
|
unlisted_extra: {
|
||||||
|
id: 'privacy.unlisted.additional',
|
||||||
|
defaultMessage:
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PrivacyDropdownProps {
|
||||||
|
value: StatusVisibility;
|
||||||
|
onChange: (value: StatusVisibility) => void;
|
||||||
|
noDirect?: boolean;
|
||||||
|
container?: OverlayProps['container'];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrivacyDropdown: React.FC<PrivacyDropdownProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
noDirect,
|
||||||
|
container,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const overlayTargetRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const previousFocusTargetRef = useRef<HTMLElement | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (isOpen && previousFocusTargetRef.current) {
|
||||||
|
previousFocusTargetRef.current.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
}, [handleClose, isOpen]);
|
||||||
|
|
||||||
|
const registerPreviousFocusTarget = useCallback(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
previousFocusTargetRef.current = document.activeElement as HTMLElement;
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleButtonKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if ([' ', 'Enter'].includes(e.key)) {
|
||||||
|
registerPreviousFocusTarget();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[registerPreviousFocusTarget],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
icon: 'globe',
|
||||||
|
iconComponent: PublicIcon,
|
||||||
|
value: 'public',
|
||||||
|
text: intl.formatMessage(messages.public_short),
|
||||||
|
meta: intl.formatMessage(messages.public_long),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'unlock',
|
||||||
|
iconComponent: QuietTimeIcon,
|
||||||
|
value: 'unlisted',
|
||||||
|
text: intl.formatMessage(messages.unlisted_short),
|
||||||
|
meta: intl.formatMessage(messages.unlisted_long),
|
||||||
|
extra: intl.formatMessage(messages.unlisted_extra),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'lock',
|
||||||
|
iconComponent: LockIcon,
|
||||||
|
value: 'private',
|
||||||
|
text: intl.formatMessage(messages.private_short),
|
||||||
|
meta: intl.formatMessage(messages.private_long),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!noDirect) {
|
||||||
|
options.push({
|
||||||
|
icon: 'at',
|
||||||
|
iconComponent: AlternateEmailIcon,
|
||||||
|
value: 'direct',
|
||||||
|
text: intl.formatMessage(messages.direct_short),
|
||||||
|
meta: intl.formatMessage(messages.direct_long),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOption =
|
||||||
|
options.find((item) => item.value === value) ?? options.at(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={overlayTargetRef}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
title={intl.formatMessage(messages.change_privacy)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
onClick={handleToggle}
|
||||||
|
onMouseDown={registerPreviousFocusTarget}
|
||||||
|
onKeyDown={handleButtonKeyDown}
|
||||||
|
disabled={disabled}
|
||||||
|
className={classNames('dropdown-button', { active: isOpen })}
|
||||||
|
>
|
||||||
|
{selectedOption && (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
id={selectedOption.icon}
|
||||||
|
icon={selectedOption.iconComponent}
|
||||||
|
/>
|
||||||
|
<span className='dropdown-button__label'>
|
||||||
|
{selectedOption.text}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Overlay
|
||||||
|
show={isOpen}
|
||||||
|
offset={[5, 5]}
|
||||||
|
placement='bottom'
|
||||||
|
flip
|
||||||
|
target={overlayTargetRef}
|
||||||
|
container={container}
|
||||||
|
popperConfig={{ strategy: 'fixed' }}
|
||||||
|
>
|
||||||
|
{({ props, placement }) => (
|
||||||
|
<div {...props}>
|
||||||
|
<div
|
||||||
|
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||||||
|
>
|
||||||
|
<DropdownSelector
|
||||||
|
items={options}
|
||||||
|
value={value}
|
||||||
|
onClose={handleClose}
|
||||||
|
// @ts-expect-error DropdownSelector doesn't yet return the correct type for onChange
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default PrivacyDropdown;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { changeComposeVisibility } from '@/flavours/glitch/actions/compose_typed';
|
|
||||||
|
|
||||||
import PrivacyDropdown from '../components/privacy_dropdown';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.getIn(['compose', 'privacy']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onChange (value) {
|
|
||||||
dispatch(changeComposeVisibility(value));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
|
|
||||||
@@ -53,7 +53,10 @@ export const BoostModal: React.FC<{
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const findContainer = useCallback(
|
const findContainer = useCallback(
|
||||||
() => document.getElementsByClassName('modal-root__container')[0],
|
() =>
|
||||||
|
document.getElementsByClassName(
|
||||||
|
'modal-root__container',
|
||||||
|
)[0] as HTMLDivElement,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user