Merge commit 'e5826777b6c06a32b97388657beaca1e5eccb421' into glitch-soc/merge-upstream
Conflicts: - `config/settings.yml`: Not a real conflict, upstream removed settings that are identical in glitch-soc but textually adjacent to glitch-soc-only settings. Removed what upstream removed.
This commit is contained in:
@@ -6,10 +6,6 @@
|
|||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
Lint/NonLocalExitFromIterator:
|
|
||||||
Exclude:
|
|
||||||
- 'app/helpers/json_ld_helper.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 90
|
Max: 90
|
||||||
|
|||||||
22
Gemfile.lock
22
Gemfile.lock
@@ -96,7 +96,7 @@ GEM
|
|||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1131.0)
|
aws-partitions (1.1135.0)
|
||||||
aws-sdk-core (3.215.1)
|
aws-sdk-core (3.215.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
@@ -233,7 +233,7 @@ GEM
|
|||||||
fabrication (3.0.0)
|
fabrication (3.0.0)
|
||||||
faker (3.5.2)
|
faker (3.5.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.13.2)
|
faraday (2.13.4)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
@@ -345,7 +345,7 @@ GEM
|
|||||||
azure-blob (~> 0.5.2)
|
azure-blob (~> 0.5.2)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.13.0)
|
json (2.13.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.16.7)
|
json-jwt (1.16.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
@@ -438,7 +438,7 @@ GEM
|
|||||||
mime-types (3.7.0)
|
mime-types (3.7.0)
|
||||||
logger
|
logger
|
||||||
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||||
mime-types-data (3.2025.0715)
|
mime-types-data (3.2025.0722)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.5)
|
minitest (5.25.5)
|
||||||
@@ -468,7 +468,7 @@ GEM
|
|||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.1)
|
omniauth-cas (3.0.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
@@ -601,13 +601,13 @@ GEM
|
|||||||
ox (2.14.23)
|
ox (2.14.23)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.8.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.9)
|
pg (1.6.0)
|
||||||
pghero (3.7.0)
|
pghero (3.7.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
playwright-ruby-client (1.54.0)
|
playwright-ruby-client (1.54.0)
|
||||||
@@ -731,7 +731,7 @@ GEM
|
|||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.1)
|
rexml (3.4.1)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.2)
|
rouge (4.6.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (3.1.0)
|
rqrcode (3.1.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@@ -860,7 +860,7 @@ GEM
|
|||||||
stoplight (4.1.1)
|
stoplight (4.1.1)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.7)
|
stringio (3.1.7)
|
||||||
strong_migrations (2.4.0)
|
strong_migrations (2.5.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
swd (2.0.3)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
@@ -868,7 +868,7 @@ GEM
|
|||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.3)
|
temple (0.10.4)
|
||||||
terminal-table (4.0.0)
|
terminal-table (4.0.0)
|
||||||
unicode-display_width (>= 1.1.1, < 4)
|
unicode-display_width (>= 1.1.1, < 4)
|
||||||
terrapin (1.1.1)
|
terrapin (1.1.1)
|
||||||
@@ -1108,4 +1108,4 @@ RUBY VERSION
|
|||||||
ruby 3.4.1p0
|
ruby 3.4.1p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.7.0
|
2.7.1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module Admin
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :audit_log, :index?
|
authorize :audit_log, :index?
|
||||||
@auditable_accounts = Account.auditable.select(:id, :username)
|
@auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @appeal, :approve?
|
authorize @appeal, :reject?
|
||||||
log_action :reject, @appeal
|
log_action :reject, @appeal
|
||||||
@appeal.reject!(current_account)
|
@appeal.reject!(current_account)
|
||||||
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
|
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :update?
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -129,7 +129,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def requires_confirmation?
|
def requires_confirmation?
|
||||||
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm]
|
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.suspend? && !params[:confirm]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
77
app/controllers/admin/username_blocks_controller.rb
Normal file
77
app/controllers/admin/username_blocks_controller.rb
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::UsernameBlocksController < Admin::BaseController
|
||||||
|
before_action :set_username_block, only: [:edit, :update]
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :username_block, :index?
|
||||||
|
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
|
||||||
|
@form = Form::UsernameBlockBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
authorize :username_block, :index?
|
||||||
|
|
||||||
|
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_username_blocks_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize :username_block, :create?
|
||||||
|
@username_block = UsernameBlock.new(exact: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
authorize @username_block, :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :username_block, :create?
|
||||||
|
|
||||||
|
@username_block = UsernameBlock.new(resource_params)
|
||||||
|
|
||||||
|
if @username_block.save
|
||||||
|
log_action :create, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @username_block, :update?
|
||||||
|
|
||||||
|
if @username_block.update(resource_params)
|
||||||
|
log_action :update, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_username_block
|
||||||
|
@username_block = UsernameBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_username_block_batch_params
|
||||||
|
params
|
||||||
|
.expect(form_username_block_batch: [username_block_ids: []])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params
|
||||||
|
.expect(username_block: [:username, :comparison, :allow_with_approval])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
'delete' if params[:delete]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -66,7 +66,11 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
add_async_refresh_header(async_refresh)
|
add_async_refresh_header(async_refresh)
|
||||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||||
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
||||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id)
|
|
||||||
|
WorkerBatch.new.within do |batch|
|
||||||
|
batch.connect(refresh_key, threshold: 1.0)
|
||||||
|
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if current_account.moved_to_account_id.present?
|
if current_account.moved?
|
||||||
current_account.update!(moved_to_account: nil)
|
current_account.update!(moved_to_account: nil)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class StatusesController < ApplicationController
|
|||||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :redirect_to_original, only: :show
|
before_action :redirect_to_original, only: :show
|
||||||
|
before_action :verify_embed_allowed, only: :embed
|
||||||
|
|
||||||
after_action :set_link_headers
|
after_action :set_link_headers
|
||||||
|
|
||||||
@@ -40,8 +41,6 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
return not_found if @status.hidden? || @status.reblog?
|
|
||||||
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers.delete('X-Frame-Options')
|
||||||
|
|
||||||
@@ -50,6 +49,10 @@ class StatusesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def verify_embed_allowed
|
||||||
|
not_found if @status.hidden? || @status.reblog?
|
||||||
|
end
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new(
|
response.headers['Link'] = LinkHeader.new(
|
||||||
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
|
|||||||
end
|
end
|
||||||
when 'UserRole'
|
when 'UserRole'
|
||||||
link_to log.human_identifier, admin_roles_path(log.target_id)
|
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||||
|
when 'UsernameBlock'
|
||||||
|
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
|
||||||
when 'Report'
|
when 'Report'
|
||||||
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
||||||
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ module FormattingHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rss_content_preroll(status)
|
def rss_content_preroll(status)
|
||||||
if status.spoiler_text?
|
return unless status.spoiler_text?
|
||||||
safe_join [
|
|
||||||
tag.p { spoiler_with_warning(status) },
|
safe_join [
|
||||||
tag.hr,
|
tag.p { spoiler_with_warning(status) },
|
||||||
]
|
tag.hr,
|
||||||
end
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def spoiler_with_warning(status)
|
def spoiler_with_warning(status)
|
||||||
@@ -81,10 +81,10 @@ module FormattingHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rss_content_postroll(status)
|
def rss_content_postroll(status)
|
||||||
if status.preloadable_poll
|
return unless status.preloadable_poll
|
||||||
tag.p do
|
|
||||||
poll_option_tags(status)
|
tag.p do
|
||||||
end
|
poll_option_tags(status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ module JsonLdHelper
|
|||||||
patch_for_forwarding!(value, compacted_value)
|
patch_for_forwarding!(value, compacted_value)
|
||||||
elsif value.is_a?(Array)
|
elsif value.is_a?(Array)
|
||||||
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
||||||
return if value.size != compacted_value.size
|
return nil if value.size != compacted_value.size
|
||||||
|
|
||||||
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
||||||
if v.is_a?(Hash) && vc.is_a?(Hash)
|
if v.is_a?(Hash) && vc.is_a?(Hash)
|
||||||
|
|||||||
@@ -28,24 +28,24 @@ module ThemeHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def custom_stylesheet
|
def custom_stylesheet
|
||||||
if active_custom_stylesheet.present?
|
return if active_custom_stylesheet.blank?
|
||||||
stylesheet_link_tag(
|
|
||||||
custom_css_path(active_custom_stylesheet),
|
stylesheet_link_tag(
|
||||||
host: root_url,
|
custom_css_path(active_custom_stylesheet),
|
||||||
media: :all,
|
host: root_url,
|
||||||
skip_pipeline: true
|
media: :all,
|
||||||
)
|
skip_pipeline: true
|
||||||
end
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def active_custom_stylesheet
|
def active_custom_stylesheet
|
||||||
if cached_custom_css_digest.present?
|
return if cached_custom_css_digest.blank?
|
||||||
[:custom, cached_custom_css_digest.to_s.first(8)]
|
|
||||||
.compact_blank
|
[:custom, cached_custom_css_digest.to_s.first(8)]
|
||||||
.join('-')
|
.compact_blank
|
||||||
end
|
.join('-')
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_custom_css_digest
|
def cached_custom_css_digest
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ type KeyMatcher = (
|
|||||||
*/
|
*/
|
||||||
function just(keyName: string): KeyMatcher {
|
function just(keyName: string): KeyMatcher {
|
||||||
return (event) => ({
|
return (event) => ({
|
||||||
isMatch: normalizeKey(event.key) === keyName,
|
isMatch:
|
||||||
|
normalizeKey(event.key) === keyName &&
|
||||||
|
!event.altKey &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey,
|
||||||
priority: hotkeyPriority.singleKey,
|
priority: hotkeyPriority.singleKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
63
app/javascript/mastodon/components/learn_more_link.tsx
Normal file
63
app/javascript/mastodon/components/learn_more_link.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useRef, useCallback, useId } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const accessibilityId = useId();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
setOpen(!open);
|
||||||
|
}, [open, setOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className='link-button'
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={accessibilityId}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='learn_more_link.learn_more'
|
||||||
|
defaultMessage='Learn more'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Overlay
|
||||||
|
show={open}
|
||||||
|
rootClose
|
||||||
|
onHide={handleClick}
|
||||||
|
offset={[5, 5]}
|
||||||
|
placement='bottom-end'
|
||||||
|
target={triggerRef}
|
||||||
|
>
|
||||||
|
{({ props }) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
role='region'
|
||||||
|
id={accessibilityId}
|
||||||
|
className='account__domain-pill__popout learn-more__popout dropdown-animation'
|
||||||
|
>
|
||||||
|
<div className='learn-more__popout__content'>{children}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button className='link-button' onClick={handleClick}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='learn_more_link.got_it'
|
||||||
|
defaultMessage='Got it'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -48,13 +48,13 @@ class TranslateButton extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='translate-button'>
|
<div className='translate-button'>
|
||||||
<div className='translate-button__meta'>
|
|
||||||
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className='link-button' onClick={onClick}>
|
<button className='link-button' onClick={onClick}>
|
||||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className='translate-button__meta'>
|
||||||
|
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -138,6 +138,16 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove quote fallback link from the DOM so it doesn't
|
||||||
|
// mess with paragraph margins
|
||||||
|
if (!!status.get('quote')) {
|
||||||
|
const inlineQuote = node.querySelector('.quote-inline');
|
||||||
|
|
||||||
|
if (inlineQuote) {
|
||||||
|
inlineQuote.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseEnter = ({ currentTarget }) => {
|
handleMouseEnter = ({ currentTarget }) => {
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
|
import { LearnMoreLink } from 'mastodon/components/learn_more_link';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import StatusContainer from 'mastodon/containers/status_container';
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
import type { Status } from 'mastodon/models/status';
|
import type { Status } from 'mastodon/models/status';
|
||||||
import type { RootState } from 'mastodon/store';
|
import type { RootState } from 'mastodon/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import QuoteIcon from '../../images/quote.svg?react';
|
|
||||||
import { fetchStatus } from '../actions/statuses';
|
import { fetchStatus } from '../actions/statuses';
|
||||||
import { makeGetStatus } from '../selectors';
|
import { makeGetStatus } from '../selectors';
|
||||||
|
|
||||||
@@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{
|
|||||||
'status__quote--error': isError,
|
'status__quote--error': isError,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{
|
|||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const quoteAuthorName = account?.display_name_html;
|
const quoteAuthorName = account?.acct;
|
||||||
|
|
||||||
if (!quoteAuthorName) {
|
if (!quoteAuthorName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quoteAuthorElement = (
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
|
|
||||||
);
|
|
||||||
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={quoteUrl} className='status__quote-author-button'>
|
<div className='status__quote-author-button'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.quote_post_author'
|
id='status.quote_post_author'
|
||||||
defaultMessage='Post by {name}'
|
defaultMessage='Quoted a post by @{name}'
|
||||||
values={{ name: quoteAuthorElement }}
|
values={{ name: quoteAuthorName }}
|
||||||
/>
|
/>
|
||||||
<Icon id='chevron_right' icon={ChevronRightIcon} />
|
</div>
|
||||||
<Icon id='article' icon={ArticleIcon} />
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{
|
|||||||
defaultMessage='Hidden due to one of your filters'
|
defaultMessage='Hidden due to one of your filters'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (quoteState === 'deleted') {
|
|
||||||
quoteError = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='status.quote_error.removed'
|
|
||||||
defaultMessage='This post was removed by its author.'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (quoteState === 'unauthorized') {
|
|
||||||
quoteError = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='status.quote_error.unauthorized'
|
|
||||||
defaultMessage='This post cannot be displayed as you are not authorized to view it.'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (quoteState === 'pending') {
|
} else if (quoteState === 'pending') {
|
||||||
quoteError = (
|
quoteError = (
|
||||||
<FormattedMessage
|
<>
|
||||||
id='status.quote_error.pending_approval'
|
<FormattedMessage
|
||||||
defaultMessage='This post is pending approval from the original author.'
|
id='status.quote_error.pending_approval'
|
||||||
/>
|
defaultMessage='Post pending'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LearnMoreLink>
|
||||||
|
<h6>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.quote_error.pending_approval_popout.title'
|
||||||
|
defaultMessage='Pending quote? Remain calm'
|
||||||
|
/>
|
||||||
|
</h6>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.quote_error.pending_approval_popout.body'
|
||||||
|
defaultMessage='Quotes shared across the Fediverse may take time to display, as different servers have different protocols.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</LearnMoreLink>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
} else if (quoteState === 'rejected' || quoteState === 'revoked') {
|
} else if (
|
||||||
|
!status ||
|
||||||
|
!quotedStatusId ||
|
||||||
|
quoteState === 'deleted' ||
|
||||||
|
quoteState === 'rejected' ||
|
||||||
|
quoteState === 'revoked' ||
|
||||||
|
quoteState === 'unauthorized'
|
||||||
|
) {
|
||||||
quoteError = (
|
quoteError = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.quote_error.rejected'
|
id='status.quote_error.not_available'
|
||||||
defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.'
|
defaultMessage='Post unavailable'
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (!status || !quotedStatusId) {
|
|
||||||
quoteError = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='status.quote_error.not_found'
|
|
||||||
defaultMessage='This post cannot be displayed.'
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{
|
|||||||
isQuotedPost
|
isQuotedPost
|
||||||
id={quotedStatusId}
|
id={quotedStatusId}
|
||||||
contextType={contextType}
|
contextType={contextType}
|
||||||
avatarSize={40}
|
avatarSize={32}
|
||||||
>
|
>
|
||||||
{canRenderChildQuote && (
|
{canRenderChildQuote && (
|
||||||
<QuotedStatus
|
<QuotedStatus
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react';
|
|||||||
|
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchContext,
|
fetchContext,
|
||||||
completeContextRefresh,
|
completeContextRefresh,
|
||||||
@@ -22,11 +20,15 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
export const RefreshController: React.FC<{
|
export const RefreshController: React.FC<{
|
||||||
statusId: string;
|
statusId: string;
|
||||||
withBorder?: boolean;
|
}> = ({ statusId }) => {
|
||||||
}> = ({ statusId, withBorder }) => {
|
|
||||||
const refresh = useAppSelector(
|
const refresh = useAppSelector(
|
||||||
(state) => state.contexts.refreshing[statusId],
|
(state) => state.contexts.refreshing[statusId],
|
||||||
);
|
);
|
||||||
|
const autoRefresh = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
!state.contexts.replies[statusId] ||
|
||||||
|
state.contexts.replies[statusId].length === 0,
|
||||||
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
@@ -42,6 +44,11 @@ export const RefreshController: React.FC<{
|
|||||||
dispatch(completeContextRefresh({ statusId }));
|
dispatch(completeContextRefresh({ statusId }));
|
||||||
|
|
||||||
if (result.async_refresh.result_count > 0) {
|
if (result.async_refresh.result_count > 0) {
|
||||||
|
if (autoRefresh) {
|
||||||
|
void dispatch(fetchContext({ statusId }));
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
setReady(true);
|
setReady(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -60,7 +67,7 @@ export const RefreshController: React.FC<{
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [dispatch, setReady, statusId, refresh]);
|
}, [dispatch, setReady, statusId, refresh, autoRefresh]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -78,12 +85,7 @@ export const RefreshController: React.FC<{
|
|||||||
|
|
||||||
if (ready && !loading) {
|
if (ready && !loading) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button className='load-more load-gap' onClick={handleClick}>
|
||||||
className={classNames('load-more load-gap', {
|
|
||||||
'timeline-hint--with-descendants': withBorder,
|
|
||||||
})}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.context.load_new_replies'
|
id='status.context.load_new_replies'
|
||||||
defaultMessage='New replies available'
|
defaultMessage='New replies available'
|
||||||
@@ -98,9 +100,7 @@ export const RefreshController: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('load-more load-gap', {
|
className='load-more load-gap'
|
||||||
'timeline-hint--with-descendants': withBorder,
|
|
||||||
})}
|
|
||||||
aria-busy
|
aria-busy
|
||||||
aria-live='polite'
|
aria-live='polite'
|
||||||
aria-label={intl.formatMessage(messages.loading)}
|
aria-label={intl.formatMessage(messages.loading)}
|
||||||
|
|||||||
@@ -580,7 +580,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
remoteHint = (
|
remoteHint = (
|
||||||
<RefreshController
|
<RefreshController
|
||||||
statusId={status.get('id')}
|
statusId={status.get('id')}
|
||||||
withBorder={!!descendants}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -653,8 +652,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
</Hotkeys>
|
</Hotkeys>
|
||||||
|
|
||||||
{descendants}
|
|
||||||
{remoteHint}
|
{remoteHint}
|
||||||
|
{descendants}
|
||||||
</div>
|
</div>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
"announcement.announcement": "إعلان",
|
"announcement.announcement": "إعلان",
|
||||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||||
"annual_report.summary.archetype.lurker": "المتصفح الصامت",
|
"annual_report.summary.archetype.lurker": "المتصفح الصامت",
|
||||||
"annual_report.summary.archetype.oracle": "حكيم",
|
"annual_report.summary.archetype.oracle": "الحكيم",
|
||||||
"annual_report.summary.archetype.pollster": "مستطلع للرأي",
|
"annual_report.summary.archetype.pollster": "مستطلع للرأي",
|
||||||
"annual_report.summary.archetype.replier": "الفراشة الاجتماعية",
|
"annual_report.summary.archetype.replier": "الفراشة الاجتماعية",
|
||||||
"annual_report.summary.followers.followers": "المُتابِعُون",
|
"annual_report.summary.followers.followers": "المُتابِعُون",
|
||||||
@@ -845,6 +845,7 @@
|
|||||||
"status.bookmark": "أضفه إلى الفواصل المرجعية",
|
"status.bookmark": "أضفه إلى الفواصل المرجعية",
|
||||||
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
||||||
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
||||||
|
"status.context.load_new_replies": "الردود الجديدة المتاحة",
|
||||||
"status.continued_thread": "تكملة للخيط",
|
"status.continued_thread": "تكملة للخيط",
|
||||||
"status.copy": "انسخ رابط الرسالة",
|
"status.copy": "انسخ رابط الرسالة",
|
||||||
"status.delete": "احذف",
|
"status.delete": "احذف",
|
||||||
|
|||||||
@@ -324,7 +324,7 @@
|
|||||||
"empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
|
"empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
|
||||||
"empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.",
|
"empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.",
|
||||||
"empty_column.hashtag": "Der er intet med dette hashtag endnu.",
|
"empty_column.hashtag": "Der er intet med dette hashtag endnu.",
|
||||||
"empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at fylde den op.",
|
"empty_column.home": "Din hjem-tidslinje er tom! Følg nogle personer, for at fylde den op.",
|
||||||
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.",
|
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.",
|
||||||
"empty_column.mutes": "Du har endnu ikke skjult nogle brugere.",
|
"empty_column.mutes": "Du har endnu ikke skjult nogle brugere.",
|
||||||
"empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.",
|
"empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.",
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
"keyboard_shortcuts.favourites": "Åbn favoritlisten",
|
"keyboard_shortcuts.favourites": "Åbn favoritlisten",
|
||||||
"keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
|
"keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
|
||||||
"keyboard_shortcuts.heading": "Tastaturgenveje",
|
"keyboard_shortcuts.heading": "Tastaturgenveje",
|
||||||
"keyboard_shortcuts.home": "Åbn hjemmetidslinje",
|
"keyboard_shortcuts.home": "Åbn hjem-tidslinje",
|
||||||
"keyboard_shortcuts.hotkey": "Hurtigtast",
|
"keyboard_shortcuts.hotkey": "Hurtigtast",
|
||||||
"keyboard_shortcuts.legend": "Vis dette symbol",
|
"keyboard_shortcuts.legend": "Vis dette symbol",
|
||||||
"keyboard_shortcuts.local": "Åbn lokal tidslinje",
|
"keyboard_shortcuts.local": "Åbn lokal tidslinje",
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
"lists.done": "Færdig",
|
"lists.done": "Færdig",
|
||||||
"lists.edit": "Redigér liste",
|
"lists.edit": "Redigér liste",
|
||||||
"lists.exclusive": "Skjul medlemmer i Hjem",
|
"lists.exclusive": "Skjul medlemmer i Hjem",
|
||||||
"lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.",
|
"lists.exclusive_hint": "Hvis nogen er på denne liste, så skjul dem i hjem-feed for at undgå at se deres indlæg to gange.",
|
||||||
"lists.find_users_to_add": "Find brugere at tilføje",
|
"lists.find_users_to_add": "Find brugere at tilføje",
|
||||||
"lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}",
|
"lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}",
|
||||||
"lists.list_name": "Listetitel",
|
"lists.list_name": "Listetitel",
|
||||||
@@ -792,7 +792,7 @@
|
|||||||
"report.thanks.title": "Ønsker ikke at se dette?",
|
"report.thanks.title": "Ønsker ikke at se dette?",
|
||||||
"report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.",
|
"report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.",
|
||||||
"report.unfollow": "Følg ikke længere @{name}",
|
"report.unfollow": "Følg ikke længere @{name}",
|
||||||
"report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i din hjemmestrøm, kan du stoppe med at følge dem.",
|
"report.unfollow_explanation": "Du følger denne konto. Hvis du ikke længere vil se vedkommendes indlæg i dit hjem-feed, så stop med at følge dem.",
|
||||||
"report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet",
|
"report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet",
|
||||||
"report_notification.categories.legal": "Juridisk",
|
"report_notification.categories.legal": "Juridisk",
|
||||||
"report_notification.categories.legal_sentence": "ikke-tilladt indhold",
|
"report_notification.categories.legal_sentence": "ikke-tilladt indhold",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"account.followers": "Follower",
|
"account.followers": "Follower",
|
||||||
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
||||||
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
|
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
|
||||||
"account.followers_you_know_counter": "{counter} Follower kennst Du",
|
"account.followers_you_know_counter": "{counter} bekannt",
|
||||||
"account.following": "Folge ich",
|
"account.following": "Folge ich",
|
||||||
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
|
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
|
||||||
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
||||||
|
|||||||
@@ -498,6 +498,8 @@
|
|||||||
"keyboard_shortcuts.translate": "to translate a post",
|
"keyboard_shortcuts.translate": "to translate a post",
|
||||||
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
|
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
|
||||||
"keyboard_shortcuts.up": "Move up in the list",
|
"keyboard_shortcuts.up": "Move up in the list",
|
||||||
|
"learn_more_link.got_it": "Got it",
|
||||||
|
"learn_more_link.learn_more": "Learn more",
|
||||||
"lightbox.close": "Close",
|
"lightbox.close": "Close",
|
||||||
"lightbox.next": "Next",
|
"lightbox.next": "Next",
|
||||||
"lightbox.previous": "Previous",
|
"lightbox.previous": "Previous",
|
||||||
@@ -873,12 +875,11 @@
|
|||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.quote_error.filtered": "Hidden due to one of your filters",
|
"status.quote_error.filtered": "Hidden due to one of your filters",
|
||||||
"status.quote_error.not_found": "This post cannot be displayed.",
|
"status.quote_error.not_available": "Post unavailable",
|
||||||
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
|
"status.quote_error.pending_approval": "Post pending",
|
||||||
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
|
"status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
|
||||||
"status.quote_error.removed": "This post was removed by its author.",
|
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
|
||||||
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.",
|
"status.quote_post_author": "Quoted a post by @{name}",
|
||||||
"status.quote_post_author": "Post by {name}",
|
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.reblog_private": "Boost with original visibility",
|
"status.reblog_private": "Boost with original visibility",
|
||||||
|
|||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "Järjehoidja",
|
"status.bookmark": "Järjehoidja",
|
||||||
"status.cancel_reblog_private": "Lõpeta jagamine",
|
"status.cancel_reblog_private": "Lõpeta jagamine",
|
||||||
"status.cannot_reblog": "Seda postitust ei saa jagada",
|
"status.cannot_reblog": "Seda postitust ei saa jagada",
|
||||||
|
"status.context.load_new_replies": "Leidub uusi vastuseid",
|
||||||
|
"status.context.loading": "Kontrollin täiendavate vastuste olemasolu",
|
||||||
"status.continued_thread": "Jätkatud lõim",
|
"status.continued_thread": "Jätkatud lõim",
|
||||||
"status.copy": "Kopeeri postituse link",
|
"status.copy": "Kopeeri postituse link",
|
||||||
"status.delete": "Kustuta",
|
"status.delete": "Kustuta",
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
||||||
"confirmations.logout.title": "خروج؟",
|
"confirmations.logout.title": "خروج؟",
|
||||||
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
|
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
|
||||||
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
|
"confirmations.missing_alt_text.message": "فرستهتان رسانههایی بدون متن جایگزین دارد. افزودن شرح به دسترسپذیر شدن محتوایتان برای افراد بیشتری کمک میکند.",
|
||||||
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
|
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
|
||||||
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
|
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
|
||||||
"confirmations.mute.confirm": "خموش",
|
"confirmations.mute.confirm": "خموش",
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
"hints.profiles.see_more_followers": "دیدن پیگیرندگان بیشتر روی {domain}",
|
"hints.profiles.see_more_followers": "دیدن پیگیرندگان بیشتر روی {domain}",
|
||||||
"hints.profiles.see_more_follows": "دیدن پیگرفتههای بیشتر روی {domain}",
|
"hints.profiles.see_more_follows": "دیدن پیگرفتههای بیشتر روی {domain}",
|
||||||
"hints.profiles.see_more_posts": "دیدن فرستههای بیشتر روی {domain}",
|
"hints.profiles.see_more_posts": "دیدن فرستههای بیشتر روی {domain}",
|
||||||
"home.column_settings.show_quotes": "نمایش نقلقولها",
|
"home.column_settings.show_quotes": "نمایش نقلها",
|
||||||
"home.column_settings.show_reblogs": "نمایش تقویتها",
|
"home.column_settings.show_reblogs": "نمایش تقویتها",
|
||||||
"home.column_settings.show_replies": "نمایش پاسخها",
|
"home.column_settings.show_replies": "نمایش پاسخها",
|
||||||
"home.hide_announcements": "نهفتن اعلامیهها",
|
"home.hide_announcements": "نهفتن اعلامیهها",
|
||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "نشانک",
|
"status.bookmark": "نشانک",
|
||||||
"status.cancel_reblog_private": "ناتقویت",
|
"status.cancel_reblog_private": "ناتقویت",
|
||||||
"status.cannot_reblog": "این فرسته قابل تقویت نیست",
|
"status.cannot_reblog": "این فرسته قابل تقویت نیست",
|
||||||
|
"status.context.load_new_replies": "پاسخهای جدیدی موجودند",
|
||||||
|
"status.context.loading": "بررسی کردن برای پاسخهای بیشتر",
|
||||||
"status.continued_thread": "رشتهٔ دنباله دار",
|
"status.continued_thread": "رشتهٔ دنباله دار",
|
||||||
"status.copy": "رونوشت از پیوند فرسته",
|
"status.copy": "رونوشت از پیوند فرسته",
|
||||||
"status.delete": "حذف",
|
"status.delete": "حذف",
|
||||||
@@ -873,7 +875,7 @@
|
|||||||
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایههایتان",
|
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایههایتان",
|
||||||
"status.quote_error.not_found": "این فرسته قابل نمایش نیست.",
|
"status.quote_error.not_found": "این فرسته قابل نمایش نیست.",
|
||||||
"status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.",
|
"status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.",
|
||||||
"status.quote_error.rejected": "از آنجا که نگارندهٔ اصلی فرسته اجازهٔ نقل قولش را نمیدهد این فرسته قابل نمایش نیست.",
|
"status.quote_error.rejected": "از آنجا که نگارندهٔ اصلی این فرسته اجازهٔ نقلش را نمیدهد قابل نمایش نیست.",
|
||||||
"status.quote_error.removed": "این فرسته به دست نگارندهاش برداشته شده.",
|
"status.quote_error.removed": "این فرسته به دست نگارندهاش برداشته شده.",
|
||||||
"status.quote_error.unauthorized": "از آنجا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.",
|
"status.quote_error.unauthorized": "از آنجا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.",
|
||||||
"status.quote_post_author": "فرسته توسط {name}",
|
"status.quote_post_author": "فرسته توسط {name}",
|
||||||
|
|||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "Blêdwizer tafoegje",
|
"status.bookmark": "Blêdwizer tafoegje",
|
||||||
"status.cancel_reblog_private": "Net langer booste",
|
"status.cancel_reblog_private": "Net langer booste",
|
||||||
"status.cannot_reblog": "Dit berjocht kin net boost wurde",
|
"status.cannot_reblog": "Dit berjocht kin net boost wurde",
|
||||||
|
"status.context.load_new_replies": "Nije reaksjes beskikber",
|
||||||
|
"status.context.loading": "Op nije reaksjes oan it kontrolearjen",
|
||||||
"status.continued_thread": "Ferfolgje it petear",
|
"status.continued_thread": "Ferfolgje it petear",
|
||||||
"status.copy": "Copy link to status",
|
"status.copy": "Copy link to status",
|
||||||
"status.delete": "Fuortsmite",
|
"status.delete": "Fuortsmite",
|
||||||
|
|||||||
@@ -346,7 +346,7 @@
|
|||||||
"featured_carousel.post": "הודעה",
|
"featured_carousel.post": "הודעה",
|
||||||
"featured_carousel.previous": "הקודם",
|
"featured_carousel.previous": "הקודם",
|
||||||
"featured_carousel.slide": "{index} מתוך {total}",
|
"featured_carousel.slide": "{index} מתוך {total}",
|
||||||
"filter_modal.added.context_mismatch_explanation": "קטגוריית המסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.",
|
"filter_modal.added.context_mismatch_explanation": "קטגוריית הסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.",
|
||||||
"filter_modal.added.context_mismatch_title": "אין התאמה להקשר!",
|
"filter_modal.added.context_mismatch_title": "אין התאמה להקשר!",
|
||||||
"filter_modal.added.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.",
|
"filter_modal.added.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.",
|
||||||
"filter_modal.added.expired_title": "פג תוקף המסנן!",
|
"filter_modal.added.expired_title": "פג תוקף המסנן!",
|
||||||
|
|||||||
@@ -513,7 +513,7 @@
|
|||||||
"lists.add_to_lists": "리스트에 {name} 추가",
|
"lists.add_to_lists": "리스트에 {name} 추가",
|
||||||
"lists.create": "생성",
|
"lists.create": "생성",
|
||||||
"lists.create_a_list_to_organize": "새 리스트를 만들어 홈 피드를 정리하세요",
|
"lists.create_a_list_to_organize": "새 리스트를 만들어 홈 피드를 정리하세요",
|
||||||
"lists.create_list": "리스트 생성",
|
"lists.create_list": "리스트 만들기",
|
||||||
"lists.delete": "리스트 삭제",
|
"lists.delete": "리스트 삭제",
|
||||||
"lists.done": "완료",
|
"lists.done": "완료",
|
||||||
"lists.edit": "리스트 편집",
|
"lists.edit": "리스트 편집",
|
||||||
|
|||||||
@@ -292,6 +292,7 @@
|
|||||||
"emoji_button.search_results": "Paieškos rezultatai",
|
"emoji_button.search_results": "Paieškos rezultatai",
|
||||||
"emoji_button.symbols": "Simboliai",
|
"emoji_button.symbols": "Simboliai",
|
||||||
"emoji_button.travel": "Kelionės ir vietos",
|
"emoji_button.travel": "Kelionės ir vietos",
|
||||||
|
"empty_column.account_featured_other.unknown": "Ši paskyra dar nieko neparodė.",
|
||||||
"empty_column.account_hides_collections": "Šis (-i) naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą.",
|
"empty_column.account_hides_collections": "Šis (-i) naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą.",
|
||||||
"empty_column.account_suspended": "Paskyra pristabdyta.",
|
"empty_column.account_suspended": "Paskyra pristabdyta.",
|
||||||
"empty_column.account_timeline": "Nėra čia įrašų.",
|
"empty_column.account_timeline": "Nėra čia įrašų.",
|
||||||
@@ -794,6 +795,8 @@
|
|||||||
"status.bookmark": "Pridėti į žymės",
|
"status.bookmark": "Pridėti į žymės",
|
||||||
"status.cancel_reblog_private": "Nebepasidalinti",
|
"status.cancel_reblog_private": "Nebepasidalinti",
|
||||||
"status.cannot_reblog": "Šis įrašas negali būti pakeltas.",
|
"status.cannot_reblog": "Šis įrašas negali būti pakeltas.",
|
||||||
|
"status.context.load_new_replies": "Yra naujų atsakymų",
|
||||||
|
"status.context.loading": "Tikrinama dėl daugiau atsakymų",
|
||||||
"status.continued_thread": "Tęsiama gijoje",
|
"status.continued_thread": "Tęsiama gijoje",
|
||||||
"status.copy": "Kopijuoti nuorodą į įrašą",
|
"status.copy": "Kopijuoti nuorodą į įrašą",
|
||||||
"status.delete": "Ištrinti",
|
"status.delete": "Ištrinti",
|
||||||
|
|||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "冊籤",
|
"status.bookmark": "冊籤",
|
||||||
"status.cancel_reblog_private": "取消轉送",
|
"status.cancel_reblog_private": "取消轉送",
|
||||||
"status.cannot_reblog": "Tsit篇PO文bē當轉送",
|
"status.cannot_reblog": "Tsit篇PO文bē當轉送",
|
||||||
|
"status.context.load_new_replies": "有新ê回應",
|
||||||
|
"status.context.loading": "Leh檢查其他ê回應",
|
||||||
"status.continued_thread": "接續ê討論線",
|
"status.continued_thread": "接續ê討論線",
|
||||||
"status.copy": "Khóo-pih PO文ê連結",
|
"status.copy": "Khóo-pih PO文ê連結",
|
||||||
"status.delete": "Thâi掉",
|
"status.delete": "Thâi掉",
|
||||||
|
|||||||
@@ -224,6 +224,8 @@
|
|||||||
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.",
|
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.",
|
||||||
"confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?",
|
"confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?",
|
||||||
"confirmations.discard_draft.post.cancel": "Continuar rascunho",
|
"confirmations.discard_draft.post.cancel": "Continuar rascunho",
|
||||||
|
"confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.",
|
||||||
|
"confirmations.discard_draft.post.title": "Eliminar seu esboço de publicação?",
|
||||||
"confirmations.discard_edit_media.confirm": "Descartar",
|
"confirmations.discard_edit_media.confirm": "Descartar",
|
||||||
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
|
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
|
||||||
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
|
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
|
||||||
@@ -333,9 +335,13 @@
|
|||||||
"errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência",
|
"errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência",
|
||||||
"errors.unexpected_crash.report_issue": "Reportar problema",
|
"errors.unexpected_crash.report_issue": "Reportar problema",
|
||||||
"explore.suggested_follows": "Pessoas",
|
"explore.suggested_follows": "Pessoas",
|
||||||
|
"explore.title": "Em alta",
|
||||||
"explore.trending_links": "Notícias",
|
"explore.trending_links": "Notícias",
|
||||||
"explore.trending_statuses": "Publicações",
|
"explore.trending_statuses": "Publicações",
|
||||||
"explore.trending_tags": "Hashtags",
|
"explore.trending_tags": "Hashtags",
|
||||||
|
"featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}",
|
||||||
|
"featured_carousel.next": "Próximo",
|
||||||
|
"featured_carousel.previous": "Anterior",
|
||||||
"filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.",
|
"filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.",
|
||||||
"filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!",
|
"filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!",
|
||||||
"filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.",
|
"filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.",
|
||||||
@@ -550,6 +556,7 @@
|
|||||||
"navigation_bar.lists": "Listas",
|
"navigation_bar.lists": "Listas",
|
||||||
"navigation_bar.logout": "Sair",
|
"navigation_bar.logout": "Sair",
|
||||||
"navigation_bar.moderation": "Moderação",
|
"navigation_bar.moderation": "Moderação",
|
||||||
|
"navigation_bar.more": "Mais",
|
||||||
"navigation_bar.mutes": "Usuários silenciados",
|
"navigation_bar.mutes": "Usuários silenciados",
|
||||||
"navigation_bar.opened_in_classic_interface": "Publicações, contas e outras páginas específicas são abertas por padrão na interface 'web' clássica.",
|
"navigation_bar.opened_in_classic_interface": "Publicações, contas e outras páginas específicas são abertas por padrão na interface 'web' clássica.",
|
||||||
"navigation_bar.preferences": "Preferências",
|
"navigation_bar.preferences": "Preferências",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"account.edit_profile": "Upraviť profil",
|
"account.edit_profile": "Upraviť profil",
|
||||||
"account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}",
|
"account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}",
|
||||||
"account.endorse": "Zobraziť na vlastnom profile",
|
"account.endorse": "Zobraziť na vlastnom profile",
|
||||||
|
"account.familiar_followers_one": "Nasledovanie od {name1}",
|
||||||
|
"account.familiar_followers_two": "Nasledovanie od {name1} a {name2}",
|
||||||
"account.featured": "Zviditeľnené",
|
"account.featured": "Zviditeľnené",
|
||||||
"account.featured.accounts": "Profily",
|
"account.featured.accounts": "Profily",
|
||||||
"account.featured.hashtags": "Hashtagy",
|
"account.featured.hashtags": "Hashtagy",
|
||||||
|
|||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "Bokmärk",
|
"status.bookmark": "Bokmärk",
|
||||||
"status.cancel_reblog_private": "Sluta boosta",
|
"status.cancel_reblog_private": "Sluta boosta",
|
||||||
"status.cannot_reblog": "Detta inlägg kan inte boostas",
|
"status.cannot_reblog": "Detta inlägg kan inte boostas",
|
||||||
|
"status.context.load_new_replies": "Nya svar finns",
|
||||||
|
"status.context.loading": "Letar efter fler svar",
|
||||||
"status.continued_thread": "Fortsatt tråd",
|
"status.continued_thread": "Fortsatt tråd",
|
||||||
"status.copy": "Kopiera inläggslänk",
|
"status.copy": "Kopiera inläggslänk",
|
||||||
"status.delete": "Radera",
|
"status.delete": "Radera",
|
||||||
|
|||||||
@@ -845,6 +845,8 @@
|
|||||||
"status.bookmark": "Yer işareti ekle",
|
"status.bookmark": "Yer işareti ekle",
|
||||||
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
|
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
|
||||||
"status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
|
"status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
|
||||||
|
"status.context.load_new_replies": "Yeni yanıtlar mevcut",
|
||||||
|
"status.context.loading": "Daha fazla yanıt için kontrol ediliyor",
|
||||||
"status.continued_thread": "Devam eden akış",
|
"status.continued_thread": "Devam eden akış",
|
||||||
"status.copy": "Gönderi bağlantısını kopyala",
|
"status.copy": "Gönderi bağlantısını kopyala",
|
||||||
"status.delete": "Sil",
|
"status.delete": "Sil",
|
||||||
|
|||||||
@@ -301,6 +301,9 @@
|
|||||||
"emoji_button.search_results": "搜索结果",
|
"emoji_button.search_results": "搜索结果",
|
||||||
"emoji_button.symbols": "符号",
|
"emoji_button.symbols": "符号",
|
||||||
"emoji_button.travel": "旅行与地点",
|
"emoji_button.travel": "旅行与地点",
|
||||||
|
"empty_column.account_featured.me": "你尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。",
|
||||||
|
"empty_column.account_featured.other": "{acct} 尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。",
|
||||||
|
"empty_column.account_featured_other.unknown": "该用户尚未设置任何精选。",
|
||||||
"empty_column.account_hides_collections": "该用户选择不公开此信息",
|
"empty_column.account_hides_collections": "该用户选择不公开此信息",
|
||||||
"empty_column.account_suspended": "账号已被停用",
|
"empty_column.account_suspended": "账号已被停用",
|
||||||
"empty_column.account_timeline": "这里没有嘟文!",
|
"empty_column.account_timeline": "这里没有嘟文!",
|
||||||
@@ -402,8 +405,10 @@
|
|||||||
"hashtag.counter_by_accounts": "{count, plural,other {{counter} 人讨论}}",
|
"hashtag.counter_by_accounts": "{count, plural,other {{counter} 人讨论}}",
|
||||||
"hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}",
|
"hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}",
|
||||||
"hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}",
|
"hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}",
|
||||||
|
"hashtag.feature": "设为精选",
|
||||||
"hashtag.follow": "关注话题",
|
"hashtag.follow": "关注话题",
|
||||||
"hashtag.mute": "停止提醒 #{hashtag}",
|
"hashtag.mute": "停止提醒 #{hashtag}",
|
||||||
|
"hashtag.unfeature": "取消精选",
|
||||||
"hashtag.unfollow": "取消关注话题",
|
"hashtag.unfollow": "取消关注话题",
|
||||||
"hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}",
|
"hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}",
|
||||||
"hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。",
|
"hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。",
|
||||||
@@ -558,6 +563,7 @@
|
|||||||
"navigation_bar.preferences": "偏好设置",
|
"navigation_bar.preferences": "偏好设置",
|
||||||
"navigation_bar.privacy_and_reach": "隐私与可达性",
|
"navigation_bar.privacy_and_reach": "隐私与可达性",
|
||||||
"navigation_bar.search": "搜索",
|
"navigation_bar.search": "搜索",
|
||||||
|
"navigation_bar.search_trends": "搜索/热门趋势",
|
||||||
"navigation_panel.collapse_lists": "收起菜单列表",
|
"navigation_panel.collapse_lists": "收起菜单列表",
|
||||||
"navigation_panel.expand_lists": "展开菜单列表",
|
"navigation_panel.expand_lists": "展开菜单列表",
|
||||||
"not_signed_in_indicator.not_signed_in": "你需要登录才能访问此资源。",
|
"not_signed_in_indicator.not_signed_in": "你需要登录才能访问此资源。",
|
||||||
@@ -786,6 +792,7 @@
|
|||||||
"report_notification.categories.violation": "违反规则",
|
"report_notification.categories.violation": "违反规则",
|
||||||
"report_notification.categories.violation_sentence": "违反规则",
|
"report_notification.categories.violation_sentence": "违反规则",
|
||||||
"report_notification.open": "打开举报",
|
"report_notification.open": "打开举报",
|
||||||
|
"search.clear": "清空搜索内容",
|
||||||
"search.no_recent_searches": "无最近搜索",
|
"search.no_recent_searches": "无最近搜索",
|
||||||
"search.placeholder": "搜索",
|
"search.placeholder": "搜索",
|
||||||
"search.quick_action.account_search": "包含 {x} 的账号",
|
"search.quick_action.account_search": "包含 {x} 的账号",
|
||||||
@@ -827,6 +834,8 @@
|
|||||||
"status.bookmark": "添加到书签",
|
"status.bookmark": "添加到书签",
|
||||||
"status.cancel_reblog_private": "取消转嘟",
|
"status.cancel_reblog_private": "取消转嘟",
|
||||||
"status.cannot_reblog": "不能转嘟这条嘟文",
|
"status.cannot_reblog": "不能转嘟这条嘟文",
|
||||||
|
"status.context.load_new_replies": "有新回复",
|
||||||
|
"status.context.loading": "正在检查更多回复",
|
||||||
"status.continued_thread": "上接嘟文串",
|
"status.continued_thread": "上接嘟文串",
|
||||||
"status.copy": "复制嘟文链接",
|
"status.copy": "复制嘟文链接",
|
||||||
"status.delete": "删除",
|
"status.delete": "删除",
|
||||||
@@ -854,8 +863,10 @@
|
|||||||
"status.pin": "在个人资料页面置顶",
|
"status.pin": "在个人资料页面置顶",
|
||||||
"status.quote_error.filtered": "已根据你的筛选器过滤",
|
"status.quote_error.filtered": "已根据你的筛选器过滤",
|
||||||
"status.quote_error.not_found": "无法显示这篇贴文。",
|
"status.quote_error.not_found": "无法显示这篇贴文。",
|
||||||
|
"status.quote_error.pending_approval": "此嘟文正在等待原作者批准。",
|
||||||
"status.quote_error.rejected": "由于原作者不允许引用转发,无法显示这篇贴文。",
|
"status.quote_error.rejected": "由于原作者不允许引用转发,无法显示这篇贴文。",
|
||||||
"status.quote_error.removed": "该帖子已被作者删除。",
|
"status.quote_error.removed": "该帖子已被作者删除。",
|
||||||
|
"status.quote_error.unauthorized": "你无权查看此嘟文,因此无法显示。",
|
||||||
"status.quote_post_author": "{name} 的嘟文",
|
"status.quote_post_author": "{name} 的嘟文",
|
||||||
"status.read_more": "查看更多",
|
"status.read_more": "查看更多",
|
||||||
"status.reblog": "转嘟",
|
"status.reblog": "转嘟",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM477-161q45 0 86-11.5t77-33.5l-91-91q-38 17-74.5 47.5T412-168q16 3 32 5t33 2Zm-124-25q35-72 79.5-107t67.5-47q-29-9-58.5-14.5T380-360q-45 0-89 11t-85 31q26 43 63.5 77.5T353-186Zm461-74L690-384q31-10 50.5-36t19.5-60q0-42-29-71t-71-29q-34 0-60 19.5T564-510l-44-44q2-61-41-104.5T374-700L260-814q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM380-420q11 0 20.5-1.5T420-426L246-600q-3 10-4.5 19.5T240-560q0 58 41 99t99 41Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM412-168q26-51 62-81.5t75-47.5L204-642q-21 36-32.5 76.5T160-480q0 45 11.5 86t34.5 76q41-20 85-31t89-11q32 0 61.5 5.5T500-340q-23 12-43.5 28T418-278q-12-2-20.5-2H380q-32 0-63.5 7T256-252q32 32 71.5 53.5T412-168Zm402-92-58-58q21-35 32.5-76t11.5-86q0-134-93-227t-227-93q-45 0-85.5 11.5T318-756l-58-58q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM520-554 374-700q62-2 105 41.5T520-554ZM380-420q-58 0-99-41t-41-99q0-33 14.5-60.5T292-668l196 196q-20 23-47.5 37.5T380-420Zm310 36L564-510q10-31 36-50.5t60-19.5q42 0 71 29t29 71q0 34-19.5 60T690-384ZM537-537ZM423-423Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 843 B |
@@ -12,6 +12,8 @@ body {
|
|||||||
--background-color: #fff;
|
--background-color: #fff;
|
||||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||||
--background-filter: blur(10px);
|
--background-filter: blur(10px);
|
||||||
|
--surface-variant-background-color: #f1ebfb;
|
||||||
|
--surface-border-color: #cac4d0;
|
||||||
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
|
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
|
||||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||||
|
|||||||
@@ -1433,10 +1433,6 @@ body > [data-popper-placement] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status--has-quote .quote-inline {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
@@ -1470,10 +1466,6 @@ body > [data-popper-placement] {
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--is-quote {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--in-thread {
|
&--in-thread {
|
||||||
--thread-margin: calc(46px + 8px);
|
--thread-margin: calc(46px + 8px);
|
||||||
|
|
||||||
@@ -1860,79 +1852,99 @@ body > [data-popper-placement] {
|
|||||||
// --status-gutter-width is currently only set inside of
|
// --status-gutter-width is currently only set inside of
|
||||||
// .notification-ungrouped, so everywhere else this will fall back
|
// .notification-ungrouped, so everywhere else this will fall back
|
||||||
// to the pixel values
|
// to the pixel values
|
||||||
--quote-margin: var(--status-gutter-width, 36px);
|
--quote-margin: var(--status-gutter-width);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-block-start: 16px;
|
margin-block-start: 16px;
|
||||||
margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
|
margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
color: var(--nested-card-text);
|
color: var(--nested-card-text);
|
||||||
background: var(--nested-card-background);
|
border: 1px solid var(--surface-border-color);
|
||||||
border: var(--nested-card-border);
|
|
||||||
|
|
||||||
@container (width > 460px) {
|
|
||||||
--quote-margin: var(--status-gutter-width, 56px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__quote--error {
|
.status__quote--error {
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
min-height: 56px;
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__quote-author-button {
|
.status__quote-author-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
width: auto;
|
margin-top: 8px;
|
||||||
margin-block-start: 10px;
|
padding: 8px 12px;
|
||||||
padding: 5px 12px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 400;
|
||||||
line-height: normal;
|
line-height: 20px;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0.25px;
|
||||||
text-decoration: none;
|
color: $darker-text-color;
|
||||||
color: $highlight-text-color;
|
background: var(--surface-variant-background-color);
|
||||||
background: var(--nested-card-background);
|
border-radius: 8px;
|
||||||
border: var(--nested-card-border);
|
cursor: default;
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
border-color: lighten($highlight-text-color, 4%);
|
|
||||||
color: lighten($highlight-text-color, 4%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: $ui-button-icon-focus-outline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__quote-icon {
|
.status--is-quote {
|
||||||
position: absolute;
|
border: none;
|
||||||
inset-block-start: 18px;
|
padding: 12px;
|
||||||
inset-inline-start: -40px;
|
|
||||||
display: block;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
padding: 5px;
|
|
||||||
color: #6a49ba;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.status__quote--error & {
|
.status__info {
|
||||||
inset-block-start: 50%;
|
padding-bottom: 8px;
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (width > 460px) {
|
.display-name,
|
||||||
inset-inline-start: -50px;
|
.status__relative-time {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name__account {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__content {
|
||||||
|
display: -webkit-box;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
line-height: 20px;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-gallery,
|
||||||
|
.video-player,
|
||||||
|
.audio-player,
|
||||||
|
.attachment-list,
|
||||||
|
.poll {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2152,6 +2164,27 @@ body > [data-popper-placement] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.learn-more__popout {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: inherit;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account__wrapper {
|
.account__wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
--surface-background-color: #{darken($ui-base-color, 4%)};
|
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||||
--surface-variant-background-color: #{$ui-base-color};
|
--surface-variant-background-color: #{$ui-base-color};
|
||||||
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
|
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
|
||||||
|
--surface-border-color: #{lighten($ui-base-color, 8%)};
|
||||||
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)};
|
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)};
|
||||||
--avatar-border-radius: 8px;
|
--avatar-border-radius: 8px;
|
||||||
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
|
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
@quote.save
|
@quote.save
|
||||||
|
|
||||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context'])
|
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context'])
|
||||||
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id])
|
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id], depth: @options[:depth])
|
||||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||||
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] })
|
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -65,4 +65,16 @@ class Admin::Metrics::Dimension::BaseDimension
|
|||||||
def canonicalized_params
|
def canonicalized_params
|
||||||
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def earliest_status_id
|
||||||
|
snowflake_id(@start_at.beginning_of_day)
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_status_id
|
||||||
|
snowflake_id(@end_at.end_of_day)
|
||||||
|
end
|
||||||
|
|
||||||
|
def snowflake_id(datetime)
|
||||||
|
Mastodon::Snowflake.id_at(datetime, with_random: false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
[sql_query_string, { domain: params[:domain], earliest_status_id:, latest_status_id:, limit: @limit }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -36,14 +36,6 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di
|
|||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
def params
|
||||||
@params.permit(:domain)
|
@params.permit(:domain)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
[sql_query_string, { earliest_status_id:, latest_status_id:, limit: @limit }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -28,12 +28,4 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B
|
|||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
|
|||||||
|
|
||||||
def media_size
|
def media_size
|
||||||
value = [
|
value = [
|
||||||
MediaAttachment.sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')),
|
MediaAttachment.sum(MediaAttachment.combined_media_file_size),
|
||||||
CustomEmoji.sum(:image_file_size),
|
CustomEmoji.sum(:image_file_size),
|
||||||
PreviewCard.sum(:image_file_size),
|
PreviewCard.sum(:image_file_size),
|
||||||
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
[sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi
|
|||||||
params[:id]
|
params[:id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
def params
|
||||||
@params.permit(:id)
|
@params.permit(:id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
[sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension
|
|||||||
params[:id]
|
params[:id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
def params
|
||||||
@params.permit(:id)
|
@params.permit(:id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -104,4 +104,16 @@ class Admin::Metrics::Measure::BaseMeasure
|
|||||||
def canonicalized_params
|
def canonicalized_params
|
||||||
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def earliest_status_id
|
||||||
|
snowflake_id(@start_at.beginning_of_day)
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_status_id
|
||||||
|
snowflake_id(@end_at.end_of_day)
|
||||||
|
end
|
||||||
|
|
||||||
|
def snowflake_id(datetime)
|
||||||
|
Mastodon::Snowflake.id_at(datetime, with_random: false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
|||||||
def perform_total_query
|
def perform_total_query
|
||||||
domain = params[:domain]
|
domain = params[:domain]
|
||||||
domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains]
|
domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains]
|
||||||
MediaAttachment.joins(:account).merge(Account.where(domain: domain)).sum('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')
|
MediaAttachment.joins(:account).merge(Account.where(domain: domain)).sum(MediaAttachment.combined_media_file_size)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform_previous_total_query
|
def perform_previous_total_query
|
||||||
@@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
|||||||
<<~SQL.squish
|
<<~SQL.squish
|
||||||
SELECT axis.*, (
|
SELECT axis.*, (
|
||||||
WITH new_media_attachments AS (
|
WITH new_media_attachments AS (
|
||||||
SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size
|
SELECT #{media_size_total} AS size
|
||||||
FROM media_attachments
|
FROM media_attachments
|
||||||
INNER JOIN accounts ON accounts.id = media_attachments.account_id
|
INNER JOIN accounts ON accounts.id = media_attachments.account_id
|
||||||
WHERE date_trunc('day', media_attachments.created_at)::date = axis.period
|
WHERE date_trunc('day', media_attachments.created_at)::date = axis.period
|
||||||
@@ -58,6 +58,10 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
|||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def media_size_total
|
||||||
|
MediaAttachment.combined_media_file_size.to_sql
|
||||||
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
@params.permit(:domain, :include_subdomains)
|
@params.permit(:domain, :include_subdomains)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }]
|
[sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id:, latest_status_id: }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -50,14 +50,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
|
|||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
def params
|
||||||
@params.permit(:domain, :include_subdomains)
|
@params.permit(:domain, :include_subdomains)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sql_array
|
def sql_array
|
||||||
[sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }]
|
[sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id:, latest_status_id: }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def sql_query_string
|
def sql_query_string
|
||||||
@@ -45,14 +45,6 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
|
|||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_status_id
|
|
||||||
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag
|
def tag
|
||||||
@tag ||= Tag.find(params[:id])
|
@tag ||= Tag.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ class Antispam
|
|||||||
end
|
end
|
||||||
|
|
||||||
def local_preflight_check!(status)
|
def local_preflight_check!(status)
|
||||||
return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
return unless considered_spam?(status)
|
||||||
return unless suspicious_reply_or_mention?(status)
|
|
||||||
return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago
|
|
||||||
|
|
||||||
report_if_needed!(status.account)
|
report_if_needed!(status.account)
|
||||||
|
|
||||||
@@ -44,10 +42,26 @@ class Antispam
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def considered_spam?(status)
|
||||||
|
(all_time_suspicious?(status) || recent_suspicious?(status)) && suspicious_reply_or_mention?(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_time_suspicious?(status)
|
||||||
|
all_time_spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_suspicious?(status)
|
||||||
|
status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||||
|
end
|
||||||
|
|
||||||
def spammy_texts
|
def spammy_texts
|
||||||
redis.smembers('antispam:spammy_texts')
|
redis.smembers('antispam:spammy_texts')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def all_time_spammy_texts
|
||||||
|
redis.smembers('antispam:all_time_spammy_texts')
|
||||||
|
end
|
||||||
|
|
||||||
def suspicious_reply_or_mention?(status)
|
def suspicious_reply_or_mention?(status)
|
||||||
parent = status.thread
|
parent = status.thread
|
||||||
return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id)
|
return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id)
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ class StatusReachFinder
|
|||||||
[
|
[
|
||||||
replied_to_account_id,
|
replied_to_account_id,
|
||||||
reblog_of_account_id,
|
reblog_of_account_id,
|
||||||
|
quote_of_account_id,
|
||||||
mentioned_account_ids,
|
mentioned_account_ids,
|
||||||
reblogs_account_ids,
|
reblogs_account_ids,
|
||||||
|
quotes_account_ids,
|
||||||
favourites_account_ids,
|
favourites_account_ids,
|
||||||
replies_account_ids,
|
replies_account_ids,
|
||||||
].tap do |arr|
|
].tap do |arr|
|
||||||
@@ -46,6 +48,10 @@ class StatusReachFinder
|
|||||||
@status.in_reply_to_account_id if distributable?
|
@status.in_reply_to_account_id if distributable?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_of_account_id
|
||||||
|
@status.quote&.quoted_account_id
|
||||||
|
end
|
||||||
|
|
||||||
def reblog_of_account_id
|
def reblog_of_account_id
|
||||||
@status.reblog.account_id if @status.reblog?
|
@status.reblog.account_id if @status.reblog?
|
||||||
end
|
end
|
||||||
@@ -54,6 +60,11 @@ class StatusReachFinder
|
|||||||
@status.mentions.pluck(:account_id)
|
@status.mentions.pluck(:account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Beware: Quotes can be created without the author having had access to the status
|
||||||
|
def quotes_account_ids
|
||||||
|
@status.quotes.pluck(:account_id) if distributable? || unsafe?
|
||||||
|
end
|
||||||
|
|
||||||
# Beware: Reblogs can be created without the author having had access to the status
|
# Beware: Reblogs can be created without the author having had access to the status
|
||||||
def reblogs_account_ids
|
def reblogs_account_ids
|
||||||
@status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).pluck(:account_id) if distributable? || unsafe?
|
@status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).pluck(:account_id) if distributable? || unsafe?
|
||||||
|
|||||||
@@ -84,22 +84,18 @@ class Webfinger
|
|||||||
|
|
||||||
def body_from_host_meta
|
def body_from_host_meta
|
||||||
host_meta_request.perform do |res|
|
host_meta_request.perform do |res|
|
||||||
if res.code == 200
|
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" unless res.code == 200
|
||||||
body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false)
|
|
||||||
else
|
body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false)
|
||||||
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def url_from_template(str)
|
def url_from_template(str)
|
||||||
link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')
|
link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')
|
||||||
|
|
||||||
if link.present?
|
raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" if link.blank?
|
||||||
link['template'].gsub('{uri}', @uri)
|
|
||||||
else
|
link['template'].gsub('{uri}', @uri)
|
||||||
raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
|
|
||||||
end
|
|
||||||
rescue Nokogiri::XML::XPath::SyntaxError
|
rescue Nokogiri::XML::XPath::SyntaxError
|
||||||
raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
|
raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -54,11 +54,9 @@ class WebfingerResource
|
|||||||
end
|
end
|
||||||
|
|
||||||
def username_from_acct
|
def username_from_acct
|
||||||
if domain_matches_local?
|
raise ActiveRecord::RecordNotFound unless domain_matches_local?
|
||||||
local_username
|
|
||||||
else
|
local_username
|
||||||
raise ActiveRecord::RecordNotFound
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def split_acct
|
def split_acct
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class AccountMigration < ApplicationRecord
|
|||||||
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
||||||
else
|
else
|
||||||
errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account))
|
errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account))
|
||||||
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
|
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id
|
||||||
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
|
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AccountSuggestions::FriendsOfFriendsSource < AccountSuggestions::Source
|
|||||||
AND NOT EXISTS (SELECT 1 FROM mutes m WHERE m.target_account_id = follows.target_account_id AND m.account_id = :id)
|
AND NOT EXISTS (SELECT 1 FROM mutes m WHERE m.target_account_id = follows.target_account_id AND m.account_id = :id)
|
||||||
AND (accounts.domain IS NULL OR NOT EXISTS (SELECT 1 FROM account_domain_blocks b WHERE b.account_id = :id AND b.domain = accounts.domain))
|
AND (accounts.domain IS NULL OR NOT EXISTS (SELECT 1 FROM account_domain_blocks b WHERE b.account_id = :id AND b.domain = accounts.domain))
|
||||||
AND NOT EXISTS (SELECT 1 FROM follows f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id)
|
AND NOT EXISTS (SELECT 1 FROM follows f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM follow_requests f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id)
|
||||||
AND follows.target_account_id <> :id
|
AND follows.target_account_id <> :id
|
||||||
AND accounts.discoverable
|
AND accounts.discoverable
|
||||||
AND accounts.suspended_at IS NULL
|
AND accounts.suspended_at IS NULL
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ class Admin::ActionLogFilter
|
|||||||
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
||||||
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
||||||
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
||||||
|
create_username_block: { target_type: 'UsernameBlock', action: 'create' }.freeze,
|
||||||
|
update_username_block: { target_type: 'UsernameBlock', action: 'update' }.freeze,
|
||||||
|
destroy_username_block: { target_type: 'UsernameBlock', action: 'destroy' }.freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
attr_reader :params
|
attr_reader :params
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
class AnnouncementReaction < ApplicationRecord
|
class AnnouncementReaction < ApplicationRecord
|
||||||
before_validation :set_custom_emoji
|
before_validation :set_custom_emoji, if: :name?
|
||||||
after_commit :queue_publish
|
after_commit :queue_publish
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_custom_emoji
|
def set_custom_emoji
|
||||||
self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) if name.present?
|
self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def queue_publish
|
def queue_publish
|
||||||
|
|||||||
@@ -3,12 +3,8 @@
|
|||||||
module RateLimitable
|
module RateLimitable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def rate_limit=(value)
|
included do
|
||||||
@rate_limit = value
|
attribute :rate_limit, :boolean, default: false
|
||||||
end
|
|
||||||
|
|
||||||
def rate_limit?
|
|
||||||
@rate_limit
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rate_limiter(by, options = {})
|
def rate_limiter(by, options = {})
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ module Status::FetchRepliesConcern
|
|||||||
|
|
||||||
def should_fetch_replies?
|
def should_fetch_replies?
|
||||||
# we aren't brand new, and we haven't fetched replies since the debounce window
|
# we aren't brand new, and we haven't fetched replies since the debounce window
|
||||||
!local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
|
!local? && distributable? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
|
||||||
fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago
|
fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
28
app/models/concerns/user/activity.rb
Normal file
28
app/models/concerns/user/activity.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module User::Activity
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
# The home and list feeds will be stored for this amount of time, and status
|
||||||
|
# fan-out to followers will include only people active within this time frame.
|
||||||
|
#
|
||||||
|
# Lowering the duration may improve performance if many people sign up, but
|
||||||
|
# most will not check their feed every day. Raising the duration reduces the
|
||||||
|
# amount of background processing that happens when people become active.
|
||||||
|
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
|
||||||
|
|
||||||
|
included do
|
||||||
|
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
|
||||||
|
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_in_recently?
|
||||||
|
current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def inactive_since_duration?
|
||||||
|
last_sign_in_at < ACTIVE_DURATION.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
22
app/models/concerns/user/confirmation.rb
Normal file
22
app/models/concerns/user/confirmation.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module User::Confirmation
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||||
|
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
||||||
|
|
||||||
|
def confirm
|
||||||
|
wrap_email_confirmation { super }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirmed?
|
||||||
|
confirmed_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unconfirmed?
|
||||||
|
!confirmed?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -37,7 +37,7 @@ class FollowRequest < ApplicationRecord
|
|||||||
if account.local?
|
if account.local?
|
||||||
ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id)
|
ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id)
|
||||||
MergeWorker.perform_async(target_account.id, account.id, 'home')
|
MergeWorker.perform_async(target_account.id, account.id, 'home')
|
||||||
MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id|
|
MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id|
|
||||||
[target_account.id, list_id, 'list']
|
[target_account.id, list_id, 'list']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Form::Redirect
|
|||||||
if target_account.nil?
|
if target_account.nil?
|
||||||
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
||||||
else
|
else
|
||||||
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
|
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id
|
||||||
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
|
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
31
app/models/form/username_block_batch.rb
Normal file
31
app/models/form/username_block_batch.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Form::UsernameBlockBatch < Form::BaseBatch
|
||||||
|
attr_accessor :username_block_ids
|
||||||
|
|
||||||
|
def save
|
||||||
|
case action
|
||||||
|
when 'delete'
|
||||||
|
delete!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def username_blocks
|
||||||
|
@username_blocks ||= UsernameBlock.where(id: username_block_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete!
|
||||||
|
verify_authorization(:destroy?)
|
||||||
|
|
||||||
|
username_blocks.each do |username_block|
|
||||||
|
username_block.destroy
|
||||||
|
log_action :destroy, username_block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_authorization(permission)
|
||||||
|
username_blocks.each { |username_block| authorize(username_block, permission) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -32,6 +32,8 @@ class List < ApplicationRecord
|
|||||||
|
|
||||||
before_destroy :clean_feed_manager
|
before_destroy :clean_feed_manager
|
||||||
|
|
||||||
|
scope :with_list_account, ->(account) { joins(:list_accounts).where(list_accounts: { account: }) }
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_account_lists_limit
|
def validate_account_lists_limit
|
||||||
|
|||||||
@@ -298,6 +298,10 @@ class MediaAttachment < ApplicationRecord
|
|||||||
IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
|
IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def combined_media_file_size
|
||||||
|
arel_table.coalesce(arel_table[:file_file_size], 0) + arel_table.coalesce(arel_table[:thumbnail_file_size], 0)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def file_styles(attachment)
|
def file_styles(attachment)
|
||||||
|
|||||||
@@ -170,10 +170,9 @@ class PreviewCard < ApplicationRecord
|
|||||||
private
|
private
|
||||||
|
|
||||||
def serialized_authors
|
def serialized_authors
|
||||||
if author_name? || author_url? || author_account_id?
|
return unless author_name? || author_url? || author_account_id?
|
||||||
PreviewCard::Author
|
|
||||||
.new(self)
|
PreviewCard::Author.new(self)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_dimensions
|
def extract_dimensions
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class Status < ApplicationRecord
|
|||||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||||
has_many :media_attachments, dependent: :nullify
|
has_many :media_attachments, dependent: :nullify
|
||||||
|
has_many :quotes, foreign_key: 'quoted_status_id', inverse_of: :quoted_status, dependent: :nullify
|
||||||
|
|
||||||
# The `dependent` option is enabled by the initial `mentions` association declaration
|
# The `dependent` option is enabled by the initial `mentions` association declaration
|
||||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||||
|
|||||||
@@ -164,9 +164,10 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_display_name_change
|
def validate_display_name_change
|
||||||
unless HashtagNormalizer.new.normalize(display_name).casecmp(name).zero?
|
errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless display_name_matches_name?
|
||||||
errors.add(:display_name,
|
end
|
||||||
I18n.t('tags.does_not_match_previous_name'))
|
|
||||||
end
|
def display_name_matches_name?
|
||||||
|
HashtagNormalizer.new.normalize(display_name).casecmp(name).zero?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,20 +58,13 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
include LanguagesHelper
|
include LanguagesHelper
|
||||||
include Redisable
|
include Redisable
|
||||||
|
include User::Activity
|
||||||
|
include User::Confirmation
|
||||||
include User::HasSettings
|
include User::HasSettings
|
||||||
include User::LdapAuthenticable
|
include User::LdapAuthenticable
|
||||||
include User::Omniauthable
|
include User::Omniauthable
|
||||||
include User::PamAuthenticable
|
include User::PamAuthenticable
|
||||||
|
|
||||||
# The home and list feeds will be stored in Redis for this amount
|
|
||||||
# of time, and status fan-out to followers will include only people
|
|
||||||
# within this time frame. Lowering the duration may improve performance
|
|
||||||
# if lots of people sign up, but not a lot of them check their feed
|
|
||||||
# every day. Raising the duration reduces the amount of expensive
|
|
||||||
# RegenerationWorker jobs that need to be run when those people come
|
|
||||||
# to check their feed
|
|
||||||
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze
|
|
||||||
|
|
||||||
devise :two_factor_authenticatable,
|
devise :two_factor_authenticatable,
|
||||||
otp_secret_length: 32
|
otp_secret_length: 32
|
||||||
|
|
||||||
@@ -118,13 +111,9 @@ class User < ApplicationRecord
|
|||||||
scope :recent, -> { order(id: :desc) }
|
scope :recent, -> { order(id: :desc) }
|
||||||
scope :pending, -> { where(approved: false) }
|
scope :pending, -> { where(approved: false) }
|
||||||
scope :approved, -> { where(approved: true) }
|
scope :approved, -> { where(approved: true) }
|
||||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
|
||||||
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
|
||||||
scope :enabled, -> { where(disabled: false) }
|
scope :enabled, -> { where(disabled: false) }
|
||||||
scope :disabled, -> { where(disabled: true) }
|
scope :disabled, -> { where(disabled: true) }
|
||||||
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
|
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
|
||||||
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
|
|
||||||
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
|
|
||||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||||
scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) }
|
scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) }
|
||||||
|
|
||||||
@@ -143,7 +132,10 @@ class User < ApplicationRecord
|
|||||||
delegate :can?, to: :role
|
delegate :can?, to: :role
|
||||||
|
|
||||||
attr_reader :invite_code, :date_of_birth
|
attr_reader :invite_code, :date_of_birth
|
||||||
attr_writer :external, :bypass_registration_checks, :current_account
|
attr_writer :current_account
|
||||||
|
|
||||||
|
attribute :external, :boolean, default: false
|
||||||
|
attribute :bypass_registration_checks, :boolean, default: false
|
||||||
|
|
||||||
def self.those_who_can(*any_of_privileges)
|
def self.those_who_can(*any_of_privileges)
|
||||||
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
||||||
@@ -178,14 +170,6 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_in_recently?
|
|
||||||
current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago
|
|
||||||
end
|
|
||||||
|
|
||||||
def confirmed?
|
|
||||||
confirmed_at.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def invited?
|
def invited?
|
||||||
invite_id.present?
|
invite_id.present?
|
||||||
end
|
end
|
||||||
@@ -210,12 +194,6 @@ class User < ApplicationRecord
|
|||||||
account_id
|
account_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirm
|
|
||||||
wrap_email_confirmation do
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Mark current email as confirmed, bypassing Devise
|
# Mark current email as confirmed, bypassing Devise
|
||||||
def mark_email_as_confirmed!
|
def mark_email_as_confirmed!
|
||||||
wrap_email_confirmation do
|
wrap_email_confirmation do
|
||||||
@@ -231,16 +209,11 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_sign_in!(new_sign_in: false)
|
def update_sign_in!(new_sign_in: false)
|
||||||
old_current = current_sign_in_at
|
|
||||||
new_current = Time.now.utc
|
new_current = Time.now.utc
|
||||||
|
self.last_sign_in_at = current_sign_in_at || new_current
|
||||||
self.last_sign_in_at = old_current || new_current
|
|
||||||
self.current_sign_in_at = new_current
|
self.current_sign_in_at = new_current
|
||||||
|
|
||||||
if new_sign_in
|
increment(:sign_in_count) if new_sign_in
|
||||||
self.sign_in_count ||= 0
|
|
||||||
self.sign_in_count += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
save(validate: false) unless new_record?
|
save(validate: false) unless new_record?
|
||||||
prepare_returning_user!
|
prepare_returning_user!
|
||||||
@@ -262,10 +235,6 @@ class User < ApplicationRecord
|
|||||||
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial?
|
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial?
|
||||||
end
|
end
|
||||||
|
|
||||||
def unconfirmed?
|
|
||||||
!confirmed?
|
|
||||||
end
|
|
||||||
|
|
||||||
def unconfirmed_or_pending?
|
def unconfirmed_or_pending?
|
||||||
unconfirmed? || pending?
|
unconfirmed? || pending?
|
||||||
end
|
end
|
||||||
@@ -443,7 +412,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
def set_approved
|
def set_approved
|
||||||
self.approved = begin
|
self.approved = begin
|
||||||
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval?
|
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval? || sign_up_username_requires_approval?
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
open_registrations? || valid_invitation? || external?
|
open_registrations? || valid_invitation? || external?
|
||||||
@@ -466,16 +435,17 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
if new_user
|
after_confirmation_tasks if new_user
|
||||||
# Avoid extremely unlikely race condition when approving and confirming
|
end
|
||||||
# the user at the same time
|
|
||||||
reload unless approved?
|
|
||||||
|
|
||||||
if approved?
|
def after_confirmation_tasks
|
||||||
prepare_new_user!
|
# Handle condition when approving and confirming a user at the same time
|
||||||
else
|
reload unless approved?
|
||||||
notify_staff_about_pending_account!
|
|
||||||
end
|
if approved?
|
||||||
|
prepare_new_user!
|
||||||
|
else
|
||||||
|
notify_staff_about_pending_account!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -498,18 +468,14 @@ class User < ApplicationRecord
|
|||||||
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_up_username_requires_approval?
|
||||||
|
account.username? && UsernameBlock.matches?(account.username, allow_with_approval: true)
|
||||||
|
end
|
||||||
|
|
||||||
def open_registrations?
|
def open_registrations?
|
||||||
Setting.registrations_mode == 'open'
|
Setting.registrations_mode == 'open'
|
||||||
end
|
end
|
||||||
|
|
||||||
def external?
|
|
||||||
!!@external
|
|
||||||
end
|
|
||||||
|
|
||||||
def bypass_registration_checks?
|
|
||||||
@bypass_registration_checks
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize_role
|
def sanitize_role
|
||||||
self.role = nil if role.present? && role.everyone?
|
self.role = nil if role.present? && role.everyone?
|
||||||
end
|
end
|
||||||
@@ -526,7 +492,7 @@ class User < ApplicationRecord
|
|||||||
return unless confirmed?
|
return unless confirmed?
|
||||||
|
|
||||||
ActivityTracker.record('activity:logins', id)
|
ActivityTracker.record('activity:logins', id)
|
||||||
regenerate_feed! if needs_feed_update?
|
regenerate_feed! if inactive_since_duration?
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_staff_about_pending_account!
|
def notify_staff_about_pending_account!
|
||||||
@@ -539,14 +505,10 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
def regenerate_feed!
|
def regenerate_feed!
|
||||||
home_feed = HomeFeed.new(account)
|
home_feed = HomeFeed.new(account)
|
||||||
unless home_feed.regenerating?
|
return if home_feed.regenerating?
|
||||||
home_feed.regeneration_in_progress!
|
|
||||||
RegenerationWorker.perform_async(account_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def needs_feed_update?
|
home_feed.regeneration_in_progress!
|
||||||
last_sign_in_at < ACTIVE_DURATION.ago
|
RegenerationWorker.perform_async(account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_email_dns?
|
def validate_email_dns?
|
||||||
|
|||||||
62
app/models/username_block.rb
Normal file
62
app/models/username_block.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: username_blocks
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# allow_with_approval :boolean default(FALSE), not null
|
||||||
|
# exact :boolean default(FALSE), not null
|
||||||
|
# normalized_username :string not null
|
||||||
|
# username :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class UsernameBlock < ApplicationRecord
|
||||||
|
HOMOGLYPHS = {
|
||||||
|
'1' => 'i',
|
||||||
|
'2' => 'z',
|
||||||
|
'3' => 'e',
|
||||||
|
'4' => 'a',
|
||||||
|
'5' => 's',
|
||||||
|
'7' => 't',
|
||||||
|
'8' => 'b',
|
||||||
|
'9' => 'g',
|
||||||
|
'0' => 'o',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
validates :username, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) }
|
||||||
|
scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) }
|
||||||
|
|
||||||
|
before_save :set_normalized_username
|
||||||
|
|
||||||
|
def comparison
|
||||||
|
exact? ? 'equals' : 'contains'
|
||||||
|
end
|
||||||
|
|
||||||
|
def comparison=(val)
|
||||||
|
self.exact = val == 'equals'
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.matches?(str, allow_with_approval: false)
|
||||||
|
normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||||
|
where(allow_with_approval: allow_with_approval).matches_exactly(normalized_str).or(matches_partially(normalized_str)).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
username
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_normalized_username
|
||||||
|
self.normalized_username = normalize(username)
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize(str)
|
||||||
|
str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,19 +19,21 @@ class WorkerBatch
|
|||||||
redis.hset(key, { 'async_refresh_key' => async_refresh_key, 'threshold' => threshold })
|
redis.hset(key, { 'async_refresh_key' => async_refresh_key, 'threshold' => threshold })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def within
|
||||||
|
raise NoBlockGivenError unless block_given?
|
||||||
|
|
||||||
|
begin
|
||||||
|
Thread.current[:batch] = self
|
||||||
|
yield(self)
|
||||||
|
ensure
|
||||||
|
Thread.current[:batch] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Add jobs to the batch. Usually when the batch is created.
|
# Add jobs to the batch. Usually when the batch is created.
|
||||||
# @param [Array<String>] jids
|
# @param [Array<String>] jids
|
||||||
def add_jobs(jids)
|
def add_jobs(jids)
|
||||||
if jids.blank?
|
return if jids.empty?
|
||||||
async_refresh_key = redis.hget(key, 'async_refresh_key')
|
|
||||||
|
|
||||||
if async_refresh_key.present?
|
|
||||||
async_refresh = AsyncRefresh.new(async_refresh_key)
|
|
||||||
async_refresh.finish!
|
|
||||||
end
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
redis.multi do |pipeline|
|
redis.multi do |pipeline|
|
||||||
pipeline.sadd(key('jobs'), jids)
|
pipeline.sadd(key('jobs'), jids)
|
||||||
@@ -43,7 +45,7 @@ class WorkerBatch
|
|||||||
|
|
||||||
# Remove a job from the batch, such as when it's been processed or it has failed.
|
# Remove a job from the batch, such as when it's been processed or it has failed.
|
||||||
# @param [String] jid
|
# @param [String] jid
|
||||||
def remove_job(jid)
|
def remove_job(jid, increment: false)
|
||||||
_, pending, processed, async_refresh_key, threshold = redis.multi do |pipeline|
|
_, pending, processed, async_refresh_key, threshold = redis.multi do |pipeline|
|
||||||
pipeline.srem(key('jobs'), jid)
|
pipeline.srem(key('jobs'), jid)
|
||||||
pipeline.hincrby(key, 'pending', -1)
|
pipeline.hincrby(key, 'pending', -1)
|
||||||
@@ -52,11 +54,24 @@ class WorkerBatch
|
|||||||
pipeline.hget(key, 'threshold')
|
pipeline.hget(key, 'threshold')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
async_refresh = AsyncRefresh.new(async_refresh_key) if async_refresh_key.present?
|
||||||
|
async_refresh&.increment_result_count(by: 1) if increment
|
||||||
|
|
||||||
|
if pending.zero? || processed >= (threshold || 1.0).to_f * (processed + pending)
|
||||||
|
async_refresh&.finish!
|
||||||
|
cleanup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish!
|
||||||
|
async_refresh_key = redis.hget(key, 'async_refresh_key')
|
||||||
|
|
||||||
if async_refresh_key.present?
|
if async_refresh_key.present?
|
||||||
async_refresh = AsyncRefresh.new(async_refresh_key)
|
async_refresh = AsyncRefresh.new(async_refresh_key)
|
||||||
async_refresh.increment_result_count(by: 1)
|
async_refresh.finish!
|
||||||
async_refresh.finish! if pending.zero? || processed >= threshold.to_f * (processed + pending)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
cleanup
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get pending jobs.
|
# Get pending jobs.
|
||||||
@@ -76,4 +91,8 @@ class WorkerBatch
|
|||||||
def key(suffix = nil)
|
def key(suffix = nil)
|
||||||
"worker_batch:#{@id}#{":#{suffix}" if suffix}"
|
"worker_batch:#{@id}#{":#{suffix}" if suffix}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
redis.del(key, key('jobs'))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
19
app/policies/username_block_policy.rb
Normal file
19
app/policies/username_block_policy.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UsernameBlockPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService
|
|||||||
# Limit of replies to fetch per status
|
# Limit of replies to fetch per status
|
||||||
MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i
|
MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i
|
||||||
|
|
||||||
def call(status_uri, collection_or_uri, max_pages: 1, async_refresh_key: nil, request_id: nil)
|
def call(status_uri, collection_or_uri, max_pages: 1, batch_id: nil, request_id: nil)
|
||||||
@status_uri = status_uri
|
@status_uri = status_uri
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
|||||||
DISCOVERIES_PER_REQUEST = 1000
|
DISCOVERIES_PER_REQUEST = 1000
|
||||||
|
|
||||||
# Should be called when uri has already been checked for locality
|
# Should be called when uri has already been checked for locality
|
||||||
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
|
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil, depth: nil)
|
||||||
return if domain_not_allowed?(uri)
|
return if domain_not_allowed?(uri)
|
||||||
|
|
||||||
|
@depth = depth || 0
|
||||||
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
||||||
@json = if prefetched_body.nil?
|
@json = if prefetched_body.nil?
|
||||||
fetch_status(uri, true, on_behalf_of)
|
fetch_status(uri, true, on_behalf_of)
|
||||||
@@ -52,7 +53,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
|||||||
return nil if discoveries > DISCOVERIES_PER_REQUEST
|
return nil if discoveries > DISCOVERIES_PER_REQUEST
|
||||||
end
|
end
|
||||||
|
|
||||||
ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id).perform
|
ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id, depth: @depth).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||||||
# Limit of fetched replies
|
# Limit of fetched replies
|
||||||
MAX_REPLIES = 5
|
MAX_REPLIES = 5
|
||||||
|
|
||||||
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, async_refresh_key: nil, request_id: nil)
|
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, batch_id: nil, request_id: nil)
|
||||||
@reference_uri = reference_uri
|
@reference_uri = reference_uri
|
||||||
@allow_synchronous_requests = allow_synchronous_requests
|
@allow_synchronous_requests = allow_synchronous_requests
|
||||||
|
|
||||||
@@ -15,9 +15,11 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||||||
|
|
||||||
@items = filter_replies(@items)
|
@items = filter_replies(@items)
|
||||||
|
|
||||||
batch = WorkerBatch.new
|
WorkerBatch.new(batch_id).within do |batch|
|
||||||
batch.connect(async_refresh_key) if async_refresh_key.present?
|
FetchReplyWorker.push_bulk(@items) do |reply_uri|
|
||||||
batch.add_jobs(FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] })
|
[reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
[@items, n_pages]
|
[@items, n_pages]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
class ActivityPub::VerifyQuoteService < BaseService
|
class ActivityPub::VerifyQuoteService < BaseService
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
|
||||||
|
MAX_SYNCHRONOUS_DEPTH = 2
|
||||||
|
|
||||||
# Optionally fetch quoted post, and verify the quote is authorized
|
# Optionally fetch quoted post, and verify the quote is authorized
|
||||||
def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil)
|
def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil, depth: nil)
|
||||||
@request_id = request_id
|
@request_id = request_id
|
||||||
|
@depth = depth || 0
|
||||||
@quote = quote
|
@quote = quote
|
||||||
@fetching_error = nil
|
@fetching_error = nil
|
||||||
|
|
||||||
@@ -72,10 +75,12 @@ class ActivityPub::VerifyQuoteService < BaseService
|
|||||||
return if uri.nil? || @quote.quoted_status.present?
|
return if uri.nil? || @quote.quoted_status.present?
|
||||||
|
|
||||||
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||||
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id)
|
raise Mastodon::RecursionLimitExceededError if @depth > MAX_SYNCHRONOUS_DEPTH && status.nil?
|
||||||
|
|
||||||
|
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1)
|
||||||
|
|
||||||
@quote.update(quoted_status: status) if status.present?
|
@quote.update(quoted_status: status) if status.present?
|
||||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||||
@fetching_error = e
|
@fetching_error = e
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -90,7 +95,7 @@ class ActivityPub::VerifyQuoteService < BaseService
|
|||||||
# It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations
|
# It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations
|
||||||
return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id'])
|
return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id'])
|
||||||
|
|
||||||
status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id)
|
status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth)
|
||||||
|
|
||||||
if status.present?
|
if status.present?
|
||||||
@quote.update(quoted_status: status)
|
@quote.update(quoted_status: status)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class FollowService < BaseService
|
|||||||
|
|
||||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow')
|
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow')
|
||||||
MergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
|
MergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
|
||||||
MergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:id)) do |list_id|
|
MergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:id)) do |list_id|
|
||||||
[@target_account.id, list_id, 'list']
|
[@target_account.id, list_id, 'list']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class UnfollowService < BaseService
|
|||||||
|
|
||||||
unless @options[:skip_unmerge]
|
unless @options[:skip_unmerge]
|
||||||
UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
|
UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
|
||||||
UnmergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:list_id)) do |list_id|
|
UnmergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:list_id)) do |list_id|
|
||||||
[@target_account.id, list_id, 'list']
|
[@target_account.id, list_id, 'list']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class UnmuteService < BaseService
|
|||||||
if account.following?(target_account)
|
if account.following?(target_account)
|
||||||
MergeWorker.perform_async(target_account.id, account.id, 'home')
|
MergeWorker.perform_async(target_account.id, account.id, 'home')
|
||||||
|
|
||||||
MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id|
|
MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id|
|
||||||
[target_account.id, list_id, 'list']
|
[target_account.id, list_id, 'list']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ class UnreservedUsernameValidator < ActiveModel::Validator
|
|||||||
end
|
end
|
||||||
|
|
||||||
def settings_username_reserved?
|
def settings_username_reserved?
|
||||||
settings_has_reserved_usernames? && settings_reserves_username?
|
UsernameBlock.matches?(@username, allow_with_approval: false)
|
||||||
end
|
|
||||||
|
|
||||||
def settings_has_reserved_usernames?
|
|
||||||
Setting.reserved_usernames.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def settings_reserves_username?
|
|
||||||
Setting.reserved_usernames.include?(@username.downcase)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
= t('admin.accounts.suspended')
|
= t('admin.accounts.suspended')
|
||||||
- elsif account.silenced?
|
- elsif account.silenced?
|
||||||
= t('admin.accounts.silenced')
|
= t('admin.accounts.silenced')
|
||||||
- elsif account.local? && account.user&.disabled?
|
- elsif account.local? && account.user_disabled?
|
||||||
= t('admin.accounts.disabled')
|
= t('admin.accounts.disabled')
|
||||||
- elsif account.local? && !account.user&.confirmed?
|
- elsif account.local? && !account.user_confirmed?
|
||||||
= t('admin.accounts.confirming')
|
= t('admin.accounts.confirming')
|
||||||
- elsif account.local? && !account.user_approved?
|
- elsif account.local? && !account.user_approved?
|
||||||
= t('admin.accounts.pending')
|
= t('admin.accounts.pending')
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
%tr
|
%tr
|
||||||
%th= t('admin.accounts.email_status')
|
%th= t('admin.accounts.email_status')
|
||||||
%td
|
%td
|
||||||
- if account.user&.confirmed?
|
- if account.user_confirmed?
|
||||||
= t('admin.accounts.confirmed')
|
= t('admin.accounts.confirmed')
|
||||||
- else
|
- else
|
||||||
= t('admin.accounts.confirming')
|
= t('admin.accounts.confirming')
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
%span.red= t('admin.accounts.suspended')
|
%span.red= t('admin.accounts.suspended')
|
||||||
- elsif target_account.silenced?
|
- elsif target_account.silenced?
|
||||||
%span.red= t('admin.accounts.silenced')
|
%span.red= t('admin.accounts.silenced')
|
||||||
- elsif target_account.user&.disabled?
|
- elsif target_account.user_disabled?
|
||||||
%span.red= t('admin.accounts.disabled')
|
%span.red= t('admin.accounts.disabled')
|
||||||
- else
|
- else
|
||||||
%span.neutral= t('admin.accounts.no_limits_imposed')
|
%span.neutral= t('admin.accounts.no_limits_imposed')
|
||||||
|
|||||||
16
app/views/admin/username_blocks/_form.html.haml
Normal file
16
app/views/admin/username_blocks/_form.html.haml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.fields-group
|
||||||
|
= form.input :username,
|
||||||
|
wrapper: :with_block_label,
|
||||||
|
input_html: { autocomplete: 'new-password', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= form.input :comparison,
|
||||||
|
as: :select,
|
||||||
|
wrapper: :with_block_label,
|
||||||
|
collection: %w(equals contains),
|
||||||
|
include_blank: false,
|
||||||
|
label_method: ->(type) { I18n.t(type, scope: 'admin.username_blocks.comparison') }
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= form.input :allow_with_approval,
|
||||||
|
wrapper: :with_label
|
||||||
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.batch-table__row
|
||||||
|
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||||
|
= f.check_box :username_block_ids, { multiple: true, include_hidden: false }, username_block.id
|
||||||
|
.sr-only= username_block.username
|
||||||
|
.batch-table__row__content.pending-account
|
||||||
|
.pending-account__header
|
||||||
|
= t(username_block.exact? ? 'admin.username_blocks.matches_exactly_html' : 'admin.username_blocks.contains_html', string: content_tag(:samp, link_to(username_block.username, edit_admin_username_block_path(username_block))))
|
||||||
|
%br/
|
||||||
|
- if username_block.allow_with_approval?
|
||||||
|
= t('admin.email_domain_blocks.allow_registrations_with_approval')
|
||||||
|
- else
|
||||||
|
= t('admin.username_blocks.block_registrations')
|
||||||
10
app/views/admin/username_blocks/edit.html.haml
Normal file
10
app/views/admin/username_blocks/edit.html.haml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.edit.title')
|
||||||
|
|
||||||
|
= simple_form_for @username_block, url: admin_username_block_path(@username_block) do |form|
|
||||||
|
= render 'shared/error_messages', object: @username_block
|
||||||
|
|
||||||
|
= render form
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= form.button :button, t('generic.save_changes'), type: :submit
|
||||||
26
app/views/admin/username_blocks/index.html.haml
Normal file
26
app/views/admin/username_blocks/index.html.haml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.title')
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
= link_to t('admin.username_blocks.add_new'), new_admin_username_block_path, class: 'button'
|
||||||
|
|
||||||
|
= form_with model: @form, url: batch_admin_username_blocks_path do |f|
|
||||||
|
= hidden_field_tag :page, params[:page] || 1
|
||||||
|
|
||||||
|
.batch-table
|
||||||
|
.batch-table__toolbar
|
||||||
|
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||||
|
= check_box_tag :batch_checkbox_all, nil, false
|
||||||
|
.batch-table__toolbar__actions
|
||||||
|
= f.button safe_join([material_symbol('close'), t('admin.username_blocks.delete')]),
|
||||||
|
class: 'table-action-link',
|
||||||
|
data: { confirm: t('admin.reports.are_you_sure') },
|
||||||
|
name: :delete,
|
||||||
|
type: :submit
|
||||||
|
.batch-table__body
|
||||||
|
- if @username_blocks.empty?
|
||||||
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
|
- else
|
||||||
|
= render partial: 'username_block', collection: @username_blocks, locals: { f: f }
|
||||||
|
|
||||||
|
= paginate @username_blocks
|
||||||
10
app/views/admin/username_blocks/new.html.haml
Normal file
10
app/views/admin/username_blocks/new.html.haml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.username_blocks.new.title')
|
||||||
|
|
||||||
|
= simple_form_for @username_block, url: admin_username_blocks_path do |form|
|
||||||
|
= render 'shared/error_messages', object: @username_block
|
||||||
|
|
||||||
|
= render form
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= form.button :button, t('admin.username_blocks.new.create'), type: :submit
|
||||||
@@ -16,7 +16,9 @@ class ActivityPub::FetchAllRepliesWorker
|
|||||||
MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
|
MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
|
||||||
|
|
||||||
def perform(root_status_id, options = {})
|
def perform(root_status_id, options = {})
|
||||||
|
@batch = WorkerBatch.new(options['batch_id'])
|
||||||
@root_status = Status.remote.find_by(id: root_status_id)
|
@root_status = Status.remote.find_by(id: root_status_id)
|
||||||
|
|
||||||
return unless @root_status&.should_fetch_replies?
|
return unless @root_status&.should_fetch_replies?
|
||||||
|
|
||||||
@root_status.touch(:fetched_replies_at)
|
@root_status.touch(:fetched_replies_at)
|
||||||
@@ -45,6 +47,8 @@ class ActivityPub::FetchAllRepliesWorker
|
|||||||
|
|
||||||
# Workers shouldn't be returning anything, but this is used in tests
|
# Workers shouldn't be returning anything, but this is used in tests
|
||||||
fetched_uris
|
fetched_uris
|
||||||
|
ensure
|
||||||
|
@batch.remove_job(jid)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -53,9 +57,10 @@ class ActivityPub::FetchAllRepliesWorker
|
|||||||
# status URI, or the prefetched body of the Note object
|
# status URI, or the prefetched body of the Note object
|
||||||
def get_replies(status, max_pages, options = {})
|
def get_replies(status, max_pages, options = {})
|
||||||
replies_collection_or_uri = get_replies_uri(status)
|
replies_collection_or_uri = get_replies_uri(status)
|
||||||
|
|
||||||
return if replies_collection_or_uri.nil?
|
return if replies_collection_or_uri.nil?
|
||||||
|
|
||||||
ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, async_refresh_key: "context:#{@root_status.id}:refresh", **options.deep_symbolize_keys)
|
ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get the URI of the replies collection of a status
|
# Get the URI of the replies collection of a status
|
||||||
@@ -78,9 +83,10 @@ class ActivityPub::FetchAllRepliesWorker
|
|||||||
# @param root_status_uri [String]
|
# @param root_status_uri [String]
|
||||||
def get_root_replies(root_status_uri, options = {})
|
def get_root_replies(root_status_uri, options = {})
|
||||||
root_status_body = fetch_resource(root_status_uri, true)
|
root_status_body = fetch_resource(root_status_uri, true)
|
||||||
|
|
||||||
return if root_status_body.nil?
|
return if root_status_body.nil?
|
||||||
|
|
||||||
FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body })
|
FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys.except('batch_id'), 'prefetched_body' => root_status_body })
|
||||||
|
|
||||||
get_replies(root_status_body, MAX_PAGES, options)
|
get_replies(root_status_body, MAX_PAGES, options)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ class FetchReplyWorker
|
|||||||
sidekiq_options queue: 'pull', retry: 3
|
sidekiq_options queue: 'pull', retry: 3
|
||||||
|
|
||||||
def perform(child_url, options = {})
|
def perform(child_url, options = {})
|
||||||
batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id']
|
batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id']
|
||||||
FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys)
|
result = FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys)
|
||||||
ensure
|
ensure
|
||||||
batch&.remove_job(jid)
|
batch&.remove_job(jid, increment: result.present?)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class MoveWorker
|
|||||||
|
|
||||||
def copy_account_notes!
|
def copy_account_notes!
|
||||||
AccountNote.where(target_account: @source_account).find_each do |note|
|
AccountNote.where(target_account: @source_account).find_each do |note|
|
||||||
text = I18n.with_locale(note.account.user&.locale.presence || I18n.default_locale) do
|
text = I18n.with_locale(note.account.user_locale.presence || I18n.default_locale) do
|
||||||
I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct)
|
I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class MoveWorker
|
|||||||
|
|
||||||
def carry_blocks_over!
|
def carry_blocks_over!
|
||||||
@source_account.blocked_by_relationships.where(account: Account.local).find_each do |block|
|
@source_account.blocked_by_relationships.where(account: Account.local).find_each do |block|
|
||||||
unless block.account.blocking?(@target_account) || block.account.following?(@target_account)
|
unless skip_block_move?(block)
|
||||||
BlockService.new.call(block.account, @target_account)
|
BlockService.new.call(block.account, @target_account)
|
||||||
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
|
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
|
||||||
end
|
end
|
||||||
@@ -115,19 +115,29 @@ class MoveWorker
|
|||||||
|
|
||||||
def carry_mutes_over!
|
def carry_mutes_over!
|
||||||
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
|
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
|
||||||
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account)
|
unless skip_mute_move?(mute)
|
||||||
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications)
|
||||||
|
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
||||||
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
@deferred_error = e
|
@deferred_error = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_account_note_if_needed!(account, id)
|
def add_account_note_if_needed!(account, id)
|
||||||
unless AccountNote.exists?(account: account, target_account: @target_account)
|
return if AccountNote.exists?(account: account, target_account: @target_account)
|
||||||
text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do
|
|
||||||
I18n.t(id, acct: @source_account.acct)
|
text = I18n.with_locale(account.user_locale.presence || I18n.default_locale) do
|
||||||
end
|
I18n.t(id, acct: @source_account.acct)
|
||||||
AccountNote.create!(account: account, target_account: @target_account, comment: text)
|
|
||||||
end
|
end
|
||||||
|
AccountNote.create!(account: account, target_account: @target_account, comment: text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_mute_move?(mute)
|
||||||
|
mute.account.muting?(@target_account) || mute.account.following?(@target_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_block_move?(block)
|
||||||
|
block.account.blocking?(@target_account) || block.account.following?(@target_account)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class PublishScheduledStatusWorker
|
|||||||
scheduled_status = ScheduledStatus.find(scheduled_status_id)
|
scheduled_status = ScheduledStatus.find(scheduled_status_id)
|
||||||
scheduled_status.destroy!
|
scheduled_status.destroy!
|
||||||
|
|
||||||
return true if scheduled_status.account.user.disabled?
|
return true if scheduled_status.account.user_disabled?
|
||||||
|
|
||||||
PostStatusService.new.call(
|
PostStatusService.new.call(
|
||||||
scheduled_status.account,
|
scheduled_status.account,
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ ignore_unused:
|
|||||||
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||||
- 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
- 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||||
- 'admin.terms_of_service.generate' # temporarily disabled
|
- 'admin.terms_of_service.generate' # temporarily disabled
|
||||||
|
- 'admin.username_blocks.matches_exactly_html'
|
||||||
|
- 'admin.username_blocks.contains_html'
|
||||||
|
|
||||||
ignore_inconsistent_interpolations:
|
ignore_inconsistent_interpolations:
|
||||||
- '*.one'
|
- '*.one'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative '../../lib/mastodon/sidekiq_middleware'
|
require_relative '../../lib/mastodon/sidekiq_middleware'
|
||||||
|
require_relative '../../lib/mastodon/worker_batch_middleware'
|
||||||
|
|
||||||
Sidekiq.configure_server do |config|
|
Sidekiq.configure_server do |config|
|
||||||
config.redis = REDIS_CONFIGURATION.sidekiq
|
config.redis = REDIS_CONFIGURATION.sidekiq
|
||||||
@@ -72,14 +73,12 @@ Sidekiq.configure_server do |config|
|
|||||||
|
|
||||||
config.server_middleware do |chain|
|
config.server_middleware do |chain|
|
||||||
chain.add Mastodon::SidekiqMiddleware
|
chain.add Mastodon::SidekiqMiddleware
|
||||||
end
|
|
||||||
|
|
||||||
config.server_middleware do |chain|
|
|
||||||
chain.add SidekiqUniqueJobs::Middleware::Server
|
chain.add SidekiqUniqueJobs::Middleware::Server
|
||||||
end
|
end
|
||||||
|
|
||||||
config.client_middleware do |chain|
|
config.client_middleware do |chain|
|
||||||
chain.add SidekiqUniqueJobs::Middleware::Client
|
chain.add SidekiqUniqueJobs::Middleware::Client
|
||||||
|
chain.add Mastodon::WorkerBatchMiddleware
|
||||||
end
|
end
|
||||||
|
|
||||||
config.on(:startup) do
|
config.on(:startup) do
|
||||||
@@ -105,6 +104,7 @@ Sidekiq.configure_client do |config|
|
|||||||
|
|
||||||
config.client_middleware do |chain|
|
config.client_middleware do |chain|
|
||||||
chain.add SidekiqUniqueJobs::Middleware::Client
|
chain.add SidekiqUniqueJobs::Middleware::Client
|
||||||
|
chain.add Mastodon::WorkerBatchMiddleware
|
||||||
end
|
end
|
||||||
|
|
||||||
config.logger.level = Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
|
config.logger.level = Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
|
||||||
|
|||||||
@@ -560,6 +560,8 @@ br:
|
|||||||
one: "%{count} skeudenn"
|
one: "%{count} skeudenn"
|
||||||
other: "%{count} skeudenn"
|
other: "%{count} skeudenn"
|
||||||
two: "%{count} skeudenn"
|
two: "%{count} skeudenn"
|
||||||
|
errors:
|
||||||
|
quoted_status_not_found: War a seblant, n'eus ket eus an embannadenn emaoc'h o klask menegiñ.
|
||||||
pin_errors:
|
pin_errors:
|
||||||
ownership: N'hallit ket spilhennañ embannadurioù ar re all
|
ownership: N'hallit ket spilhennañ embannadurioù ar re all
|
||||||
quote_policies:
|
quote_policies:
|
||||||
|
|||||||
@@ -1870,6 +1870,7 @@ ca:
|
|||||||
edited_at_html: Editat %{date}
|
edited_at_html: Editat %{date}
|
||||||
errors:
|
errors:
|
||||||
in_reply_not_found: El tut al qual intentes respondre sembla que no existeix.
|
in_reply_not_found: El tut al qual intentes respondre sembla que no existeix.
|
||||||
|
quoted_status_not_found: Sembla que la publicació que vols citar no existeix.
|
||||||
over_character_limit: Límit de caràcters de %{max} superat
|
over_character_limit: Límit de caràcters de %{max} superat
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats
|
direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats
|
||||||
|
|||||||
@@ -1958,6 +1958,7 @@ cs:
|
|||||||
edited_at_html: Upraven %{date}
|
edited_at_html: Upraven %{date}
|
||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Příspěvek, na který se pokoušíte odpovědět, neexistuje.
|
in_reply_not_found: Příspěvek, na který se pokoušíte odpovědět, neexistuje.
|
||||||
|
quoted_status_not_found: Zdá se, že příspěvek, který se pokoušíte citovat neexistuje.
|
||||||
over_character_limit: byl překročen limit %{max} znaků
|
over_character_limit: byl překročen limit %{max} znaků
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout
|
direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user