Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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'])) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -4,7 +4,7 @@ module DomainNormalizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_validation :normalize_domain
|
||||
before_save :normalize_domain
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -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) }
|
||||
|
@ -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}%")) }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
17
app/validators/domain_validator.rb
Normal file
17
app/validators/domain_validator.rb
Normal file
@ -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
|
@ -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
|
||||
|
Reference in New Issue
Block a user