From 695f9a7d088df8964cae012d0799957423061fae Mon Sep 17 00:00:00 2001 From: noellabo Date: Sun, 25 Sep 2022 19:58:11 +0900 Subject: [PATCH] Add phrase confirmation to domain block dialog --- .../settings/preferences_controller.rb | 1 + .../mastodon/containers/domain_container.js | 21 +------ .../mastodon/containers/status_container.js | 5 +- .../containers/header_container.js | 5 +- .../mastodon/features/status/index.js | 5 +- .../ui/components/confirmation_modal.js | 47 ++++++++++++++-- app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + .../styles/mastodon-light/diff.scss | 8 +++ .../styles/mastodon/components.scss | 55 +++++++++++++++++++ app/lib/user_settings_decorator.rb | 7 ++- app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 1 + .../preferences/appearance/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + config/settings.yml | 1 + 18 files changed, 137 insertions(+), 29 deletions(-) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index af2d51650..d695afa35 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -96,6 +96,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_default_search_searchability, :setting_show_reload_button, :setting_default_column_width, + :setting_confirm_domain_block, notification_emails: %i(follow follow_request reblog favourite emoji_reaction status_reference mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm must_be_dm_to_send_email must_be_following_reference) ) diff --git a/app/javascript/mastodon/containers/domain_container.js b/app/javascript/mastodon/containers/domain_container.js index 8a8ba1df1..0fd6ba1e1 100644 --- a/app/javascript/mastodon/containers/domain_container.js +++ b/app/javascript/mastodon/containers/domain_container.js @@ -1,13 +1,6 @@ -import React from 'react'; import { connect } from 'react-redux'; -import { blockDomain, unblockDomain } from '../actions/domain_blocks'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { unblockDomain } from '../actions/domain_blocks'; import Domain from '../components/domain'; -import { openModal } from '../actions/modal'; - -const messages = defineMessages({ - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, -}); const makeMapStateToProps = () => { const mapStateToProps = () => ({}); @@ -15,18 +8,10 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = (dispatch, { intl }) => ({ - onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), - })); - }, - +const mapDispatchToProps = (dispatch) => ({ onUnblockDomain (domain) { dispatch(unblockDomain(domain)); }, }); -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain)); +export default connect(makeMapStateToProps, mapDispatchToProps)(Domain); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index a7c5e5fcc..41213d256 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -52,7 +52,7 @@ import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { deployPictureInPicture } from '../actions/picture_in_picture'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { boostModal, deleteModal, unfollowModal, unsubscribeModal } from '../initial_state'; +import { boostModal, deleteModal, unfollowModal, unsubscribeModal, confirmDomainBlock } from '../initial_state'; import { showAlertForError } from '../actions/alerts'; import { createSelector } from 'reselect'; @@ -68,6 +68,7 @@ const messages = defineMessages({ quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' }, }); @@ -254,6 +255,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ message: {domain} }} />, confirm: intl.formatMessage(messages.blockDomainConfirm), onConfirm: () => dispatch(blockDomain(domain)), + passphrase: confirmDomainBlock && intl.formatMessage(messages.blockDomainPassphrase), + destructive: true, })); }, diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 117c16bde..9c6df518d 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -22,13 +22,14 @@ import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { unfollowModal, unsubscribeModal } from '../../../initial_state'; +import { unfollowModal, unsubscribeModal, confirmDomainBlock } from '../../../initial_state'; import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' }, }); const makeMapStateToProps = () => { @@ -146,6 +147,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ message: {domain} }} />, confirm: intl.formatMessage(messages.blockDomainConfirm), onConfirm: () => dispatch(blockDomain(domain)), + passphrase: confirmDomainBlock && intl.formatMessage(messages.blockDomainPassphrase), + destructive: true, })); }, diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 70046c62d..d385301e9 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -60,7 +60,7 @@ import { openModal } from '../../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; -import { boostModal, deleteModal, enableStatusReference } from '../../initial_state'; +import { boostModal, deleteModal, confirmDomainBlock, enableStatusReference } from '../../initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import Icon from 'mastodon/components/icon'; @@ -82,6 +82,7 @@ const messages = defineMessages({ quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' }, }); const makeMapStateToProps = () => { @@ -430,6 +431,8 @@ class Status extends ImmutablePureComponent { message: {domain} }} />, confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), onConfirm: () => this.props.dispatch(blockDomain(domain)), + passphrase: confirmDomainBlock && this.props.intl.formatMessage(messages.blockDomainPassphrase), + destructive: true, })); } diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.js b/app/javascript/mastodon/features/ui/components/confirmation_modal.js index 65d97ca16..3bdfd08a1 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modal.js +++ b/app/javascript/mastodon/features/ui/components/confirmation_modal.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import Button from '../../../components/button'; +import classNames from 'classnames'; export default @injectIntl class ConfirmationModal extends React.PureComponent { @@ -14,6 +15,8 @@ class ConfirmationModal extends React.PureComponent { secondary: PropTypes.string, onSecondary: PropTypes.func, closeWhenConfirm: PropTypes.bool, + destructive: PropTypes.bool, + passphrase: PropTypes.string, intl: PropTypes.object.isRequired, }; @@ -21,15 +24,30 @@ class ConfirmationModal extends React.PureComponent { closeWhenConfirm: true, }; + state = { + passphrase: '', + }; + componentDidMount() { - this.button.focus(); + if (this.props.passphrase) { + this.passphraseInput.focus(); + } else { + this.button.focus(); + } } handleClick = () => { - if (this.props.closeWhenConfirm) { - this.props.onClose(); + const { passphrase, closeWhenConfirm, onClose, onConfirm } = this.props; + + if (passphrase && this.state.passphrase !== passphrase) { + this.passphraseInput.focus(); + return; } - this.props.onConfirm(); + + if (closeWhenConfirm) { + onClose(); + } + onConfirm(); } handleSecondary = () => { @@ -41,12 +59,20 @@ class ConfirmationModal extends React.PureComponent { this.props.onClose(); } + handleChange = (e) => { + this.setState({ passphrase: e.target.value }); + } + setRef = (c) => { this.button = c; } + setPassphraseRef = (c) => { + this.passphraseInput = c; + } + render () { - const { message, confirm, secondary } = this.props; + const { message, confirm, secondary, destructive, passphrase } = this.props; return (
@@ -54,6 +80,15 @@ class ConfirmationModal extends React.PureComponent { {message}
+ {passphrase && ( +
+
+ {passphrase} }} />, +
+ +
+ )} +
); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d24bfaf54..9639eb04d 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -31,6 +31,7 @@ export const showTrends = getMeta('trends'); export const title = getMeta('title'); export const cropImages = getMeta('crop_images'); export const disableSwiping = getMeta('disable_swiping'); +export const confirmDomainBlock = getMeta('confirm_domain_block'); export const show_follow_button_on_timeline = getMeta('show_follow_button_on_timeline'); export const show_subscribe_button_on_timeline = getMeta('show_subscribe_button_on_timeline'); export const show_followed_by = getMeta('show_followed_by'); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index dcef07bb4..a82d2aa00 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -185,11 +185,13 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Block entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.domain_block.passphrase": "block", "confirmations.logout.confirm": "Log out", "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.passphrase": "Please type \"{passphrase}\" to confirm", "confirmations.post_reference.confirm": "Post", "confirmations.post_reference.message": "It contains references, do you want to post it?", "confirmations.quote.confirm": "Quote", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index c5f4af3bc..9be27ad5a 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -185,11 +185,13 @@ "confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?", "confirmations.domain_block.confirm": "ドメイン全体をブロック", "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。公開タイムラインにそのドメインのコンテンツが表示されなくなり、通知も届かなくなります。そのドメインのフォロワーはアンフォローされます。", + "confirmations.domain_block.passphrase": "ブロック", "confirmations.logout.confirm": "ログアウト", "confirmations.logout.message": "本当にログアウトしますか?", "confirmations.mute.confirm": "ミュート", "confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。", "confirmations.mute.message": "本当に{name}さんをミュートしますか?", + "confirmations.passphrase": "確認のため \"{passphrase}\" と入力してください", "confirmations.post_reference.confirm": "投稿", "confirmations.post_reference.message": "参照を含んでいますが、投稿しますか?", "confirmations.quote.confirm": "引用", diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 5d6ea7130..f690918cb 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -382,6 +382,14 @@ html { border: 1px solid lighten($ui-base-color, 8%); } +.confirmation-modal__passphrase .passphrase__input { + border: 1px solid lighten($ui-base-color, 8%); + + &.invalid { + background-color: darken($error-value-color, 35%); + } +} + .reactions-bar__item { &:hover:enabled, &:focus:enabled, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 326f0dcda..15ec8de92 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -84,6 +84,16 @@ } } + &.always-destructive { + background-color: $error-red; + + &:disabled, + &.disabled { + background-color: $ui-primary-color; + cursor: default; + } + } + &:disabled, &.disabled { background-color: $ui-primary-color; @@ -5747,6 +5757,51 @@ a.status-card.compact:hover { text-align: center; } +.confirmation-modal__passphrase { + text-align: center; + padding: 0 30px 30px; + font-size: 16px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + .passphrase__label { + flex: 1 1 auto; + + strong { + font-weight: 500; + + @each $lang in $cjk-langs { + &:lang(#{$lang}) { + font-weight: 700; + } + } + } + } + + .passphrase__input { + flex: 1 1 auto; + min-width: 50%; + display: block; + box-sizing: border-box; + padding: 10px; + margin: 0; + color: $inverted-text-color; + background: $simple-background-color; + font-family: inherit; + font-size: 16px; + resize: vertical; + border: 0; + outline: 0; + border-radius: 4px; + + &.invalid { + background-color: lighten($error-value-color, 35%); + } + } +} + .block-modal, .mute-modal { &__explanation { diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 4eda7a581..4d8092107 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -57,6 +57,7 @@ class UserSettingsDecorator user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') user.settings['trends'] = trends_preference if change?('setting_trends') user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images') + user.settings['confirm_domain_block'] = confirm_domain_block_preference if change?('setting_confirm_domain_block') user.settings['show_follow_button_on_timeline'] = show_follow_button_on_timeline_preference if change?('setting_show_follow_button_on_timeline') user.settings['show_subscribe_button_on_timeline'] = show_subscribe_button_on_timeline_preference if change?('setting_show_subscribe_button_on_timeline') user.settings['show_followed_by'] = show_followed_by_preference if change?('setting_show_followed_by') @@ -90,7 +91,7 @@ class UserSettingsDecorator user.settings['default_search_searchability'] = default_search_searchability_preference if change?('setting_default_search_searchability') user.settings['show_reload_button'] = show_reload_button_preference if change?('setting_show_reload_button') user.settings['default_column_width'] = default_column_width_preference if change?('setting_default_column_width') -end + end def merged_notification_emails user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h @@ -204,6 +205,10 @@ end boolean_cast_setting 'setting_crop_images' end + def confirm_domain_block_preference + boolean_cast_setting 'setting_confirm_domain_block' + end + def show_follow_button_on_timeline_preference boolean_cast_setting 'setting_show_follow_button_on_timeline' end diff --git a/app/models/user.rb b/app/models/user.rb index f2efad61f..8d10a4265 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -125,7 +125,7 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, - :disable_swiping, + :disable_swiping, :confirm_domain_block, :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target, :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_followed_by, :show_target, :follow_button_to_list_adder, :show_navigation_panel, :show_quote_button, :show_bookmark_button, diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index e7f17e946..e53343e23 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -42,6 +42,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:is_staff] = object.current_account.user.staff? store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:crop_images] = object.current_account.user.setting_crop_images + store[:confirm_domain_block] = object.current_account.user.setting_confirm_domain_block store[:show_follow_button_on_timeline] = object.current_account.user.setting_show_follow_button_on_timeline store[:show_subscribe_button_on_timeline] = object.current_account.user.setting_show_subscribe_button_on_timeline store[:show_followed_by] = object.current_account.user.setting_show_followed_by diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 42aae6abc..5fa1b5965 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -98,6 +98,7 @@ = f.input :setting_unsubscribe_modal, as: :boolean, wrapper: :with_label, fedibird_features: true = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label + = f.input :setting_confirm_domain_block, as: :boolean, wrapper: :with_label = f.input :setting_post_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true = f.input :setting_add_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true = f.input :setting_unselect_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 3402fbc66..5df9c8169 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -220,6 +220,7 @@ en: setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting setting_compact_reaction: Compact display of reaction + setting_confirm_domain_block: Require domain input for domain block setting_confirm_follow_from_bot: Require follow requests from bot setting_content_emoji_reaction_size: Emoji reaction size setting_content_font_size: Content font size diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index ede5d861d..2669207fc 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -220,6 +220,7 @@ ja: setting_auto_play_gif: アニメーションGIFを自動再生する setting_boost_modal: ブーストする前に確認ダイアログを表示する setting_compact_reaction: リアクションをコンパクトに表示 + setting_confirm_domain_block: ドメインブロックにドメイン入力を要求する setting_confirm_follow_from_bot: Bot承認制アカウントにする setting_content_emoji_reaction_size: 投稿の絵文字リアクションのサイズ setting_content_font_size: 投稿のフォントサイズ diff --git a/config/settings.yml b/config/settings.yml index 4596601a6..e8b1061b4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -42,6 +42,7 @@ defaults: &defaults trends: true trendable_by_default: false crop_images: true + confirm_domain_block: true show_follow_button_on_timeline: false show_subscribe_button_on_timeline: false show_followed_by: false