about summary refs log tree commit diff
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2022-02-09 17:25:57 +0100
committerGitHub <noreply@github.com>2022-02-09 17:25:57 +0100
commit8987ea4d6b236657b8ea97d619902668768ae8ff (patch)
treed1be73fd2f80572f4b54857c51bae95327314e40
parent2fd1db7c9d0fe8c1cca159b9b0818c72e7c318aa (diff)
parent0bb3d445ab0167ac33bd804333c7f9741c7526ae (diff)
Merge pull request #1682 from ClearlyClaire/glitch-soc/fixes/dropdowns-modals
Refactor and fix dropdown/action dialog
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js11
-rw-r--r--app/javascript/flavours/glitch/containers/dropdown_menu_container.js10
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown.js78
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js66
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js75
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js69
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/actions_modal.js79
7 files changed, 174 insertions, 214 deletions
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index 023fecb9a..a4d0dfc50 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -116,7 +116,7 @@ class DropdownMenu extends React.PureComponent {
 
     if (typeof action === 'function') {
       e.preventDefault();
-      action();
+      action(e);
     } else if (to) {
       e.preventDefault();
       this.context.router.history.push(to);
@@ -128,11 +128,11 @@ class DropdownMenu extends React.PureComponent {
       return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
     }
 
-    const { text, href = '#' } = option;
+    const { text, href = '#', target = '_blank', method } = option;
 
     return (
       <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}
         </a>
       </li>
@@ -149,7 +149,7 @@ class DropdownMenu extends React.PureComponent {
           // It should not be transformed when mounting because the resulting
           // size will be used to determine the coordinate of the menu by
           // 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 }} />
 
             <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];
 
     this.handleClose();
diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
index 1c0385b74..0c4a2b50f 100644
--- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
+++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
@@ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
   onOpen(id, onItemClick, dropdownPlacement, keyboard) {
     dispatch(isUserTouching() ? openModal('ACTIONS', {
       status,
-      actions: items.map(
-        (item, i) => item ? {
-          ...item,
-          name: `${item.text}-${i}`,
-          onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null,
-        } : null
-      ),
+      actions: items,
+      onClick: onItemClick,
     }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
   },
+
   onClose(id) {
     dispatch(closeModal('ACTIONS'));
     dispatch(closeDropdownMenu(id));
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
index abf7cbba1..9f70d6b79 100644
--- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
@@ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
     icon: PropTypes.string,
     items: PropTypes.arrayOf(PropTypes.shape({
       icon: PropTypes.string,
-      meta: PropTypes.node,
+      meta: PropTypes.string,
       name: PropTypes.string.isRequired,
-      on: PropTypes.bool,
-      text: PropTypes.node,
+      text: PropTypes.string,
     })).isRequired,
     onModalOpen: PropTypes.func,
     onModalClose: PropTypes.func,
     title: PropTypes.string,
     value: PropTypes.string,
     onChange: PropTypes.func,
-    noModal: PropTypes.bool,
     container: PropTypes.func,
+    renderItemContents: PropTypes.func,
+    closeOnChange: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    closeOnChange: true,
   };
 
   state = {
-    needsModalUpdate: false,
     open: false,
     openedViaKeyboard: undefined,
     placement: 'bottom',
@@ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
 
   //  Toggles opening and closing the dropdown.
   handleToggle = ({ target, type }) => {
-    const { onModalOpen, noModal } = this.props;
+    const { onModalOpen } = this.props;
     const { open } = this.state;
 
-    if (!noModal && isUserTouching()) {
+    if (isUserTouching()) {
       if (this.state.open) {
         this.props.onModalClose();
       } else {
@@ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
     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.
   handleMakeModal = () => {
-    const component = this;
     const {
       items,
       onChange,
@@ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
 
     //  The object.
     return {
+      renderItemContents: this.props.renderItemContents,
+      onClick: this.handleItemClick,
       actions: items.map(
         ({
           name,
@@ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
           ...rest,
           active: value && name === value,
           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.
   render () {
     const {
@@ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
       onChange,
       value,
       container,
+      renderItemContents,
+      closeOnChange,
     } = this.props;
     const { open, placement } = this.state;
     const computedClass = classNames('composer--options--dropdown', {
@@ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
         >
           <DropdownMenu
             items={items}
+            renderItemContents={renderItemContents}
             onChange={onChange}
             onClose={this.handleClose}
             value={value}
             openedViaKeyboard={this.state.openedViaKeyboard}
+            closeOnChange={closeOnChange}
           />
         </Overlay>
       </div>
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
index bee06e64c..0649fe1ca 100644
--- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
@@ -2,7 +2,6 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import spring from 'react-motion/lib/spring';
-import Toggle from 'react-toggle';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import classNames from 'classnames';
 
@@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
       icon: PropTypes.string,
       meta: PropTypes.node,
       name: PropTypes.string.isRequired,
-      on: PropTypes.bool,
       text: PropTypes.node,
     })),
     onChange: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     style: PropTypes.object,
     value: PropTypes.string,
+    renderItemContents: PropTypes.func,
     openedViaKeyboard: PropTypes.bool,
+    closeOnChange: PropTypes.bool,
   };
 
   static defaultProps = {
     style: {},
+    closeOnChange: true,
   };
 
   state = {
@@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
     document.removeEventListener('touchend', this.handleDocumentClick, withPassive);
   }
 
-  handleClick = (name, e) => {
+  handleClick = (e) => {
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+
     const {
       onChange,
       onClose,
+      closeOnChange,
       items,
     } = 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
-    if ((on === null || typeof on === 'undefined')) {
+    if (closeOnChange) {
       onClose();
     }
     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 index = items.findIndex(item => {
-      return (item.name === name);
-    });
     let element = null;
 
     switch(e.key) {
@@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
 
     if (element) {
       element.focus();
-      this.handleChange(element.getAttribute('data-index'));
+      this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name);
       e.preventDefault();
       e.stopPropagation();
     }
@@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
     this.focusedItem = c;
   }
 
-  renderItem = (item) => {
-    const { name, icon, meta, on, text } = item;
+  renderItem = (item, i) => {
+    const { name, icon, meta, text } = item;
 
     const active = (name === (this.props.value || this.state.value));
 
-    const computedClass = classNames('composer--options--dropdown--content--item', {
-      active,
-      lengthy: meta,
-      'toggled-off': !on && on !== null && typeof on !== 'undefined',
-      'toggled-on': on,
-      'with-icon': icon,
-    });
+    const computedClass = classNames('composer--options--dropdown--content--item', { active });
+
+    let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
 
-    let prefix = null;
+    if (!contents) {
+      contents = (
+        <React.Fragment>
+          {icon && <Icon className='icon' fixedWidth id={icon} />}
 
-    if (on !== null && typeof on !== 'undefined') {
-      prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />;
-    } else if (icon) {
-      prefix = <Icon className='icon' fixedWidth id={icon} />
+          <div className='content'>
+            <strong>{text}</strong>
+            {meta}
+          </div>
+        </React.Fragment>
+      );
     }
 
     return (
       <div
         className={computedClass}
-        onClick={this.handleClick.bind(this, name)}
-        onKeyDown={this.handleKeyDown.bind(this, name)}
+        onClick={this.handleClick}
+        onKeyDown={this.handleKeyDown}
         role='option'
         tabIndex='0'
         key={name}
-        data-index={name}
+        data-index={i}
         ref={active ? this.setFocusRef : null}
       >
-        {prefix}
-
-        <div className='content'>
-          <strong>{text}</strong>
-          {meta}
-        </div>
+        {contents}
       </div>
     );
   }
@@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
               transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
             }}
           >
-            {!!items && items.map(item => this.renderItem(item))}
+            {!!items && items.map((item, i) => this.renderItem(item, i))}
           </div>
         )}
       </Motion>
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index f9212bbae..085da18ea 100644
--- a/app/javascript/flavours/glitch/features/compose/components/options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/options.js
@@ -2,8 +2,10 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 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 Toggle from 'react-toggle';
+import { connect } from 'react-redux';
 
 //  Components.
 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
 class ComposerOptions extends ImmutablePureComponent {
 
@@ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent {
     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.
   render () {
     const {
@@ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent {
       hasMedia,
       allowPoll,
       hasPoll,
-      intl,
       onChangeAdvancedOption,
       onChangeContentType,
       onChangeVisibility,
@@ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent {
       resetFileKey,
       spoiler,
       showContentTypeChoice,
+      intl: { formatMessage },
     } = this.props;
 
     const contentTypeItems = {
       plain: {
         icon: 'file-text',
         name: 'text/plain',
-        text: <FormattedMessage {...messages.plain} />,
+        text: formatMessage(messages.plain),
       },
       html: {
         icon: 'code',
         name: 'text/html',
-        text: <FormattedMessage {...messages.html} />,
+        text: formatMessage(messages.html),
       },
       markdown: {
         icon: 'arrow-circle-down',
         name: 'text/markdown',
-        text: <FormattedMessage {...messages.markdown} />,
+        text: formatMessage(messages.markdown),
       },
     };
 
@@ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent {
             {
               icon: 'cloud-upload',
               name: 'upload',
-              text: <FormattedMessage {...messages.upload} />,
+              text: formatMessage(messages.upload),
             },
             {
               icon: 'paint-brush',
               name: 'doodle',
-              text: <FormattedMessage {...messages.doodle} />,
+              text: formatMessage(messages.doodle),
             },
           ]}
           onChange={this.handleClickAttach}
           onModalClose={onModalClose}
           onModalOpen={onModalOpen}
-          title={intl.formatMessage(messages.attach)}
+          title={formatMessage(messages.attach)}
         />
         {!!pollLimits && (
           <IconButton
@@ -229,7 +268,7 @@ class ComposerOptions extends ImmutablePureComponent {
               height: null,
               lineHeight: null,
             }}
-            title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
+            title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
           />
         )}
         <hr />
@@ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent {
             onChange={onChangeContentType}
             onModalClose={onModalClose}
             onModalOpen={onModalOpen}
-            title={intl.formatMessage(messages.content_type)}
+            title={formatMessage(messages.content_type)}
             value={contentType}
           />
         )}
@@ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent {
             ariaControls='glitch.composer.spoiler.input'
             label='CW'
             onClick={onToggleSpoiler}
-            title={intl.formatMessage(messages.spoiler)}
+            title={formatMessage(messages.spoiler)}
           />
         )}
         <Dropdown
@@ -271,22 +310,22 @@ class ComposerOptions extends ImmutablePureComponent {
           icon='ellipsis-h'
           items={advancedOptions ? [
             {
-              meta: <FormattedMessage {...messages.local_only_long} />,
+              meta: formatMessage(messages.local_only_long),
               name: 'do_not_federate',
-              on: advancedOptions.get('do_not_federate'),
-              text: <FormattedMessage {...messages.local_only_short} />,
+              text: formatMessage(messages.local_only_short),
             },
             {
-              meta: <FormattedMessage {...messages.threaded_mode_long} />,
+              meta: formatMessage(messages.threaded_mode_long),
               name: 'threaded_mode',
-              on: advancedOptions.get('threaded_mode'),
-              text: <FormattedMessage {...messages.threaded_mode_short} />,
+              text: formatMessage(messages.threaded_mode_short),
             },
           ] : null}
           onChange={onChangeAdvancedOption}
+          renderItemContents={this.renderToggleItemContents}
           onModalClose={onModalClose}
           onModalOpen={onModalOpen}
-          title={intl.formatMessage(messages.advanced_options_icon_title)}
+          title={formatMessage(messages.advanced_options_icon_title)}
+          closeOnChange={false}
         />
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
index 39f7c7bd1..3bf25b3a4 100644
--- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
@@ -1,46 +1,19 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import Dropdown from './dropdown';
 
 const messages = defineMessages({
-  change_privacy: {
-    defaultMessage: 'Adjust status privacy',
-    id: 'privacy.change',
-  },
-  direct_long: {
-    defaultMessage: 'Visible for mentioned users only',
-    id: 'privacy.direct.long',
-  },
-  direct_short: {
-    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',
-  },
+  public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
+  public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
+  unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
+  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
+  private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
+  private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
+  direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
+  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
+  change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
 });
 
 export default @injectIntl
@@ -53,40 +26,39 @@ class PrivacyDropdown extends React.PureComponent {
     value: PropTypes.string.isRequired,
     onChange: PropTypes.func.isRequired,
     noDirect: PropTypes.bool,
-    noModal: PropTypes.bool,
     container: PropTypes.func,
     intl: PropTypes.object.isRequired,
   };
 
   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
     //  dropdown icon later.
     const privacyItems = {
       direct: {
         icon: 'envelope',
-        meta: <FormattedMessage {...messages.direct_long} />,
+        meta: formatMessage(messages.direct_long),
         name: 'direct',
-        text: <FormattedMessage {...messages.direct_short} />,
+        text: formatMessage(messages.direct_short),
       },
       private: {
         icon: 'lock',
-        meta: <FormattedMessage {...messages.private_long} />,
+        meta: formatMessage(messages.private_long),
         name: 'private',
-        text: <FormattedMessage {...messages.private_short} />,
+        text: formatMessage(messages.private_short),
       },
       public: {
         icon: 'globe',
-        meta: <FormattedMessage {...messages.public_long} />,
+        meta: formatMessage(messages.public_long),
         name: 'public',
-        text: <FormattedMessage {...messages.public_short} />,
+        text: formatMessage(messages.public_short),
       },
       unlisted: {
         icon: 'unlock',
-        meta: <FormattedMessage {...messages.unlisted_long} />,
+        meta: formatMessage(messages.unlisted_long),
         name: 'unlisted',
-        text: <FormattedMessage {...messages.unlisted_short} />,
+        text: formatMessage(messages.unlisted_short),
       },
     };
 
@@ -104,9 +76,8 @@ class PrivacyDropdown extends React.PureComponent {
         onChange={onChange}
         onModalClose={onModalClose}
         onModalOpen={onModalOpen}
-        title={intl.formatMessage(messages.change_privacy)}
+        title={formatMessage(messages.change_privacy)}
         container={container}
-        noModal={noModal}
         value={value}
       />
     );
diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
index 24169036c..aae2e4426 100644
--- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
@@ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar';
 import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 import DisplayName from 'flavours/glitch/components/display_name';
 import classNames from 'classnames';
-import Icon from 'flavours/glitch/components/icon';
-import Link from 'flavours/glitch/components/link';
-import Toggle from 'react-toggle';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 export default class ActionsModal extends ImmutablePureComponent {
 
   static propTypes = {
     status: ImmutablePropTypes.map,
+    onClick: PropTypes.func,
     actions: PropTypes.arrayOf(PropTypes.shape({
       active: PropTypes.bool,
       href: PropTypes.string,
       icon: PropTypes.string,
-      meta: PropTypes.node,
+      meta: PropTypes.string,
       name: PropTypes.string,
-      on: PropTypes.bool,
-      onPassiveClick: PropTypes.func,
-      text: PropTypes.node,
+      text: PropTypes.string,
     })),
+    renderItemContents: PropTypes.func,
   };
 
   renderAction = (action, i) => {
@@ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent {
       return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
     }
 
-    const {
-      active,
-      href,
-      icon,
-      meta,
-      name,
-      on,
-      onClick,
-      onPassiveClick,
-      text,
-    } = action;
+    const { icon = null, text, meta = null, active = false, href = '#' } = action;
+    let contents = this.props.renderItemContents && this.props.renderItemContents(action, i);
 
-    return (
-      <li key={name || i}>
-        <Link
-          className={classNames('link', { active })}
-          href={href}
-          onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
-          role={onClick ? 'button' : null}
-        >
-          {function () {
+    if (!contents) {
+      contents = (
+        <React.Fragment>
+          {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
+          <div>
+            <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
+            <div>{meta}</div>
+          </div>
+        </React.Fragment>
+      );
+    }
 
-            //  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>
+    return (
+      <li key={`${text}-${i}`}>
+        <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}>
+          {contents}
+        </a>
       </li>
     );
   }