Merge commit 'c858fc77ef194be0217fd98eae84efd261dba798' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-10-09 17:59:13 +02:00
21 changed files with 273 additions and 88 deletions

View File

@@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.6"
ARG RUBY_VERSION="3.4.7"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"

View File

@@ -9,10 +9,16 @@ module Admin
@pending_appeals_count = Appeal.pending.async_count
@pending_reports_count = Report.unresolved.async_count
@pending_tags_count = Tag.pending_review.async_count
@pending_tags_count = pending_tags.async_count
@pending_users_count = User.pending.async_count
@system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
end
private
def pending_tags
::Trends::TagFilter.new(status: :pending_review).results
end
end
end

View File

@@ -8,12 +8,32 @@ import { HoverCardController } from '../hover_card_controller';
import type { HandledLinkProps } from './handled_link';
import { HandledLink } from './handled_link';
type HandledLinkStoryProps = Pick<
HandledLinkProps,
'href' | 'text' | 'prevText'
> & {
mentionAccount: 'local' | 'remote' | 'none';
hashtagAccount: boolean;
};
const meta = {
title: 'Components/Status/HandledLink',
render(args) {
render({ mentionAccount, hashtagAccount, ...args }) {
let mention: HandledLinkProps['mention'] | undefined;
if (mentionAccount === 'local') {
mention = { id: '1', acct: 'testuser' };
} else if (mentionAccount === 'remote') {
mention = { id: '2', acct: 'remoteuser@mastodon.social' };
}
return (
<>
<HandledLink {...args} mentionAccountId='1' hashtagAccountId='1' />
<HandledLink
{...args}
mention={mention}
hashtagAccountId={hashtagAccount ? '1' : undefined}
>
<span>{args.text}</span>
</HandledLink>
<HashtagMenuController />
<HoverCardController />
</>
@@ -22,15 +42,24 @@ const meta = {
args: {
href: 'https://example.com/path/subpath?query=1#hash',
text: 'https://example.com',
mentionAccount: 'none',
hashtagAccount: false,
},
argTypes: {
mentionAccount: {
control: { type: 'select' },
options: ['local', 'remote', 'none'],
defaultValue: 'none',
},
},
parameters: {
state: {
accounts: {
'1': accountFactoryState(),
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
},
},
},
} satisfies Meta<Pick<HandledLinkProps, 'href' | 'text'>>;
} satisfies Meta<HandledLinkStoryProps>;
export default meta;
@@ -38,15 +67,23 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Simple: Story = {
args: {
href: 'https://example.com/test',
},
};
export const Hashtag: Story = {
args: {
text: '#example',
hashtagAccount: true,
},
};
export const Mention: Story = {
args: {
text: '@user',
mentionAccount: 'local',
},
};

View File

@@ -1,111 +1,109 @@
import { useCallback } from 'react';
import type { ComponentProps, FC } from 'react';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
import type { OnElementHandler } from '@/mastodon/utils/html';
export interface HandledLinkProps {
href: string;
text: string;
prevText?: string;
hashtagAccountId?: string;
mentionAccountId?: string;
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
}
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
href,
text,
prevText,
hashtagAccountId,
mentionAccountId,
mention,
className,
children,
...props
}) => {
// Handle hashtags
if (text.startsWith('#')) {
if (text.startsWith('#') || prevText?.endsWith('#')) {
const hashtag = text.slice(1).trim();
return (
<Link
{...props}
className='mention hashtag'
className={classNames('mention hashtag', className)}
to={`/tags/${hashtag}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
#<span>{hashtag}</span>
{children}
</Link>
);
} else if (text.startsWith('@')) {
} else if ((text.startsWith('@') || prevText?.endsWith('@')) && mention) {
// Handle mentions
const mention = text.slice(1).trim();
return (
<Link
{...props}
className='mention'
to={`/@${mention}`}
title={`@${mention}`}
data-hover-card-account={mentionAccountId}
className={classNames('mention', className)}
to={`/@${mention.acct}`}
title={`@${mention.acct}`}
data-hover-card-account={mention.id}
>
@<span>{mention}</span>
{children}
</Link>
);
}
// Non-absolute paths treated as internal links.
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
if (href.startsWith('/')) {
return (
<Link {...props} className='unhandled-link' to={href}>
{text}
<Link className={classNames('unhandled-link', className)} to={href}>
{children}
</Link>
);
}
try {
const url = new URL(href);
const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash.
return (
<a
{...props}
href={href}
title={href}
className='unhandled-link'
target='_blank'
rel='noreferrer noopener'
translate='no'
>
<span className='invisible'>{url.protocol + '//'}</span>
<span className='ellipsis'>{`${url.hostname}/${first ?? ''}`}</span>
<span className='invisible'>{'/' + rest.join('/')}</span>
</a>
);
} catch {
return text;
}
return (
<a
{...props}
href={href}
title={href}
className={classNames('unhandled-link', className)}
target='_blank'
rel='noreferrer noopener'
translate='no'
>
{children}
</a>
);
};
export const useElementHandledLink = ({
hashtagAccountId,
hrefToMentionAccountId,
hrefToMention,
}: {
hashtagAccountId?: string;
hrefToMentionAccountId?: (href: string) => string | undefined;
hrefToMention?: (href: string) => ApiMentionJSON | undefined;
} = {}) => {
const onElement = useCallback<OnElementHandler>(
(element, { key, ...props }) => {
(element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
const mentionId = hrefToMentionAccountId?.(element.href);
const mention = hrefToMention?.(element.href);
return (
<HandledLink
{...props}
key={key as string} // React requires keys to not be part of spread props.
href={element.href}
text={element.innerText}
prevText={element.previousSibling?.textContent ?? undefined}
hashtagAccountId={hashtagAccountId}
mentionAccountId={mentionId}
/>
mention={mention}
>
{children}
</HandledLink>
);
}
return undefined;
},
[hashtagAccountId, hrefToMentionAccountId],
[hashtagAccountId, hrefToMention],
);
return { onElement };
};

View File

@@ -204,7 +204,7 @@ class StatusContent extends PureComponent {
this.node = c;
};
handleElement = (element, { key, ...props }) => {
handleElement = (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
return (
@@ -213,9 +213,11 @@ class StatusContent extends PureComponent {
href={element.href}
text={element.innerText}
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
mentionAccountId={mention?.get('id')}
mention={mention?.toJSON()}
key={key}
/>
>
{children}
</HandledLink>
);
} else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) {
return null;

View File

@@ -1,23 +0,0 @@
import PropTypes from 'prop-types';
import { Avatar } from 'mastodon/components/avatar';
import { useAppSelector } from 'mastodon/store';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
export const AuthorLink = ({ accountId }) => {
const account = useAppSelector(state => state.getIn(['accounts', accountId]));
if (!account) {
return null;
}
return (
<LinkedDisplayName displayProps={{account}} className='story__details__shared__author-link'>
<Avatar account={account} size={16} />
</LinkedDisplayName>
);
};
AuthorLink.propTypes = {
accountId: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,22 @@
import type { FC } from 'react';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { Avatar } from 'mastodon/components/avatar';
import { useAppSelector } from 'mastodon/store';
export const AuthorLink: FC<{ accountId: string }> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
if (!account) {
return null;
}
return (
<LinkedDisplayName
displayProps={{ account, variant: 'simple' }}
className='story__details__shared__author-link'
>
<Avatar account={account} size={16} />
</LinkedDisplayName>
);
};

View File

@@ -48,9 +48,8 @@ export const EmbeddedStatusContent: React.FC<{
);
const htmlHandlers = useElementHandledLink({
hashtagAccountId: status.get('account') as string | undefined,
hrefToMentionAccountId(href) {
const mention = mentions.find((item) => item.url === href);
return mention?.id;
hrefToMention(href) {
return mentions.find((item) => item.url === href);
},
});

View File

@@ -113,6 +113,7 @@
"alt_text_modal.describe_for_people_with_visual_impairments": "Përshkruajeni këtë për persona me mangësi shikimi…",
"alt_text_modal.done": "U bë",
"announcement.announcement": "Lajmërim",
"annual_report.summary.archetype.oracle": "Orakulli",
"annual_report.summary.followers.followers": "ndjekës",
"annual_report.summary.followers.total": "{count} gjithsej",
"annual_report.summary.here_it_is": "Ja {year} juaj e shqyrtuar:",
@@ -299,6 +300,7 @@
"domain_pill.your_handle": "Targa juaj:",
"domain_pill.your_server": "Shtëpia juaj dixhitale, kur gjenden krejt postimet tuaja. Sju pëlqen kjo këtu? Shpërngulni shërbyes kur të doni dhe sillni edhe ndjekësit tuaj.",
"domain_pill.your_username": "Identifikuesi juja unik në këtë shërbyes. Është e mundur të gjenden përdorues me të njëjtin emër përdoruesi në shërbyes të ndryshëm.",
"dropdown.empty": "Përzgjidhni një mundësi",
"embed.instructions": "Trupëzojeni këtë gjendje në sajtin tuaj duke kopjuar kodin më poshtë.",
"embed.preview": "Ja si do të duket:",
"emoji_button.activity": "Veprimtari",
@@ -568,6 +570,8 @@
"navigation_bar.follows_and_followers": "Ndjekje dhe ndjekës",
"navigation_bar.import_export": "Importim dhe eksportim",
"navigation_bar.lists": "Lista",
"navigation_bar.live_feed_local": "Pryrje e atypëratyshme (vendore)",
"navigation_bar.live_feed_public": "Prurje e atypëratyshme (publike)",
"navigation_bar.logout": "Dalje",
"navigation_bar.moderation": "Moderim",
"navigation_bar.more": "Më tepër",
@@ -737,11 +741,18 @@
"privacy.private.short": "Ndjekës",
"privacy.public.long": "Cilido që hyn e del në Mastodon",
"privacy.public.short": "Publik",
"privacy.quote.anyone": "{visibility}, mund të citojë cilido",
"privacy.quote.disabled": "{visibility}, citimet janë çaktivizuar",
"privacy.quote.limited": "{visibility}, citime të kufizuara",
"privacy.unlisted.additional": "Ky sillet saktësisht si publik, vetëm se postimi sdo të shfaqet në prurje të drejtpërdrejta, ose në hashtag-ë, te eksploroni, apo kërkim në Mastodon, edhe kur keni zgjedhur të jetë për tërë llogarinë.",
"privacy.unlisted.long": "Fshehur nga përfundime kërkimi në Mastodon, rrjedha kohore gjërash në modë dhe publike",
"privacy.unlisted.short": "Publik i qetë",
"privacy_policy.last_updated": "Përditësuar së fundi më {date}",
"privacy_policy.title": "Rregulla Privatësie",
"quote_error.poll": "Me pyetësorët nuk lejohet citim.",
"quote_error.quote": "Lejohet vetëm një citim në herë.",
"quote_error.unauthorized": "Sjen i autorizuar ta citoni këtë postim.",
"quote_error.upload": "Me bashkëngjitjet media nuk lejohet citim.",
"recommended": "E rekomanduar",
"refresh": "Rifreskoje",
"regeneration_indicator.please_stand_by": "Ju lutemi, mos u largoni.",
@@ -851,6 +862,7 @@
"status.admin_account": "Hap ndërfaqe moderimi për @{name}",
"status.admin_domain": "Hap ndërfaqe moderimi për {domain}",
"status.admin_status": "Hape këtë mesazh te ndërfaqja e moderimit",
"status.all_disabled": "Përforcimet dhe citime janë të çaktivizuara",
"status.block": "Blloko @{name}",
"status.bookmark": "Faqeruaje",
"status.cancel_reblog_private": "Shpërforcojeni",
@@ -889,6 +901,8 @@
"status.mute_conversation": "Heshtoje bisedën",
"status.open": "Zgjeroje këtë mesazh",
"status.pin": "Fiksoje në profil",
"status.quote": "Citojeni",
"status.quote.cancel": "Anuloje citimin",
"status.quote_error.filtered": "Fshehur për shkak të njërit nga filtrat tuaj",
"status.quote_error.limited_account_hint.action": "Shfaqe, sido qoftë",
"status.quote_error.limited_account_hint.title": "Kjo llogari është fshehur nga moderatorët e {domain}.",
@@ -899,7 +913,9 @@
"status.quote_followers_only": "Këtë postim mund ta citojnë vetëm ndjekës",
"status.quote_manual_review": "Autori do ta shqyrtojë dorazi",
"status.quote_noun": "Citim",
"status.quote_policy_change": "Ndryshoni cilët mund të citojnë",
"status.quote_post_author": "U citua një postim nga @{name}",
"status.quote_private": "Postimet private smund të citohen",
"status.quotes": "{count, plural, one {citim} other {citime}}",
"status.quotes.empty": "Këtë postim ende se ka cituar kush. Kur dikush ta bëjë, do të shfaqet këtu.",
"status.quotes.local_other_disclaimer": "Citimet e hedhura poshtë nga autori sdo të shfaqen.",
@@ -959,6 +975,7 @@
"upload_button.label": "Shtoni figura, një video ose një kartelë audio",
"upload_error.limit": "U tejkalua kufi ngarkimi kartelash.",
"upload_error.poll": "Me pyetësorët slejohet ngarkim kartelash.",
"upload_error.quote": "Nuk lejohet ngarkim kartelash me citime.",
"upload_form.drag_and_drop.instructions": "Që të merrni një bashkëngjitje media, shtypni tastin Space ose Enter. Teksa bëhet tërheqje, përdorni tastet shigjetë për ta shpënë bashkëngjitjen media në cilëndo drejtori që doni. Shtypni sërish Space ose Enter që të lihet bashkëngjitja media në pozicionin e vet të ri, ose shtypni Esc, që të anulohet veprimi.",
"upload_form.drag_and_drop.on_drag_cancel": "Tërheqja u anulua. Bashkëngjitja media {item} u la.",
"upload_form.drag_and_drop.on_drag_end": "Bashkëngjitja media {item} u la.",
@@ -982,13 +999,18 @@
"video.unmute": "Hiqi heshtimin",
"video.volume_down": "Ulje volumi",
"video.volume_up": "Ngritje volumi",
"visibility_modal.button_title": "Caktoni dukshmëri",
"visibility_modal.header": "Dukshmëri dhe ndërveprim",
"visibility_modal.helper.direct_quoting": "Përmendje private të krijuara në Mastodon smund të citohen nga të tjerë.",
"visibility_modal.helper.privacy_editing": "Dukshmëria smund të ndryshohet pasi postimi botohet.",
"visibility_modal.helper.privacy_private_self_quote": "Citimet nga ju vetë të postime private smund të bëhen publike.",
"visibility_modal.helper.private_quoting": "Postime vetëm për ndjekësit, të krijuara në Mastodon smund të citohen nga të tjerë.",
"visibility_modal.helper.unlisted_quoting": "Kur njerëzit ju citojnë, nga rrjedha kohore e gjërave në modë do të kalohen si të fshehura edhe postimet e tyre.",
"visibility_modal.instructions": "Kontrolloni cilët mund të ndërveprojnë me këtë postim. Rregullime mund të aplikooni edhe mbi krejt postimet e ardshme, që nga <link>Parapëlqime > Parazgjedhje postimi</link>.",
"visibility_modal.privacy_label": "Dukshmëri",
"visibility_modal.quote_followers": "Vetëm ndjekës",
"visibility_modal.quote_label": "Cilët mund të citojnë",
"visibility_modal.quote_nobody": "Thjesht unë",
"visibility_modal.quote_public": "Cilido",
"visibility_modal.save": "Ruaje"
}

View File

@@ -218,11 +218,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def process_quote
@quote_uri = @status_parser.quote_uri
return if @quote_uri.blank?
return unless @status_parser.quote?
approval_uri = @status_parser.quote_approval_uri
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending)
end
def process_hashtag(tag)

View File

@@ -120,6 +120,14 @@ class ActivityPub::Parser::StatusParser
flags
end
def quote?
%w(quote _misskey_quote quoteUrl quoteUri).any? { |key| @object[key].present? }
end
def deleted_quote?
@object['quote'].is_a?(Hash) && @object['quote']['type'] == 'Tombstone'
end
def quote_uri
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
value_or_id(as_array(@object[key]).first)

View File

@@ -25,7 +25,7 @@ class Quote < ApplicationRecord
REFRESH_DEADLINE = 6.hours
enum :state,
{ pending: 0, accepted: 1, rejected: 2, revoked: 3 },
{ pending: 0, accepted: 1, rejected: 2, revoked: 3, deleted: 4 },
validate: true
belongs_to :status

View File

@@ -34,8 +34,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :voters_count, if: :poll_and_voters_count?
attribute :quote, if: :quote?
attribute :quote, key: :_misskey_quote, if: :quote?
attribute :quote, key: :quote_uri, if: :quote?
attribute :quote, key: :_misskey_quote, if: :serializable_quote?
attribute :quote, key: :quote_uri, if: :serializable_quote?
attribute :quote_authorization, if: :quote_authorization?
attribute :interaction_policy
@@ -226,13 +226,17 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
object.quote&.present?
end
def serializable_quote?
object.quote&.quoted_status&.present?
end
def quote_authorization?
object.quote.present? && ActivityPub::TagManager.instance.approval_uri_for(object.quote).present?
end
def quote
# TODO: handle inlining self-quotes
ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status)
object.quote.quoted_status.present? ? ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status) : { type: 'Tombstone' }
end
def quote_authorization

View File

@@ -74,6 +74,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
update_quote_approval!
update_counts!
end
broadcast_updates! if @status.quote&.state_previously_changed?
end
def update_interaction_policies!
@@ -298,7 +300,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
def update_quote!
quote_uri = @status_parser.quote_uri
if quote_uri.present?
if @status_parser.quote?
approval_uri = @status_parser.quote_approval_uri
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
@@ -308,7 +310,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
# Revoke the quote while we get a chance… maybe this should be a `before_destroy` hook?
RevokeQuoteService.new.call(@status.quote) if @status.quote.quoted_account&.local? && @status.quote.accepted?
@status.quote.destroy
quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending)
@quote_changed = true
else
quote = @status.quote

View File

@@ -10,6 +10,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker
def perform(quote_id, quoted_uri, options = {})
quote = Quote.find(quote_id)
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id])
::DistributionWorker.perform_async(quote.status_id, { 'update' => true }) if quote.state_previously_changed?
rescue ActiveRecord::RecordNotFound
# Do nothing
true

View File

@@ -148,6 +148,9 @@ sq:
min_age: Sduhet të jetë nën moshën minimum të domosdoshme nga ligjet në juridiksionin tuaj.
user:
chosen_languages: Në iu vëntë shenjë, te rrjedha kohore publike do të shfaqen vetëm mesazhe në gjuhët e përzgjedhura
date_of_birth:
one: Na duhet të sigurohemi se jeni të paktën %{count} që të përdorni %{domain}. Sdo ta depozitojmë këtë.
other: Na duhet të sigurohemi se jeni të paktën %{count} që të përdorni %{domain}. Sdo ta depozitojmë këtë.
role: Roli kontrollon cilat leje ka përdoruesi.
user_role:
color: Ngjyrë për tu përdorur për rolin nëpër UI, si RGB në format gjashtëmbëdhjetësh
@@ -237,6 +240,7 @@ sq:
setting_display_media_default: Parazgjedhje
setting_display_media_hide_all: Fshihi krejt
setting_display_media_show_all: Shfaqi krejt
setting_emoji_style: Stil emoji-sh
setting_expand_spoilers: Mesazhet me sinjalizime mbi lëndën, zgjeroji përherë
setting_hide_network: Fshiheni rrjetin tuaj
setting_missing_alt_text_modal: Shfaq dialog ripohimi, para postimi mediash pa tekst alternativ
@@ -273,12 +277,16 @@ sq:
content_cache_retention_period: Periudhë mbajtjeje lënde të largët
custom_css: CSS Vetjake
favicon: Favikonë
local_live_feed_access: Hyrje te prurje të atypëratyshme që përmbajnë postime vendore
local_topic_feed_access: Hyrje te prurje hashtag-ësh dhe lidhjesh që përmbajnë postime vendore
mascot: Simbol vetjak (e dikurshme)
media_cache_retention_period: Periudhë mbajtjeje lënde media
min_age: Domosdosmëri moshe minimum
peers_api_enabled: Publiko te API listë shërbyesish të zbuluar
profile_directory: Aktivizo drejtori profilesh
registrations_mode: Kush mund të regjistrohet
remote_live_feed_access: Hyrje te prurje të atypëratyshme që përmbajnë postime nga larg
remote_topic_feed_access: Hyrje te prurje hashtag-ësh dhe lidhjesh që përmbajnë postime nga larg
require_invite_text: Kërko një arsye për pjesëmarrje
show_domain_blocks: Shfaq bllokime përkatësish
show_domain_blocks_rationale: Shfaq pse janë bllokuar përkatësitë
@@ -365,6 +373,10 @@ sq:
name: Emër
permissions_as_keys: Leje
position: Përparësi
username_block:
allow_with_approval: Lejo regjistrim me miratim
comparison: Metodë krahasimi
username: Fjalë për tu vëzhguar
webhook:
events: Akte të aktivizuar
template: Gjedhe ngarkese

View File

@@ -190,6 +190,7 @@ sq:
create_relay: Krijoni Rele
create_unavailable_domain: Krijo Përkatësi të Papërdorshme
create_user_role: Krijoni Rol
create_username_block: Krijoni Rregull Emrash Përdoruesish
demote_user: Zhgradoje Përdoruesin
destroy_announcement: Fshije Lajmërimin
destroy_canonical_email_block: Fshini Bllokim Email-esh
@@ -203,6 +204,7 @@ sq:
destroy_status: Fshi Gjendje
destroy_unavailable_domain: Fshi Përkatësi të Papërdorshme
destroy_user_role: Asgjësoje Rolin
destroy_username_block: Fshini Rregull Emrash Përdoruesish
disable_2fa_user: Çaktivizo 2FA-në
disable_custom_emoji: Çaktivizo Emotikon Vetjak
disable_relay: Çaktivizoje Relenë
@@ -237,6 +239,7 @@ sq:
update_report: Përditësoni Raportimin
update_status: Përditëso Gjendjen
update_user_role: Përditësoni Rol
update_username_block: Përditësoni Rregull Emrash Përdoruesish
actions:
approve_appeal_html: "%{name} miratoi apelim vendimi moderimi nga %{target}"
approve_user_html: "%{name} miratoi regjistrim nga %{target}"
@@ -255,6 +258,7 @@ sq:
create_relay_html: "%{name} krijoi një rele %{target}"
create_unavailable_domain_html: "%{name} ndali dërgimin drejt përkatësisë %{target}"
create_user_role_html: "%{name} krijoi rolin %{target}"
create_username_block_html: "%{name} shtoi rregull për emra përdoruesish që përmbajnë %{target}"
demote_user_html: "%{name} zhgradoi përdoruesin %{target}"
destroy_announcement_html: "%{name} fshiu lajmërimin për %{target}"
destroy_canonical_email_block_html: "%{name} zhbllokoi email me hashin %{target}"
@@ -268,6 +272,7 @@ sq:
destroy_status_html: "%{name} hoqi gjendje nga %{target}"
destroy_unavailable_domain_html: "%{name} rinisi dërgimin drejt përkatësisë %{target}"
destroy_user_role_html: "%{name} fshiu rolin %{target}"
destroy_username_block_html: "%{name} hoqi rregull për emra përdoruesish që përmbajnë %{target}"
disable_2fa_user_html: "%{name} çaktivizoi domosdoshmërinë për dyfaktorësh për përdoruesin %{target}"
disable_custom_emoji_html: "%{name} çaktivizoi emoxhin %{target}"
disable_relay_html: "%{name} çaktivizoi relenë %{target}"
@@ -302,6 +307,7 @@ sq:
update_report_html: "%{name} përditësoi raportimin %{target}"
update_status_html: "%{name} përditësoi gjendjen me %{target}"
update_user_role_html: "%{name} ndryshoi rolin për %{target}"
update_username_block_html: "%{name} përditësoi rregull për emra përdoruesish që përmbajnë %{target}"
deleted_account: fshiu llogarinë
empty: Su gjetën regjistra.
filter_by_action: Filtroji sipas veprimit
@@ -505,6 +511,7 @@ sq:
select_capabilities: Përzgjidhni Aftësi
sign_in: Hyni
status: Gjendje
title: Shërbyes Shërbimesh Ndihmëse Fediversi
title: FASP
follow_recommendations:
description_html: "<strong>Rekomandimet për ndjekje ndihmojnë përdoruesit e rinj të gjejnë shpejt lëndë me interes</strong>. Kur një përdorues nuk ka ndërvepruar mjaftueshëm me të tjerët, që të formohen rekomandime të personalizuara ndjekjeje, rekomandohen këto llogari. Ato përzgjidhen çdo ditë, prej një përzierje llogarish me shkallën më të lartë të angazhimit dhe numrin më të lartë të ndjekësve vendorë për një gjuhë të dhënë."
@@ -1090,6 +1097,14 @@ sq:
delete: Fshije
edit:
title: Përpunoni rregull emrash përdoruesi
matches_exactly_html: Baras me %{string}
new:
create: Krijoni rregull
title: Krijoni rregull të ri emrash përdoruesish
no_username_block_selected: Su ndryshua ndonjë rregull emrash përdoruesishm ngaqë su përzgjodh ndonjë
not_permitted: Jo i lejuar
title: Rregulla emrash përdoruesish
updated_msg: Rregulli i emrave të përdoruesve u përditësua me sukses
warning_presets:
add_new: Shtoni të ri
delete: Fshije
@@ -1880,6 +1895,7 @@ sq:
edited_at_html: Përpunuar më %{date}
errors:
in_reply_not_found: Gjendja të cilës po provoni ti përgjigjeni sduket se ekziston.
quoted_status_not_found: Postimi që po rrekeni të citoni nuk duket se ekziston.
over_character_limit: u tejkalua kufi shenjash prej %{max}
pin_errors:
direct: Postimet që janë të dukshme vetëm për përdoruesit e përmendur smund të fiksohen

View File

@@ -988,6 +988,30 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with an unverifiable quote of a dead post' do
let(:quoted_status) { Fabricate(:status) }
let(:object_json) do
build_object(
type: 'Note',
content: 'woah what she said is amazing',
quote: { type: 'Tombstone' }
)
end
it 'creates a status with an unverified quote' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.quote).to_not be_nil
expect(status.quote).to have_attributes(
state: 'deleted',
approval_uri: nil
)
end
end
context 'with an unverifiable unknown post' do
let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' }

View File

@@ -58,6 +58,21 @@ RSpec.describe ActivityPub::NoteSerializer do
end
end
context 'with a deleted quote' do
let(:quoted_status) { Fabricate(:status) }
before do
Fabricate(:quote, status: parent, quoted_status: nil, state: :accepted)
end
it 'has the expected shape' do
expect(subject).to include({
'type' => 'Note',
'quote' => { 'type' => 'Tombstone' },
})
end
end
context 'with a quote policy' do
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }

View File

@@ -1053,6 +1053,44 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
end
end
context 'when the status swaps a verified quote with an ID-less Tombstone through an explicit update' do
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
let(:second_quoted_status) { Fabricate(:status, account: quoted_account) }
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
let(:payload) do
{
'@context': [
'https://www.w3.org/ns/activitystreams',
{
'@id': 'https://w3id.org/fep/044f#quote',
'@type': '@id',
},
{
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
'@type': '@id',
},
],
id: 'foo',
type: 'Note',
summary: 'Show more',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
quote: { type: 'Tombstone' },
}
end
it 'updates the URI and unverifies the quote' do
expect { subject.call(status, json, json) }
.to change { status.quote.quoted_status }.from(quoted_status).to(nil)
.and change { status.quote.state }.from('accepted').to('deleted')
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when the status swaps a verified quote with another verifiable quote through an explicit update' do
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
let(:second_quoted_account) { Fabricate(:account, domain: 'second-quoted.example.com') }

View File

@@ -9,6 +9,7 @@ RSpec.describe 'Admin Dashboard' do
before do
stub_system_checks
Fabricate :software_update
Fabricate :tag, requested_review_at: 5.minutes.ago
sign_in(user)
end
@@ -18,6 +19,7 @@ RSpec.describe 'Admin Dashboard' do
expect(page)
.to have_title(I18n.t('admin.dashboard.title'))
.and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
.and have_content('0 pending hashtags')
end
private