diff --git a/Dockerfile b/Dockerfile
index f2164ffd94..ad8150552a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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"
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 5b0867dcfb..fe314daeca 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -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
diff --git a/app/javascript/mastodon/components/status/handled_link.stories.tsx b/app/javascript/mastodon/components/status/handled_link.stories.tsx
index a45e33626a..e343833704 100644
--- a/app/javascript/mastodon/components/status/handled_link.stories.tsx
+++ b/app/javascript/mastodon/components/status/handled_link.stories.tsx
@@ -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 (
<>
-
+
+ {args.text}
+
>
@@ -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>;
+} satisfies Meta;
export default meta;
@@ -38,15 +67,23 @@ type Story = StoryObj;
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',
},
};
diff --git a/app/javascript/mastodon/components/status/handled_link.tsx b/app/javascript/mastodon/components/status/handled_link.tsx
index 83262886e8..3c8973992b 100644
--- a/app/javascript/mastodon/components/status/handled_link.tsx
+++ b/app/javascript/mastodon/components/status/handled_link.tsx
@@ -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;
}
export const HandledLink: FC> = ({
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 (
- #{hashtag}
+ {children}
);
- } else if (text.startsWith('@')) {
+ } else if ((text.startsWith('@') || prevText?.endsWith('@')) && mention) {
// Handle mentions
- const mention = text.slice(1).trim();
return (
- @{mention}
+ {children}
);
}
- // 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 (
-
- {text}
+
+ {children}
);
}
- try {
- const url = new URL(href);
- const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash.
- return (
-
- {url.protocol + '//'}
- {`${url.hostname}/${first ?? ''}`}
- {'/' + rest.join('/')}
-
- );
- } catch {
- return text;
- }
+ return (
+
+ {children}
+
+ );
};
export const useElementHandledLink = ({
hashtagAccountId,
- hrefToMentionAccountId,
+ hrefToMention,
}: {
hashtagAccountId?: string;
- hrefToMentionAccountId?: (href: string) => string | undefined;
+ hrefToMention?: (href: string) => ApiMentionJSON | undefined;
} = {}) => {
const onElement = useCallback(
- (element, { key, ...props }) => {
+ (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
- const mentionId = hrefToMentionAccountId?.(element.href);
+ const mention = hrefToMention?.(element.href);
return (
+ mention={mention}
+ >
+ {children}
+
);
}
return undefined;
},
- [hashtagAccountId, hrefToMentionAccountId],
+ [hashtagAccountId, hrefToMention],
);
return { onElement };
};
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index ede98cc81a..14779ce3a4 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -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}
+
);
} else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) {
return null;
diff --git a/app/javascript/mastodon/features/explore/components/author_link.jsx b/app/javascript/mastodon/features/explore/components/author_link.jsx
deleted file mode 100644
index cf92ebc78b..0000000000
--- a/app/javascript/mastodon/features/explore/components/author_link.jsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-};
-
-AuthorLink.propTypes = {
- accountId: PropTypes.string.isRequired,
-};
diff --git a/app/javascript/mastodon/features/explore/components/author_link.tsx b/app/javascript/mastodon/features/explore/components/author_link.tsx
new file mode 100644
index 0000000000..a4667693a5
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/components/author_link.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx
index 91c3abde38..b7dc998a47 100644
--- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx
@@ -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);
},
});
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 059a97b0f3..8e76cd8fe0 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -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. S’ju 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 s’do 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": "S’jen 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 s’mund të citohen",
"status.quotes": "{count, plural, one {citim} other {citime}}",
"status.quotes.empty": "Këtë postim ende s’e ka cituar kush. Kur dikush ta bëjë, do të shfaqet këtu.",
"status.quotes.local_other_disclaimer": "Citimet e hedhura poshtë nga autori s’do 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 s’lejohet 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 s’mund të citohen nga të tjerë.",
"visibility_modal.helper.privacy_editing": "Dukshmëria s’mund të ndryshohet pasi postimi botohet.",
"visibility_modal.helper.privacy_private_self_quote": "Citimet nga ju vetë të postime private s’mund të bëhen publike.",
"visibility_modal.helper.private_quoting": "Postime vetëm për ndjekësit, të krijuara në Mastodon s’mund 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 Parapëlqime > Parazgjedhje postimi.",
"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"
}
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index d5da46a351..01ef33d061 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -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)
diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb
index f7d34f3195..cbbea73056 100644
--- a/app/lib/activitypub/parser/status_parser.rb
+++ b/app/lib/activitypub/parser/status_parser.rb
@@ -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)
diff --git a/app/models/quote.rb b/app/models/quote.rb
index dcfcd3b353..0d24cb239a 100644
--- a/app/models/quote.rb
+++ b/app/models/quote.rb
@@ -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
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index f99d15a6d9..0ca76d61c0 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -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
diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 66c8da2b60..7e26734258 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -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
diff --git a/app/workers/activitypub/refetch_and_verify_quote_worker.rb b/app/workers/activitypub/refetch_and_verify_quote_worker.rb
index 0c7ecd9b2a..e2df023103 100644
--- a/app/workers/activitypub/refetch_and_verify_quote_worker.rb
+++ b/app/workers/activitypub/refetch_and_verify_quote_worker.rb
@@ -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
diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml
index db975ec65a..15d9e09e29 100644
--- a/config/locales/simple_form.sq.yml
+++ b/config/locales/simple_form.sq.yml
@@ -148,6 +148,9 @@ sq:
min_age: S’duhet 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}. S’do ta depozitojmë këtë.
+ other: Na duhet të sigurohemi se jeni të paktën %{count} që të përdorni %{domain}. S’do ta depozitojmë këtë.
role: Roli kontrollon cilat leje ka përdoruesi.
user_role:
color: Ngjyrë për t’u 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 t’u vëzhguar
webhook:
events: Akte të aktivizuar
template: Gjedhe ngarkese
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index b0ce596564..3f1978f7c8 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -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: S’u 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: "Rekomandimet për ndjekje ndihmojnë përdoruesit e rinj të gjejnë shpejt lëndë me interes. 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: S’u ndryshua ndonjë rregull emrash përdoruesishm ngaqë s’u 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 t’i përgjigjeni s’duket 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 s’mund të fiksohen
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 72579dc51b..9457962ac0 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -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' }
diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb
index 04179e9bf4..0d11386d57 100644
--- a/spec/serializers/activitypub/note_serializer_spec.rb
+++ b/spec/serializers/activitypub/note_serializer_spec.rb
@@ -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) }
diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb
index 7af2c67387..56a8c71cbe 100644
--- a/spec/services/activitypub/process_status_update_service_spec.rb
+++ b/spec/services/activitypub/process_status_update_service_spec.rb
@@ -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') }
diff --git a/spec/system/admin/dashboard_spec.rb b/spec/system/admin/dashboard_spec.rb
index 06d31cde44..d0cedd2ed1 100644
--- a/spec/system/admin/dashboard_spec.rb
+++ b/spec/system/admin/dashboard_spec.rb
@@ -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