mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 00:08:46 +00:00
Merge commit 'e5826777b6c06a32b97388657beaca1e5eccb421' into glitch-soc/merge-upstream
Conflicts: - `config/settings.yml`: Not a real conflict, upstream removed settings that are identical in glitch-soc but textually adjacent to glitch-soc-only settings. Removed what upstream removed.
This commit is contained in:
@@ -6,10 +6,6 @@
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# 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.
|
||||
Metrics/AbcSize:
|
||||
Max: 90
|
||||
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -96,7 +96,7 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1131.0)
|
||||
aws-partitions (1.1135.0)
|
||||
aws-sdk-core (3.215.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -233,7 +233,7 @@ GEM
|
||||
fabrication (3.0.0)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.13.2)
|
||||
faraday (2.13.4)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -345,7 +345,7 @@ GEM
|
||||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.13.0)
|
||||
json (2.13.2)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.16.7)
|
||||
activesupport (>= 4.2)
|
||||
@@ -438,7 +438,7 @@ GEM
|
||||
mime-types (3.7.0)
|
||||
logger
|
||||
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_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
@@ -468,7 +468,7 @@ GEM
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-cas (3.0.1)
|
||||
omniauth-cas (3.0.2)
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
@@ -601,13 +601,13 @@ GEM
|
||||
ox (2.14.23)
|
||||
bigdecimal (>= 3.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.9)
|
||||
pg (1.6.0)
|
||||
pghero (3.7.0)
|
||||
activerecord (>= 7.1)
|
||||
playwright-ruby-client (1.54.0)
|
||||
@@ -731,7 +731,7 @@ GEM
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.1)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rouge (4.6.0)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
@@ -860,7 +860,7 @@ GEM
|
||||
stoplight (4.1.1)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.7)
|
||||
strong_migrations (2.4.0)
|
||||
strong_migrations (2.5.0)
|
||||
activerecord (>= 7.1)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
@@ -868,7 +868,7 @@ GEM
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.3)
|
||||
temple (0.10.4)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
@@ -1108,4 +1108,4 @@ RUBY VERSION
|
||||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.7.0
|
||||
2.7.1
|
||||
|
||||
@@ -6,7 +6,7 @@ module Admin
|
||||
|
||||
def index
|
||||
authorize :audit_log, :index?
|
||||
@auditable_accounts = Account.auditable.select(:id, :username)
|
||||
@auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @appeal, :approve?
|
||||
authorize @appeal, :reject?
|
||||
log_action :reject, @appeal
|
||||
@appeal.reject!(current_account)
|
||||
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
|
||||
|
||||
@@ -36,7 +36,7 @@ module Admin
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize :domain_block, :create?
|
||||
authorize :domain_block, :update?
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -129,7 +129,7 @@ module Admin
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
77
app/controllers/admin/username_blocks_controller.rb
Normal file
77
app/controllers/admin/username_blocks_controller.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::UsernameBlocksController < Admin::BaseController
|
||||
before_action :set_username_block, only: [:edit, :update]
|
||||
|
||||
def index
|
||||
authorize :username_block, :index?
|
||||
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
|
||||
@form = Form::UsernameBlockBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
authorize :username_block, :index?
|
||||
|
||||
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
|
||||
rescue Mastodon::NotPermittedError
|
||||
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
|
||||
ensure
|
||||
redirect_to admin_username_blocks_path
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :username_block, :create?
|
||||
@username_block = UsernameBlock.new(exact: true)
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @username_block, :update?
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :username_block, :create?
|
||||
|
||||
@username_block = UsernameBlock.new(resource_params)
|
||||
|
||||
if @username_block.save
|
||||
log_action :create, @username_block
|
||||
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @username_block, :update?
|
||||
|
||||
if @username_block.update(resource_params)
|
||||
log_action :update, @username_block
|
||||
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_username_block
|
||||
@username_block = UsernameBlock.find(params[:id])
|
||||
end
|
||||
|
||||
def form_username_block_batch_params
|
||||
params
|
||||
.expect(form_username_block_batch: [username_block_ids: []])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params
|
||||
.expect(username_block: [:username, :comparison, :allow_with_approval])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
'delete' if params[:delete]
|
||||
end
|
||||
end
|
||||
@@ -66,7 +66,11 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
add_async_refresh_header(async_refresh)
|
||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||
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
|
||||
|
||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
|
||||
@@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
if current_account.moved_to_account_id.present?
|
||||
if current_account.moved?
|
||||
current_account.update!(moved_to_account: nil)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ class StatusesController < ApplicationController
|
||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_status
|
||||
before_action :redirect_to_original, only: :show
|
||||
before_action :verify_embed_allowed, only: :embed
|
||||
|
||||
after_action :set_link_headers
|
||||
|
||||
@@ -40,8 +41,6 @@ class StatusesController < ApplicationController
|
||||
end
|
||||
|
||||
def embed
|
||||
return not_found if @status.hidden? || @status.reblog?
|
||||
|
||||
expires_in 180, public: true
|
||||
response.headers.delete('X-Frame-Options')
|
||||
|
||||
@@ -50,6 +49,10 @@ class StatusesController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def verify_embed_allowed
|
||||
not_found if @status.hidden? || @status.reblog?
|
||||
end
|
||||
|
||||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
||||
|
||||
@@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
|
||||
end
|
||||
when 'UserRole'
|
||||
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'
|
||||
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
||||
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
||||
|
||||
@@ -65,13 +65,13 @@ module FormattingHelper
|
||||
end
|
||||
|
||||
def rss_content_preroll(status)
|
||||
if status.spoiler_text?
|
||||
return unless status.spoiler_text?
|
||||
|
||||
safe_join [
|
||||
tag.p { spoiler_with_warning(status) },
|
||||
tag.hr,
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def spoiler_with_warning(status)
|
||||
safe_join [
|
||||
@@ -81,12 +81,12 @@ module FormattingHelper
|
||||
end
|
||||
|
||||
def rss_content_postroll(status)
|
||||
if status.preloadable_poll
|
||||
return unless status.preloadable_poll
|
||||
|
||||
tag.p do
|
||||
poll_option_tags(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def poll_option_tags(status)
|
||||
safe_join(
|
||||
|
||||
@@ -134,7 +134,7 @@ module JsonLdHelper
|
||||
patch_for_forwarding!(value, compacted_value)
|
||||
elsif 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|
|
||||
if v.is_a?(Hash) && vc.is_a?(Hash)
|
||||
|
||||
@@ -28,7 +28,8 @@ module ThemeHelper
|
||||
end
|
||||
|
||||
def custom_stylesheet
|
||||
if active_custom_stylesheet.present?
|
||||
return if active_custom_stylesheet.blank?
|
||||
|
||||
stylesheet_link_tag(
|
||||
custom_css_path(active_custom_stylesheet),
|
||||
host: root_url,
|
||||
@@ -36,17 +37,16 @@ module ThemeHelper
|
||||
skip_pipeline: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
.join('-')
|
||||
end
|
||||
end
|
||||
|
||||
def cached_custom_css_digest
|
||||
Rails.cache.fetch(:setting_digest_custom_css) do
|
||||
|
||||
@@ -40,7 +40,11 @@ type KeyMatcher = (
|
||||
*/
|
||||
function just(keyName: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
isMatch: normalizeKey(event.key) === keyName,
|
||||
isMatch:
|
||||
normalizeKey(event.key) === keyName &&
|
||||
!event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey,
|
||||
priority: hotkeyPriority.singleKey,
|
||||
});
|
||||
}
|
||||
|
||||
63
app/javascript/mastodon/components/learn_more_link.tsx
Normal file
63
app/javascript/mastodon/components/learn_more_link.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useState, useRef, useCallback, useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const accessibilityId = useId();
|
||||
const [open, setOpen] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(!open);
|
||||
}, [open, setOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className='link-button'
|
||||
ref={triggerRef}
|
||||
onClick={handleClick}
|
||||
aria-expanded={open}
|
||||
aria-controls={accessibilityId}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='learn_more_link.learn_more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
show={open}
|
||||
rootClose
|
||||
onHide={handleClick}
|
||||
offset={[5, 5]}
|
||||
placement='bottom-end'
|
||||
target={triggerRef}
|
||||
>
|
||||
{({ props }) => (
|
||||
<div
|
||||
{...props}
|
||||
role='region'
|
||||
id={accessibilityId}
|
||||
className='account__domain-pill__popout learn-more__popout dropdown-animation'
|
||||
>
|
||||
<div className='learn-more__popout__content'>{children}</div>
|
||||
|
||||
<div>
|
||||
<button className='link-button' onClick={handleClick}>
|
||||
<FormattedMessage
|
||||
id='learn_more_link.got_it'
|
||||
defaultMessage='Got it'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -48,13 +48,13 @@ class TranslateButton extends PureComponent {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||
</button>
|
||||
|
||||
<div className='translate-button__meta'>
|
||||
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -138,6 +138,16 @@ class StatusContent extends PureComponent {
|
||||
|
||||
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 }) => {
|
||||
|
||||
@@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LearnMoreLink } from 'mastodon/components/learn_more_link';
|
||||
import StatusContainer from 'mastodon/containers/status_container';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import QuoteIcon from '../../images/quote.svg?react';
|
||||
import { fetchStatus } from '../actions/statuses';
|
||||
import { makeGetStatus } from '../selectors';
|
||||
|
||||
@@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{
|
||||
'status__quote--error': isError,
|
||||
})}
|
||||
>
|
||||
<Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
|
||||
const quoteAuthorName = account?.display_name_html;
|
||||
const quoteAuthorName = account?.acct;
|
||||
|
||||
if (!quoteAuthorName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const quoteAuthorElement = (
|
||||
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
|
||||
);
|
||||
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
|
||||
|
||||
return (
|
||||
<Link to={quoteUrl} className='status__quote-author-button'>
|
||||
<div className='status__quote-author-button'>
|
||||
<FormattedMessage
|
||||
id='status.quote_post_author'
|
||||
defaultMessage='Post by {name}'
|
||||
values={{ name: quoteAuthorElement }}
|
||||
defaultMessage='Quoted a post by @{name}'
|
||||
values={{ name: quoteAuthorName }}
|
||||
/>
|
||||
<Icon id='chevron_right' icon={ChevronRightIcon} />
|
||||
<Icon id='article' icon={ArticleIcon} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{
|
||||
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') {
|
||||
quoteError = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.pending_approval'
|
||||
defaultMessage='This post is pending approval from the original author.'
|
||||
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 = (
|
||||
<FormattedMessage
|
||||
id='status.quote_error.rejected'
|
||||
defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.'
|
||||
/>
|
||||
);
|
||||
} else if (!status || !quotedStatusId) {
|
||||
quoteError = (
|
||||
<FormattedMessage
|
||||
id='status.quote_error.not_found'
|
||||
defaultMessage='This post cannot be displayed.'
|
||||
id='status.quote_error.not_available'
|
||||
defaultMessage='Post unavailable'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{
|
||||
isQuotedPost
|
||||
id={quotedStatusId}
|
||||
contextType={contextType}
|
||||
avatarSize={40}
|
||||
avatarSize={32}
|
||||
>
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
fetchContext,
|
||||
completeContextRefresh,
|
||||
@@ -22,11 +20,15 @@ const messages = defineMessages({
|
||||
|
||||
export const RefreshController: React.FC<{
|
||||
statusId: string;
|
||||
withBorder?: boolean;
|
||||
}> = ({ statusId, withBorder }) => {
|
||||
}> = ({ statusId }) => {
|
||||
const refresh = useAppSelector(
|
||||
(state) => state.contexts.refreshing[statusId],
|
||||
);
|
||||
const autoRefresh = useAppSelector(
|
||||
(state) =>
|
||||
!state.contexts.replies[statusId] ||
|
||||
state.contexts.replies[statusId].length === 0,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const [ready, setReady] = useState(false);
|
||||
@@ -42,6 +44,11 @@ export const RefreshController: React.FC<{
|
||||
dispatch(completeContextRefresh({ statusId }));
|
||||
|
||||
if (result.async_refresh.result_count > 0) {
|
||||
if (autoRefresh) {
|
||||
void dispatch(fetchContext({ statusId }));
|
||||
return '';
|
||||
}
|
||||
|
||||
setReady(true);
|
||||
}
|
||||
} else {
|
||||
@@ -60,7 +67,7 @@ export const RefreshController: React.FC<{
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [dispatch, setReady, statusId, refresh]);
|
||||
}, [dispatch, setReady, statusId, refresh, autoRefresh]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setLoading(true);
|
||||
@@ -78,12 +85,7 @@ export const RefreshController: React.FC<{
|
||||
|
||||
if (ready && !loading) {
|
||||
return (
|
||||
<button
|
||||
className={classNames('load-more load-gap', {
|
||||
'timeline-hint--with-descendants': withBorder,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<button className='load-more load-gap' onClick={handleClick}>
|
||||
<FormattedMessage
|
||||
id='status.context.load_new_replies'
|
||||
defaultMessage='New replies available'
|
||||
@@ -98,9 +100,7 @@ export const RefreshController: React.FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('load-more load-gap', {
|
||||
'timeline-hint--with-descendants': withBorder,
|
||||
})}
|
||||
className='load-more load-gap'
|
||||
aria-busy
|
||||
aria-live='polite'
|
||||
aria-label={intl.formatMessage(messages.loading)}
|
||||
|
||||
@@ -580,7 +580,6 @@ class Status extends ImmutablePureComponent {
|
||||
remoteHint = (
|
||||
<RefreshController
|
||||
statusId={status.get('id')}
|
||||
withBorder={!!descendants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -653,8 +652,8 @@ class Status extends ImmutablePureComponent {
|
||||
</div>
|
||||
</Hotkeys>
|
||||
|
||||
{descendants}
|
||||
{remoteHint}
|
||||
{descendants}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"announcement.announcement": "إعلان",
|
||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||
"annual_report.summary.archetype.lurker": "المتصفح الصامت",
|
||||
"annual_report.summary.archetype.oracle": "حكيم",
|
||||
"annual_report.summary.archetype.oracle": "الحكيم",
|
||||
"annual_report.summary.archetype.pollster": "مستطلع للرأي",
|
||||
"annual_report.summary.archetype.replier": "الفراشة الاجتماعية",
|
||||
"annual_report.summary.followers.followers": "المُتابِعُون",
|
||||
@@ -845,6 +845,7 @@
|
||||
"status.bookmark": "أضفه إلى الفواصل المرجعية",
|
||||
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
||||
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
||||
"status.context.load_new_replies": "الردود الجديدة المتاحة",
|
||||
"status.continued_thread": "تكملة للخيط",
|
||||
"status.copy": "انسخ رابط الرسالة",
|
||||
"status.delete": "احذف",
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
"empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
|
||||
"empty_column.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.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.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.",
|
||||
@@ -476,7 +476,7 @@
|
||||
"keyboard_shortcuts.favourites": "Åbn favoritlisten",
|
||||
"keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
|
||||
"keyboard_shortcuts.heading": "Tastaturgenveje",
|
||||
"keyboard_shortcuts.home": "Åbn hjemmetidslinje",
|
||||
"keyboard_shortcuts.home": "Åbn hjem-tidslinje",
|
||||
"keyboard_shortcuts.hotkey": "Hurtigtast",
|
||||
"keyboard_shortcuts.legend": "Vis dette symbol",
|
||||
"keyboard_shortcuts.local": "Åbn lokal tidslinje",
|
||||
@@ -518,7 +518,7 @@
|
||||
"lists.done": "Færdig",
|
||||
"lists.edit": "Redigér liste",
|
||||
"lists.exclusive": "Skjul medlemmer i Hjem",
|
||||
"lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.",
|
||||
"lists.exclusive_hint": "Hvis nogen er på denne liste, så skjul dem i hjem-feed for at undgå at se deres indlæg to gange.",
|
||||
"lists.find_users_to_add": "Find brugere at tilføje",
|
||||
"lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}",
|
||||
"lists.list_name": "Listetitel",
|
||||
@@ -792,7 +792,7 @@
|
||||
"report.thanks.title": "Ønsker ikke at se 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_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.categories.legal": "Juridisk",
|
||||
"report_notification.categories.legal_sentence": "ikke-tilladt indhold",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"account.followers": "Follower",
|
||||
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
||||
"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_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
|
||||
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
||||
|
||||
@@ -498,6 +498,8 @@
|
||||
"keyboard_shortcuts.translate": "to translate a post",
|
||||
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
|
||||
"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.next": "Next",
|
||||
"lightbox.previous": "Previous",
|
||||
@@ -873,12 +875,11 @@
|
||||
"status.open": "Expand this post",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.quote_error.filtered": "Hidden due to one of your filters",
|
||||
"status.quote_error.not_found": "This post cannot be displayed.",
|
||||
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
|
||||
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
|
||||
"status.quote_error.removed": "This post was removed by its author.",
|
||||
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.",
|
||||
"status.quote_post_author": "Post by {name}",
|
||||
"status.quote_error.not_available": "Post unavailable",
|
||||
"status.quote_error.pending_approval": "Post pending",
|
||||
"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.pending_approval_popout.title": "Pending quote? Remain calm",
|
||||
"status.quote_post_author": "Quoted a post by @{name}",
|
||||
"status.read_more": "Read more",
|
||||
"status.reblog": "Boost",
|
||||
"status.reblog_private": "Boost with original visibility",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Järjehoidja",
|
||||
"status.cancel_reblog_private": "Lõpeta jagamine",
|
||||
"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.copy": "Kopeeri postituse link",
|
||||
"status.delete": "Kustuta",
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
||||
"confirmations.logout.title": "خروج؟",
|
||||
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
|
||||
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
|
||||
"confirmations.missing_alt_text.message": "فرستهتان رسانههایی بدون متن جایگزین دارد. افزودن شرح به دسترسپذیر شدن محتوایتان برای افراد بیشتری کمک میکند.",
|
||||
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
|
||||
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
|
||||
"confirmations.mute.confirm": "خموش",
|
||||
@@ -424,7 +424,7 @@
|
||||
"hints.profiles.see_more_followers": "دیدن پیگیرندگان بیشتر روی {domain}",
|
||||
"hints.profiles.see_more_follows": "دیدن پیگرفتههای بیشتر روی {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_replies": "نمایش پاسخها",
|
||||
"home.hide_announcements": "نهفتن اعلامیهها",
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "نشانک",
|
||||
"status.cancel_reblog_private": "ناتقویت",
|
||||
"status.cannot_reblog": "این فرسته قابل تقویت نیست",
|
||||
"status.context.load_new_replies": "پاسخهای جدیدی موجودند",
|
||||
"status.context.loading": "بررسی کردن برای پاسخهای بیشتر",
|
||||
"status.continued_thread": "رشتهٔ دنباله دار",
|
||||
"status.copy": "رونوشت از پیوند فرسته",
|
||||
"status.delete": "حذف",
|
||||
@@ -873,7 +875,7 @@
|
||||
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایههایتان",
|
||||
"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.unauthorized": "از آنجا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.",
|
||||
"status.quote_post_author": "فرسته توسط {name}",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Blêdwizer tafoegje",
|
||||
"status.cancel_reblog_private": "Net langer booste",
|
||||
"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.copy": "Copy link to status",
|
||||
"status.delete": "Fuortsmite",
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
"featured_carousel.post": "הודעה",
|
||||
"featured_carousel.previous": "הקודם",
|
||||
"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.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.",
|
||||
"filter_modal.added.expired_title": "פג תוקף המסנן!",
|
||||
|
||||
@@ -513,7 +513,7 @@
|
||||
"lists.add_to_lists": "리스트에 {name} 추가",
|
||||
"lists.create": "생성",
|
||||
"lists.create_a_list_to_organize": "새 리스트를 만들어 홈 피드를 정리하세요",
|
||||
"lists.create_list": "리스트 생성",
|
||||
"lists.create_list": "리스트 만들기",
|
||||
"lists.delete": "리스트 삭제",
|
||||
"lists.done": "완료",
|
||||
"lists.edit": "리스트 편집",
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"emoji_button.search_results": "Paieškos rezultatai",
|
||||
"emoji_button.symbols": "Simboliai",
|
||||
"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_suspended": "Paskyra pristabdyta.",
|
||||
"empty_column.account_timeline": "Nėra čia įrašų.",
|
||||
@@ -794,6 +795,8 @@
|
||||
"status.bookmark": "Pridėti į žymės",
|
||||
"status.cancel_reblog_private": "Nebepasidalinti",
|
||||
"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.copy": "Kopijuoti nuorodą į įrašą",
|
||||
"status.delete": "Ištrinti",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "冊籤",
|
||||
"status.cancel_reblog_private": "取消轉送",
|
||||
"status.cannot_reblog": "Tsit篇PO文bē當轉送",
|
||||
"status.context.load_new_replies": "有新ê回應",
|
||||
"status.context.loading": "Leh檢查其他ê回應",
|
||||
"status.continued_thread": "接續ê討論線",
|
||||
"status.copy": "Khóo-pih PO文ê連結",
|
||||
"status.delete": "Thâi掉",
|
||||
|
||||
@@ -224,6 +224,8 @@
|
||||
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.",
|
||||
"confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?",
|
||||
"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.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",
|
||||
@@ -333,9 +335,13 @@
|
||||
"errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência",
|
||||
"errors.unexpected_crash.report_issue": "Reportar problema",
|
||||
"explore.suggested_follows": "Pessoas",
|
||||
"explore.title": "Em alta",
|
||||
"explore.trending_links": "Notícias",
|
||||
"explore.trending_statuses": "Publicações",
|
||||
"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_title": "Incompatibilidade de contexto!",
|
||||
"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.logout": "Sair",
|
||||
"navigation_bar.moderation": "Moderação",
|
||||
"navigation_bar.more": "Mais",
|
||||
"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.preferences": "Preferências",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"account.edit_profile": "Upraviť profil",
|
||||
"account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}",
|
||||
"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.accounts": "Profily",
|
||||
"account.featured.hashtags": "Hashtagy",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Bokmärk",
|
||||
"status.cancel_reblog_private": "Sluta boosta",
|
||||
"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.copy": "Kopiera inläggslänk",
|
||||
"status.delete": "Radera",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Yer işareti ekle",
|
||||
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
|
||||
"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.copy": "Gönderi bağlantısını kopyala",
|
||||
"status.delete": "Sil",
|
||||
|
||||
@@ -301,6 +301,9 @@
|
||||
"emoji_button.search_results": "搜索结果",
|
||||
"emoji_button.symbols": "符号",
|
||||
"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_suspended": "账号已被停用",
|
||||
"empty_column.account_timeline": "这里没有嘟文!",
|
||||
@@ -402,8 +405,10 @@
|
||||
"hashtag.counter_by_accounts": "{count, plural,other {{counter} 人讨论}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}",
|
||||
"hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}",
|
||||
"hashtag.feature": "设为精选",
|
||||
"hashtag.follow": "关注话题",
|
||||
"hashtag.mute": "停止提醒 #{hashtag}",
|
||||
"hashtag.unfeature": "取消精选",
|
||||
"hashtag.unfollow": "取消关注话题",
|
||||
"hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}",
|
||||
"hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。",
|
||||
@@ -558,6 +563,7 @@
|
||||
"navigation_bar.preferences": "偏好设置",
|
||||
"navigation_bar.privacy_and_reach": "隐私与可达性",
|
||||
"navigation_bar.search": "搜索",
|
||||
"navigation_bar.search_trends": "搜索/热门趋势",
|
||||
"navigation_panel.collapse_lists": "收起菜单列表",
|
||||
"navigation_panel.expand_lists": "展开菜单列表",
|
||||
"not_signed_in_indicator.not_signed_in": "你需要登录才能访问此资源。",
|
||||
@@ -786,6 +792,7 @@
|
||||
"report_notification.categories.violation": "违反规则",
|
||||
"report_notification.categories.violation_sentence": "违反规则",
|
||||
"report_notification.open": "打开举报",
|
||||
"search.clear": "清空搜索内容",
|
||||
"search.no_recent_searches": "无最近搜索",
|
||||
"search.placeholder": "搜索",
|
||||
"search.quick_action.account_search": "包含 {x} 的账号",
|
||||
@@ -827,6 +834,8 @@
|
||||
"status.bookmark": "添加到书签",
|
||||
"status.cancel_reblog_private": "取消转嘟",
|
||||
"status.cannot_reblog": "不能转嘟这条嘟文",
|
||||
"status.context.load_new_replies": "有新回复",
|
||||
"status.context.loading": "正在检查更多回复",
|
||||
"status.continued_thread": "上接嘟文串",
|
||||
"status.copy": "复制嘟文链接",
|
||||
"status.delete": "删除",
|
||||
@@ -854,8 +863,10 @@
|
||||
"status.pin": "在个人资料页面置顶",
|
||||
"status.quote_error.filtered": "已根据你的筛选器过滤",
|
||||
"status.quote_error.not_found": "无法显示这篇贴文。",
|
||||
"status.quote_error.pending_approval": "此嘟文正在等待原作者批准。",
|
||||
"status.quote_error.rejected": "由于原作者不允许引用转发,无法显示这篇贴文。",
|
||||
"status.quote_error.removed": "该帖子已被作者删除。",
|
||||
"status.quote_error.unauthorized": "你无权查看此嘟文,因此无法显示。",
|
||||
"status.quote_post_author": "{name} 的嘟文",
|
||||
"status.read_more": "查看更多",
|
||||
"status.reblog": "转嘟",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM477-161q45 0 86-11.5t77-33.5l-91-91q-38 17-74.5 47.5T412-168q16 3 32 5t33 2Zm-124-25q35-72 79.5-107t67.5-47q-29-9-58.5-14.5T380-360q-45 0-89 11t-85 31q26 43 63.5 77.5T353-186Zm461-74L690-384q31-10 50.5-36t19.5-60q0-42-29-71t-71-29q-34 0-60 19.5T564-510l-44-44q2-61-41-104.5T374-700L260-814q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM380-420q11 0 20.5-1.5T420-426L246-600q-3 10-4.5 19.5T240-560q0 58 41 99t99 41Z"/></svg>
|
||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM412-168q26-51 62-81.5t75-47.5L204-642q-21 36-32.5 76.5T160-480q0 45 11.5 86t34.5 76q41-20 85-31t89-11q32 0 61.5 5.5T500-340q-23 12-43.5 28T418-278q-12-2-20.5-2H380q-32 0-63.5 7T256-252q32 32 71.5 53.5T412-168Zm402-92-58-58q21-35 32.5-76t11.5-86q0-134-93-227t-227-93q-45 0-85.5 11.5T318-756l-58-58q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM520-554 374-700q62-2 105 41.5T520-554ZM380-420q-58 0-99-41t-41-99q0-33 14.5-60.5T292-668l196 196q-20 23-47.5 37.5T380-420Zm310 36L564-510q10-31 36-50.5t60-19.5q42 0 71 29t29 71q0 34-19.5 60T690-384ZM537-537ZM423-423Z"/></svg>
|
||||
|
After Width: | Height: | Size: 843 B |
@@ -12,6 +12,8 @@ body {
|
||||
--background-color: #fff;
|
||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--surface-variant-background-color: #f1ebfb;
|
||||
--surface-border-color: #cac4d0;
|
||||
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
|
||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||
|
||||
@@ -1433,10 +1433,6 @@ body > [data-popper-placement] {
|
||||
}
|
||||
}
|
||||
|
||||
.status--has-quote .quote-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 16px;
|
||||
min-height: 54px;
|
||||
@@ -1470,10 +1466,6 @@ body > [data-popper-placement] {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&--is-quote {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&--in-thread {
|
||||
--thread-margin: calc(46px + 8px);
|
||||
|
||||
@@ -1860,79 +1852,99 @@ body > [data-popper-placement] {
|
||||
// --status-gutter-width is currently only set inside of
|
||||
// .notification-ungrouped, so everywhere else this will fall back
|
||||
// to the pixel values
|
||||
--quote-margin: var(--status-gutter-width, 36px);
|
||||
--quote-margin: var(--status-gutter-width);
|
||||
|
||||
position: relative;
|
||||
margin-block-start: 16px;
|
||||
margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
color: var(--nested-card-text);
|
||||
background: var(--nested-card-background);
|
||||
border: var(--nested-card-border);
|
||||
|
||||
@container (width > 460px) {
|
||||
--quote-margin: var(--status-gutter-width, 56px);
|
||||
}
|
||||
border: 1px solid var(--surface-border-color);
|
||||
}
|
||||
|
||||
.status__quote--error {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
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 {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
margin-block-start: 10px;
|
||||
padding: 5px 12px;
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
letter-spacing: 0;
|
||||
text-decoration: none;
|
||||
color: $highlight-text-color;
|
||||
background: var(--nested-card-background);
|
||||
border: var(--nested-card-border);
|
||||
border-radius: 4px;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: lighten($highlight-text-color, 4%);
|
||||
color: lighten($highlight-text-color, 4%);
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
color: $darker-text-color;
|
||||
background: var(--surface-variant-background-color);
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $ui-button-icon-focus-outline;
|
||||
.status--is-quote {
|
||||
border: none;
|
||||
padding: 12px;
|
||||
|
||||
.status__info {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.display-name,
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status__quote-icon {
|
||||
position: absolute;
|
||||
inset-block-start: 18px;
|
||||
inset-inline-start: -40px;
|
||||
display: block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 5px;
|
||||
color: #6a49ba;
|
||||
z-index: 10;
|
||||
|
||||
.status__quote--error & {
|
||||
inset-block-start: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@container (width > 460px) {
|
||||
inset-inline-start: -50px;
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||
--surface-variant-background-color: #{$ui-base-color};
|
||||
--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)};
|
||||
--avatar-border-radius: 8px;
|
||||
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
|
||||
|
||||
@@ -206,8 +206,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
@quote.save
|
||||
|
||||
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])
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
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::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] })
|
||||
end
|
||||
|
||||
|
||||
@@ -65,4 +65,16 @@ class Admin::Metrics::Dimension::BaseDimension
|
||||
def canonicalized_params
|
||||
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
||||
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
|
||||
|
||||
@@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -36,14 +36,6 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di
|
||||
SQL
|
||||
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
|
||||
@params.permit(:domain)
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -28,12 +28,4 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B
|
||||
LIMIT :limit
|
||||
SQL
|
||||
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
|
||||
|
||||
@@ -40,7 +40,7 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
|
||||
|
||||
def media_size
|
||||
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),
|
||||
PreviewCard.sum(:image_file_size),
|
||||
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
||||
|
||||
@@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi
|
||||
params[:id]
|
||||
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
|
||||
@params.permit(:id)
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension
|
||||
params[:id]
|
||||
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
|
||||
@params.permit(:id)
|
||||
end
|
||||
|
||||
@@ -104,4 +104,16 @@ class Admin::Metrics::Measure::BaseMeasure
|
||||
def canonicalized_params
|
||||
params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';')
|
||||
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
|
||||
|
||||
@@ -29,7 +29,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
||||
def perform_total_query
|
||||
domain = params[:domain]
|
||||
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
|
||||
|
||||
def perform_previous_total_query
|
||||
@@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
||||
<<~SQL.squish
|
||||
SELECT axis.*, (
|
||||
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
|
||||
INNER JOIN accounts ON accounts.id = media_attachments.account_id
|
||||
WHERE date_trunc('day', media_attachments.created_at)::date = axis.period
|
||||
@@ -58,6 +58,10 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
|
||||
SQL
|
||||
end
|
||||
|
||||
def media_size_total
|
||||
MediaAttachment.combined_media_file_size.to_sql
|
||||
end
|
||||
|
||||
def params
|
||||
@params.permit(:domain, :include_subdomains)
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -50,14 +50,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
|
||||
SQL
|
||||
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
|
||||
@params.permit(:domain, :include_subdomains)
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def sql_query_string
|
||||
@@ -45,14 +45,6 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
|
||||
SQL
|
||||
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
|
||||
@tag ||= Tag.find(params[:id])
|
||||
end
|
||||
|
||||
@@ -33,9 +33,7 @@ class Antispam
|
||||
end
|
||||
|
||||
def local_preflight_check!(status)
|
||||
return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||
return unless suspicious_reply_or_mention?(status)
|
||||
return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago
|
||||
return unless considered_spam?(status)
|
||||
|
||||
report_if_needed!(status.account)
|
||||
|
||||
@@ -44,10 +42,26 @@ class Antispam
|
||||
|
||||
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
|
||||
redis.smembers('antispam:spammy_texts')
|
||||
end
|
||||
|
||||
def all_time_spammy_texts
|
||||
redis.smembers('antispam:all_time_spammy_texts')
|
||||
end
|
||||
|
||||
def suspicious_reply_or_mention?(status)
|
||||
parent = status.thread
|
||||
return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id)
|
||||
|
||||
@@ -30,8 +30,10 @@ class StatusReachFinder
|
||||
[
|
||||
replied_to_account_id,
|
||||
reblog_of_account_id,
|
||||
quote_of_account_id,
|
||||
mentioned_account_ids,
|
||||
reblogs_account_ids,
|
||||
quotes_account_ids,
|
||||
favourites_account_ids,
|
||||
replies_account_ids,
|
||||
].tap do |arr|
|
||||
@@ -46,6 +48,10 @@ class StatusReachFinder
|
||||
@status.in_reply_to_account_id if distributable?
|
||||
end
|
||||
|
||||
def quote_of_account_id
|
||||
@status.quote&.quoted_account_id
|
||||
end
|
||||
|
||||
def reblog_of_account_id
|
||||
@status.reblog.account_id if @status.reblog?
|
||||
end
|
||||
@@ -54,6 +60,11 @@ class StatusReachFinder
|
||||
@status.mentions.pluck(:account_id)
|
||||
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
|
||||
def reblogs_account_ids
|
||||
@status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).pluck(:account_id) if distributable? || unsafe?
|
||||
|
||||
@@ -84,22 +84,18 @@ class Webfinger
|
||||
|
||||
def body_from_host_meta
|
||||
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
|
||||
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def url_from_template(str)
|
||||
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
|
||||
raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
|
||||
end
|
||||
rescue Nokogiri::XML::XPath::SyntaxError
|
||||
raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
|
||||
end
|
||||
|
||||
@@ -54,11 +54,9 @@ class WebfingerResource
|
||||
end
|
||||
|
||||
def username_from_acct
|
||||
if domain_matches_local?
|
||||
raise ActiveRecord::RecordNotFound unless domain_matches_local?
|
||||
|
||||
local_username
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
def split_acct
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountMigration < ApplicationRecord
|
||||
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
||||
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.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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +26,7 @@ class AccountSuggestions::FriendsOfFriendsSource < AccountSuggestions::Source
|
||||
AND NOT EXISTS (SELECT 1 FROM mutes m WHERE m.target_account_id = follows.target_account_id AND m.account_id = :id)
|
||||
AND (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 follow_requests f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id)
|
||||
AND follows.target_account_id <> :id
|
||||
AND accounts.discoverable
|
||||
AND accounts.suspended_at IS NULL
|
||||
|
||||
@@ -77,6 +77,9 @@ class Admin::ActionLogFilter
|
||||
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
||||
update_ip_block: { target_type: 'IpBlock', action: 'update' }.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
|
||||
|
||||
attr_reader :params
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#
|
||||
|
||||
class AnnouncementReaction < ApplicationRecord
|
||||
before_validation :set_custom_emoji
|
||||
before_validation :set_custom_emoji, if: :name?
|
||||
after_commit :queue_publish
|
||||
|
||||
belongs_to :account
|
||||
@@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
def queue_publish
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
module RateLimitable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def rate_limit=(value)
|
||||
@rate_limit = value
|
||||
end
|
||||
|
||||
def rate_limit?
|
||||
@rate_limit
|
||||
included do
|
||||
attribute :rate_limit, :boolean, default: false
|
||||
end
|
||||
|
||||
def rate_limiter(by, options = {})
|
||||
|
||||
@@ -33,7 +33,7 @@ module Status::FetchRepliesConcern
|
||||
|
||||
def should_fetch_replies?
|
||||
# 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
|
||||
)
|
||||
end
|
||||
|
||||
28
app/models/concerns/user/activity.rb
Normal file
28
app/models/concerns/user/activity.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module User::Activity
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# The home and list feeds will be stored for this amount of time, and status
|
||||
# fan-out to followers will include only people active within this time frame.
|
||||
#
|
||||
# Lowering the duration may improve performance if many people sign up, but
|
||||
# most will not check their feed every day. Raising the duration reduces the
|
||||
# amount of background processing that happens when people become active.
|
||||
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
|
||||
|
||||
included do
|
||||
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
|
||||
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
|
||||
end
|
||||
|
||||
def signed_in_recently?
|
||||
current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inactive_since_duration?
|
||||
last_sign_in_at < ACTIVE_DURATION.ago
|
||||
end
|
||||
end
|
||||
22
app/models/concerns/user/confirmation.rb
Normal file
22
app/models/concerns/user/confirmation.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module User::Confirmation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
||||
|
||||
def confirm
|
||||
wrap_email_confirmation { super }
|
||||
end
|
||||
end
|
||||
|
||||
def confirmed?
|
||||
confirmed_at.present?
|
||||
end
|
||||
|
||||
def unconfirmed?
|
||||
!confirmed?
|
||||
end
|
||||
end
|
||||
@@ -37,7 +37,7 @@ class FollowRequest < ApplicationRecord
|
||||
if account.local?
|
||||
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.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']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ class Form::Redirect
|
||||
if target_account.nil?
|
||||
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
31
app/models/form/username_block_batch.rb
Normal file
31
app/models/form/username_block_batch.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::UsernameBlockBatch < Form::BaseBatch
|
||||
attr_accessor :username_block_ids
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'delete'
|
||||
delete!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def username_blocks
|
||||
@username_blocks ||= UsernameBlock.where(id: username_block_ids)
|
||||
end
|
||||
|
||||
def delete!
|
||||
verify_authorization(:destroy?)
|
||||
|
||||
username_blocks.each do |username_block|
|
||||
username_block.destroy
|
||||
log_action :destroy, username_block
|
||||
end
|
||||
end
|
||||
|
||||
def verify_authorization(permission)
|
||||
username_blocks.each { |username_block| authorize(username_block, permission) }
|
||||
end
|
||||
end
|
||||
@@ -32,6 +32,8 @@ class List < ApplicationRecord
|
||||
|
||||
before_destroy :clean_feed_manager
|
||||
|
||||
scope :with_list_account, ->(account) { joins(:list_accounts).where(list_accounts: { account: }) }
|
||||
|
||||
private
|
||||
|
||||
def validate_account_lists_limit
|
||||
|
||||
@@ -298,6 +298,10 @@ class MediaAttachment < ApplicationRecord
|
||||
IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
|
||||
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
|
||||
|
||||
def file_styles(attachment)
|
||||
|
||||
@@ -170,10 +170,9 @@ class PreviewCard < ApplicationRecord
|
||||
private
|
||||
|
||||
def serialized_authors
|
||||
if author_name? || author_url? || author_account_id?
|
||||
PreviewCard::Author
|
||||
.new(self)
|
||||
end
|
||||
return unless author_name? || author_url? || author_account_id?
|
||||
|
||||
PreviewCard::Author.new(self)
|
||||
end
|
||||
|
||||
def extract_dimensions
|
||||
|
||||
@@ -80,6 +80,7 @@ class Status < ApplicationRecord
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
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
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
|
||||
@@ -164,9 +164,10 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
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'))
|
||||
end
|
||||
errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless display_name_matches_name?
|
||||
end
|
||||
|
||||
def display_name_matches_name?
|
||||
HashtagNormalizer.new.normalize(display_name).casecmp(name).zero?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,20 +58,13 @@ class User < ApplicationRecord
|
||||
|
||||
include LanguagesHelper
|
||||
include Redisable
|
||||
include User::Activity
|
||||
include User::Confirmation
|
||||
include User::HasSettings
|
||||
include User::LdapAuthenticable
|
||||
include User::Omniauthable
|
||||
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,
|
||||
otp_secret_length: 32
|
||||
|
||||
@@ -118,13 +111,9 @@ class User < ApplicationRecord
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :pending, -> { where(approved: false) }
|
||||
scope :approved, -> { where(approved: true) }
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
||||
scope :enabled, -> { where(disabled: false) }
|
||||
scope :disabled, -> { where(disabled: true) }
|
||||
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_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) }
|
||||
|
||||
@@ -143,7 +132,10 @@ class User < ApplicationRecord
|
||||
delegate :can?, to: :role
|
||||
|
||||
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)
|
||||
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
||||
@@ -178,14 +170,6 @@ class User < ApplicationRecord
|
||||
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?
|
||||
invite_id.present?
|
||||
end
|
||||
@@ -210,12 +194,6 @@ class User < ApplicationRecord
|
||||
account_id
|
||||
end
|
||||
|
||||
def confirm
|
||||
wrap_email_confirmation do
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Mark current email as confirmed, bypassing Devise
|
||||
def mark_email_as_confirmed!
|
||||
wrap_email_confirmation do
|
||||
@@ -231,16 +209,11 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def update_sign_in!(new_sign_in: false)
|
||||
old_current = current_sign_in_at
|
||||
new_current = Time.now.utc
|
||||
|
||||
self.last_sign_in_at = old_current || new_current
|
||||
self.last_sign_in_at = current_sign_in_at || new_current
|
||||
self.current_sign_in_at = new_current
|
||||
|
||||
if new_sign_in
|
||||
self.sign_in_count ||= 0
|
||||
self.sign_in_count += 1
|
||||
end
|
||||
increment(:sign_in_count) if new_sign_in
|
||||
|
||||
save(validate: false) unless new_record?
|
||||
prepare_returning_user!
|
||||
@@ -262,10 +235,6 @@ class User < ApplicationRecord
|
||||
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial?
|
||||
end
|
||||
|
||||
def unconfirmed?
|
||||
!confirmed?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
unconfirmed? || pending?
|
||||
end
|
||||
@@ -443,7 +412,7 @@ class User < ApplicationRecord
|
||||
|
||||
def set_approved
|
||||
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
|
||||
else
|
||||
open_registrations? || valid_invitation? || external?
|
||||
@@ -466,9 +435,11 @@ class User < ApplicationRecord
|
||||
|
||||
yield
|
||||
|
||||
if new_user
|
||||
# Avoid extremely unlikely race condition when approving and confirming
|
||||
# the user at the same time
|
||||
after_confirmation_tasks if new_user
|
||||
end
|
||||
|
||||
def after_confirmation_tasks
|
||||
# Handle condition when approving and confirming a user at the same time
|
||||
reload unless approved?
|
||||
|
||||
if approved?
|
||||
@@ -477,7 +448,6 @@ class User < ApplicationRecord
|
||||
notify_staff_about_pending_account!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sign_up_from_ip_requires_approval?
|
||||
sign_up_ip.present? && IpBlock.severity_sign_up_requires_approval.containing(sign_up_ip.to_s).exists?
|
||||
@@ -498,18 +468,14 @@ class User < ApplicationRecord
|
||||
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
||||
end
|
||||
|
||||
def sign_up_username_requires_approval?
|
||||
account.username? && UsernameBlock.matches?(account.username, allow_with_approval: true)
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
Setting.registrations_mode == 'open'
|
||||
end
|
||||
|
||||
def external?
|
||||
!!@external
|
||||
end
|
||||
|
||||
def bypass_registration_checks?
|
||||
@bypass_registration_checks
|
||||
end
|
||||
|
||||
def sanitize_role
|
||||
self.role = nil if role.present? && role.everyone?
|
||||
end
|
||||
@@ -526,7 +492,7 @@ class User < ApplicationRecord
|
||||
return unless confirmed?
|
||||
|
||||
ActivityTracker.record('activity:logins', id)
|
||||
regenerate_feed! if needs_feed_update?
|
||||
regenerate_feed! if inactive_since_duration?
|
||||
end
|
||||
|
||||
def notify_staff_about_pending_account!
|
||||
@@ -539,15 +505,11 @@ class User < ApplicationRecord
|
||||
|
||||
def regenerate_feed!
|
||||
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?
|
||||
last_sign_in_at < ACTIVE_DURATION.ago
|
||||
end
|
||||
|
||||
def validate_email_dns?
|
||||
email_changed? && !external? && !self.class.skip_mx_check?
|
||||
|
||||
62
app/models/username_block.rb
Normal file
62
app/models/username_block.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: username_blocks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# allow_with_approval :boolean default(FALSE), not null
|
||||
# exact :boolean default(FALSE), not null
|
||||
# normalized_username :string not null
|
||||
# username :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UsernameBlock < ApplicationRecord
|
||||
HOMOGLYPHS = {
|
||||
'1' => 'i',
|
||||
'2' => 'z',
|
||||
'3' => 'e',
|
||||
'4' => 'a',
|
||||
'5' => 's',
|
||||
'7' => 't',
|
||||
'8' => 'b',
|
||||
'9' => 'g',
|
||||
'0' => 'o',
|
||||
}.freeze
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
|
||||
scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) }
|
||||
scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) }
|
||||
|
||||
before_save :set_normalized_username
|
||||
|
||||
def comparison
|
||||
exact? ? 'equals' : 'contains'
|
||||
end
|
||||
|
||||
def comparison=(val)
|
||||
self.exact = val == 'equals'
|
||||
end
|
||||
|
||||
def self.matches?(str, allow_with_approval: false)
|
||||
normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||
where(allow_with_approval: allow_with_approval).matches_exactly(normalized_str).or(matches_partially(normalized_str)).any?
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
username
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_normalized_username
|
||||
self.normalized_username = normalize(username)
|
||||
end
|
||||
|
||||
def normalize(str)
|
||||
str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||
end
|
||||
end
|
||||
@@ -19,19 +19,21 @@ class WorkerBatch
|
||||
redis.hset(key, { 'async_refresh_key' => async_refresh_key, 'threshold' => threshold })
|
||||
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.
|
||||
# @param [Array<String>] jids
|
||||
def add_jobs(jids)
|
||||
if jids.blank?
|
||||
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
|
||||
return if jids.empty?
|
||||
|
||||
redis.multi do |pipeline|
|
||||
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.
|
||||
# @param [String] jid
|
||||
def remove_job(jid)
|
||||
def remove_job(jid, increment: false)
|
||||
_, pending, processed, async_refresh_key, threshold = redis.multi do |pipeline|
|
||||
pipeline.srem(key('jobs'), jid)
|
||||
pipeline.hincrby(key, 'pending', -1)
|
||||
@@ -52,11 +54,24 @@ class WorkerBatch
|
||||
pipeline.hget(key, 'threshold')
|
||||
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?
|
||||
async_refresh = AsyncRefresh.new(async_refresh_key)
|
||||
async_refresh.increment_result_count(by: 1)
|
||||
async_refresh.finish! if pending.zero? || processed >= threshold.to_f * (processed + pending)
|
||||
async_refresh.finish!
|
||||
end
|
||||
|
||||
cleanup
|
||||
end
|
||||
|
||||
# Get pending jobs.
|
||||
@@ -76,4 +91,8 @@ class WorkerBatch
|
||||
def key(suffix = nil)
|
||||
"worker_batch:#{@id}#{":#{suffix}" if suffix}"
|
||||
end
|
||||
|
||||
def cleanup
|
||||
redis.del(key, key('jobs'))
|
||||
end
|
||||
end
|
||||
|
||||
19
app/policies/username_block_policy.rb
Normal file
19
app/policies/username_block_policy.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsernameBlockPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def create?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def update?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService
|
||||
# Limit of replies to fetch per status
|
||||
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
|
||||
|
||||
super
|
||||
|
||||
@@ -8,9 +8,10 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
||||
DISCOVERIES_PER_REQUEST = 1000
|
||||
|
||||
# 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)
|
||||
|
||||
@depth = depth || 0
|
||||
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
||||
@json = if prefetched_body.nil?
|
||||
fetch_status(uri, true, on_behalf_of)
|
||||
@@ -52,7 +53,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
||||
return nil if discoveries > DISCOVERIES_PER_REQUEST
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
@@ -6,7 +6,7 @@ class ActivityPub::FetchRepliesService < BaseService
|
||||
# Limit of fetched replies
|
||||
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
|
||||
@allow_synchronous_requests = allow_synchronous_requests
|
||||
|
||||
@@ -15,9 +15,11 @@ class ActivityPub::FetchRepliesService < BaseService
|
||||
|
||||
@items = filter_replies(@items)
|
||||
|
||||
batch = WorkerBatch.new
|
||||
batch.connect(async_refresh_key) if async_refresh_key.present?
|
||||
batch.add_jobs(FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] })
|
||||
WorkerBatch.new(batch_id).within do |batch|
|
||||
FetchReplyWorker.push_bulk(@items) do |reply_uri|
|
||||
[reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }]
|
||||
end
|
||||
end
|
||||
|
||||
[@items, n_pages]
|
||||
end
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
class ActivityPub::VerifyQuoteService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
MAX_SYNCHRONOUS_DEPTH = 2
|
||||
|
||||
# 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
|
||||
@depth = depth || 0
|
||||
@quote = quote
|
||||
@fetching_error = nil
|
||||
|
||||
@@ -72,10 +75,12 @@ class ActivityPub::VerifyQuoteService < BaseService
|
||||
return if uri.nil? || @quote.quoted_status.present?
|
||||
|
||||
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?
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
@fetching_error = e
|
||||
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
|
||||
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?
|
||||
@quote.update(quoted_status: status)
|
||||
|
||||
@@ -82,7 +82,7 @@ class FollowService < BaseService
|
||||
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow')
|
||||
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']
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class UnfollowService < BaseService
|
||||
|
||||
unless @options[:skip_unmerge]
|
||||
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']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ class UnmuteService < BaseService
|
||||
if account.following?(target_account)
|
||||
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']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,14 +28,6 @@ class UnreservedUsernameValidator < ActiveModel::Validator
|
||||
end
|
||||
|
||||
def settings_username_reserved?
|
||||
settings_has_reserved_usernames? && settings_reserves_username?
|
||||
end
|
||||
|
||||
def settings_has_reserved_usernames?
|
||||
Setting.reserved_usernames.present?
|
||||
end
|
||||
|
||||
def settings_reserves_username?
|
||||
Setting.reserved_usernames.include?(@username.downcase)
|
||||
UsernameBlock.matches?(@username, allow_with_approval: false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif account.silenced?
|
||||
= t('admin.accounts.silenced')
|
||||
- elsif account.local? && account.user&.disabled?
|
||||
- elsif account.local? && account.user_disabled?
|
||||
= t('admin.accounts.disabled')
|
||||
- elsif account.local? && !account.user&.confirmed?
|
||||
- elsif account.local? && !account.user_confirmed?
|
||||
= t('admin.accounts.confirming')
|
||||
- elsif account.local? && !account.user_approved?
|
||||
= t('admin.accounts.pending')
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
%tr
|
||||
%th= t('admin.accounts.email_status')
|
||||
%td
|
||||
- if account.user&.confirmed?
|
||||
- if account.user_confirmed?
|
||||
= t('admin.accounts.confirmed')
|
||||
- else
|
||||
= t('admin.accounts.confirming')
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
%span.red= t('admin.accounts.suspended')
|
||||
- elsif target_account.silenced?
|
||||
%span.red= t('admin.accounts.silenced')
|
||||
- elsif target_account.user&.disabled?
|
||||
- elsif target_account.user_disabled?
|
||||
%span.red= t('admin.accounts.disabled')
|
||||
- else
|
||||
%span.neutral= t('admin.accounts.no_limits_imposed')
|
||||
|
||||
16
app/views/admin/username_blocks/_form.html.haml
Normal file
16
app/views/admin/username_blocks/_form.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
.fields-group
|
||||
= form.input :username,
|
||||
wrapper: :with_block_label,
|
||||
input_html: { autocomplete: 'new-password', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }
|
||||
|
||||
.fields-group
|
||||
= form.input :comparison,
|
||||
as: :select,
|
||||
wrapper: :with_block_label,
|
||||
collection: %w(equals contains),
|
||||
include_blank: false,
|
||||
label_method: ->(type) { I18n.t(type, scope: 'admin.username_blocks.comparison') }
|
||||
|
||||
.fields-group
|
||||
= form.input :allow_with_approval,
|
||||
wrapper: :with_label
|
||||
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
@@ -0,0 +1,12 @@
|
||||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||
= f.check_box :username_block_ids, { multiple: true, include_hidden: false }, username_block.id
|
||||
.sr-only= username_block.username
|
||||
.batch-table__row__content.pending-account
|
||||
.pending-account__header
|
||||
= t(username_block.exact? ? 'admin.username_blocks.matches_exactly_html' : 'admin.username_blocks.contains_html', string: content_tag(:samp, link_to(username_block.username, edit_admin_username_block_path(username_block))))
|
||||
%br/
|
||||
- if username_block.allow_with_approval?
|
||||
= t('admin.email_domain_blocks.allow_registrations_with_approval')
|
||||
- else
|
||||
= t('admin.username_blocks.block_registrations')
|
||||
10
app/views/admin/username_blocks/edit.html.haml
Normal file
10
app/views/admin/username_blocks/edit.html.haml
Normal file
@@ -0,0 +1,10 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.username_blocks.edit.title')
|
||||
|
||||
= simple_form_for @username_block, url: admin_username_block_path(@username_block) do |form|
|
||||
= render 'shared/error_messages', object: @username_block
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button, t('generic.save_changes'), type: :submit
|
||||
26
app/views/admin/username_blocks/index.html.haml
Normal file
26
app/views/admin/username_blocks/index.html.haml
Normal file
@@ -0,0 +1,26 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.username_blocks.title')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.username_blocks.add_new'), new_admin_username_block_path, class: 'button'
|
||||
|
||||
= form_with model: @form, url: batch_admin_username_blocks_path do |f|
|
||||
= hidden_field_tag :page, params[:page] || 1
|
||||
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([material_symbol('close'), t('admin.username_blocks.delete')]),
|
||||
class: 'table-action-link',
|
||||
data: { confirm: t('admin.reports.are_you_sure') },
|
||||
name: :delete,
|
||||
type: :submit
|
||||
.batch-table__body
|
||||
- if @username_blocks.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'username_block', collection: @username_blocks, locals: { f: f }
|
||||
|
||||
= paginate @username_blocks
|
||||
10
app/views/admin/username_blocks/new.html.haml
Normal file
10
app/views/admin/username_blocks/new.html.haml
Normal file
@@ -0,0 +1,10 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.username_blocks.new.title')
|
||||
|
||||
= simple_form_for @username_block, url: admin_username_blocks_path do |form|
|
||||
= render 'shared/error_messages', object: @username_block
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button, t('admin.username_blocks.new.create'), type: :submit
|
||||
@@ -16,7 +16,9 @@ class ActivityPub::FetchAllRepliesWorker
|
||||
MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
|
||||
|
||||
def perform(root_status_id, options = {})
|
||||
@batch = WorkerBatch.new(options['batch_id'])
|
||||
@root_status = Status.remote.find_by(id: root_status_id)
|
||||
|
||||
return unless @root_status&.should_fetch_replies?
|
||||
|
||||
@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
|
||||
fetched_uris
|
||||
ensure
|
||||
@batch.remove_job(jid)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -53,9 +57,10 @@ class ActivityPub::FetchAllRepliesWorker
|
||||
# status URI, or the prefetched body of the Note object
|
||||
def get_replies(status, max_pages, options = {})
|
||||
replies_collection_or_uri = get_replies_uri(status)
|
||||
|
||||
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
|
||||
|
||||
# Get the URI of the replies collection of a status
|
||||
@@ -78,9 +83,10 @@ class ActivityPub::FetchAllRepliesWorker
|
||||
# @param root_status_uri [String]
|
||||
def get_root_replies(root_status_uri, options = {})
|
||||
root_status_body = fetch_resource(root_status_uri, true)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ class FetchReplyWorker
|
||||
|
||||
def perform(child_url, options = {})
|
||||
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
|
||||
batch&.remove_job(jid)
|
||||
batch&.remove_job(jid, increment: result.present?)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@ class MoveWorker
|
||||
|
||||
def copy_account_notes!
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -104,7 +104,7 @@ class MoveWorker
|
||||
|
||||
def carry_blocks_over!
|
||||
@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)
|
||||
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
|
||||
end
|
||||
@@ -115,19 +115,29 @@ class MoveWorker
|
||||
|
||||
def carry_mutes_over!
|
||||
@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)
|
||||
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
|
||||
@deferred_error = e
|
||||
end
|
||||
end
|
||||
|
||||
def add_account_note_if_needed!(account, id)
|
||||
unless AccountNote.exists?(account: account, target_account: @target_account)
|
||||
text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -9,7 +9,7 @@ class PublishScheduledStatusWorker
|
||||
scheduled_status = ScheduledStatus.find(scheduled_status_id)
|
||||
scheduled_status.destroy!
|
||||
|
||||
return true if scheduled_status.account.user.disabled?
|
||||
return true if scheduled_status.account.user_disabled?
|
||||
|
||||
PostStatusService.new.call(
|
||||
scheduled_status.account,
|
||||
|
||||
@@ -80,6 +80,8 @@ ignore_unused:
|
||||
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||
- '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.username_blocks.matches_exactly_html'
|
||||
- 'admin.username_blocks.contains_html'
|
||||
|
||||
ignore_inconsistent_interpolations:
|
||||
- '*.one'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../lib/mastodon/sidekiq_middleware'
|
||||
require_relative '../../lib/mastodon/worker_batch_middleware'
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = REDIS_CONFIGURATION.sidekiq
|
||||
@@ -72,14 +73,12 @@ Sidekiq.configure_server do |config|
|
||||
|
||||
config.server_middleware do |chain|
|
||||
chain.add Mastodon::SidekiqMiddleware
|
||||
end
|
||||
|
||||
config.server_middleware do |chain|
|
||||
chain.add SidekiqUniqueJobs::Middleware::Server
|
||||
end
|
||||
|
||||
config.client_middleware do |chain|
|
||||
chain.add SidekiqUniqueJobs::Middleware::Client
|
||||
chain.add Mastodon::WorkerBatchMiddleware
|
||||
end
|
||||
|
||||
config.on(:startup) do
|
||||
@@ -105,6 +104,7 @@ Sidekiq.configure_client do |config|
|
||||
|
||||
config.client_middleware do |chain|
|
||||
chain.add SidekiqUniqueJobs::Middleware::Client
|
||||
chain.add Mastodon::WorkerBatchMiddleware
|
||||
end
|
||||
|
||||
config.logger.level = Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
|
||||
|
||||
@@ -560,6 +560,8 @@ br:
|
||||
one: "%{count} skeudenn"
|
||||
other: "%{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:
|
||||
ownership: N'hallit ket spilhennañ embannadurioù ar re all
|
||||
quote_policies:
|
||||
|
||||
@@ -1870,6 +1870,7 @@ ca:
|
||||
edited_at_html: Editat %{date}
|
||||
errors:
|
||||
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
|
||||
pin_errors:
|
||||
direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats
|
||||
|
||||
@@ -1958,6 +1958,7 @@ cs:
|
||||
edited_at_html: Upraven %{date}
|
||||
errors:
|
||||
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ů
|
||||
pin_errors:
|
||||
direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user