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:
Claire
2025-07-30 20:05:45 +02:00
172 changed files with 2202 additions and 647 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)]]]

View File

@@ -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'

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
}); });
} }

View 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>
</>
);
};

View File

@@ -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 }) => {

View File

@@ -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

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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": "احذف",

View File

@@ -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, 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",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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}",

View File

@@ -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",

View File

@@ -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": "פג תוקף המסנן!",

View File

@@ -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": "리스트 편집",

View File

@@ -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",

View File

@@ -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掉",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "转嘟",

View File

@@ -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

View File

@@ -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

View File

@@ -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%);

View File

@@ -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;

View File

@@ -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)};

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)')),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {})

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View 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

View 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')

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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'

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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