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