Merge branch 'main' into glitch-soc/merge-upstream
This commit is contained in:
@@ -22,7 +22,7 @@ module Admin
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
|
||||
@domain_block.errors[:domain].clear
|
||||
@domain_block.errors.delete(:domain)
|
||||
render :new
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
|
@@ -31,7 +31,9 @@ module CacheConcern
|
||||
def cache_collection(raw, klass)
|
||||
return raw unless klass.respond_to?(:with_includes)
|
||||
|
||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||
return [] if raw.empty?
|
||||
|
||||
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
||||
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
||||
|
||||
|
@@ -55,6 +55,16 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<ClearColumnButton onClick={onClear} />
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' />
|
||||
</span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
||||
|
@@ -60,8 +60,8 @@ const mapStateToProps = state => ({
|
||||
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
|
||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
||||
lastReadId: state.getIn(['notifications', 'readMarkerId']),
|
||||
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
||||
lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0',
|
||||
canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
||||
needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
|
||||
});
|
||||
|
||||
|
@@ -45,6 +45,7 @@ const initialState = ImmutableMap({
|
||||
}),
|
||||
|
||||
dismissPermissionBanner: false,
|
||||
showUnread: true,
|
||||
|
||||
shows: ImmutableMap({
|
||||
follow: true,
|
||||
|
@@ -16,7 +16,9 @@ class EntityCache
|
||||
end
|
||||
|
||||
def emoji(shortcodes, domain)
|
||||
shortcodes = Array(shortcodes)
|
||||
shortcodes = Array(shortcodes)
|
||||
return [] if shortcodes.empty?
|
||||
|
||||
cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
|
||||
uncached_ids = []
|
||||
|
||||
|
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mastodon
|
||||
class Error < StandardError; end
|
||||
class NotPermittedError < Error; end
|
||||
class ValidationError < Error; end
|
||||
class HostValidationError < ValidationError; end
|
||||
class LengthValidationError < ValidationError; end
|
||||
class DimensionsValidationError < ValidationError; end
|
||||
class StreamValidationError < ValidationError; end
|
||||
class RaceConditionError < Error; end
|
||||
class RateLimitExceededError < Error; end
|
||||
|
||||
class UnexpectedResponseError < Error
|
||||
def initialize(response = nil)
|
||||
if response.respond_to? :uri
|
||||
super("#{response.uri} returned code #{response.code}")
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@@ -1,7 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'singleton'
|
||||
require_relative './sanitize_config'
|
||||
|
||||
class HTMLRenderer < Redcarpet::Render::HTML
|
||||
def block_code(code, language)
|
||||
@@ -223,9 +222,9 @@ class Formatter
|
||||
original_url, static_url = emoji
|
||||
replacement = begin
|
||||
if animate
|
||||
"<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(original_url)}\" />"
|
||||
image_tag(original_url, draggable: false, class: 'emojione', alt: ":#{shortcode}:", title: ":#{shortcode}:")
|
||||
else
|
||||
"<img draggable=\"false\" class=\"emojione custom-emoji\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(static_url)}\" data-original=\"#{original_url}\" data-static=\"#{static_url}\" />"
|
||||
image_tag(original_url, draggable: false, class: 'emojione custom-emoji', alt: ":#{shortcode}:", title: ":#{shortcode}:", data: { original: original_url, static: static_url })
|
||||
end
|
||||
end
|
||||
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
|
||||
|
@@ -1,139 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Sanitize
|
||||
module Config
|
||||
HTTP_PROTOCOLS = %w(
|
||||
http
|
||||
https
|
||||
).freeze
|
||||
|
||||
LINK_PROTOCOLS = %w(
|
||||
http
|
||||
https
|
||||
dat
|
||||
dweb
|
||||
ipfs
|
||||
ipns
|
||||
ssb
|
||||
gopher
|
||||
xmpp
|
||||
magnet
|
||||
gemini
|
||||
).freeze
|
||||
|
||||
CLASS_WHITELIST_TRANSFORMER = lambda do |env|
|
||||
node = env[:node]
|
||||
class_list = node['class']&.split(/[\t\n\f\r ]/)
|
||||
|
||||
return unless class_list
|
||||
|
||||
class_list.keep_if do |e|
|
||||
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
||||
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
||||
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
||||
end
|
||||
|
||||
node['class'] = class_list.join(' ')
|
||||
end
|
||||
|
||||
IMG_TAG_TRANSFORMER = lambda do |env|
|
||||
node = env[:node]
|
||||
|
||||
return unless env[:node_name] == 'img'
|
||||
|
||||
node.name = 'a'
|
||||
|
||||
node['href'] = node['src']
|
||||
if node['alt'].present?
|
||||
node.content = "[🖼 #{node['alt']}]"
|
||||
else
|
||||
url = node['href']
|
||||
prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
|
||||
text = url[prefix.length, 30]
|
||||
text = text + "…" if url[prefix.length..-1].length > 30
|
||||
node.content = "[🖼 #{text}]"
|
||||
end
|
||||
end
|
||||
|
||||
LINK_REL_TRANSFORMER = lambda do |env|
|
||||
return unless env[:node_name] == 'a' and env[:node]['href']
|
||||
|
||||
node = env[:node]
|
||||
|
||||
rel = (node['rel'] || '').split(' ') & ['tag']
|
||||
unless env[:config][:outgoing] && TagManager.instance.local_url?(node['href'])
|
||||
rel += ['nofollow', 'noopener', 'noreferrer']
|
||||
end
|
||||
node['rel'] = rel.join(' ')
|
||||
end
|
||||
|
||||
UNSUPPORTED_HREF_TRANSFORMER = lambda do |env|
|
||||
return unless env[:node_name] == 'a'
|
||||
|
||||
current_node = env[:node]
|
||||
|
||||
scheme = begin
|
||||
if current_node['href'] =~ Sanitize::REGEX_PROTOCOL
|
||||
Regexp.last_match(1).downcase
|
||||
else
|
||||
:relative
|
||||
end
|
||||
end
|
||||
|
||||
current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme)
|
||||
end
|
||||
|
||||
MASTODON_STRICT ||= freeze_config(
|
||||
elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li),
|
||||
|
||||
attributes: {
|
||||
'a' => %w(href rel class title),
|
||||
'span' => %w(class),
|
||||
'abbr' => %w(title),
|
||||
'blockquote' => %w(cite),
|
||||
'ol' => %w(start reversed),
|
||||
'li' => %w(value),
|
||||
},
|
||||
|
||||
add_attributes: {
|
||||
'a' => {
|
||||
'target' => '_blank',
|
||||
},
|
||||
},
|
||||
|
||||
protocols: {
|
||||
'a' => { 'href' => LINK_PROTOCOLS },
|
||||
'blockquote' => { 'cite' => LINK_PROTOCOLS },
|
||||
},
|
||||
|
||||
transformers: [
|
||||
CLASS_WHITELIST_TRANSFORMER,
|
||||
IMG_TAG_TRANSFORMER,
|
||||
UNSUPPORTED_HREF_TRANSFORMER,
|
||||
LINK_REL_TRANSFORMER,
|
||||
]
|
||||
)
|
||||
|
||||
MASTODON_OEMBED ||= freeze_config merge(
|
||||
RELAXED,
|
||||
elements: RELAXED[:elements] + %w(audio embed iframe source video),
|
||||
|
||||
attributes: merge(
|
||||
RELAXED[:attributes],
|
||||
'audio' => %w(controls),
|
||||
'embed' => %w(height src type width),
|
||||
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
|
||||
'source' => %w(src type),
|
||||
'video' => %w(controls height loop width),
|
||||
'div' => [:data]
|
||||
),
|
||||
|
||||
protocols: merge(
|
||||
RELAXED[:protocols],
|
||||
'embed' => { 'src' => HTTP_PROTOCOLS },
|
||||
'iframe' => { 'src' => HTTP_PROTOCOLS },
|
||||
'source' => { 'src' => HTTP_PROTOCOLS }
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
@@ -4,7 +4,7 @@ class NotificationMailer < ApplicationMailer
|
||||
helper :accounts
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
helper RoutingHelper
|
||||
|
||||
def mention(recipient, notification)
|
||||
@me = recipient
|
||||
|
@@ -8,7 +8,7 @@ class UserMailer < Devise::Mailer
|
||||
helper :instance
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
helper RoutingHelper
|
||||
|
||||
def confirmation_instructions(user, token, **)
|
||||
@resource = user
|
||||
|
@@ -18,46 +18,4 @@ class AccountStat < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :account_stat
|
||||
|
||||
update_index('accounts#account', :account)
|
||||
|
||||
def increment_count!(key)
|
||||
update(attributes_for_increment(key))
|
||||
rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique
|
||||
begin
|
||||
reload_with_id
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
return
|
||||
end
|
||||
|
||||
retry
|
||||
end
|
||||
|
||||
def decrement_count!(key)
|
||||
update(attributes_for_decrement(key))
|
||||
rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique
|
||||
begin
|
||||
reload_with_id
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
return
|
||||
end
|
||||
|
||||
retry
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attributes_for_increment(key)
|
||||
attrs = { key => public_send(key) + 1 }
|
||||
attrs[:last_status_at] = Time.now.utc if key == :statuses_count
|
||||
attrs
|
||||
end
|
||||
|
||||
def attributes_for_decrement(key)
|
||||
attrs = { key => [public_send(key) - 1, 0].max }
|
||||
attrs
|
||||
end
|
||||
|
||||
def reload_with_id
|
||||
self.id = self.class.find_by!(account: account).id if new_record?
|
||||
reload
|
||||
end
|
||||
end
|
||||
|
@@ -3,6 +3,8 @@
|
||||
module AccountCounters
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
|
||||
|
||||
included do
|
||||
has_one :account_stat, inverse_of: :account
|
||||
after_save :save_account_stat
|
||||
@@ -14,11 +16,65 @@ module AccountCounters
|
||||
:following_count=,
|
||||
:followers_count,
|
||||
:followers_count=,
|
||||
:increment_count!,
|
||||
:decrement_count!,
|
||||
:last_status_at,
|
||||
to: :account_stat
|
||||
|
||||
# @param [Symbol] key
|
||||
def increment_count!(key)
|
||||
update_count!(key, 1)
|
||||
end
|
||||
|
||||
# @param [Symbol] key
|
||||
def decrement_count!(key)
|
||||
update_count!(key, -1)
|
||||
end
|
||||
|
||||
# @param [Symbol] key
|
||||
# @param [Integer] value
|
||||
def update_count!(key, value)
|
||||
raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key)
|
||||
raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id)
|
||||
|
||||
value = value.to_i
|
||||
default_value = value.positive? ? value : 0
|
||||
|
||||
# We do an upsert using manually written SQL, as Rails' upsert method does
|
||||
# not seem to support writing expressions in the UPDATE clause, but only
|
||||
# re-insert the provided values instead.
|
||||
# Even ARel seem to be missing proper handling of upserts.
|
||||
sql = if value.positive? && key == :statuses_count
|
||||
<<-SQL.squish
|
||||
INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at)
|
||||
VALUES (:account_id, :default_value, now(), now(), now())
|
||||
ON CONFLICT (account_id) DO UPDATE
|
||||
SET #{key} = account_stats.#{key} + :value,
|
||||
last_status_at = now(),
|
||||
lock_version = account_stats.lock_version + 1,
|
||||
updated_at = now()
|
||||
RETURNING id;
|
||||
SQL
|
||||
else
|
||||
<<-SQL.squish
|
||||
INSERT INTO account_stats(account_id, #{key}, created_at, updated_at)
|
||||
VALUES (:account_id, :default_value, now(), now())
|
||||
ON CONFLICT (account_id) DO UPDATE
|
||||
SET #{key} = account_stats.#{key} + :value,
|
||||
lock_version = account_stats.lock_version + 1,
|
||||
updated_at = now()
|
||||
RETURNING id;
|
||||
SQL
|
||||
end
|
||||
|
||||
sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value])
|
||||
account_stat_id = AccountStat.connection.exec_query(sql)[0]['id']
|
||||
|
||||
# Reload account_stat if it was loaded, taking into account newly-created unsaved records
|
||||
if association(:account_stat).loaded?
|
||||
account_stat.id = account_stat_id if account_stat.new_record?
|
||||
account_stat.reload
|
||||
end
|
||||
end
|
||||
|
||||
def account_stat
|
||||
super || build_account_stat
|
||||
end
|
||||
|
@@ -49,12 +49,12 @@ class Notification < ApplicationRecord
|
||||
belongs_to :from_account, class_name: 'Account', optional: true
|
||||
belongs_to :activity, polymorphic: true, optional: true
|
||||
|
||||
belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :mention, foreign_key: 'activity_id', optional: true
|
||||
belongs_to :status, foreign_key: 'activity_id', optional: true
|
||||
belongs_to :follow, foreign_key: 'activity_id', optional: true
|
||||
belongs_to :follow_request, foreign_key: 'activity_id', optional: true
|
||||
belongs_to :favourite, foreign_key: 'activity_id', optional: true
|
||||
belongs_to :poll, foreign_key: 'activity_id', optional: true
|
||||
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UrlValidator < ActiveModel::EachValidator
|
||||
class URLValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value)
|
||||
end
|
||||
|
Reference in New Issue
Block a user