diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 064a49f5c8..e038dac88b 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -192,6 +192,9 @@ export function directCompose(account) { }; } +/** + * @param {null | string} overridePrivacy + */ export function submitCompose(overridePrivacy = null) { return function (dispatch, getState) { let status = getState().getIn(['compose', 'text'], ''); diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index b423c61e5f..957141cd78 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -10,6 +10,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; +import { missingAltTextModal } from 'flavours/glitch/initial_state'; + import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import { Button } from '../../../components/button'; @@ -72,9 +74,8 @@ class ComposeForm extends ImmutablePureComponent { autoFocus: PropTypes.bool, withoutNavigation: PropTypes.bool, anyMedia: PropTypes.bool, + missingAltText: PropTypes.bool, media: ImmutablePropTypes.list, - mediaDescriptionConfirmation: PropTypes.bool, - onMediaDescriptionConfirm: PropTypes.func.isRequired, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, lang: PropTypes.string, @@ -131,17 +132,11 @@ class ComposeForm extends ImmutablePureComponent { return; } + this.props.onSubmit(missingAltTextModal && this.props.missingAltText, overridePrivacy); + if (e) { e.preventDefault(); } - - // Submit unless there are media with missing descriptions - if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) { - const firstWithoutDescription = this.props.media.find(item => !item.get('description')); - this.props.onMediaDescriptionConfirm(firstWithoutDescription.get('id'), overridePrivacy); - } else { - this.props.onSubmit(overridePrivacy); - } }; handleSecondarySubmit = (e) => { diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index 227e793869..4a2f247900 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -1,9 +1,7 @@ -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import { privacyPreference } from 'flavours/glitch/utils/privacy_preference'; - import { changeCompose, submitCompose, @@ -13,27 +11,11 @@ import { changeComposeSpoilerText, insertEmojiCompose, uploadCompose, -} from '../../../actions/compose'; -import { changeLocalSetting } from '../../../actions/local_settings'; -import { - openModal, -} from '../../../actions/modal'; -import ComposeForm from '../components/compose_form'; +} from 'flavours/glitch/actions/compose'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { privacyPreference } from 'flavours/glitch/utils/privacy_preference'; -const messages = defineMessages({ - missingDescriptionMessage: { - id: 'confirmations.missing_media_description.message', - defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.', - }, - missingDescriptionConfirm: { - id: 'confirmations.missing_media_description.confirm', - defaultMessage: 'Send anyway', - }, - missingDescriptionEdit: { - id: 'confirmations.missing_media_description.edit', - defaultMessage: 'Edit media', - }, -}); +import ComposeForm from '../components/compose_form'; const sideArmPrivacy = state => { const inReplyTo = state.getIn(['compose', 'in_reply_to']); @@ -68,22 +50,29 @@ 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']), sideArm: sideArmPrivacy(state), media: state.getIn(['compose', 'media_attachments']), - mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']), maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500), }); -const mapDispatchToProps = (dispatch, { intl }) => ({ +const mapDispatchToProps = (dispatch) => ({ onChange (text) { dispatch(changeCompose(text)); }, - onSubmit (overridePrivacy = null) { - dispatch(submitCompose(overridePrivacy)); + onSubmit (missingAltText, overridePrivacy = null) { + if (missingAltText) { + dispatch(openModal({ + modalType: 'CONFIRM_MISSING_ALT_TEXT', + modalProps: { overridePrivacy }, + })); + } else { + dispatch(submitCompose(overridePrivacy)); + } }, onClearSuggestions () { @@ -110,25 +99,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(insertEmojiCompose(position, data, needsSpace)); }, - onMediaDescriptionConfirm (mediaId, overridePrivacy = null) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.missingDescriptionMessage), - confirm: intl.formatMessage(messages.missingDescriptionConfirm), - onConfirm: () => { - dispatch(submitCompose(overridePrivacy)); - }, - secondary: intl.formatMessage(messages.missingDescriptionEdit), - onSecondary: () => dispatch(openModal({ - modalType: 'FOCAL_POINT', - modalProps: { id: mediaId }, - })), - onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)), - }, - })); - }, - }); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm)); diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx index 05c2599b4d..c2a6dcc1a7 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx @@ -200,14 +200,6 @@ class LocalSettingsPage extends PureComponent { - - - - {secondary && ( - <> - {secondary} - - - > - )} - + {secondary && ( + <> + + + {secondary} + + > + )} + {/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */} {confirm} diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts index 16478d0d11..4893fb096a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts @@ -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'; diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/missing_alt_text.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/missing_alt_text.tsx new file mode 100644 index 0000000000..9f044678e6 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/missing_alt_text.tsx @@ -0,0 +1,83 @@ +import { useCallback } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +import { submitCompose } from 'flavours/glitch/actions/compose'; +import { openModal } from 'flavours/glitch/actions/modal'; +import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/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< + { + overridePrivacy: null | string; + } & BaseConfirmationModalProps +> = ({ onClose, overridePrivacy }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const mediaId = useAppSelector( + (state) => + ( + (state.compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ) + .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(overridePrivacy)); + }, [dispatch, overridePrivacy]); + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 89bf73a19d..89d92e4842 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -38,6 +38,7 @@ import { ConfirmClearNotificationsModal, ConfirmLogOutModal, ConfirmFollowToListModal, + ConfirmMissingAltTextModal, } from './confirmation_modals'; import DeprecatedSettingsModal from './deprecated_settings_modal'; import DoodleModal from './doodle_modal'; @@ -64,6 +65,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, diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 7fb25697ae..9332b7ebc6 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -16,6 +16,7 @@ * @property {boolean=} favourite_modal * @property {boolean} crop_images * @property {boolean=} delete_modal + * @property {boolean=} missing_alt_text_modal * @property {boolean=} disable_swiping * @property {boolean=} disable_hover_cards * @property {string=} disabled_account_id @@ -107,6 +108,7 @@ export const autoPlayGif = getMeta('auto_play_gif'); export const boostModal = getMeta('boost_modal'); export const cropImages = getMeta('crop_images'); 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'); diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 567bd72459..d4a7481395 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -31,9 +31,6 @@ "confirmation_modal.do_not_ask_again": "Do not ask for confirmation again", "confirmations.deprecated_settings.confirm": "Use Mastodon preferences", "confirmations.deprecated_settings.message": "Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:", - "confirmations.missing_media_description.confirm": "Send anyway", - "confirmations.missing_media_description.edit": "Edit media", - "confirmations.missing_media_description.message": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.", "direct.group_by_conversations": "Group by conversation", "favourite_modal.favourite": "Favourite post?", "federation.federated.long": "Allow this post to reach other servers", @@ -57,7 +54,6 @@ "settings.compose_box_opts": "Compose box", "settings.confirm_before_clearing_draft": "Show confirmation dialog before overwriting the message being composed", "settings.confirm_boost_missing_media_description": "Show confirmation dialog before boosting toots lacking media descriptions", - "settings.confirm_missing_media_description": "Show confirmation dialog before sending toots lacking media descriptions", "settings.content_warnings": "Content Warnings", "settings.content_warnings.regexp": "Regular expression", "settings.content_warnings_filter": "Content warnings to not automatically unfold:", diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index ef3fdb38bf..225938ca22 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -11,7 +11,6 @@ const initialState = ImmutableMap({ side_arm_reply_mode : 'keep', show_reply_count : false, always_show_spoilers_field: false, - confirm_missing_media_description: false, confirm_boost_missing_media_description: false, confirm_before_clearing_draft: true, prepend_cw_re: true,