From 206b5dbf0446238613a3c247c9c7c5443ca9359e Mon Sep 17 00:00:00 2001 From: noellabo Date: Mon, 3 Oct 2022 16:13:07 +0900 Subject: [PATCH] Add safety and privacy features --- app/controllers/api/v1/accounts_controller.rb | 6 + .../api/v1/domain_blocks_controller.rb | 2 + .../api/v1/notifications_controller.rb | 2 + .../v1/statuses/emoji_reactions_controller.rb | 2 + .../api/v1/statuses/favourites_controller.rb | 2 + .../api/v1/statuses/reblogs_controller.rb | 2 + app/controllers/api/v1/statuses_controller.rb | 2 + .../settings/preferences/safety_controller.rb | 9 + .../settings/preferences_controller.rb | 10 + app/javascript/mastodon/actions/accounts.js | 26 +- app/javascript/mastodon/components/account.js | 11 +- .../mastodon/components/account_action_bar.js | 6 +- .../components/emoji_reactions_bar.js | 10 +- .../mastodon/components/status_action_bar.js | 37 +- .../mastodon/components/status_content.js | 4 +- .../features/account/components/header.js | 31 +- .../account/components/header_common.js | 40 +- .../compose/components/compose_form.js | 11 +- .../compose/components/privacy_dropdown.js | 17 +- .../components/searchability_dropdown.js | 6 +- .../containers/compose_form_container.js | 2 + .../containers/privacy_dropdown_container.js | 1 + .../searchability_dropdown_container.js | 1 + .../directory/components/account_card.js | 11 +- .../components/account_card.js | 11 +- .../group_timeline/components/group_detail.js | 11 +- .../features/list_adder/components/account.js | 6 +- .../mastodon/features/list_adder/index.js | 10 +- .../components/column_settings.js | 6 +- .../features/status/components/action_bar.js | 44 +- .../features/ui/components/boost_modal.js | 8 +- app/javascript/mastodon/initial_state.js | 8 + app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + app/javascript/mastodon/reducers/compose.js | 10 + app/javascript/styles/mastodon/tables.scss | 5 + app/lib/user_settings_decorator.rb | 422 ++++-------------- app/models/user.rb | 2 + app/serializers/initial_state_serializer.rb | 22 +- app/services/post_status_service.rb | 15 + app/services/reblog_service.rb | 2 + app/views/relationships/show.html.haml | 12 +- app/views/settings/deletes/show.html.haml | 5 +- .../preferences/safety/show.html.haml | 45 ++ config/locales/en.yml | 9 + config/locales/ja.yml | 9 + config/locales/simple_form.en.yml | 20 + config/locales/simple_form.ja.yml | 20 + config/navigation.rb | 1 + config/routes.rb | 1 + config/settings.yml | 10 + 51 files changed, 492 insertions(+), 477 deletions(-) create mode 100644 app/controllers/settings/preferences/safety_controller.rb create mode 100644 app/views/settings/preferences/safety/show.html.haml diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index e29631a26..0a6846fa1 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -38,6 +38,8 @@ class Api::V1::AccountsController < Api::BaseController end def follow + raise Mastodon::NotPermittedError if current_user.setting_disable_follow && !current_user.account.following?(@account) + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, delivery: params.key?(:delivery) ? truthy_param?(:delivery) : nil, with_rate_limit: true) options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => true }, @@ -56,6 +58,8 @@ class Api::V1::AccountsController < Api::BaseController end def block + raise Mastodon::NotPermittedError if current_user.setting_disable_block + BlockService.new.call(current_user.account, @account) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end @@ -66,6 +70,8 @@ class Api::V1::AccountsController < Api::BaseController end def unfollow + raise Mastodon::NotPermittedError if current_user.setting_disable_unfollow && (current_user.account.following?(@account) || !current_user.account.requested?(@account)) + UnfollowService.new.call(current_user.account, @account) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index 5bb02d834..a9ac0aae3 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -14,6 +14,8 @@ class Api::V1::DomainBlocksController < Api::BaseController end def create + raise Mastodon::NotPermittedError if current_user.setting_disable_domain_block + current_account.block_domain!(domain_block_params[:domain]) AfterAccountDomainBlockWorker.perform_async(current_account.id, domain_block_params[:domain]) render_empty diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 1aecf71a3..aa47d1b9d 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -19,6 +19,8 @@ class Api::V1::NotificationsController < Api::BaseController end def clear + raise Mastodon::NotPermittedError if current_user.setting_disable_clear_all_notifications + current_account.notifications.delete_all render_empty end diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index be1a0867a..8f840b6c0 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -8,6 +8,8 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController before_action :set_status def update + raise Mastodon::NotPermittedError if current_user.setting_disable_reactions + if EmojiReactionService.new.call(current_account, @status, params[:id]).present? @status = Status.include_expired.find(params[:status_id]) end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index fecfedd28..79709ff79 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -8,6 +8,8 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController before_action :set_status, only: [:create] def create + raise Mastodon::NotPermittedError if current_user.setting_disable_reactions + FavouriteService.new.call(current_account, @status) render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1beb39fef..40406ceee 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -11,6 +11,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create + raise Mastodon::NotPermittedError if current_user.setting_disable_reactions + @status = ReblogService.new.call(current_account, @reblog, reblog_params) render json: @status, serializer: REST::StatusSerializer diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index cf6ab65b8..237798c44 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -54,6 +54,8 @@ class Api::V1::StatusesController < Api::BaseController end def create + raise Mastodon::NotPermittedError if current_user.setting_disable_post + @status = PostStatusService.new.call(current_user.account, text: status_params[:status], thread: @thread, diff --git a/app/controllers/settings/preferences/safety_controller.rb b/app/controllers/settings/preferences/safety_controller.rb new file mode 100644 index 000000000..8b9bc0a7d --- /dev/null +++ b/app/controllers/settings/preferences/safety_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::SafetyController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_safety_path + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f77999392..93eb35908 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -101,6 +101,16 @@ class Settings::PreferencesController < Settings::BaseController :setting_confirm_domain_block, :setting_default_expires_in, :setting_default_expires_action, + :setting_disable_post, + :setting_disable_reactions, + :setting_disable_follow, + :setting_disable_unfollow, + :setting_disable_block, + :setting_disable_domain_block, + :setting_disable_clear_all_notifications, + :setting_disable_account_delete, + :setting_prohibited_words, + setting_prohibited_visibilities: [], 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/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 578a8fd04..3c8f8827e 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -204,7 +204,7 @@ export function followAccount(id, options = { reblogs: true, delivery: true }) { api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { - dispatch(followAccountFail(error, locked)); + dispatch(followAccountFail(id, error, locked)); }); }; }; @@ -216,7 +216,7 @@ export function unfollowAccount(id) { api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { - dispatch(unfollowAccountFail(error)); + dispatch(unfollowAccountFail(id, error)); }); }; }; @@ -239,9 +239,10 @@ export function followAccountSuccess(relationship, alreadyFollowing) { }; }; -export function followAccountFail(error, locked) { +export function followAccountFail(id, error, locked) { return { type: ACCOUNT_FOLLOW_FAIL, + id, error, locked, skipLoading: true, @@ -265,9 +266,10 @@ export function unfollowAccountSuccess(relationship, statuses) { }; }; -export function unfollowAccountFail(error) { +export function unfollowAccountFail(id, error) { return { type: ACCOUNT_UNFOLLOW_FAIL, + id, error, skipLoading: true, }; @@ -283,7 +285,7 @@ export function subscribeAccount(id, reblogs = true, list_id = null) { api(getState).post(`/api/v1/accounts/${id}/subscribe`, { reblogs, list_id }).then(response => { dispatch(subscribeAccountSuccess(response.data, alreadySubscribe)); }).catch(error => { - dispatch(subscribeAccountFail(error, locked)); + dispatch(subscribeAccountFail(id, error, locked)); }); }; }; @@ -295,7 +297,7 @@ export function unsubscribeAccount(id, list_id = null) { api(getState).post(`/api/v1/accounts/${id}/unsubscribe`, { list_id }).then(response => { dispatch(unsubscribeAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { - dispatch(unsubscribeAccountFail(error)); + dispatch(unsubscribeAccountFail(id, error)); }); }; }; @@ -318,9 +320,10 @@ export function subscribeAccountSuccess(relationship, alreadySubscribe) { }; }; -export function subscribeAccountFail(error, locked) { +export function subscribeAccountFail(id, error, locked) { return { type: ACCOUNT_SUBSCRIBE_FAIL, + id, error, locked, skipLoading: true, @@ -344,9 +347,10 @@ export function unsubscribeAccountSuccess(relationship, statuses) { }; }; -export function unsubscribeAccountFail(error) { +export function unsubscribeAccountFail(id, error) { return { type: ACCOUNT_UNSUBSCRIBE_FAIL, + id, error, skipLoading: true, }; @@ -392,9 +396,10 @@ export function blockAccountSuccess(relationship, statuses) { }; }; -export function blockAccountFail(error) { +export function blockAccountFail(id, error) { return { type: ACCOUNT_BLOCK_FAIL, + id, error, }; }; @@ -413,9 +418,10 @@ export function unblockAccountSuccess(relationship) { }; }; -export function unblockAccountFail(error) { +export function unblockAccountFail(id, error) { return { type: ACCOUNT_UNBLOCK_FAIL, + id, error, }; }; diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 96e21e42b..b124557df 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -7,7 +7,7 @@ import Permalink from './permalink'; import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, show_followed_by, follow_button_to_list_adder } from '../initial_state'; +import { me, show_followed_by, follow_button_to_list_adder, disableFollow, disableUnfollow } from '../initial_state'; import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ @@ -132,9 +132,7 @@ class Account extends ImmutablePureComponent { subscribing_buttons = ( ; } else { - following_buttons = ; + following_buttons = ; } } - buttons = {subscribing_buttons}{following_buttons} + buttons = {subscribing_buttons}{following_buttons}; } return ( diff --git a/app/javascript/mastodon/components/emoji_reactions_bar.js b/app/javascript/mastodon/components/emoji_reactions_bar.js index 0799b2051..0b3b586a8 100644 --- a/app/javascript/mastodon/components/emoji_reactions_bar.js +++ b/app/javascript/mastodon/components/emoji_reactions_bar.js @@ -9,7 +9,7 @@ import Emoji from './emoji'; import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import AnimatedNumber from 'mastodon/components/animated_number'; -import { reduceMotion, me } from 'mastodon/initial_state'; +import { reduceMotion, me, disableReactions } from 'mastodon/initial_state'; import spring from 'react-motion/lib/spring'; import Overlay from 'react-overlays/lib/Overlay'; import { isUserTouching } from 'mastodon/is_mobile'; @@ -110,7 +110,7 @@ class EmojiReaction extends ImmutablePureComponent { const { emojiReaction, status, myReaction } = this.props; if (!emojiReaction) { - return ; + return ; } let shortCode = emojiReaction.get('name'); @@ -122,7 +122,7 @@ class EmojiReaction extends ImmutablePureComponent { return (
- @@ -157,11 +157,11 @@ export default class EmojiReactionsBar extends ImmutablePureComponent { render () { const { status } = this.props; - const emoji_reactions = status.get("emoji_reactions"); + const emoji_reactions = status.get('emoji_reactions'); const visibleReactions = emoji_reactions.filter(x => x.get('count') > 0); if (visibleReactions.isEmpty() ) { - return ; + return ; } const styles = visibleReactions.map(emoji_reaction => { diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index dc677334c..deb1d4b1a 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import DropdownMenuContainer from '../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, isStaff, show_bookmark_button, show_quote_button, enableReaction, compactReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal } from '../initial_state'; +import { me, isStaff, show_bookmark_button, show_quote_button, enableReaction, compactReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal, disablePost, disableReactions, disableBlock, disableDomainBlock } from '../initial_state'; import classNames from 'classnames'; import { openModal } from '../actions/modal'; @@ -404,11 +404,15 @@ class StatusActionBar extends ImmutablePureComponent { if (writtenByMe) { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + if (!disablePost) { + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + } } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); - menu.push(null); + if (!disablePost) { + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); + menu.push(null); + } if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); @@ -418,18 +422,18 @@ class StatusActionBar extends ImmutablePureComponent { if (relationship && relationship.get('blocking')) { menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); - } else { + } else if (!disableBlock) { menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); } menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport }); if (domain) { - menu.push(null); - if (relationship && relationship.get('domain_blocking')) { + menu.push(null); menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); - } else { + } else if (!disableDomainBlock) { + menu.push(null); menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); } } @@ -474,22 +478,21 @@ class StatusActionBar extends ImmutablePureComponent { return (
- - {enableStatusReference && me && } - - - {show_quote_button && } + + {enableStatusReference && me && } + + + {show_quote_button && } {shareButton} {show_bookmark_button && } - {enableReaction &&
+ {enableReaction &&
+ ); if (status.get('spoiler_text').length > 0) { diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index ca3c722d1..b0e53ec10 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; import Button from 'mastodon/components/button'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder } from 'mastodon/initial_state'; +import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder, disablePost, disableBlock, disableDomainBlock, disableFollow, disableUnfollow } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import IconButton from 'mastodon/components/icon_button'; @@ -191,7 +191,11 @@ class Header extends ImmutablePureComponent { } else if (account.getIn(['relationship', 'requested'])) { actionBtn =
+
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index f753af5cd..b91ace184 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; import Overlay from 'react-overlays/lib/Overlay'; @@ -22,6 +23,8 @@ const messages = defineMessages({ direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, limited_long: { id: 'privacy.limited.long', defaultMessage: 'Visible for circle users only' }, + none_short: { id: 'privacy.none.short', defaultMessage: 'None' }, + none_long: { id: 'privacy.none.long', defaultMessage: 'No visibility allowed' }, change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); @@ -160,6 +163,7 @@ class PrivacyDropdown extends React.PureComponent { onModalOpen: PropTypes.func, onModalClose: PropTypes.func, value: PropTypes.string.isRequired, + prohibitedVisibilities: ImmutablePropTypes.set, onChange: PropTypes.func.isRequired, noDirect: PropTypes.bool, container: PropTypes.func, @@ -235,28 +239,25 @@ class PrivacyDropdown extends React.PureComponent { } componentWillMount () { - const { intl: { formatMessage } } = this.props; + const { intl: { formatMessage }, prohibitedVisibilities } = this.props; this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) }, - ]; - - if (!this.props.noDirect) { - this.options.push( + ...!this.props.noDirect && [ { icon: 'user-circle', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ); - } + ], + ].filter(option => !prohibitedVisibilities?.includes(option.value)); } render () { const { value, container, intl } = this.props; const { open, placement } = this.state; - const valueOption = this.options.find(item => item.value === value); + const valueOption = this.options.find(item => item.value === value) || { icon: 'ban', value: 'none', text: intl.formatMessage(messages.none_short), meta: intl.formatMessage(messages.none_long) }; return (
diff --git a/app/javascript/mastodon/features/compose/components/searchability_dropdown.js b/app/javascript/mastodon/features/compose/components/searchability_dropdown.js index a06ebc43e..dfa4088e3 100644 --- a/app/javascript/mastodon/features/compose/components/searchability_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/searchability_dropdown.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; import Overlay from 'react-overlays/lib/Overlay'; @@ -155,6 +156,7 @@ class SearchabilityDropdown extends React.PureComponent { onModalOpen: PropTypes.func, onModalClose: PropTypes.func, value: PropTypes.string.isRequired, + prohibitedVisibilities: ImmutablePropTypes.set, onChange: PropTypes.func.isRequired, noDirect: PropTypes.bool, container: PropTypes.func, @@ -230,13 +232,13 @@ class SearchabilityDropdown extends React.PureComponent { } componentWillMount () { - const { intl: { formatMessage } } = this.props; + const { intl: { formatMessage }, prohibitedVisibilities } = this.props; this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ]; + ].filter(option => !prohibitedVisibilities?.includes(option.value)); } render () { diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 67df63736..10e8bfd8a 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -27,6 +27,8 @@ const mapStateToProps = state => ({ isCircleUnselected: state.getIn(['compose', 'privacy']) === 'limited' && state.getIn(['compose', 'reply_status', 'visibility']) !== 'limited' && !state.getIn(['compose', 'circle_id']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, + prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']), + prohibitedWords: state.getIn(['compose', 'prohibited_words']), }); const mapDispatchToProps = (dispatch, { intl }) => ({ diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js index e5bbb94da..c1dda62c9 100644 --- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js @@ -6,6 +6,7 @@ import { isUserTouching } from '../../../is_mobile'; const mapStateToProps = state => ({ value: state.getIn(['compose', 'privacy']), + prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js index 3b4a83a1f..3a240b63c 100644 --- a/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js @@ -6,6 +6,7 @@ import { isUserTouching } from '../../../is_mobile'; const mapStateToProps = state => ({ value: state.getIn(['compose', 'searchability']), + prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index c50d8ad85..886632cfc 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -17,6 +17,8 @@ import { unsubscribeModal, show_followed_by, follow_button_to_list_adder, + disableFollow, + disableUnfollow, } from 'mastodon/initial_state'; import ShortNumber from 'mastodon/components/short_number'; import { @@ -245,9 +247,7 @@ class AccountCard extends ImmutablePureComponent { subscribing_buttons = ( ({ +const MapStateToProps = () => ({ }); const mapDispatchToProps = (dispatch, { intl }) => ({ @@ -68,7 +68,7 @@ class Account extends ImmutablePureComponent { if (requested) { buttons = ; } else { - buttons = ; + buttons = ; } } } diff --git a/app/javascript/mastodon/features/list_adder/index.js b/app/javascript/mastodon/features/list_adder/index.js index 61c2ecab3..5700e0c30 100644 --- a/app/javascript/mastodon/features/list_adder/index.js +++ b/app/javascript/mastodon/features/list_adder/index.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl } from 'react-intl'; import { setupListAdder, resetListAdder } from '../../actions/lists'; import { createSelector } from 'reselect'; import { makeGetAccount } from '../../selectors'; @@ -34,7 +33,6 @@ const mapDispatchToProps = dispatch => ({ }); export default @connect(mapStateToProps, mapDispatchToProps) -@injectIntl class ListAdder extends ImmutablePureComponent { static propTypes = { @@ -57,21 +55,21 @@ class ListAdder extends ImmutablePureComponent { } render () { - const { account, listIds, intl } = this.props; + const { account, listIds } = this.props; const following = account.getIn(['relationship', 'following']); return (
- +
- - {listIds.map(ListId => )} + + {listIds.map(ListId => )}
); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 769111cba..efe9dd98e 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import SettingToggle from './setting_toggle'; -import { enableReaction, enableStatusReference } from 'mastodon/initial_state'; +import { enableReaction, enableStatusReference, disableClearAllNotifications } from 'mastodon/initial_state'; export default class ColumnSettings extends React.PureComponent { @@ -52,9 +52,9 @@ export default class ColumnSettings extends React.PureComponent {
)} -
+ {!disableClearAllNotifications &&
-
+
}
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index c9fb22a59..c3d326004 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -5,7 +5,7 @@ import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; -import { me, isStaff, show_quote_button, enableReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal } from '../../../initial_state'; +import { me, isStaff, show_quote_button, enableReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal, disablePost, disableReactions, disableBlock, disableDomainBlock } from '../../../initial_state'; import classNames from 'classnames'; import ReactionPickerDropdownContainer from 'mastodon/containers/reaction_picker_dropdown_container'; import { openModal } from '../../../actions/modal'; @@ -295,11 +295,11 @@ class ActionBar extends React.PureComponent { const reblogsCount = status.get('reblogs_count'); const referredByCount = status.get('status_referred_by_count'); const favouritesCount = status.get('favourites_count'); - const [ _, domain ] = account.get('acct').split('@'); + const [ , domain ] = account.get('acct').split('@'); - const expires_at = status.get('expires_at') - const expires_date = expires_at && new Date(expires_at) - const expired = expires_date && expires_date.getTime() < intl.now() + const expires_at = status.get('expires_at'); + const expires_date = expires_at && new Date(expires_at); + const expired = expires_date && expires_date.getTime() < intl.now(); let menu = []; @@ -350,12 +350,18 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); } + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + + if (!disablePost) { + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + } } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); - menu.push(null); + if (!disablePost) { + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); + menu.push(null); + } if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); @@ -365,18 +371,18 @@ class ActionBar extends React.PureComponent { if (relationship && relationship.get('blocking')) { menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); - } else { + } else if (!disableBlock) { menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); } menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); if (domain) { - menu.push(null); - if (relationship && relationship.get('domain_blocking')) { + menu.push(null); menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); - } else { + } else if (!disableDomainBlock) { + menu.push(null); menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); } } @@ -416,17 +422,17 @@ class ActionBar extends React.PureComponent { return (
-
- {enableStatusReference && me &&
} -
-
- {show_quote_button &&
} +
+ {enableStatusReference && me &&
} +
+
+ {show_quote_button &&
} {shareButton}
{enableReaction &&
{ return { privacy: state.getIn(['boosts', 'new', 'privacy']), + prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']), }; }; @@ -40,7 +41,7 @@ const mapDispatchToProps = dispatch => { }; }; -export default @connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true }) @injectIntl class BoostModal extends ImmutablePureComponent { @@ -88,7 +89,7 @@ class BoostModal extends ImmutablePureComponent { } render () { - const { status, privacy, intl } = this.props; + const { status, privacy, prohibitedVisibilities, intl } = this.props; const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; const visibilityIconInfo = { @@ -138,11 +139,12 @@ class BoostModal extends ImmutablePureComponent { )} -
); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 2d8051cf0..2767d4c3d 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -55,5 +55,13 @@ export const enableEmptyColumn = getMeta('enable_empty_column'); export const showReloadButton = getMeta('show_reload_button'); export const defaultColumnWidth = getMeta('default_column_width'); export const pickerEmojiSize = getMeta('picker_emoji_size'); +export const disablePost = getMeta('disable_post'); +export const disableReactions = getMeta('disable_reactions'); +export const disableFollow = getMeta('disable_follow'); +export const disableUnfollow = getMeta('disable_unfollow'); +export const disableBlock = getMeta('disable_block'); +export const disableDomainBlock = getMeta('disable_domain_block'); +export const disableClearAllNotifications = getMeta('disable_clear_all_notifications'); +export const disableAccountDelete = getMeta('disable_account_delete'); export default initialState; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index a82d2aa00..df659abde 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -495,6 +495,8 @@ "privacy.limited.short": "Circle", "privacy.mutual.long": "Visible for mutual followers only (Supported servers only)", "privacy.mutual.short": "Mutual-followers-only", + "privacy.none.long": "No visibility allowed", + "privacy.none.short": "None", "privacy.private.long": "Visible for followers only", "privacy.private.short": "Followers-only", "privacy.public.long": "Visible for all, shown in public timelines", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index a1a9ddb1d..1e85236dd 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -495,6 +495,8 @@ "privacy.limited.short": "サークル", "privacy.mutual.long": "相互フォローのみ閲覧可(対応サーバのみ)", "privacy.mutual.short": "相互フォロー限定", + "privacy.none.long": "許可された公開範囲なし", + "privacy.none.short": "なし", "privacy.private.long": "フォロワーのみ閲覧可", "privacy.private.short": "フォロワー限定", "privacy.public.long": "誰でも閲覧可、公開TLに表示", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 129ae1281..f81c6f2ed 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -111,6 +111,8 @@ const initialState = ImmutableMap({ expires_action: 'mark', references: ImmutableSet(), context_references: ImmutableSet(), + prohibited_visibilities: ImmutableSet(), + prohibited_words: ImmutableSet(), }); const initialPoll = ImmutableMap({ @@ -266,6 +268,14 @@ const hydrate = (state, hydratedState) => { state = state.set('text', hydratedState.get('text')); } + if (hydratedState.has('prohibited_visibilities')) { + state = state.set('prohibited_visibilities', hydratedState.get('prohibited_visibilities').toSet()); + } + + if (hydratedState.has('prohibited_words')) { + state = state.set('prohibited_words', hydratedState.get('prohibited_words').toSet()); + } + return state; }; diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 62ac2ded2..c2472fee3 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -134,6 +134,11 @@ a.table-action-link { &:first-child { padding-left: 0; } + + &:disabled, &:disabled:hover { + color: darken($ui-primary-color, 30%); + cursor: default; + } } .batch-table { diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index f4ffd57a7..b45e406ac 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -24,341 +24,107 @@ class UserSettingsDecorator setting_enable_reaction ).freeze + NESTED_KEYS = %w( + notification_emails + interactions + ).freeze + + BOOLEAN_KEYS = %w( + default_sensitive + unfollow_modal + unsubscribe_modal + boost_modal + delete_modal + post_reference_modal + add_reference_modal + unselect_reference_modal + auto_play_gif + expand_spoiers + reduce_motion + disable_swiping + system_font_ui + noindex + hide_network + aggregate_reblogs + show_application + advanced_layout + use_blurhash + use_pending_items + trends + crop_images + confirm_domain_block + show_follow_button_on_timeline + show_subscribe_button_on_timeline + show_followed_by + follow_button_to_list_adder + show_navigation_panel + show_quote_button + show_bookmark_button + show_target + place_tab_bar_at_bottom + show_tab_bar_label + enable_limited_timeline + enable_reaction + compact_reaction + show_reply_tree_button + hide_statuses_count + hide_following_count + hide_followers_count + disable_joke_appearance + theme_public + enable_status_reference + match_visibility_of_references + hexagon_avatar + enable_empty_column + hide_bot_on_public_timeline + confirm_follow_from_bot + show_reload_button + disable_post + disable_reactions + disable_follow + disable_unfollow + disable_block + disable_domain_block + disable_clear_all_notifications + disable_account_delete + ).freeze + + STRING_KEYS = %w( + default_privacy + default_language + theme + display_media + new_features_policy + theme_instance_ticker + content_font_size + info_font_size + content_emoji_reaction_size + emoji_scale + picker_emoji_size + default_search_searchability + default_column_width + default_expires_in + default_expires_action + prohibited_visibilities + prohibited_words + ).freeze + def profile_change? settings.keys.intersection(PROFILE_KEYS).any? end def process_update - user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') - user.settings['interactions'] = merged_interactions if change?('interactions') - user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') - user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') - user.settings['default_language'] = default_language_preference if change?('setting_default_language') - user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') - user.settings['unsubscribe_modal'] = unsubscribe_modal_preference if change?('setting_unsubscribe_modal') - user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') - user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') - user.settings['post_reference_modal'] = post_reference_modal_preference if change?('setting_post_reference_modal') - user.settings['add_reference_modal'] = add_reference_modal_preference if change?('setting_add_reference_modal') - user.settings['unselect_reference_modal'] = unselect_reference_modal_preference if change?('setting_unselect_reference_modal') - user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') - user.settings['display_media'] = display_media_preference if change?('setting_display_media') - user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') - user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') - user.settings['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') - user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') - user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['theme'] = theme_preference if change?('setting_theme') - user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') - user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') - user.settings['show_application'] = show_application_preference if change?('setting_show_application') - user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') - user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') - 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') - user.settings['follow_button_to_list_adder'] = follow_button_to_list_adder_preference if change?('setting_follow_button_to_list_adder') - user.settings['show_navigation_panel'] = show_navigation_panel_preference if change?('setting_show_navigation_panel') - user.settings['show_quote_button'] = show_quote_button_preference if change?('setting_show_quote_button') - user.settings['show_bookmark_button'] = show_bookmark_button_preference if change?('setting_show_bookmark_button') - user.settings['show_target'] = show_target_preference if change?('setting_show_target') - user.settings['place_tab_bar_at_bottom'] = place_tab_bar_at_bottom_preference if change?('setting_place_tab_bar_at_bottom') - user.settings['show_tab_bar_label'] = show_tab_bar_label_preference if change?('setting_show_tab_bar_label') - user.settings['enable_limited_timeline'] = enable_limited_timeline_preference if change?('setting_enable_limited_timeline') - user.settings['enable_reaction'] = enable_reaction_preference if change?('setting_enable_reaction') - user.settings['compact_reaction'] = compact_reaction_preference if change?('setting_compact_reaction') - user.settings['show_reply_tree_button'] = show_reply_tree_button_preference if change?('setting_show_reply_tree_button') - user.settings['hide_statuses_count'] = hide_statuses_count_preference if change?('setting_hide_statuses_count') - user.settings['hide_following_count'] = hide_following_count_preference if change?('setting_hide_following_count') - user.settings['hide_followers_count'] = hide_followers_count_preference if change?('setting_hide_followers_count') - user.settings['disable_joke_appearance'] = disable_joke_appearance_preference if change?('setting_disable_joke_appearance') - user.settings['new_features_policy'] = new_features_policy if change?('setting_new_features_policy') - user.settings['theme_instance_ticker'] = theme_instance_ticker if change?('setting_theme_instance_ticker') - user.settings['theme_public'] = theme_public if change?('setting_theme_public') - user.settings['enable_status_reference'] = enable_status_reference_preference if change?('setting_enable_status_reference') - user.settings['match_visibility_of_references'] = match_visibility_of_references_preference if change?('setting_match_visibility_of_references') - user.settings['hexagon_avatar'] = hexagon_avatar_preference if change?('setting_hexagon_avatar') - user.settings['enable_empty_column'] = enable_empty_column_preference if change?('setting_enable_empty_column') - user.settings['content_font_size'] = content_font_size_preference if change?('setting_content_font_size') - user.settings['info_font_size'] = info_font_size_preference if change?('setting_info_font_size') - user.settings['content_emoji_reaction_size'] = content_emoji_reaction_size_preference if change?('setting_content_emoji_reaction_size') - user.settings['emoji_scale'] = emoji_scale_preference if change?('setting_emoji_scale') - user.settings['picker_emoji_size'] = picker_emoji_size_preference if change?('setting_picker_emoji_size') - user.settings['hide_bot_on_public_timeline'] = hide_bot_on_public_timeline_preference if change?('setting_hide_bot_on_public_timeline') - user.settings['confirm_follow_from_bot'] = confirm_follow_from_bot_preference if change?('setting_confirm_follow_from_bot') - 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') - user.settings['default_expires_in'] = default_expires_in_preference if change?('setting_default_expires_in') - user.settings['default_expires_action'] = default_expires_action_preference if change?('setting_default_expires_action') -end + NESTED_KEYS.each do |key| + user.settings[key] = user.settings[key].merge coerced_settings(key) if change?(key) + end - def merged_notification_emails - user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h - end + STRING_KEYS.each do |key| + user.settings[key] = settings["setting_#{key}"] if change?("setting_#{key}") + end - def merged_interactions - user.settings['interactions'].merge coerced_settings('interactions').to_h - end - - def default_privacy_preference - settings['setting_default_privacy'] - end - - def default_sensitive_preference - boolean_cast_setting 'setting_default_sensitive' - end - - def unfollow_modal_preference - boolean_cast_setting 'setting_unfollow_modal' - end - - def unsubscribe_modal_preference - boolean_cast_setting 'setting_unsubscribe_modal' - end - - def boost_modal_preference - boolean_cast_setting 'setting_boost_modal' - end - - def delete_modal_preference - boolean_cast_setting 'setting_delete_modal' - end - - def post_reference_modal_preference - boolean_cast_setting 'setting_post_reference_modal' - end - - def add_reference_modal_preference - boolean_cast_setting 'setting_add_reference_modal' - end - - def unselect_reference_modal_preference - boolean_cast_setting 'setting_unselect_reference_modal' - end - - def system_font_ui_preference - boolean_cast_setting 'setting_system_font_ui' - end - - def auto_play_gif_preference - boolean_cast_setting 'setting_auto_play_gif' - end - - def display_media_preference - settings['setting_display_media'] - end - - def expand_spoilers_preference - boolean_cast_setting 'setting_expand_spoilers' - end - - def reduce_motion_preference - boolean_cast_setting 'setting_reduce_motion' - end - - def disable_swiping_preference - boolean_cast_setting 'setting_disable_swiping' - end - - def noindex_preference - boolean_cast_setting 'setting_noindex' - end - - def hide_network_preference - boolean_cast_setting 'setting_hide_network' - end - - def show_application_preference - boolean_cast_setting 'setting_show_application' - end - - def theme_preference - settings['setting_theme'] - end - - def default_language_preference - settings['setting_default_language'] - end - - def aggregate_reblogs_preference - boolean_cast_setting 'setting_aggregate_reblogs' - end - - def advanced_layout_preference - boolean_cast_setting 'setting_advanced_layout' - end - - def use_blurhash_preference - boolean_cast_setting 'setting_use_blurhash' - end - - def use_pending_items_preference - boolean_cast_setting 'setting_use_pending_items' - end - - def trends_preference - boolean_cast_setting 'setting_trends' - end - - def crop_images_preference - 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 - - def show_subscribe_button_on_timeline_preference - boolean_cast_setting 'setting_show_subscribe_button_on_timeline' - end - - def show_followed_by_preference - boolean_cast_setting 'setting_show_followed_by' - end - - def follow_button_to_list_adder_preference - boolean_cast_setting 'setting_follow_button_to_list_adder' - end - - def show_navigation_panel_preference - boolean_cast_setting 'setting_show_navigation_panel' - end - - def show_quote_button_preference - boolean_cast_setting 'setting_show_quote_button' - end - - def show_bookmark_button_preference - boolean_cast_setting 'setting_show_bookmark_button' - end - - def show_target_preference - boolean_cast_setting 'setting_show_target' - end - - def place_tab_bar_at_bottom_preference - boolean_cast_setting 'setting_place_tab_bar_at_bottom' - end - - def show_tab_bar_label_preference - boolean_cast_setting 'setting_show_tab_bar_label' - end - - def enable_limited_timeline_preference - boolean_cast_setting 'setting_enable_limited_timeline' - end - - def enable_reaction_preference - boolean_cast_setting 'setting_enable_reaction' - end - - def compact_reaction_preference - boolean_cast_setting 'setting_compact_reaction' - end - - def show_reply_tree_button_preference - boolean_cast_setting 'setting_show_reply_tree_button' - end - - def hide_statuses_count_preference - boolean_cast_setting 'setting_hide_statuses_count' - end - - def hide_following_count_preference - boolean_cast_setting 'setting_hide_following_count' - end - - def hide_followers_count_preference - boolean_cast_setting 'setting_hide_followers_count' - end - - def disable_joke_appearance_preference - boolean_cast_setting 'setting_disable_joke_appearance' - end - - def new_features_policy - settings['setting_new_features_policy'] - end - - def theme_instance_ticker - settings['setting_theme_instance_ticker'] - end - - def theme_public - boolean_cast_setting 'setting_theme_public' - end - - def hexagon_avatar_preference - boolean_cast_setting 'setting_hexagon_avatar' - end - - def enable_status_reference_preference - boolean_cast_setting 'setting_enable_status_reference' - end - - def match_visibility_of_references_preference - boolean_cast_setting 'setting_match_visibility_of_references' - end - - def enable_empty_column_preference - boolean_cast_setting 'setting_enable_empty_column' - end - - def content_font_size_preference - settings['setting_content_font_size'] - end - - def info_font_size_preference - settings['setting_info_font_size'] - end - - def content_emoji_reaction_size_preference - settings['setting_content_emoji_reaction_size'] - end - - def emoji_scale_preference - settings['setting_emoji_scale'] - end - - def picker_emoji_size_preference - settings['setting_picker_emoji_size'] - end - - def hide_bot_on_public_timeline_preference - boolean_cast_setting 'setting_hide_bot_on_public_timeline' - end - - def confirm_follow_from_bot_preference - boolean_cast_setting 'setting_confirm_follow_from_bot' - end - - def default_search_searchability_preference - settings['setting_default_search_searchability'] - end - - def show_reload_button_preference - boolean_cast_setting 'setting_show_reload_button' - end - - def default_column_width_preference - settings['setting_default_column_width'] - end - - def default_expires_in_preference - settings['setting_default_expires_in'] - end - - def default_expires_action_preference - settings['setting_default_expires_action'] + BOOLEAN_KEYS.each do |key| + user.settings[key] = boolean_cast_setting "setting_#{key}" if change?("setting_#{key}") + end end def boolean_cast_setting(key) diff --git a/app/models/user.rb b/app/models/user.rb index 36399f83e..ae88221c8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -141,6 +141,8 @@ class User < ApplicationRecord :hide_bot_on_public_timeline, :confirm_follow_from_bot, :default_search_searchability, :default_expires_in, :default_expires_action, :show_reload_button, :default_column_width, + :disable_post, :disable_reactions, :disable_follow, :disable_unfollow, :disable_block, :disable_domain_block, :disable_clear_all_notifications, :disable_account_delete, + :prohibited_visibilities, :prohibited_words, to: :settings, prefix: :setting, allow_nil: false diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 3a7bd0da1..1c19192dd 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -76,6 +76,14 @@ class InitialStateSerializer < ActiveModel::Serializer store[:confirm_follow_from_bot] = object.current_account.user.setting_confirm_follow_from_bot store[:show_reload_button] = object.current_account.user.setting_show_reload_button store[:default_column_width] = object.current_account.user.setting_default_column_width + store[:disable_post] = object.current_account.user.setting_disable_post + store[:disable_reactions] = object.current_account.user.setting_disable_reactions + store[:disable_follow] = object.current_account.user.setting_disable_follow + store[:disable_unfollow] = object.current_account.user.setting_disable_unfollow + store[:disable_block] = object.current_account.user.setting_disable_block + store[:disable_domain_block] = object.current_account.user.setting_disable_domain_block + store[:disable_clear_all_notifications] = object.current_account.user.setting_disable_clear_all_notifications + store[:disable_account_delete] = object.current_account.user.setting_disable_account_delete else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media @@ -91,12 +99,14 @@ class InitialStateSerializer < ActiveModel::Serializer store = {} if object.current_account - store[:me] = object.current_account.id.to_s - store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy - store[:default_searchability] = object.current_account.searchability - store[:default_sensitive] = object.current_account.user.setting_default_sensitive - store[:default_expires_in] = object.current_account.user.setting_default_expires_in - store[:default_expires_action] = object.current_account.user.setting_default_expires_action + store[:me] = object.current_account.id.to_s + store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy + store[:default_searchability] = object.current_account.searchability + store[:default_sensitive] = object.current_account.user.setting_default_sensitive + store[:default_expires_in] = object.current_account.user.setting_default_expires_in + store[:default_expires_action] = object.current_account.user.setting_default_expires_action + store[:prohibited_visibilities] = object.current_account.user.setting_prohibited_visibilities.filter(&:present?) + store[:prohibited_words] = (object.current_account.user.setting_prohibited_words || '').split(',').map(&:strip).filter(&:present?) end store[:text] = object.text if object.text diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index c9491fa41..e6857f977 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -38,7 +38,9 @@ class PostStatusService < BaseService validate_media! validate_expires! + validate_prohibited_words! preprocess_attributes! + validate_prohibited_visibilities! preprocess_quote! if scheduled? @@ -152,6 +154,19 @@ class PostStatusService < BaseService expires_at.present? && expires_at <= Time.now.utc + MIN_SCHEDULE_OFFSET end + def validate_prohibited_words! + return if @options[:spoiler_text].blank? && @options[:text].blank? + + text = [@options[:spoiler_text], @options[:text]].join(' ') + words = (@account&.user&.setting_prohibited_words || '').split(',').map(&:strip).filter(&:present?) + + raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_words') if words.any? { |word| text.include? word } + end + + def validate_prohibited_visibilities! + raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if @account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(@visibility.to_s) + end + def validate_media! return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index cd0fc1a88..264832c3d 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -37,6 +37,8 @@ class ReblogService < BaseService end end + raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(visibility) + ApplicationRecord.transaction do reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit]) reblog.capability_tokens.create! if reblog.limited_visibility? diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index e315dfba0..4b344bf72 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -1,3 +1,7 @@ +:ruby + disable_follow = current_user.setting_disable_follow + disable_unfollow = current_user.setting_disable_unfollow + - content_for :page_title do = t('settings.relationships') @@ -48,13 +52,13 @@ %label.batch-table__toolbar__select.batch-checkbox-all = check_box_tag :batch_checkbox_all, nil, false .batch-table__toolbar__actions - = f.button safe_join([fa_icon('user-plus'), t('relationships.follow_selected_followers')]), name: :follow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? && !mutual_relationship? + = f.button safe_join([fa_icon('user-plus'), t('relationships.follow_selected_followers')]), name: :follow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_follow if followed_by_relationship? && !mutual_relationship? - = f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless followed_by_relationship? + = f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow unless followed_by_relationship? - = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship? + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow unless following_relationship? - = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow if followed_by_relationship? .batch-table__body - if @accounts.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index 08792e0af..ef5b51041 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -1,3 +1,6 @@ +:ruby + disable = current_user.setting_disable_account_delete + - content_for :page_title do = t('settings.delete') @@ -26,4 +29,4 @@ = f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username') .actions - = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative' + = f.button :button, t('deletes.proceed'), type: :submit, class: disable ? 'button disabled' : 'negative', disabled: disable diff --git a/app/views/settings/preferences/safety/show.html.haml b/app/views/settings/preferences/safety/show.html.haml new file mode 100644 index 000000000..a58e5b67d --- /dev/null +++ b/app/views/settings/preferences/safety/show.html.haml @@ -0,0 +1,45 @@ +- content_for :page_title do + = t('settings.safety') + +- content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences' + += simple_form_for current_user, url: settings_preferences_safety_path, html: { method: :put, id: 'edit_preferences' } do |f| + = render 'shared/error_messages', object: current_user + + %h4= t 'preferences.disable_actions' + + %p.hint= t 'preferences.disable_actions_hint' + + .fields-group + = f.input :setting_disable_reactions, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_follow, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_unfollow, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_block, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_domain_block, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_clear_all_notifications, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_account_delete, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_disable_post, as: :boolean, wrapper: :with_label, fedibird_features: true + + %h4= t 'preferences.post_prohibite' + + .fields-group + = f.input :setting_prohibited_visibilities, collection: Status.visibilities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| t("statuses.visibilities.#{visibility}") }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', fedibird_features: true + + .fields-group + = f.input :setting_prohibited_words, wrapper: :with_label, fedibird_features: true + diff --git a/config/locales/en.yml b/config/locales/en.yml index fea44747c..bfa76e024 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1245,10 +1245,14 @@ en: too_many_options: can't contain more than %{max} items preferences: beta_features: Beta features + disable_actions: Disable actions + disable_actions_hint: This setting is used to prevent accidents due to mishandling or to disable certain actions in accounts with limited use. It also applies to client applications. fedibird_features: Fedibird features other: Other + post_prohibite: Post prohibite posting_defaults: Posting defaults public_timelines: Public timelines + safety: Safety & Privacy searching_defaults: Searching default reactions: errors: @@ -1371,6 +1375,7 @@ en: preferences: Preferences profile: Profile relationships: Follows and followers + safety: Safety & Privacy statuses_cleanup: Automated post deletion two_factor_authentication: Two-factor Auth webauthn_authentication: Security keys @@ -1487,6 +1492,10 @@ en: expire_in_the_past: It is already past expires. You cannot specify expires before the posting time. invalid_expire_at: Invalid expires are specified. invalid_expire_action: Invalid expires_action are specified. + status_prohibit: + validations: + prohibited_visibilities: Prohibited visibility is specified. + prohibited_words: Prohibited words is specified. status_references: errors: limit: You have already reached your reference limit diff --git a/config/locales/ja.yml b/config/locales/ja.yml index c690fa9d8..c55484a4c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1191,10 +1191,14 @@ ja: too_many_options: は%{max}個までです preferences: beta_features: ベータ機能 + disable_actions: 操作の無効化 + disable_actions_hint: 誤操作による事故を防いだり、用途を限定したアカウント向けに、特定の操作を無効化するための設定です。クライアントアプリに対しても有効です。 fedibird_features: Fedibirdの機能 other: その他 + post_prohibite: 投稿の禁止 posting_defaults: デフォルトの投稿設定 public_timelines: 公開タイムライン + safety: 安全とプライバシー searching_defaults: デフォルトの検索設定 reactions: errors: @@ -1316,6 +1320,7 @@ ja: preferences: ユーザー設定 profile: プロフィール relationships: フォロー・フォロワー + safety: 安全とプライバシー two_factor_authentication: 二段階認証 webauthn_authentication: セキュリティキー statuses: @@ -1385,6 +1390,10 @@ ja: public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます unlisted: 未収載 unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません + status_prohibit: + validations: + prohibited_visibilities: 禁止された公開範囲を指定しています + prohibited_words: 禁止された単語を含んでいます status_expire: validations: expire_in_the_past: 既に公開期限を過ぎています。投稿時間より前の公開期限は指定できません diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 946acb43a..eaf147a79 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -70,7 +70,15 @@ en: The format is 1y2mo3d4h5m (1 year, 2 months, 3 days, 4 hours, 5 minutes later). setting_default_search_searchability: For clients that do not support advanced range settings, switch the settings here. Mastodon's standard behavior is "Reacted-users-only". Targeting "Public" makes it easier to discover unknown information, but if the results are noisy, narrowing the search range is effective. setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click + setting_disable_account_delete: Restrict account deletion due to temporary hesitation + setting_disable_block: Restricts you from accidentally blocking + setting_disable_clear_all_notifications: Restrict clearing all notifications + setting_disable_domain_block: Restrict domain blocking + setting_disable_follow: Restricts you from accidentally following setting_disable_joke_appearance: Disable April Fools' Day and other joke functions + setting_disable_post: Restricts you from accidentally posting + setting_disable_reactions: Restrict reactions for favorites, boosts ,emoji reactions and polls + setting_disable_unfollow: Restricts you from accidentally unfollowing setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media setting_display_media_show_all: Always show media @@ -89,6 +97,8 @@ en: setting_new_features_policy: Set the acceptance policy when new features are added to Fedibird. The recommended setting will enable many new features, so set it to disabled if it is not desirable setting_noindex: Affects your public profile and post pages setting_place_tab_bar_at_bottom: When using a touch device, you can operate tabs within the reach of your fingers. + setting_prohibited_visibilities: Prohibited visibilities + setting_prohibited_words: Prohibited words setting_show_application: The application you use to post will be displayed in the detailed view of your posts setting_show_bookmark_button: When turned off, the bookmark call will be in Mastodon's standard position (in the menu on the action bar) setting_show_follow_button_on_timeline: You can easily check the follow status and build a follow list quickly @@ -241,8 +251,16 @@ en: setting_default_search_searchability: Search range setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a post + setting_disable_account_delete: Disable account delete + setting_disable_block: Disable block + setting_disable_clear_all_notifications: Disable clear all notifications + setting_disable_domain_block: Disable domain block + setting_disable_follow: Disable follow setting_disable_joke_appearance: Disable joke feature to change appearance + setting_disable_post: Disable post + setting_disable_reactions: Disable reactions setting_disable_swiping: Disable swiping motions + setting_disable_unfollow: Disable unfollow setting_display_media: Media display setting_display_media_default: Default setting_display_media_hide_all: Hide all @@ -270,6 +288,8 @@ en: setting_picker_emoji_size: Emoji size in picker setting_place_tab_bar_at_bottom: Place the tab bar at the bottom setting_post_reference_modal: Show a confirmation dialog before making a post containing references + setting_prohibited_visibilities: Prohibited visibility + setting_prohibited_words: Prohibited words setting_reduce_motion: Reduce motion in animations setting_unselect_reference_modal: Show confirmation dialog before removing a reference setting_show_application: Disclose application used to send posts diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 401b59aeb..89d1c98d2 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -66,7 +66,15 @@ ja: setting_default_expires_in: 投稿日時を起点とする終了日時を指定します。書式は1y2mo3d4h5m(1年2ヶ月3日4時間5分後)です。 setting_default_search_searchability: 範囲の詳細設定に対応していないクライアントでは、ここで設定を切り替えてください。Mastodonの標準動作は『リアクション限定』です。『公開』を対象にすると未知の情報を発見しやすくなりますが、結果にノイズが多い場合は検索範囲を狭めると効果的です。 setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります + setting_disable_account_delete: 一時的な迷いでアカウント削除することを防ぎます + setting_disable_block: 誤ってブロックすることを防ぎます + setting_disable_clear_all_notifications: 通知の全消去を防ぎます + setting_disable_domain_block: 誤ってドメインブロックを実行することを防ぎます + setting_disable_follow: 誤ってフォローすることを防ぎます setting_disable_joke_appearance: エイプリルフール等のジョーク機能を無効にします + setting_disable_post: 誤って投稿することを防ぎます + setting_disable_reactions: 誤ってお気に入り・ブースト・絵文字リアクション・投票することを防ぎます + setting_disable_unfollow: 誤ってフォロー解除することを防ぎます setting_display_media_default: 閲覧注意としてマークされたメディアは隠す setting_display_media_hide_all: メディアを常に隠す setting_display_media_show_all: メディアを常に表示する @@ -85,6 +93,8 @@ ja: setting_new_features_policy: Fedibirdに新しい機能が追加された時の受け入れポリシーを設定します。推奨設定は多くの新機能を有効にするので、望ましくない場合は無効に設定してください setting_noindex: 公開プロフィールおよび各投稿ページに影響します setting_place_tab_bar_at_bottom: タッチデバイス使用時に、タブの操作を指の届く範囲で行えます + setting_prohibited_visibilities: 指定した公開範囲で投稿することを禁止します + setting_prohibited_words: 投稿で使用禁止する単語をカンマ区切りで指定します setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります setting_show_bookmark_button: オフにした場合、ブックマークの呼び出しはMastodon標準の位置となります(アクションバーのメニューの中) setting_show_follow_button_on_timeline: フォロー状態を確認し易くなり、素早くフォローリストを構築できます @@ -237,8 +247,16 @@ ja: setting_default_search_searchability: 検索の対象とする範囲 setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する + setting_disable_account_delete: アカウント削除を無効にする + setting_disable_block: ブロックを無効にする + setting_disable_clear_all_notifications: 通知の全消去を無効にする + setting_disable_domain_block: ドメインブロックを無効にする + setting_disable_follow: フォローを無効にする setting_disable_joke_appearance: ジョーク機能による見た目の変更を無効にする + setting_disable_post: 投稿を無効にする + setting_disable_reactions: リアクションを無効にする setting_disable_swiping: スワイプでの切り替えを無効にする + setting_disable_unfollow: フォロー解除を無効にする setting_display_media: メディアの表示 setting_display_media_default: 標準 setting_display_media_hide_all: 非表示 @@ -266,6 +284,8 @@ ja: setting_picker_emoji_size: 絵文字ピッカーの表示サイズ setting_place_tab_bar_at_bottom: タブバーを下に配置する setting_post_reference_modal: 参照を含む投稿をする前に確認ダイアログを表示する + setting_prohibited_visibilities: 投稿禁止する公開範囲 + setting_prohibited_words: 投稿禁止する単語 setting_reduce_motion: アニメーションの動きを減らす setting_unselect_reference_modal: 参照を解除する前に確認ダイアログを表示する setting_show_application: 送信したアプリを開示する diff --git a/config/navigation.rb b/config/navigation.rb index 007ca6eb6..76175a24e 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -15,6 +15,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_url s.item :favourite_domains, safe_join([fa_icon('users fw'), t('settings.favourite_domains')]), settings_favourite_domains_url s.item :favourite_tags, safe_join([fa_icon('hashtag fw'), t('settings.favourite_tags')]), settings_favourite_tags_url + s.item :safety, safe_join([fa_icon('shield fw'), t('preferences.safety')]), settings_preferences_safety_url s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url end diff --git a/config/routes.rb b/config/routes.rb index adee50d42..37dad0ef3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -114,6 +114,7 @@ Rails.application.routes.draw do namespace :preferences do resource :appearance, only: [:show, :update], controller: :appearance resource :notifications, only: [:show, :update] + resource :safety, only: [:show, :update], controller: :safety resource :other, only: [:show, :update], controller: :other end diff --git a/config/settings.yml b/config/settings.yml index 1e9185051..684e17a55 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -116,6 +116,16 @@ defaults: &defaults default_column_width: 'x100' default_expires_in: '' default_expires_action: 'mark' + disable_post: false + disable_reactions: false + disable_follow: false + disable_unfollow: false + disable_block: false + disable_domain_block: false + disable_clear_all_notifications: false + disable_account_delete: false + prohibited_visibilities: [] + prohibited_words: '' development: <<: *defaults