about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-08-13 15:45:40 -0700
committerReverite <github@reverite.sh>2019-08-13 15:45:40 -0700
commit2e221bd5b6472d1eecb654b3518af7886d3dadaf (patch)
treeafb9e3a86ba3a93b6b5f04cdec6070351992fed4 /app
parent9528d3eda280ffac20a18c6f21bfc51f594e2c86 (diff)
parentb859eb001717dfc62aebb8eba47b84c75aebe4ef (diff)
Merge branch 'glitch' into production
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/settings/preferences_controller.rb1
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/javascript/flavours/glitch/components/button.js2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js3
-rw-r--r--app/javascript/flavours/glitch/features/emoji_picker/index.js16
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js6
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js1
-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/lib/user_settings_decorator.rb5
-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/models/user.rb2
-rw-r--r--app/serializers/initial_state_serializer.rb1
-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
-rw-r--r--app/views/settings/preferences/appearance/show.html.haml1
29 files changed, 107 insertions, 48 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/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 418ea5d7a..f05dbe0ea 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_expand_spoilers,
       :setting_reduce_motion,
       :setting_system_font_ui,
+      :setting_system_emoji_font,
       :setting_noindex,
       :setting_hide_network,
       :setting_hide_followers_count,
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/flavours/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.js
index 16868010c..cd6528f58 100644
--- a/app/javascript/flavours/glitch/components/button.js
+++ b/app/javascript/flavours/glitch/components/button.js
@@ -12,9 +12,9 @@ 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,
-    title: PropTypes.string,
   };
 
   static defaultProps = {
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index e9437c0a9..b0072533c 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -15,6 +15,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont
 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' },
@@ -141,7 +142,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 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/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 9821502d3..b255cf858 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -12,6 +12,7 @@ import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import detectPassiveEvents from 'detect-passive-events';
 import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';
+import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@@ -159,12 +160,12 @@ class ModifierPickerMenu extends React.PureComponent {
 
     return (
       <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
-        <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
-        <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
-        <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
-        <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
-        <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
-        <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
       </div>
     );
   }
@@ -199,7 +200,7 @@ class ModifierPicker extends React.PureComponent {
 
     return (
       <div className='emoji-picker-dropdown__modifiers'>
-        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
+        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
         <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
       </div>
     );
@@ -344,6 +345,7 @@ class EmojiPickerMenu extends React.PureComponent {
           backgroundImageFn={backgroundImageFn}
           autoFocus
           emojiTooltip
+          native={useSystemEmojiFont}
         />
 
         <ModifierPicker
diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index 2f40f9b08..b2d13cc95 100644
--- a/app/javascript/flavours/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -1,4 +1,4 @@
-import { autoPlayGif } from 'flavours/glitch/util/initial_state';
+import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/util/initial_state';
 import unicodeMapping from './emoji_unicode_mapping_light';
 import Trie from 'substring-trie';
 
@@ -12,7 +12,7 @@ const emojify = (str, customEmojis = {}) => {
   let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
   for (;;) {
     let match, i = 0, tag;
-    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
+    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
       i += str.codePointAt(i) < 65536 ? 1 : 2;
     }
     let rend, replacement = '';
@@ -57,7 +57,7 @@ const emojify = (str, customEmojis = {}) => {
         }
       }
       i = rend;
-    } else { // matched to unicode emoji
+    } else if (!useSystemEmojiFont) { // matched to unicode emoji
       const { filename, shortCode } = unicodeMapping[match];
       const title = shortCode ? `:${shortCode}:` : '';
       replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index caaa79bb3..4b6227cac 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -31,5 +31,6 @@ export const defaultContentType = getMeta('default_content_type');
 export const forceSingleColumn = getMeta('advanced_layout') === false;
 export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
+export const useSystemEmojiFont = getMeta('system_emoji_font');
 
 export default initialState;
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/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index a52172707..c822d54de 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -29,6 +29,7 @@ class UserSettingsDecorator
     user.settings['expand_spoilers']     = expand_spoilers_preference if change?('setting_expand_spoilers')
     user.settings['reduce_motion']       = reduce_motion_preference if change?('setting_reduce_motion')
     user.settings['system_font_ui']      = system_font_ui_preference if change?('setting_system_font_ui')
+    user.settings['system_emoji_font']   = system_emoji_font_preference if change?('setting_system_emoji_font')
     user.settings['noindex']             = noindex_preference if change?('setting_noindex')
     user.settings['hide_followers_count']= hide_followers_count_preference if change?('setting_hide_followers_count')
     user.settings['flavour']             = flavour_preference if change?('setting_flavour')
@@ -79,6 +80,10 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_system_font_ui'
   end
 
+  def system_emoji_font_preference
+    boolean_cast_setting 'setting_system_emoji_font'
+  end
+
   def auto_play_gif_preference
     boolean_cast_setting 'setting_auto_play_gif'
   end
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/models/user.rb b/app/models/user.rb
index 45a4b8989..341227d3e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -108,7 +108,7 @@ class User < ApplicationRecord
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
            :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
            :advanced_layout, :use_blurhash, :use_pending_items, :trends,
-           :default_content_type,
+           :default_content_type, :system_emoji_font,
            to: :settings, prefix: :setting, allow_nil: false
 
   attr_reader :invite_code
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index c8da6e725..c8f6bec7a 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -53,6 +53,7 @@ class InitialStateSerializer < ActiveModel::Serializer
       store[:is_staff]          = object.current_account.user.staff?
       store[:trends]            = Setting.trends && object.current_account.user.setting_trends
       store[:default_content_type] = object.current_account.user.setting_default_content_type
+      store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
     end
 
     store
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
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index 0bda49f44..900a7c6fb 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -21,6 +21,7 @@
     = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true
     = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
     = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
+    = f.input :setting_system_emoji_font, as: :boolean, wrapper: :with_label
 
   %h4= t 'appearance.discovery'