Skip to content

Commit

Permalink
Change max status characters to be configurable (mastodon#12265)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwrss committed Jul 6, 2023
1 parent da7588a commit 12d2667
Show file tree
Hide file tree
Showing 11 changed files with 61 additions and 16 deletions.
Expand Up @@ -65,6 +65,7 @@ class ComposeForm extends ImmutablePureComponent {
isInReply: PropTypes.bool,
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxStatusChars: PropTypes.number.isRequired,
};

static defaultProps = {
Expand All @@ -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) => {
Expand Down Expand Up @@ -280,7 +281,7 @@ class ComposeForm extends ImmutablePureComponent {
</div>

<div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
<CharacterCounter max={this.props.maxStatusChars} text={this.getFulltextForCharacterCounting()} />
</div>
</div>

Expand Down
Expand Up @@ -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) => ({
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/mastodon/reducers/server.js
Expand Up @@ -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({
Expand All @@ -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:
Expand Down
31 changes: 23 additions & 8 deletions app/models/form/admin_settings.rb
Expand Up @@ -2,6 +2,7 @@

class Form::AdminSettings
include ActiveModel::Model
include ActiveModel::Callbacks

KEYS = %i(
site_contact_username
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion app/serializers/initial_state_serializer.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/serializers/rest/instance_serializer.rb
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion app/serializers/rest/v1/instance_serializer.rb
Expand Up @@ -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,
},
Expand Down
10 changes: 7 additions & 3 deletions 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)
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/settings/appearance/show.html.haml
Expand Up @@ -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
2 changes: 2 additions & 0 deletions config/locales/simple_form.en.yml
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/settings.yml
Expand Up @@ -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
Expand Down

0 comments on commit 12d2667

Please sign in to comment.