diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index e641d59f4d027e..b148a2330a0fb0 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -65,6 +65,7 @@ class ComposeForm extends ImmutablePureComponent { isInReply: PropTypes.bool, singleColumn: PropTypes.bool, lang: PropTypes.string, + maxStatusChars: PropTypes.number.isRequired, }; static defaultProps = { @@ -90,7 +91,7 @@ class ComposeForm extends ImmutablePureComponent { const fulltext = this.getFulltextForCharacterCounting(); const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0; - return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia)); + return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > this.props.maxStatusChars || (isOnlyWhitespace && !anyMedia)); }; handleSubmit = (e) => { @@ -280,7 +281,7 @@ class ComposeForm extends ImmutablePureComponent {
- +
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 2b764223766311..cf60293aa4254c 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,7 @@ const mapStateToProps = state => ({ anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, isInReply: state.getIn(['compose', 'in_reply_to']) !== null, lang: state.getIn(['compose', 'language']), + maxStatusChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters']), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js index db9f2b5e6b61a6..73297610abc0e3 100644 --- a/app/javascript/mastodon/reducers/server.js +++ b/app/javascript/mastodon/reducers/server.js @@ -10,6 +10,7 @@ import { SERVER_DOMAIN_BLOCKS_FETCH_FAIL, } from 'mastodon/actions/server'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { STORE_HYDRATE } from '../actions/store'; const initialState = ImmutableMap({ server: ImmutableMap({ @@ -27,8 +28,12 @@ const initialState = ImmutableMap({ }), }); +const hydrate = (state, server) => state.mergeDeep(server); + export default function server(state = initialState, action) { switch (action.type) { + case STORE_HYDRATE: + return hydrate(state, action.state.get('server')); case SERVER_FETCH_REQUEST: return state.setIn(['server', 'isLoading'], true); case SERVER_FETCH_SUCCESS: diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 070478e8ee1c21..c5cca8561615da 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -2,6 +2,7 @@ class Form::AdminSettings include ActiveModel::Model + include ActiveModel::Callbacks KEYS = %i( site_contact_username @@ -33,12 +34,14 @@ class Form::AdminSettings content_cache_retention_period backups_retention_period status_page_url + status_max_chars ).freeze INTEGER_KEYS = %i( media_cache_retention_period content_cache_retention_period backups_retention_period + status_max_chars ).freeze BOOLEAN_KEYS = %i( @@ -67,11 +70,21 @@ class Form::AdminSettings validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) } validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } - validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } + validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, + :status_max_chars, numericality: { only_integer: true }, allow_blank: true, + if: -> { + defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) || + defined?(@status_max_chars) + } validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) } validates :status_page_url, url: true, allow_blank: true validate :validate_site_uploads + define_model_callbacks :save + before_save do + @status_max_chars = StatusLengthValidator::DEFAULT_MAX_CHARS if @status_max_chars.blank? + end + KEYS.each do |key| define_method(key) do return instance_variable_get("@#{key}") if instance_variable_defined?("@#{key}") @@ -103,14 +116,16 @@ def save # So for now, return early if errors aren't empty. return false unless errors.empty? && valid? - KEYS.each do |key| - next unless instance_variable_defined?("@#{key}") + run_callbacks(:save) do + KEYS.each do |key| + next unless instance_variable_defined?("@#{key}") - if UPLOAD_KEYS.include?(key) - public_send(key).save - else - setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: typecast_value(key, instance_variable_get("@#{key}"))) + if UPLOAD_KEYS.include?(key) + public_send(key).save + else + setting = Setting.where(var: key).first_or_initialize(var: key) + setting.update(value: typecast_value(key, instance_variable_get("@#{key}"))) + end end end end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 24417bca7752c8..4d76cc13ca17f0 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, :media_attachments, :settings, - :languages + :languages, :server has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :role, serializer: REST::RoleSerializer @@ -107,6 +107,19 @@ def languages LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] } end + def server + { + server: { + configuration: + { + statuses: { + max_characters: StatusLengthValidator.max_chars, + }, + }, + }, + } + end + private def instance_presenter diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index e280f8eb63b88e..f848080dcf7244 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -53,7 +53,7 @@ def configuration }, statuses: { - max_characters: StatusLengthValidator::MAX_CHARS, + max_characters: StatusLengthValidator.max_chars, max_media_attachments: 4, characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS, }, diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 99d1b2bd62b75f..d53dc3c55c6ca5 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -63,7 +63,7 @@ def configuration }, statuses: { - max_characters: StatusLengthValidator::MAX_CHARS, + max_characters: StatusLengthValidator.max_chars, max_media_attachments: 4, characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS, }, diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index e107912b77df63..a61e57fec803be 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -1,20 +1,24 @@ # frozen_string_literal: true class StatusLengthValidator < ActiveModel::Validator - MAX_CHARS = 500 + DEFAULT_MAX_CHARS = 500 URL_PLACEHOLDER_CHARS = 23 URL_PLACEHOLDER = 'x' * 23 + def self.max_chars + Setting.status_max_chars || DEFAULT_MAX_CHARS + end + def validate(status) return unless status.local? && !status.reblog? - status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?(status) + status.errors.add(:text, I18n.t('statuses.over_character_limit', max: StatusLengthValidator.max_chars)) if too_long?(status) end private def too_long?(status) - countable_length(combined_text(status)) > MAX_CHARS + countable_length(combined_text(status)) > StatusLengthValidator.max_chars end def countable_length(str) diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml index d321c4b04bf2b2..1afdf9b9e83f64 100644 --- a/app/views/admin/settings/appearance/show.html.haml +++ b/app/views/admin/settings/appearance/show.html.haml @@ -30,5 +30,8 @@ = fa_icon 'trash fw' = t('admin.site_uploads.delete') + .fields-group + = f.input :status_max_chars, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 96b0131efe1eb9..a6c9fffad15ea0 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -92,6 +92,7 @@ en: site_terms: Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax. site_title: How people may refer to your server besides its domain name. status_page_url: URL of a page where people can see the status of this server during an outage + status_max_chars: Maximum number of characters allowed in a status. Leave blank to reset to the default. theme: Theme that logged out visitors and new users see. thumbnail: A roughly 2:1 image displayed alongside your server information. timeline_preview: Logged out visitors will be able to browse the most recent public posts available on the server. @@ -254,6 +255,7 @@ en: site_terms: Privacy Policy site_title: Server name status_page_url: Status page URL + status_max_chars: Max status characters theme: Default theme thumbnail: Server thumbnail timeline_preview: Allow unauthenticated access to public timelines diff --git a/config/settings.yml b/config/settings.yml index f0b09dd5c88700..812b8e2a806daa 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -71,6 +71,7 @@ defaults: &defaults show_domain_blocks_rationale: 'disabled' require_invite_text: false backups_retention_period: 7 + status_max_chars: 500 development: <<: *defaults