diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 47e189251..1011501ae 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -88,7 +88,7 @@ class ComposeForm extends ImmutablePureComponent {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
- if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
+ if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 512 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return;
}
@@ -181,7 +181,7 @@ class ComposeForm extends ImmutablePureComponent {
const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
- const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
+ const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 512 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@@ -243,7 +243,7 @@ class ComposeForm extends ImmutablePureComponent {
-
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index c65ebed74..cee0d85bc 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -8,3 +8,5 @@ loadPolyfills().then(() => {
}).catch(e => {
console.error(e);
});
+
+require('what-input');
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index ed713f335..56adb7a04 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -18,6 +18,12 @@ window.addEventListener('message', e => {
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
+
+ if (document.fonts && document.fonts.ready) {
+ document.fonts.ready.then(sizeBioText);
+ } else {
+ sizeBioText();
+ }
});
});
@@ -117,6 +123,25 @@ function main() {
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+ if (document.body.classList.contains('with-modals')) {
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ const scrollbarWidthStyle = document.createElement('style');
+ scrollbarWidthStyle.id = 'scrollbar-width';
+ document.head.appendChild(scrollbarWidthStyle);
+ scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0);
+ }
+
+ [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
+ const props = JSON.parse(content.getAttribute('data-props'));
+ ReactDOM.render(
, content);
+ });
+
+ if (document.fonts && document.fonts.ready) {
+ document.fonts.ready.then(sizeBioText);
+ } else {
+ sizeBioText();
+ }
});
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@@ -257,6 +282,22 @@ function main() {
target.style.display = 'block';
}
});
+
+ delegate(document, '#account_note', 'input', sizeBioText);
+
+ function sizeBioText() {
+ const noteCounter = document.querySelector('.note-counter');
+ const bioTextArea = document.querySelector('#account_note');
+
+ if (noteCounter) {
+ noteCounter.textContent = 413 - length(bioTextArea.value);
+ }
+
+ if (bioTextArea) {
+ bioTextArea.style.height = 'auto';
+ bioTextArea.style.height = (bioTextArea.scrollHeight+3) + 'px';
+ }
+ }
}
loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 32037dde9..4a0ec58e7 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -296,6 +296,12 @@
min-height: 1px;
}
+ h2 {
+ font-size: 1rem;
+ align-items: center;
+ display: flex;
+ }
+
.nav-left {
display: flex;
align-items: stretch;
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index a4a9baaee..ef35d34d1 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -90,7 +90,7 @@ class ActivityPub::Activity
crawl_links(status)
notify_about_reblog(status) if reblog_of_local_account?(status)
- notify_about_mentions(status)
+ notify_about_mentions(status) unless spammy_mentions?(status)
# Only continue if the status is supposed to have arrived in real-time.
# Note that if @options[:override_timestamps] isn't set, the status
@@ -105,6 +105,11 @@ class ActivityPub::Activity
status.reblog? && status.reblog.account.local?
end
+ def spammy_mentions?(status)
+ status.has_non_mention_links? &&
+ @account.followers.local.count == 0
+ end
+
def notify_about_reblog(status)
NotifyService.new.call(status.reblog.account, status)
end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 990b9f63e..1c8c0abfb 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -262,8 +262,9 @@ class Formatter
def link_to_mention(entity, linkable_accounts)
acct = entity[:screen_name]
+ username, domain = acct.split('@')
- return link_to_account(acct) unless linkable_accounts
+ return link_to_account(acct) unless linkable_accounts and domain != "twitter.com"
account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) }
account ? mention_html(account) : "@#{encode(acct)}"
@@ -272,6 +273,10 @@ class Formatter
def link_to_account(acct)
username, domain = acct.split('@')
+ if domain == "twitter.com"
+ return mention_twitter_html(username)
+ end
+
domain = nil if TagManager.instance.local_domain?(domain)
account = EntityCache.instance.mention(username, domain)
@@ -299,4 +304,8 @@ class Formatter
def mention_html(account)
"
@#{encode(account.username)}"
end
+
+ def mention_twitter_html(username)
+ "
@#{username}@twitter.com"
+ end
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 05936def3..0e3e53c58 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -81,6 +81,7 @@ class Account < ApplicationRecord
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
validates :note, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? }
+ validate :note_has_eight_newlines?, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
scope :remote, -> { where.not(domain: nil) }
@@ -310,9 +311,8 @@ class Account < ApplicationRecord
def save_with_optional_media!
save!
rescue ActiveRecord::RecordInvalid
- self.avatar = nil
- self.header = nil
-
+ self.avatar = nil if errors[:avatar].present?
+ self.header = nil if errors[:header].present?
save!
end
@@ -336,6 +336,10 @@ class Account < ApplicationRecord
shared_inbox_url.presence || inbox_url
end
+ def note_has_eight_newlines?
+ errors.add(:note, 'Bio can\'t have more then 8 newlines') unless note.count("\n") <= 8
+ end
+
class Field < ActiveModelSerializers::Model
attributes :name, :value, :verified_at, :account, :errors
diff --git a/app/models/status.rb b/app/models/status.rb
index 0c01a5389..50d7a1c74 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -398,6 +398,14 @@ class Status < ApplicationRecord
super || build_status_stat
end
+ def has_non_mention_links?
+ if local?
+ text.match? %r{https?://\w}
+ else
+ Nokogiri::HTML.fragment(text).css('a:not(.mention)').present?
+ end
+ end
+
private
def update_status_stat!(attrs)
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 1bd71683c..05ba27d60 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -39,6 +39,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.jpg')
end
+ def max_toot_chars
+ StatusLengthValidator::MAX_CHARS
+ end
+
def stats
{
user_count: instance_presenter.user_count,
diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb
index 93bae2fa8..3246b5ffb 100644
--- a/app/validators/status_length_validator.rb
+++ b/app/validators/status_length_validator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
- MAX_CHARS = 500
+ MAX_CHARS = 512
def validate(status)
return unless status.local? && !status.reblog?
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 9c26dbabc..53dad2967 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -21,6 +21,11 @@
= render 'header', account: @account, with_bio: true
+- if @account.user&.disabled?
+ .header
+ .nav-center
+ %h2 This user's account has been locked by a moderator.
+
.grid
.column-0
.h-feed
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index c014b5586..9480601bc 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -52,7 +52,6 @@
= fa_icon 'reply fw'
- else
= fa_icon 'reply-all fw'
- .status__action-bar__counter__label= obscured_counter status.replies_count
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
- if status.distributable?
= fa_icon 'retweet fw'
diff --git a/lib/mastodon/redis_config.rb b/lib/mastodon/redis_config.rb
index f11d94a45..088f2271b 100644
--- a/lib/mastodon/redis_config.rb
+++ b/lib/mastodon/redis_config.rb
@@ -22,7 +22,7 @@ setup_redis_env_url
setup_redis_env_url(:cache, false)
namespace = ENV.fetch('REDIS_NAMESPACE') { nil }
-cache_namespace = namespace ? namespace + '_cache' : 'cache'
+cache_namespace = [namespace, 'cache', `git rev-parse --short HEAD`.strip].compact.join('_')
REDIS_CACHE_PARAMS = {
expires_in: 10.minutes,
diff --git a/package.json b/package.json
index 433496b84..1f54bfa60 100644
--- a/package.json
+++ b/package.json
@@ -167,7 +167,8 @@
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.3.7",
"webpack-merge": "^4.2.1",
- "websocket.js": "^0.1.12"
+ "websocket.js": "^0.1.12",
+ "what-input": "^4.3.1"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
diff --git a/yarn.lock b/yarn.lock
index 74323bf5b..702edbfed 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10793,6 +10793,10 @@ websocket.js@^0.1.12:
dependencies:
backoff "^2.4.1"
+what-input@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/what-input/-/what-input-4.3.1.tgz#b8ea7554ba1d9171887c4c6addf28185fec3d31d"
+
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
version "1.0.5"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"