Merge pull request #1682 from ClearlyClaire/glitch-soc/fixes/dropdowns-modals

Refactor and fix dropdown/action dialog
This commit is contained in:
Claire 2022-02-09 17:25:57 +01:00 committed by GitHub
commit 8987ea4d6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 214 deletions

View file

@ -116,7 +116,7 @@ class DropdownMenu extends React.PureComponent {
if (typeof action === 'function') { if (typeof action === 'function') {
e.preventDefault(); e.preventDefault();
action(); action(e);
} else if (to) { } else if (to) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(to); this.context.router.history.push(to);
@ -128,11 +128,11 @@ class DropdownMenu extends React.PureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />; return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
} }
const { text, href = '#' } = option; const { text, href = '#', target = '_blank', method } = option;
return ( return (
<li className='dropdown-menu__item' key={`${text}-${i}`}> <li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text} {text}
</a> </a>
</li> </li>
@ -149,7 +149,7 @@ class DropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting // It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // react-overlays
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul> <ul>
@ -236,7 +236,8 @@ export default class Dropdown extends React.PureComponent {
} }
} }
handleItemClick = (i, e) => { handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i]; const { action, to } = this.props.items[i];
this.handleClose(); this.handleClose();

View file

@ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) { onOpen(id, onItemClick, dropdownPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', { dispatch(isUserTouching() ? openModal('ACTIONS', {
status, status,
actions: items.map( actions: items,
(item, i) => item ? { onClick: onItemClick,
...item,
name: `${item.text}-${i}`,
onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null,
} : null
),
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
}, },
onClose(id) { onClose(id) {
dispatch(closeModal('ACTIONS')); dispatch(closeModal('ACTIONS'));
dispatch(closeDropdownMenu(id)); dispatch(closeDropdownMenu(id));

View file

@ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
icon: PropTypes.string, icon: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string, icon: PropTypes.string,
meta: PropTypes.node, meta: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
on: PropTypes.bool, text: PropTypes.string,
text: PropTypes.node,
})).isRequired, })).isRequired,
onModalOpen: PropTypes.func, onModalOpen: PropTypes.func,
onModalClose: PropTypes.func, onModalClose: PropTypes.func,
title: PropTypes.string, title: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
noModal: PropTypes.bool,
container: PropTypes.func, container: PropTypes.func,
renderItemContents: PropTypes.func,
closeOnChange: PropTypes.bool,
};
static defaultProps = {
closeOnChange: true,
}; };
state = { state = {
needsModalUpdate: false,
open: false, open: false,
openedViaKeyboard: undefined, openedViaKeyboard: undefined,
placement: 'bottom', placement: 'bottom',
@ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
// Toggles opening and closing the dropdown. // Toggles opening and closing the dropdown.
handleToggle = ({ target, type }) => { handleToggle = ({ target, type }) => {
const { onModalOpen, noModal } = this.props; const { onModalOpen } = this.props;
const { open } = this.state; const { open } = this.state;
if (!noModal && isUserTouching()) { if (isUserTouching()) {
if (this.state.open) { if (this.state.open) {
this.props.onModalClose(); this.props.onModalClose();
} else { } else {
@ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
this.setState({ open: false }); this.setState({ open: false });
} }
handleItemClick = (e) => {
const {
items,
onChange,
onModalClose,
closeOnChange,
} = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const { name } = items[i];
e.preventDefault(); // Prevents focus from changing
if (closeOnChange) onModalClose();
onChange(name);
};
// Creates an action modal object. // Creates an action modal object.
handleMakeModal = () => { handleMakeModal = () => {
const component = this;
const { const {
items, items,
onChange, onChange,
@ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
// The object. // The object.
return { return {
renderItemContents: this.props.renderItemContents,
onClick: this.handleItemClick,
actions: items.map( actions: items.map(
({ ({
name, name,
@ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
...rest, ...rest,
active: value && name === value, active: value && name === value,
name, name,
onClick (e) {
e.preventDefault(); // Prevents focus from changing
onModalClose();
onChange(name);
},
onPassiveClick (e) {
e.preventDefault(); // Prevents focus from changing
onChange(name);
component.setState({ needsModalUpdate: true });
},
}) })
), ),
}; };
} }
// If our modal is open and our props update, we need to also update
// the modal.
handleUpdate = () => {
const { onModalOpen } = this.props;
const { needsModalUpdate } = this.state;
// Gets our modal object.
const modal = this.handleMakeModal();
// Reopens the modal with the new object.
if (needsModalUpdate && modal && onModalOpen) {
onModalOpen(modal);
}
}
// Updates our modal as necessary.
componentDidUpdate (prevProps) {
const { items } = this.props;
const { needsModalUpdate } = this.state;
if (needsModalUpdate && items.find(
(item, i) => item.on !== prevProps.items[i].on
)) {
this.handleUpdate();
this.setState({ needsModalUpdate: false });
}
}
// Rendering. // Rendering.
render () { render () {
const { const {
@ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
onChange, onChange,
value, value,
container, container,
renderItemContents,
closeOnChange,
} = this.props; } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
const computedClass = classNames('composer--options--dropdown', { const computedClass = classNames('composer--options--dropdown', {
@ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
> >
<DropdownMenu <DropdownMenu
items={items} items={items}
renderItemContents={renderItemContents}
onChange={onChange} onChange={onChange}
onClose={this.handleClose} onClose={this.handleClose}
value={value} value={value}
openedViaKeyboard={this.state.openedViaKeyboard} openedViaKeyboard={this.state.openedViaKeyboard}
closeOnChange={closeOnChange}
/> />
</Overlay> </Overlay>
</div> </div>

View file

@ -2,7 +2,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import Toggle from 'react-toggle';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames'; import classNames from 'classnames';
@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
icon: PropTypes.string, icon: PropTypes.string,
meta: PropTypes.node, meta: PropTypes.node,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
on: PropTypes.bool,
text: PropTypes.node, text: PropTypes.node,
})), })),
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
style: PropTypes.object, style: PropTypes.object,
value: PropTypes.string, value: PropTypes.string,
renderItemContents: PropTypes.func,
openedViaKeyboard: PropTypes.bool, openedViaKeyboard: PropTypes.bool,
closeOnChange: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
style: {}, style: {},
closeOnChange: true,
}; };
state = { state = {
@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
document.removeEventListener('touchend', this.handleDocumentClick, withPassive); document.removeEventListener('touchend', this.handleDocumentClick, withPassive);
} }
handleClick = (name, e) => { handleClick = (e) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { const {
onChange, onChange,
onClose, onClose,
closeOnChange,
items, items,
} = this.props; } = this.props;
const { on } = this.props.items.find(item => item.name === name); const { name } = this.props.items[i];
e.preventDefault(); // Prevents change in focus on click e.preventDefault(); // Prevents change in focus on click
if ((on === null || typeof on === 'undefined')) { if (closeOnChange) {
onClose(); onClose();
} }
onChange(name); onChange(name);
@ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
} }
} }
handleKeyDown = (name, e) => { handleKeyDown = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
const { items } = this.props; const { items } = this.props;
const index = items.findIndex(item => {
return (item.name === name);
});
let element = null; let element = null;
switch(e.key) { switch(e.key) {
@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
if (element) { if (element) {
element.focus(); element.focus();
this.handleChange(element.getAttribute('data-index')); this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
this.focusedItem = c; this.focusedItem = c;
} }
renderItem = (item) => { renderItem = (item, i) => {
const { name, icon, meta, on, text } = item; const { name, icon, meta, text } = item;
const active = (name === (this.props.value || this.state.value)); const active = (name === (this.props.value || this.state.value));
const computedClass = classNames('composer--options--dropdown--content--item', { const computedClass = classNames('composer--options--dropdown--content--item', { active });
active,
lengthy: meta,
'toggled-off': !on && on !== null && typeof on !== 'undefined',
'toggled-on': on,
'with-icon': icon,
});
let prefix = null; let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
if (on !== null && typeof on !== 'undefined') { if (!contents) {
prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />; contents = (
} else if (icon) { <React.Fragment>
prefix = <Icon className='icon' fixedWidth id={icon} /> {icon && <Icon className='icon' fixedWidth id={icon} />}
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
</React.Fragment>
);
} }
return ( return (
<div <div
className={computedClass} className={computedClass}
onClick={this.handleClick.bind(this, name)} onClick={this.handleClick}
onKeyDown={this.handleKeyDown.bind(this, name)} onKeyDown={this.handleKeyDown}
role='option' role='option'
tabIndex='0' tabIndex='0'
key={name} key={name}
data-index={name} data-index={i}
ref={active ? this.setFocusRef : null} ref={active ? this.setFocusRef : null}
> >
{prefix} {contents}
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
</div> </div>
); );
} }
@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
}} }}
> >
{!!items && items.map(item => this.renderItem(item))} {!!items && items.map((item, i) => this.renderItem(item, i))}
</div> </div>
)} )}
</Motion> </Motion>

View file

@ -2,8 +2,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import Toggle from 'react-toggle';
import { connect } from 'react-redux';
// Components. // Components.
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/components/icon_button';
@ -80,6 +82,36 @@ const messages = defineMessages({
}, },
}); });
@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) }))
class ToggleOption extends ImmutablePureComponent {
static propTypes = {
name: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChangeAdvancedOption: PropTypes.func.isRequired,
};
handleChange = () => {
this.props.onChangeAdvancedOption(this.props.name);
};
render() {
const { meta, text, checked } = this.props;
return (
<React.Fragment>
<Toggle checked={checked} onChange={this.handleChange} />
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
</React.Fragment>
);
}
}
export default @injectIntl export default @injectIntl
class ComposerOptions extends ImmutablePureComponent { class ComposerOptions extends ImmutablePureComponent {
@ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent {
this.fileElement = fileElement; this.fileElement = fileElement;
} }
renderToggleItemContents = (item) => {
const { onChangeAdvancedOption } = this.props;
const { name, meta, text } = item;
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
};
// Rendering. // Rendering.
render () { render () {
const { const {
@ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent {
hasMedia, hasMedia,
allowPoll, allowPoll,
hasPoll, hasPoll,
intl,
onChangeAdvancedOption, onChangeAdvancedOption,
onChangeContentType, onChangeContentType,
onChangeVisibility, onChangeVisibility,
@ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent {
resetFileKey, resetFileKey,
spoiler, spoiler,
showContentTypeChoice, showContentTypeChoice,
intl: { formatMessage },
} = this.props; } = this.props;
const contentTypeItems = { const contentTypeItems = {
plain: { plain: {
icon: 'file-text', icon: 'file-text',
name: 'text/plain', name: 'text/plain',
text: <FormattedMessage {...messages.plain} />, text: formatMessage(messages.plain),
}, },
html: { html: {
icon: 'code', icon: 'code',
name: 'text/html', name: 'text/html',
text: <FormattedMessage {...messages.html} />, text: formatMessage(messages.html),
}, },
markdown: { markdown: {
icon: 'arrow-circle-down', icon: 'arrow-circle-down',
name: 'text/markdown', name: 'text/markdown',
text: <FormattedMessage {...messages.markdown} />, text: formatMessage(messages.markdown),
}, },
}; };
@ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent {
{ {
icon: 'cloud-upload', icon: 'cloud-upload',
name: 'upload', name: 'upload',
text: <FormattedMessage {...messages.upload} />, text: formatMessage(messages.upload),
}, },
{ {
icon: 'paint-brush', icon: 'paint-brush',
name: 'doodle', name: 'doodle',
text: <FormattedMessage {...messages.doodle} />, text: formatMessage(messages.doodle),
}, },
]} ]}
onChange={this.handleClickAttach} onChange={this.handleClickAttach}
onModalClose={onModalClose} onModalClose={onModalClose}
onModalOpen={onModalOpen} onModalOpen={onModalOpen}
title={intl.formatMessage(messages.attach)} title={formatMessage(messages.attach)}
/> />
{!!pollLimits && ( {!!pollLimits && (
<IconButton <IconButton
@ -229,7 +268,7 @@ class ComposerOptions extends ImmutablePureComponent {
height: null, height: null,
lineHeight: null, lineHeight: null,
}} }}
title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
/> />
)} )}
<hr /> <hr />
@ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent {
onChange={onChangeContentType} onChange={onChangeContentType}
onModalClose={onModalClose} onModalClose={onModalClose}
onModalOpen={onModalOpen} onModalOpen={onModalOpen}
title={intl.formatMessage(messages.content_type)} title={formatMessage(messages.content_type)}
value={contentType} value={contentType}
/> />
)} )}
@ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent {
ariaControls='glitch.composer.spoiler.input' ariaControls='glitch.composer.spoiler.input'
label='CW' label='CW'
onClick={onToggleSpoiler} onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)} title={formatMessage(messages.spoiler)}
/> />
)} )}
<Dropdown <Dropdown
@ -271,22 +310,22 @@ class ComposerOptions extends ImmutablePureComponent {
icon='ellipsis-h' icon='ellipsis-h'
items={advancedOptions ? [ items={advancedOptions ? [
{ {
meta: <FormattedMessage {...messages.local_only_long} />, meta: formatMessage(messages.local_only_long),
name: 'do_not_federate', name: 'do_not_federate',
on: advancedOptions.get('do_not_federate'), text: formatMessage(messages.local_only_short),
text: <FormattedMessage {...messages.local_only_short} />,
}, },
{ {
meta: <FormattedMessage {...messages.threaded_mode_long} />, meta: formatMessage(messages.threaded_mode_long),
name: 'threaded_mode', name: 'threaded_mode',
on: advancedOptions.get('threaded_mode'), text: formatMessage(messages.threaded_mode_short),
text: <FormattedMessage {...messages.threaded_mode_short} />,
}, },
] : null} ] : null}
onChange={onChangeAdvancedOption} onChange={onChangeAdvancedOption}
renderItemContents={this.renderToggleItemContents}
onModalClose={onModalClose} onModalClose={onModalClose}
onModalOpen={onModalOpen} onModalOpen={onModalOpen}
title={intl.formatMessage(messages.advanced_options_icon_title)} title={formatMessage(messages.advanced_options_icon_title)}
closeOnChange={false}
/> />
</div> </div>
); );

View file

@ -1,46 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Dropdown from './dropdown'; import Dropdown from './dropdown';
const messages = defineMessages({ const messages = defineMessages({
change_privacy: { public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
defaultMessage: 'Adjust status privacy', public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
id: 'privacy.change', unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
}, unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
direct_long: { private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
defaultMessage: 'Visible for mentioned users only', private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
id: 'privacy.direct.long', direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
}, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
direct_short: { change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
defaultMessage: 'Direct',
id: 'privacy.direct.short',
},
private_long: {
defaultMessage: 'Visible for followers only',
id: 'privacy.private.long',
},
private_short: {
defaultMessage: 'Followers-only',
id: 'privacy.private.short',
},
public_long: {
defaultMessage: 'Visible for all, shown in public timelines',
id: 'privacy.public.long',
},
public_short: {
defaultMessage: 'Public',
id: 'privacy.public.short',
},
unlisted_long: {
defaultMessage: 'Visible for all, but not in public timelines',
id: 'privacy.unlisted.long',
},
unlisted_short: {
defaultMessage: 'Unlisted',
id: 'privacy.unlisted.short',
},
}); });
export default @injectIntl export default @injectIntl
@ -53,40 +26,39 @@ class PrivacyDropdown extends React.PureComponent {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool, noDirect: PropTypes.bool,
noModal: PropTypes.bool,
container: PropTypes.func, container: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
render () { render () {
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props; const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props;
// We predefine our privacy items so that we can easily pick the // We predefine our privacy items so that we can easily pick the
// dropdown icon later. // dropdown icon later.
const privacyItems = { const privacyItems = {
direct: { direct: {
icon: 'envelope', icon: 'envelope',
meta: <FormattedMessage {...messages.direct_long} />, meta: formatMessage(messages.direct_long),
name: 'direct', name: 'direct',
text: <FormattedMessage {...messages.direct_short} />, text: formatMessage(messages.direct_short),
}, },
private: { private: {
icon: 'lock', icon: 'lock',
meta: <FormattedMessage {...messages.private_long} />, meta: formatMessage(messages.private_long),
name: 'private', name: 'private',
text: <FormattedMessage {...messages.private_short} />, text: formatMessage(messages.private_short),
}, },
public: { public: {
icon: 'globe', icon: 'globe',
meta: <FormattedMessage {...messages.public_long} />, meta: formatMessage(messages.public_long),
name: 'public', name: 'public',
text: <FormattedMessage {...messages.public_short} />, text: formatMessage(messages.public_short),
}, },
unlisted: { unlisted: {
icon: 'unlock', icon: 'unlock',
meta: <FormattedMessage {...messages.unlisted_long} />, meta: formatMessage(messages.unlisted_long),
name: 'unlisted', name: 'unlisted',
text: <FormattedMessage {...messages.unlisted_short} />, text: formatMessage(messages.unlisted_short),
}, },
}; };
@ -104,9 +76,8 @@ class PrivacyDropdown extends React.PureComponent {
onChange={onChange} onChange={onChange}
onModalClose={onModalClose} onModalClose={onModalClose}
onModalOpen={onModalOpen} onModalOpen={onModalOpen}
title={intl.formatMessage(messages.change_privacy)} title={formatMessage(messages.change_privacy)}
container={container} container={container}
noModal={noModal}
value={value} value={value}
/> />
); );

View file

@ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name'; import DisplayName from 'flavours/glitch/components/display_name';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon'; import IconButton from 'flavours/glitch/components/icon_button';
import Link from 'flavours/glitch/components/link';
import Toggle from 'react-toggle';
export default class ActionsModal extends ImmutablePureComponent { export default class ActionsModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onClick: PropTypes.func,
actions: PropTypes.arrayOf(PropTypes.shape({ actions: PropTypes.arrayOf(PropTypes.shape({
active: PropTypes.bool, active: PropTypes.bool,
href: PropTypes.string, href: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
meta: PropTypes.node, meta: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
on: PropTypes.bool, text: PropTypes.string,
onPassiveClick: PropTypes.func,
text: PropTypes.node,
})), })),
renderItemContents: PropTypes.func,
}; };
renderAction = (action, i) => { renderAction = (action, i) => {
@ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />; return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
} }
const { const { icon = null, text, meta = null, active = false, href = '#' } = action;
active, let contents = this.props.renderItemContents && this.props.renderItemContents(action, i);
href,
icon, if (!contents) {
meta, contents = (
name, <React.Fragment>
on, {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
onClick, <div>
onPassiveClick, <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
text, <div>{meta}</div>
} = action; </div>
</React.Fragment>
);
}
return ( return (
<li key={name || i}> <li key={`${text}-${i}`}>
<Link <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}>
className={classNames('link', { active })} {contents}
href={href} </a>
onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
role={onClick ? 'button' : null}
>
{function () {
// We render a `<Toggle>` if we were provided an `on`
// property, and otherwise show an `<Icon>` if available.
switch (true) {
case on !== null && typeof on !== 'undefined':
return (
<Toggle
checked={on}
onChange={onPassiveClick || onClick}
/>
);
case !!icon:
return (
<Icon
className='icon'
fixedWidth
id={icon}
/>
);
default:
return null;
}
}()}
{meta ? (
<div>
<strong>{text}</strong>
{meta}
</div>
) : <div>{text}</div>}
</Link>
</li> </li>
); );
} }