about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/compose/components
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2022-02-13 22:15:26 -0600
committerStarfall <us@starfall.systems>2022-02-13 22:15:26 -0600
commitc0341f06be5310a00b85a5d48fa80891d47c6710 (patch)
tree907ef7f787f8bd446a6d9be1448a8bcff74e5a08 /app/javascript/flavours/glitch/features/compose/components
parent169688aa9f2a69ac3d36332c833e9cad43b5f7a5 (diff)
parent6f78c66fe01921a4e7e01aa6e2386a5fce7f3afd (diff)
Merge remote-tracking branch 'glitch/main'
Not at all sure where the admin UI is going to display English language
names now but OK.
Diffstat (limited to 'app/javascript/flavours/glitch/features/compose/components')
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js4
-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/header.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js81
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js70
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/publisher.js70
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload.js5
8 files changed, 178 insertions, 198 deletions
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index 5dfc119c1..c75906ce7 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -47,6 +47,7 @@ class ComposeForm extends ImmutablePureComponent {
     preselectDate: PropTypes.instanceOf(Date),
     isSubmitting: PropTypes.bool,
     isChangingUpload: PropTypes.bool,
+    isEditing: PropTypes.bool,
     isUploading: PropTypes.bool,
     onChange: PropTypes.func,
     onSubmit: PropTypes.func,
@@ -293,6 +294,7 @@ class ComposeForm extends ImmutablePureComponent {
       spoilerText,
       suggestions,
       spoilersAlwaysOn,
+      isEditing,
     } = this.props;
 
     const countText = this.getFulltextForCharacterCounting();
@@ -353,6 +355,7 @@ class ComposeForm extends ImmutablePureComponent {
             onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
             onUpload={onPaste}
             privacy={privacy}
+            isEditing={isEditing}
             sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
             spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
           />
@@ -364,6 +367,7 @@ class ComposeForm extends ImmutablePureComponent {
         <Publisher
           countText={countText}
           disabled={!this.canSubmit()}
+          isEditing={isEditing}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
           privacy={privacy}
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/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js
index c6e4cc36b..95add2027 100644
--- a/app/javascript/flavours/glitch/features/compose/components/header.js
+++ b/app/javascript/flavours/glitch/features/compose/components/header.js
@@ -119,7 +119,7 @@ class Header extends ImmutablePureComponent {
         <a
           aria-label={intl.formatMessage(messages.settings)}
           onClick={onSettingsClick}
-          href='#'
+          href='/settings/preferences'
           title={intl.formatMessage(messages.settings)}
         ><Icon id='cogs' /></a>
         <a
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index f9212bbae..3a31e214d 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 {
 
@@ -106,6 +138,7 @@ class ComposerOptions extends ImmutablePureComponent {
     resetFileKey: PropTypes.number,
     spoiler: PropTypes.bool,
     showContentTypeChoice: PropTypes.bool,
+    isEditing: PropTypes.bool,
   };
 
   //  Handles file selection.
@@ -141,6 +174,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 +192,6 @@ class ComposerOptions extends ImmutablePureComponent {
       hasMedia,
       allowPoll,
       hasPoll,
-      intl,
       onChangeAdvancedOption,
       onChangeContentType,
       onChangeVisibility,
@@ -164,23 +203,25 @@ class ComposerOptions extends ImmutablePureComponent {
       resetFileKey,
       spoiler,
       showContentTypeChoice,
+      isEditing,
+      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 +245,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,12 +270,12 @@ 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 />
         <PrivacyDropdown
-          disabled={disabled}
+          disabled={disabled || isEditing}
           onChange={onChangeVisibility}
           onModalClose={onModalClose}
           onModalOpen={onModalOpen}
@@ -252,7 +293,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,31 +303,31 @@ class ComposerOptions extends ImmutablePureComponent {
             ariaControls='glitch.composer.spoiler.input'
             label='CW'
             onClick={onToggleSpoiler}
-            title={intl.formatMessage(messages.spoiler)}
+            title={formatMessage(messages.spoiler)}
           />
         )}
         <Dropdown
           active={advancedOptions && advancedOptions.some(value => !!value)}
-          disabled={disabled}
+          disabled={disabled || isEditing}
           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..c8e783d22 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,40 @@ class PrivacyDropdown extends React.PureComponent {
     value: PropTypes.string.isRequired,
     onChange: PropTypes.func.isRequired,
     noDirect: PropTypes.bool,
-    noModal: PropTypes.bool,
     container: PropTypes.func,
+    disabled: PropTypes.bool,
     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 +77,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/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js
index 1531dcaa9..9a8c0f510 100644
--- a/app/javascript/flavours/glitch/features/compose/components/publisher.js
+++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js
@@ -2,7 +2,7 @@
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React from 'react';
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import { length } from 'stringz';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
@@ -23,6 +23,7 @@ const messages = defineMessages({
     defaultMessage: '{publish}!',
     id: 'compose_form.publish_loud',
   },
+  saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
 });
 
 export default @injectIntl
@@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent {
     onSubmit: PropTypes.func,
     privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
     sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
+    isEditing: PropTypes.bool,
   };
 
   handleSubmit = () => {
@@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent {
   };
 
   render () {
-    const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
+    const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
 
     const diff = maxChars - length(countText || '');
     const computedClass = classNames('composer--publisher', {
@@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent {
       over: diff < 0,
     });
 
+    const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
+
+    let publishText;
+    if (isEditing) {
+      publishText = intl.formatMessage(messages.saveChanges);
+    } else if (privacy === 'private' || privacy === 'direct') {
+      const iconId = privacyIcons[privacy];
+      publishText = (
+        <span>
+          <Icon id={iconId} /> {intl.formatMessage(messages.publish)}
+        </span>
+      );
+    } else {
+      publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
+    }
+
     return (
       <div className={computedClass}>
-        {sideArm && sideArm !== 'none' ? (
+        {sideArm && !isEditing && sideArm !== 'none' ? (
           <Button
             className='side_arm'
             disabled={disabled}
             onClick={onSecondarySubmit}
             style={{ padding: null }}
-            text={
-              <span>
-                <Icon
-                  id={{
-                    public: 'globe',
-                    unlisted: 'unlock',
-                    private: 'lock',
-                    direct: 'envelope',
-                  }[sideArm]}
-                />
-              </span>
-            }
+            text={<Icon id={privacyIcons[sideArm]} />}
             title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
           />
         ) : null}
         <Button
           className='primary'
-          text={function () {
-            switch (true) {
-            case !!sideArm && sideArm !== 'none':
-            case privacy === 'direct':
-            case privacy === 'private':
-              return (
-                <span>
-                  <Icon
-                    id={{
-                      direct: 'envelope',
-                      private: 'lock',
-                      public: 'globe',
-                      unlisted: 'unlock',
-                    }[privacy]}
-                  />
-                  {' '}
-                  <FormattedMessage {...messages.publish} />
-                </span>
-              );
-            case privacy === 'public':
-              return (
-                <span>
-                  <FormattedMessage
-                    {...messages.publishLoud}
-                    values={{ publish: <FormattedMessage {...messages.publish} /> }}
-                  />
-                </span>
-              );
-            default:
-              return <span><FormattedMessage {...messages.publish} /></span>;
-            }
-          }()}
+          text={publishText}
           title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
           onClick={this.handleSubmit}
           disabled={disabled}
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js
index 425b0fe5e..338bfca37 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.js
@@ -19,6 +19,7 @@ export default class Upload extends ImmutablePureComponent {
     media: ImmutablePropTypes.map.isRequired,
     onUndo: PropTypes.func.isRequired,
     onOpenFocalPoint: PropTypes.func.isRequired,
+    isEditingStatus: PropTypes.func.isRequired,
   };
 
   handleUndoClick = e => {
@@ -32,7 +33,7 @@ export default class Upload extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, media } = this.props;
+    const { intl, media, isEditingStatus } = this.props;
     const focusX = media.getIn(['meta', 'focus', 'x']);
     const focusY = media.getIn(['meta', 'focus', 'y']);
     const x = ((focusX /  2) + .5) * 100;
@@ -45,7 +46,7 @@ export default class Upload extends ImmutablePureComponent {
             <div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
               <div className={classNames('composer--upload_form--actions', { active: true })}>
                 <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
-                <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
+                {!isEditingStatus && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
               </div>
             </div>
           )}