Add reminder when about to post without alt text in web UI (#33760)
This commit is contained in:
		@@ -10,6 +10,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
 | 
			
		||||
import { missingAltTextModal } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
import AutosuggestInput from '../../../components/autosuggest_input';
 | 
			
		||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
 | 
			
		||||
import { Button } from '../../../components/button';
 | 
			
		||||
@@ -65,6 +67,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
			
		||||
    autoFocus: PropTypes.bool,
 | 
			
		||||
    withoutNavigation: PropTypes.bool,
 | 
			
		||||
    anyMedia: PropTypes.bool,
 | 
			
		||||
    missingAltText: PropTypes.bool,
 | 
			
		||||
    isInReply: PropTypes.bool,
 | 
			
		||||
    singleColumn: PropTypes.bool,
 | 
			
		||||
    lang: PropTypes.string,
 | 
			
		||||
@@ -117,7 +120,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.props.onSubmit();
 | 
			
		||||
    this.props.onSubmit(missingAltTextModal && this.props.missingAltText);
 | 
			
		||||
 | 
			
		||||
    if (e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,9 @@ import {
 | 
			
		||||
  changeComposeSpoilerText,
 | 
			
		||||
  insertEmojiCompose,
 | 
			
		||||
  uploadCompose,
 | 
			
		||||
} from '../../../actions/compose';
 | 
			
		||||
} from 'mastodon/actions/compose';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
 | 
			
		||||
import ComposeForm from '../components/compose_form';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
@@ -26,6 +28,7 @@ const mapStateToProps = state => ({
 | 
			
		||||
  isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
 | 
			
		||||
  isUploading: state.getIn(['compose', 'is_uploading']),
 | 
			
		||||
  anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
 | 
			
		||||
  missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
 | 
			
		||||
  isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
 | 
			
		||||
  lang: state.getIn(['compose', 'language']),
 | 
			
		||||
  maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
 | 
			
		||||
@@ -37,8 +40,15 @@ const mapDispatchToProps = (dispatch) => ({
 | 
			
		||||
    dispatch(changeCompose(text));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onSubmit () {
 | 
			
		||||
    dispatch(submitCompose());
 | 
			
		||||
  onSubmit (missingAltText) {
 | 
			
		||||
    if (missingAltText) {
 | 
			
		||||
      dispatch(openModal({
 | 
			
		||||
        modalType: 'CONFIRM_MISSING_ALT_TEXT',
 | 
			
		||||
        modalProps: {},
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(submitCompose());
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onClearSuggestions () {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,14 +56,6 @@ export const ConfirmationModal: React.FC<
 | 
			
		||||
 | 
			
		||||
      <div className='safety-action-modal__bottom'>
 | 
			
		||||
        <div className='safety-action-modal__actions'>
 | 
			
		||||
          {secondary && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Button onClick={handleSecondary}>{secondary}</Button>
 | 
			
		||||
 | 
			
		||||
              <div className='spacer' />
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <button onClick={handleCancel} className='link-button'>
 | 
			
		||||
            <FormattedMessage
 | 
			
		||||
              id='confirmation_modal.cancel'
 | 
			
		||||
@@ -71,6 +63,15 @@ export const ConfirmationModal: React.FC<
 | 
			
		||||
            />
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          {secondary && (
 | 
			
		||||
            <>
 | 
			
		||||
              <div className='spacer' />
 | 
			
		||||
              <button onClick={handleSecondary} className='link-button'>
 | 
			
		||||
                {secondary}
 | 
			
		||||
              </button>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
 | 
			
		||||
          <Button onClick={handleClick} autoFocus>
 | 
			
		||||
            {confirm}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,3 +7,4 @@ export { ConfirmUnfollowModal } from './unfollow';
 | 
			
		||||
export { ConfirmClearNotificationsModal } from './clear_notifications';
 | 
			
		||||
export { ConfirmLogOutModal } from './log_out';
 | 
			
		||||
export { ConfirmFollowToListModal } from './follow_to_list';
 | 
			
		||||
export { ConfirmMissingAltTextModal } from './missing_alt_text';
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, useIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import { submitCompose } from 'mastodon/actions/compose';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import type { BaseConfirmationModalProps } from './confirmation_modal';
 | 
			
		||||
import { ConfirmationModal } from './confirmation_modal';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  title: {
 | 
			
		||||
    id: 'confirmations.missing_alt_text.title',
 | 
			
		||||
    defaultMessage: 'Add alt text?',
 | 
			
		||||
  },
 | 
			
		||||
  confirm: {
 | 
			
		||||
    id: 'confirmations.missing_alt_text.confirm',
 | 
			
		||||
    defaultMessage: 'Add alt text',
 | 
			
		||||
  },
 | 
			
		||||
  message: {
 | 
			
		||||
    id: 'confirmations.missing_alt_text.message',
 | 
			
		||||
    defaultMessage:
 | 
			
		||||
      'Your post contains media without alt text. Adding descriptions helps make your content accessible to more people.',
 | 
			
		||||
  },
 | 
			
		||||
  secondary: {
 | 
			
		||||
    id: 'confirmations.missing_alt_text.secondary',
 | 
			
		||||
    defaultMessage: 'Post anyway',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const ConfirmMissingAltTextModal: React.FC<
 | 
			
		||||
  BaseConfirmationModalProps
 | 
			
		||||
> = ({ onClose }) => {
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
  const mediaId = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      (
 | 
			
		||||
        (state.compose as ImmutableMap<string, unknown>).get(
 | 
			
		||||
          'media_attachments',
 | 
			
		||||
        ) as ImmutableList<MediaAttachment>
 | 
			
		||||
      )
 | 
			
		||||
        .find(
 | 
			
		||||
          (media) =>
 | 
			
		||||
            ['image', 'gifv'].includes(media.get('type') as string) &&
 | 
			
		||||
            ((media.get('description') ?? '') as string).length === 0,
 | 
			
		||||
        )
 | 
			
		||||
        ?.get('id') as string,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleConfirm = useCallback(() => {
 | 
			
		||||
    dispatch(
 | 
			
		||||
      openModal({
 | 
			
		||||
        modalType: 'FOCAL_POINT',
 | 
			
		||||
        modalProps: {
 | 
			
		||||
          mediaId,
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }, [dispatch, mediaId]);
 | 
			
		||||
 | 
			
		||||
  const handleSecondary = useCallback(() => {
 | 
			
		||||
    dispatch(submitCompose());
 | 
			
		||||
  }, [dispatch]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ConfirmationModal
 | 
			
		||||
      title={intl.formatMessage(messages.title)}
 | 
			
		||||
      message={intl.formatMessage(messages.message)}
 | 
			
		||||
      confirm={intl.formatMessage(messages.confirm)}
 | 
			
		||||
      secondary={intl.formatMessage(messages.secondary)}
 | 
			
		||||
      onConfirm={handleConfirm}
 | 
			
		||||
      onSecondary={handleSecondary}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -37,6 +37,7 @@ import {
 | 
			
		||||
  ConfirmClearNotificationsModal,
 | 
			
		||||
  ConfirmLogOutModal,
 | 
			
		||||
  ConfirmFollowToListModal,
 | 
			
		||||
  ConfirmMissingAltTextModal,
 | 
			
		||||
} from './confirmation_modals';
 | 
			
		||||
import ImageModal from './image_modal';
 | 
			
		||||
import MediaModal from './media_modal';
 | 
			
		||||
@@ -58,6 +59,7 @@ export const MODAL_COMPONENTS = {
 | 
			
		||||
  'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
 | 
			
		||||
  'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
 | 
			
		||||
  'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
 | 
			
		||||
  'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
 | 
			
		||||
  'MUTE': MuteModal,
 | 
			
		||||
  'BLOCK': BlockModal,
 | 
			
		||||
  'DOMAIN_BLOCK': DomainBlockModal,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 * @property {string} admin
 | 
			
		||||
 * @property {boolean=} boost_modal
 | 
			
		||||
 * @property {boolean=} delete_modal
 | 
			
		||||
 * @property {boolean=} missing_alt_text_modal
 | 
			
		||||
 * @property {boolean=} disable_swiping
 | 
			
		||||
 * @property {boolean=} disable_hover_cards
 | 
			
		||||
 * @property {string=} disabled_account_id
 | 
			
		||||
@@ -88,6 +89,7 @@ export const activityApiEnabled = getMeta('activity_api_enabled');
 | 
			
		||||
export const autoPlayGif = getMeta('auto_play_gif');
 | 
			
		||||
export const boostModal = getMeta('boost_modal');
 | 
			
		||||
export const deleteModal = getMeta('delete_modal');
 | 
			
		||||
export const missingAltTextModal = getMeta('missing_alt_text_modal');
 | 
			
		||||
export const disableSwiping = getMeta('disable_swiping');
 | 
			
		||||
export const disableHoverCards = getMeta('disable_hover_cards');
 | 
			
		||||
export const disabledAccountId = getMeta('disabled_account_id');
 | 
			
		||||
 
 | 
			
		||||
@@ -218,6 +218,10 @@
 | 
			
		||||
  "confirmations.logout.confirm": "Log out",
 | 
			
		||||
  "confirmations.logout.message": "Are you sure you want to log out?",
 | 
			
		||||
  "confirmations.logout.title": "Log out?",
 | 
			
		||||
  "confirmations.missing_alt_text.confirm": "Add alt text",
 | 
			
		||||
  "confirmations.missing_alt_text.message": "Your post contains media without alt text. Adding descriptions helps make your content accessible to more people.",
 | 
			
		||||
  "confirmations.missing_alt_text.secondary": "Post anyway",
 | 
			
		||||
  "confirmations.missing_alt_text.title": "Add alt text?",
 | 
			
		||||
  "confirmations.mute.confirm": "Mute",
 | 
			
		||||
  "confirmations.redraft.confirm": "Delete & redraft",
 | 
			
		||||
  "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ class UserSettings
 | 
			
		||||
    setting :disable_hover_cards, default: false
 | 
			
		||||
    setting :delete_modal, default: true
 | 
			
		||||
    setting :reblog_modal, default: false
 | 
			
		||||
    setting :missing_alt_text_modal, default: true
 | 
			
		||||
    setting :reduce_motion, default: false
 | 
			
		||||
    setting :expand_content_warnings, default: false
 | 
			
		||||
    setting :display_media, default: 'default', in: %w(default show_all hide_all)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,14 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		||||
  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
			
		||||
  has_one :role, serializer: REST::RoleSerializer
 | 
			
		||||
 | 
			
		||||
  def meta
 | 
			
		||||
  def meta # rubocop:disable Metrics/AbcSize
 | 
			
		||||
    store = default_meta_store
 | 
			
		||||
 | 
			
		||||
    if object.current_account
 | 
			
		||||
      store[:me]                = object.current_account.id.to_s
 | 
			
		||||
      store[:boost_modal]       = object_account_user.setting_boost_modal
 | 
			
		||||
      store[:delete_modal]      = object_account_user.setting_delete_modal
 | 
			
		||||
      store[:missing_alt_text_modal] = object_account_user.settings['web.missing_alt_text_modal']
 | 
			
		||||
      store[:auto_play_gif]     = object_account_user.setting_auto_play_gif
 | 
			
		||||
      store[:display_media]     = object_account_user.setting_display_media
 | 
			
		||||
      store[:expand_spoilers]   = object_account_user.setting_expand_spoilers
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,7 @@
 | 
			
		||||
    .fields-group
 | 
			
		||||
      = ff.input :'web.reblog_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_boost_modal')
 | 
			
		||||
      = ff.input :'web.delete_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_delete_modal')
 | 
			
		||||
      = ff.input :'web.missing_alt_text_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_missing_alt_text_modal')
 | 
			
		||||
 | 
			
		||||
    %h4= t 'appearance.sensitive_content'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,7 @@ en:
 | 
			
		||||
        setting_display_media_show_all: Show all
 | 
			
		||||
        setting_expand_spoilers: Always expand posts marked with content warnings
 | 
			
		||||
        setting_hide_network: Hide your social graph
 | 
			
		||||
        setting_missing_alt_text_modal: Show confirmation dialog before posting media without alt text
 | 
			
		||||
        setting_reduce_motion: Reduce motion in animations
 | 
			
		||||
        setting_system_font_ui: Use system's default font
 | 
			
		||||
        setting_system_scrollbars_ui: Use system's default scrollbar
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user