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"