about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/accounts_controller.rb2
-rw-r--r--app/controllers/admin/tags_controller.rb2
-rw-r--r--app/controllers/api/base_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb3
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/javascript/mastodon/components/button.js2
-rw-r--r--app/javascript/mastodon/components/status_content.js19
-rw-r--r--app/javascript/mastodon/features/account/components/header.js3
-rw-r--r--app/models/account.rb2
-rw-r--r--app/models/account_domain_block.rb2
-rw-r--r--app/models/concerns/domain_normalizable.rb2
-rw-r--r--app/models/custom_emoji.rb4
-rw-r--r--app/models/domain_allow.rb2
-rw-r--r--app/models/domain_block.rb2
-rw-r--r--app/models/email_domain_block.rb2
-rw-r--r--app/models/trending_tags.rb3
-rw-r--r--app/services/account_search_service.rb44
-rw-r--r--app/validators/domain_validator.rb17
-rw-r--r--app/views/admin/tags/show.html.haml2
19 files changed, 82 insertions, 35 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 1aed1af8d..1a876b831 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -9,6 +9,8 @@ class AccountsController < ApplicationController
   before_action :set_cache_headers
   before_action :set_body_classes
 
+  skip_around_action :set_locale, if: -> { request.format == :json }
+
   def show
     respond_to do |format|
       format.html do
diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb
index d62361eaa..39aca2a4b 100644
--- a/app/controllers/admin/tags_controller.rb
+++ b/app/controllers/admin/tags_controller.rb
@@ -71,7 +71,7 @@ module Admin
       now = Time.now.utc.beginning_of_day.to_date
 
       (Date.commercial(now.cwyear, now.cweek)..now).map do |date|
-        date.to_time.utc.beginning_of_day.to_i
+        date.to_time(:utc).beginning_of_day.to_i
       end
     end
   end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 109e38ffa..de8fff30e 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -14,6 +14,8 @@ class Api::BaseController < ApplicationController
 
   protect_from_forgery with: :null_session
 
+  skip_around_action :set_locale
+
   rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
     render json: { error: e.to_s }, status: 422
   end
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 8cd8f8e79..13cb4caf1 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -3,7 +3,8 @@
 class Api::V1::Accounts::StatusesController < Api::BaseController
   before_action -> { authorize_if_got_token! :read, :'read:statuses' }
   before_action :set_account
-  after_action :insert_pagination_headers
+
+  after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
 
   respond_to :json
 
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 3d7e61e77..83c412d5c 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -18,6 +18,8 @@ class StatusesController < ApplicationController
   before_action :set_body_classes
   before_action :set_autoplay, only: :embed
 
+  skip_around_action :set_locale, if: -> { request.format == :json }
+
   content_security_policy only: :embed do |p|
     p.frame_ancestors(false)
   end
diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js
index 51e2e6a7a..eb8dd7dc8 100644
--- a/app/javascript/mastodon/components/button.js
+++ b/app/javascript/mastodon/components/button.js
@@ -12,6 +12,7 @@ export default class Button extends React.PureComponent {
     secondary: PropTypes.bool,
     size: PropTypes.number,
     className: PropTypes.string,
+    title: PropTypes.string,
     style: PropTypes.object,
     children: PropTypes.node,
   };
@@ -54,6 +55,7 @@ export default class Button extends React.PureComponent {
         onClick={this.handleClick}
         ref={this.setRef}
         style={style}
+        title={this.props.title}
       >
         {this.props.text || this.props.children}
       </button>
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 76117f1d9..6aa0bfcc2 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -166,11 +166,6 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  handleCollapsedClick = (e) => {
-    e.preventDefault();
-    this.setState({ collapsed: !this.state.collapsed });
-  }
-
   setRef = (c) => {
     this.node = c;
   }
@@ -234,15 +229,19 @@ export default class StatusContent extends React.PureComponent {
         </div>
       );
     } else if (this.props.onClick) {
-      return (
+      const output = [
         <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
           <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
 
-          {!!this.state.collapsed && readMoreButton}
-
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
-        </div>
-      );
+        </div>,
+      ];
+
+      if (this.state.collapsed) {
+        output.push(readMoreButton);
+      }
+
+      return output;
     } else {
       return (
         <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index cab67c607..ac97bad71 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -15,6 +15,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
   edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@@ -148,7 +149,7 @@ class Header extends ImmutablePureComponent {
       if (!account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = '';
       } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
+        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
       } else if (!account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
diff --git a/app/models/account.rb b/app/models/account.rb
index 92e60f747..38379d20e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -508,7 +508,7 @@ class Account < ApplicationRecord
   end
 
   def emojifiable_text
-    [note, display_name, fields.map(&:value)].join(' ')
+    [note, display_name, fields.map(&:name), fields.map(&:value)].join(' ')
   end
 
   def clean_feed_manager
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index 7c0d60379..3aaffde9a 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -15,7 +15,7 @@ class AccountDomainBlock < ApplicationRecord
   include DomainNormalizable
 
   belongs_to :account
-  validates :domain, presence: true, uniqueness: { scope: :account_id }
+  validates :domain, presence: true, uniqueness: { scope: :account_id }, domain: true
 
   after_commit :remove_blocking_cache
   after_commit :remove_relationship_cache
diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb
index fb84058fc..c00b3142f 100644
--- a/app/models/concerns/domain_normalizable.rb
+++ b/app/models/concerns/domain_normalizable.rb
@@ -4,7 +4,7 @@ module DomainNormalizable
   extend ActiveSupport::Concern
 
   included do
-    before_validation :normalize_domain
+    before_save :normalize_domain
   end
 
   private
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 514cf4845..643a7e46a 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -28,6 +28,8 @@ class CustomEmoji < ApplicationRecord
     :(#{SHORTCODE_RE_FRAGMENT}):
     (?=[^[:alnum:]:]|$)/x
 
+  IMAGE_MIME_TYPES = %w(image/png image/gif image/webp).freeze
+
   belongs_to :category, class_name: 'CustomEmojiCategory', optional: true
   has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode
 
@@ -35,7 +37,7 @@ class CustomEmoji < ApplicationRecord
 
   before_validation :downcase_domain
 
-  validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { less_than: LIMIT }
+  validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT }
   validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }
 
   scope :local,      -> { where(domain: nil) }
diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb
index 85018b636..5fe0e3a29 100644
--- a/app/models/domain_allow.rb
+++ b/app/models/domain_allow.rb
@@ -13,7 +13,7 @@
 class DomainAllow < ApplicationRecord
   include DomainNormalizable
 
-  validates :domain, presence: true, uniqueness: true
+  validates :domain, presence: true, uniqueness: true, domain: true
 
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 3f5b9f23e..37b8d98c6 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -19,7 +19,7 @@ class DomainBlock < ApplicationRecord
 
   enum severity: [:silence, :suspend, :noop]
 
-  validates :domain, presence: true, uniqueness: true
+  validates :domain, presence: true, uniqueness: true, domain: true
 
   has_many :accounts, foreign_key: :domain, primary_key: :domain
   delegate :count, to: :accounts, prefix: true
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index 0fcd36477..bc70dea25 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -12,7 +12,7 @@
 class EmailDomainBlock < ApplicationRecord
   include DomainNormalizable
 
-  validates :domain, presence: true, uniqueness: true
+  validates :domain, presence: true, uniqueness: true, domain: true
 
   def self.block?(email)
     _, domain = email.split('@', 2)
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index 594ae9520..3d60a7fea 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -6,6 +6,7 @@ class TrendingTags
   EXPIRE_TRENDS_AFTER  = 1.day.seconds
   THRESHOLD            = 5
   LIMIT                = 10
+  REVIEW_THRESHOLD     = 3
 
   class << self
     include Redisable
@@ -60,7 +61,7 @@ class TrendingTags
         old_rank = redis.zrevrank(key, tag.id)
 
         redis.zadd(key, score, tag.id)
-        request_review!(tag) if (old_rank.nil? || old_rank > LIMIT) && redis.zrevrank(key, tag.id) <= LIMIT && !tag.trendable? && tag.requires_review? && !tag.requested_review?
+        request_review!(tag) if (old_rank.nil? || old_rank > REVIEW_THRESHOLD) && redis.zrevrank(key, tag.id) <= REVIEW_THRESHOLD && !tag.trendable? && tag.requires_review? && !tag.requested_review?
       end
 
       redis.expire(key, EXPIRE_TRENDS_AFTER)
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index 7bdffbbd2..e1874d045 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -21,18 +21,22 @@ class AccountSearchService < BaseService
     if resolving_non_matching_remote_account?
       [ResolveAccountService.new.call("#{query_username}@#{query_domain}")].compact
     else
-      search_results_and_exact_match.compact.uniq.slice(0, limit)
+      search_results_and_exact_match.compact.uniq
     end
   end
 
   def resolving_non_matching_remote_account?
-    options[:resolve] && !exact_match && !domain_is_local?
+    offset.zero? && options[:resolve] && !exact_match? && !domain_is_local?
   end
 
   def search_results_and_exact_match
-    exact = [exact_match]
-    return exact if !exact[0].nil? && limit == 1
-    exact + search_results.to_a
+    return search_results.to_a unless offset.zero?
+
+    results = [exact_match]
+
+    return results if exact_match? && limit == 1
+
+    results + search_results.to_a
   end
 
   def query_blank_or_hashtag?
@@ -40,15 +44,15 @@ class AccountSearchService < BaseService
   end
 
   def split_query_string
-    @_split_query_string ||= query.gsub(/\A@/, '').split('@')
+    @split_query_string ||= query.gsub(/\A@/, '').split('@')
   end
 
   def query_username
-    @_query_username ||= split_query_string.first || ''
+    @query_username ||= split_query_string.first || ''
   end
 
   def query_domain
-    @_query_domain ||= query_without_split? ? nil : split_query_string.last
+    @query_domain ||= query_without_split? ? nil : split_query_string.last
   end
 
   def query_without_split?
@@ -56,15 +60,21 @@ class AccountSearchService < BaseService
   end
 
   def domain_is_local?
-    @_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
+    @domain_is_local ||= TagManager.instance.local_domain?(query_domain)
   end
 
   def search_from
     options[:following] && account ? account.following : Account
   end
 
+  def exact_match?
+    exact_match.present?
+  end
+
   def exact_match
-    @_exact_match ||= begin
+    return @exact_match if defined?(@exact_match)
+
+    @exact_match = begin
       if domain_is_local?
         search_from.without_suspended.find_local(query_username)
       else
@@ -74,7 +84,7 @@ class AccountSearchService < BaseService
   end
 
   def search_results
-    @_search_results ||= begin
+    @search_results ||= begin
       if account
         advanced_search_results
       else
@@ -84,11 +94,19 @@ class AccountSearchService < BaseService
   end
 
   def advanced_search_results
-    Account.advanced_search_for(terms_for_query, account, limit, options[:following], offset)
+    Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset)
   end
 
   def simple_search_results
-    Account.search_for(terms_for_query, limit, offset)
+    Account.search_for(terms_for_query, limit_for_non_exact_results, offset)
+  end
+
+  def limit_for_non_exact_results
+    if offset.zero? && exact_match?
+      limit - 1
+    else
+      limit
+    end
   end
 
   def terms_for_query
diff --git a/app/validators/domain_validator.rb b/app/validators/domain_validator.rb
new file mode 100644
index 000000000..ae07f1798
--- /dev/null
+++ b/app/validators/domain_validator.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class DomainValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    return if value.blank?
+
+    record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(value)
+  end
+
+  private
+
+  def compliant?(value)
+    Addressable::URI.new.tap { |uri| uri.host = value }
+  rescue Addressable::URI::InvalidURIError
+    false
+  end
+end
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index 6a1e03065..c3779d48c 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -41,5 +41,5 @@
       - @usage_by_domain.each do |(domain, count)|
         %tr
           %th= domain || site_hostname
-          %td= "#{number_with_delimiter((count.to_f / @tag.history[0][:uses].to_f) * 100)}%"
+          %td= number_to_percentage((count / @tag.history[0][:uses].to_f) * 100)
           %td= number_with_delimiter count