diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4ec92f34121..9e69426fcf7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.77.0. +# using RuboCop version 1.79.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new diff --git a/CHANGELOG.md b/CHANGELOG.md index 19be8ea68e5..97a7234382c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [4.4.2] - 2025-07-23 + +### Security + +- Update dependencies + +### Fixed + +- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion) +- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion) +- Fix quote posts styling on notifications page (#35411 by @diondiondion) +- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion) +- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion) +- Update age limit wording (#35387 by @diondiondion) +- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire) +- Improve `Dropdown` component accessibility (#35373 by @diondiondion) +- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire) +- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima) +- Fix styling of external log-in button (#35320 by @ClearlyClaire) + ## [4.4.1] - 2025-07-09 ### Fixed diff --git a/Gemfile.lock b/Gemfile.lock index 4c232743bf1..d0472d538c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -287,7 +287,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.65.0) + haml_lint (0.65.1) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -315,7 +315,7 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.1) + httplog (1.7.2) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.7) @@ -765,7 +765,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.78.0) + rubocop (1.79.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -773,8 +773,9 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.45.1, < 2.0) + rubocop-ast (>= 1.46.0, < 2.0) ruby-progressbar (~> 1.7) + tsort (>= 0.2.0) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.46.0) parser (>= 3.3.7.2) @@ -880,6 +881,7 @@ GEM bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) + tsort (0.2.0) tty-color (0.6.0) tty-cursor (0.7.1) tty-prompt (0.23.1) diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 702550eecc1..5d1555796f4 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -19,15 +19,13 @@ module Admin log_action :resend, @user - flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') - redirect_to admin_accounts_path + redirect_to admin_accounts_path, notice: t('admin.accounts.resend_confirmation.success') end private def redirect_confirmed_user - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path + redirect_to admin_accounts_path, flash: { error: t('admin.accounts.resend_confirmation.already_confirmed') } end def user_confirmed? diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 2ae5ec82556..a08375e0a41 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,8 +14,7 @@ module Admin @admin_settings = Form::AdminSettings.new(settings_params) if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path + redirect_to after_update_redirect_path, notice: t('generic.changes_saved_msg') else render :show end diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index 283383acb4a..dd272120e21 100644 --- a/app/controllers/api/v1/admin/tags_controller.rb +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Admin::TagsController < Api::BaseController include Authorization + before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write' }, only: :update diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index f2c52f2846e..3b0cda7d931 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -16,16 +16,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def create with_redis_lock("push_subscription:#{current_user.id}") do destroy_web_push_subscriptions! - - @push_subscription = Web::PushSubscription.create!( - endpoint: subscription_params[:endpoint], - key_p256dh: subscription_params[:keys][:p256dh], - key_auth: subscription_params[:keys][:auth], - standard: subscription_params[:standard] || false, - data: data_params, - user_id: current_user.id, - access_token_id: doorkeeper_token.id - ) + @push_subscription = Web::PushSubscription.create!(web_push_subscription_params) end render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer @@ -55,6 +46,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController not_found if @push_subscription.nil? end + def web_push_subscription_params + { + access_token_id: doorkeeper_token.id, + data: data_params, + endpoint: subscription_params[:endpoint], + key_auth: subscription_params[:keys][:auth], + key_p256dh: subscription_params[:keys][:p256dh], + standard: subscription_params[:standard] || false, + user_id: current_user.id, + } + end + def subscription_params params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index e25b161afd8..f047ba60466 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController before_action :set_statuses, only: [:index] before_action :set_status, only: [:show, :context] before_action :set_thread, only: [:create] + before_action :set_quoted_status, only: [:create] before_action :check_statuses_limit, only: [:index] override_rate_limit_headers :create, family: :statuses @@ -76,6 +77,7 @@ class Api::V1::StatusesController < Api::BaseController current_user.account, text: status_params[:status], thread: @thread, + quoted_status: @quoted_status, media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], @@ -147,6 +149,16 @@ class Api::V1::StatusesController < Api::BaseController render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 end + def set_quoted_status + return unless Mastodon::Feature.outgoing_quotes_enabled? + + @quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present? + authorize(@quoted_status, :quote?) if @quoted_status.present? + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + # TODO: distinguish between non-existing and non-quotable posts + render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404 + end + def check_statuses_limit raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT end @@ -163,6 +175,7 @@ class Api::V1::StatusesController < Api::BaseController params.permit( :status, :in_reply_to_id, + :quoted_status_id, :sensitive, :spoiler_text, :visibility, diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 2711071b4a5..ced68d39fc7 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -49,7 +49,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController { policy: 'all', alerts: Notification::TYPES.index_with { alerts_enabled }, - } + }.deep_stringify_keys end def alerts_enabled diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 7c1ff59671d..2680a1c5fdc 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -19,8 +19,7 @@ class Auth::PasswordsController < Devise::PasswordsController private def redirect_invalid_reset_token - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) + redirect_to new_password_path(resource_name), flash: { error: t('auth.invalid_reset_password_token') } end def reset_password_token_is_valid? diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index ee2fc5dc80f..fe59bdc4917 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -8,8 +8,7 @@ class Settings::SessionsController < Settings::BaseController def destroy @session.destroy! - flash[:notice] = I18n.t('sessions.revoke_success') - redirect_to edit_user_registration_path + redirect_to edit_user_registration_path, notice: t('sessions.revoke_success') end private diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 9714d54f954..b01f08ed8fd 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -86,13 +86,11 @@ module Settings private def redirect_invalid_otp - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path + redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') } end def redirect_invalid_webauthn - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path + redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') } end end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index c5b83326db3..7b9d3f4fc1a 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -49,8 +49,8 @@ module HomeHelper end end - def custom_field_classes(field) - if field.verified? + def field_verified_class(verified) + if verified 'verified' else 'emojify' diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index cdac41b8a7d..b720b4746d0 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -3,8 +3,8 @@ import { useCallback } from 'react'; import { useLinks } from 'mastodon/hooks/useLinks'; import { EmojiHTML } from '../features/emoji/emoji_html'; -import { isFeatureEnabled } from '../initial_state'; import { useAppSelector } from '../store'; +import { isModernEmojiEnabled } from '../utils/environment'; interface AccountBioProps { className: string; @@ -32,9 +32,7 @@ export const AccountBio: React.FC = ({ if (!account) { return ''; } - return isFeatureEnabled('modern_emojis') - ? account.note - : account.note_emojified; + return isModernEmojiEnabled() ? account.note : account.note_emojified; }); const extraEmojis = useAppSelector((state) => { const account = state.accounts.get(accountId); diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx index 8e3a434c14b..d7d0b5f2ce0 100644 --- a/app/javascript/mastodon/components/gifv.tsx +++ b/app/javascript/mastodon/components/gifv.tsx @@ -37,7 +37,6 @@ export const GIFV = forwardRef( role='button' tabIndex={0} aria-label={alt} - title={alt} lang={lang} onClick={handleClick} /> @@ -49,7 +48,6 @@ export const GIFV = forwardRef( role='button' tabIndex={0} aria-label={alt} - title={alt} lang={lang} width={width} height={height} diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 02f06ec96ac..e1fd7734e9e 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -13,8 +13,9 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react' import { Icon } from 'mastodon/components/icon'; import { Poll } from 'mastodon/components/poll'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { autoPlayGif, isFeatureEnabled, languages as preloadedLanguages } from 'mastodon/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; import { EmojiHTML } from '../features/emoji/emoji_html'; +import { isModernEmojiEnabled } from '../utils/environment'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -24,7 +25,7 @@ const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) * @returns {string} */ export function getStatusContent(status) { - if (isFeatureEnabled('modern_emojis')) { + if (isModernEmojiEnabled()) { return status.getIn(['translation', 'content']) || status.get('content'); } return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 70b7968fba1..c3055aeeab5 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -41,9 +41,11 @@ export default class StatusList extends ImmutablePureComponent { }; componentDidMount() { - this.columnHeaderHeight = parseFloat( - getComputedStyle(this.node.node).getPropertyValue('--column-header-height') - ) || 0; + this.columnHeaderHeight = this.node?.node + ? parseFloat( + getComputedStyle(this.node.node).getPropertyValue('--column-header-height') + ) || 0 + : 0; } getFeaturedStatusCount = () => { @@ -69,8 +71,8 @@ export default class StatusList extends ImmutablePureComponent { }; _selectChild = (id, index, direction) => { - const listContainer = this.node.node; - let listItem = listContainer.querySelector( + const listContainer = this.node?.node; + let listItem = listContainer?.querySelector( // :nth-child uses 1-based indexing `.item-list > :nth-child(${index + 1 + direction})` ); diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index 27af2dda279..85628e6723d 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -5,8 +5,8 @@ import type { List as ImmutableList } from 'immutable'; import { isList } from 'immutable'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; -import { isFeatureEnabled } from '@/mastodon/initial_state'; import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { useEmojiAppState } from './hooks'; import { emojifyElement } from './render'; @@ -25,7 +25,7 @@ export const EmojiHTML: React.FC = ({ extraEmojis, ...props }) => { - if (isFeatureEnabled('modern_emojis')) { + if (isModernEmojiEnabled()) { return ( ( ): Promise { const locale = toSupportedLocaleOrCustom(localeOrCustom); - let uri: string; + // Use location.origin as this script may be loaded from a CDN domain. + const url = new URL(location.origin); if (locale === 'custom') { - uri = '/api/v1/custom_emojis'; + url.pathname = '/api/v1/custom_emojis'; } else { - uri = `/packs${isDevelopment() ? '-dev' : ''}/emoji/${locale}.json`; + url.pathname = `/packs${isDevelopment() ? '-dev' : ''}/emoji/${locale}.json`; } const oldEtag = await loadLatestEtag(locale); - const response = await fetch(uri, { + const response = await fetch(url, { headers: { 'Content-Type': 'application/json', 'If-None-Match': oldEtag ?? '', // Send the old ETag to check for modifications diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx index 080aaca4512..24c88f95050 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx @@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal'; import { IconButton } from 'mastodon/components/icon_button'; import { useIdentity } from 'mastodon/identity_context'; import { me } from 'mastodon/initial_state'; +import type { Account } from 'mastodon/models/account'; import type { Status } from 'mastodon/models/status'; import { makeGetStatus } from 'mastodon/selectors'; import type { RootState } from 'mastodon/store'; @@ -66,10 +67,7 @@ export const Footer: React.FC<{ const dispatch = useAppDispatch(); const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; const status = useAppSelector((state) => getStatus(state, { id: statusId })); - const accountId = status?.get('account') as string | undefined; - const account = useAppSelector((state) => - accountId ? state.accounts.get(accountId) : undefined, - ); + const account = status?.get('account') as Account | undefined; const askReplyConfirmation = useAppSelector( (state) => (state.compose.get('text') as string).trim().length !== 0, ); diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx index 09b39d3efab..1297d243d0d 100644 --- a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx +++ b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx @@ -306,10 +306,8 @@ export const ZoomableImage: React.FC = ({ {count, plural, one {# ακόμη} other {# ακόμη}} ενίσχυσαν την ανάρτησή σου", "notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}", "notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.", @@ -847,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": "Διαγραφή", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 1702076b3fb..441cfee6d10 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "See more followers on {domain}", "hints.profiles.see_more_follows": "See more follows on {domain}", "hints.profiles.see_more_posts": "See more posts on {domain}", - "hints.threads.replies_may_be_missing": "Replies from other servers may be missing.", - "hints.threads.see_more": "See more replies on {domain}", "home.column_settings.show_quotes": "Show quotes", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", @@ -614,7 +612,7 @@ "notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.own_poll": "Your poll has ended", "notification.poll": "A poll you voted in has ended", - "notification.reblog": "{name} boosted your status", + "notification.reblog": "{name} boosted your post", "notification.reblog.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} boosted your post", "notification.relationships_severance_event": "Lost connections with {name}", "notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 3af96c22c45..92f529a8f41 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -1,7 +1,7 @@ { "about.blocks": "Reguligitaj serviloj", "about.contact": "Kontakto:", - "about.default_locale": "기본", + "about.default_locale": "Defaŭlta", "about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Kialo ne disponeblas", "about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.", @@ -331,6 +331,7 @@ "featured_carousel.next": "Antaŭen", "featured_carousel.post": "Afiŝi", "featured_carousel.previous": "Malantaŭen", + "featured_carousel.slide": "{index} de {total}", "filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.", "filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!", "filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.", @@ -409,8 +410,7 @@ "hints.profiles.see_more_followers": "Vidi pli da sekvantoj sur {domain}", "hints.profiles.see_more_follows": "Vidi pli da sekvatoj sur {domain}", "hints.profiles.see_more_posts": "Vidi pli da afiŝoj sur {domain}", - "hints.threads.replies_may_be_missing": "Respondoj de aliaj serviloj eble mankas.", - "hints.threads.see_more": "Vidi pli da respondoj sur {domain}", + "home.column_settings.show_quotes": "Montri citaĵojn", "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", "home.hide_announcements": "Kaŝi la anoncojn", @@ -533,8 +533,10 @@ "mute_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, kiuj mencias ilin.", "mute_modal.you_wont_see_posts": "Ili ankoraŭ povas vidi viajn afiŝojn, sed vi ne vidos iliajn.", "navigation_bar.about": "Pri", + "navigation_bar.account_settings": "Pasvorto kaj sekureco", "navigation_bar.administration": "Administrado", "navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon", + "navigation_bar.automated_deletion": "Aŭtomata forigo de afiŝoj", "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.bookmarks": "Legosignoj", "navigation_bar.direct": "Privataj mencioj", @@ -544,6 +546,7 @@ "navigation_bar.follow_requests": "Petoj de sekvado", "navigation_bar.followed_tags": "Sekvataj kradvortoj", "navigation_bar.follows_and_followers": "Sekvatoj kaj sekvantoj", + "navigation_bar.import_export": "Importo kaj eksporto", "navigation_bar.lists": "Listoj", "navigation_bar.logout": "Elsaluti", "navigation_bar.moderation": "Modereco", @@ -551,6 +554,7 @@ "navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.", "navigation_bar.preferences": "Preferoj", + "navigation_bar.privacy_and_reach": "Privateco kaj atingo", "navigation_bar.search": "Serĉi", "not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.", "notification.admin.report": "{name} raportis {target}", @@ -787,7 +791,7 @@ "search.quick_action.open_url": "Malfermi URL en Mastodono", "search.quick_action.status_search": "Afiŝoj kiuj konformas kun {x}", "search.search_or_paste": "Serĉu aŭ algluu URL-on", - "search_popout.full_text_search_disabled_message": "Ne havebla sur {domain}.", + "search_popout.full_text_search_disabled_message": "Ne disponebla sur {domain}.", "search_popout.full_text_search_logged_out_message": "Disponebla nur kiam ensalutinte.", "search_popout.language_code": "ISO-lingva kodo", "search_popout.options": "Serĉaj opcioj", @@ -820,6 +824,8 @@ "status.bookmark": "Aldoni al la legosignoj", "status.cancel_reblog_private": "Ne plu diskonigi", "status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi", + "status.context.load_new_replies": "Disponeblaj novaj respondoj", + "status.context.loading": "Serĉante pliajn respondojn", "status.continued_thread": "Daŭrigis fadenon", "status.copy": "Kopii la ligilon al la afiŝo", "status.delete": "Forigi", @@ -845,6 +851,9 @@ "status.mute_conversation": "Silentigi konversacion", "status.open": "Pligrandigu ĉi tiun afiŝon", "status.pin": "Alpingli al la profilo", + "status.quote_error.not_found": "Ĉi tiu afiŝo ne povas esti montrata.", + "status.quote_error.rejected": "Ĉi tiu afiŝo ne povas esti montrata ĉar la originala aŭtoro ne permesas ĝian citadon.", + "status.quote_error.removed": "Ĉi tiu afiŝo estis forigita de ĝia aŭtoro.", "status.read_more": "Legi pli", "status.reblog": "Diskonigi", "status.reblog_private": "Diskonigi kun la sama videbleco", @@ -876,6 +885,7 @@ "tabs_bar.home": "Hejmo", "tabs_bar.menu": "Menuo", "tabs_bar.notifications": "Sciigoj", + "tabs_bar.publish": "Nova afiŝo", "tabs_bar.search": "Serĉi", "terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}", "terms_of_service.title": "Kondiĉoj de uzado", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 89f3873564a..3eca9eab37c 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más seguimientos en {domain}", "hints.profiles.see_more_posts": "Ver más mensajes en {domain}", - "hints.threads.replies_may_be_missing": "Es posible que falten respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar adhesiones", "home.column_settings.show_replies": "Mostrar respuestas", @@ -847,6 +845,8 @@ "status.bookmark": "Marcar", "status.cancel_reblog_private": "Quitar adhesión", "status.cannot_reblog": "No se puede adherir a este mensaje", + "status.context.load_new_replies": "Hay nuevas respuestas", + "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuación de hilo", "status.copy": "Copiar enlace al mensaje", "status.delete": "Eliminar", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index d764b3dac97..67a91402f87 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", - "hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respuestas", @@ -847,6 +845,8 @@ "status.bookmark": "Añadir marcador", "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_reblog": "Esta publicación no puede ser impulsada", + "status.context.load_new_replies": "Nuevas respuestas disponibles", + "status.context.loading": "Comprobando si hay más respuestas", "status.continued_thread": "Hilo continuado", "status.copy": "Copiar enlace al estado", "status.delete": "Borrar", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 47d23e5b40d..cbd79ea10fd 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", - "hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respuestas", @@ -847,6 +845,8 @@ "status.bookmark": "Añadir marcador", "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_reblog": "Esta publicación no se puede impulsar", + "status.context.load_new_replies": "Hay nuevas respuestas", + "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuó el hilo", "status.copy": "Copiar enlace a la publicación", "status.delete": "Borrar", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index ffb33c7e7a6..cf6c39cad48 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}", "hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}", "hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}", - "hints.threads.replies_may_be_missing": "Vastuseid teistest serveritest võib olla puudu.", - "hints.threads.see_more": "Vaata rohkem vastuseid kohas {domain}", "home.column_settings.show_quotes": "Näita tsiteeritut", "home.column_settings.show_reblogs": "Näita jagamisi", "home.column_settings.show_replies": "Näita vastuseid", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index b757fb0cdaa..65f3e87ac15 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -30,6 +30,7 @@ "account.edit_profile": "Editatu profila", "account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean", "account.endorse": "Nabarmendu profilean", + "account.familiar_followers_many": "Jarraitzaileak: {name1}, {name2} eta beste {othersCount, plural, one {ezagun bat} other {# ezagun}}", "account.familiar_followers_one": "{name1}-k jarraitzen du", "account.familiar_followers_two": "{name1}-k eta {name2}-k jarraitzen dute", "account.featured": "Gailenak", @@ -118,6 +119,8 @@ "annual_report.summary.most_used_hashtag.most_used_hashtag": "traola erabiliena", "annual_report.summary.most_used_hashtag.none": "Bat ere ez", "annual_report.summary.new_posts.new_posts": "bidalketa berriak", + "annual_report.summary.percentile.text": "Horrek jartzen zaitu top (e)an {domain} erabiltzaileen artean ", + "annual_report.summary.percentile.we_wont_tell_bernie": "Bernieri ez diogu ezer esango ;)..", "annual_report.summary.thanks": "Eskerrik asko Mastodonen parte izateagatik!", "attachments_list.unprocessed": "(prozesatu gabe)", "audio.hide": "Ezkutatu audioa", @@ -216,6 +219,7 @@ "confirmations.discard_draft.edit.message": "Jarraitzeak editatzen ari zaren mezuan egindako aldaketak baztertuko ditu.", "confirmations.discard_draft.edit.title": "Baztertu zure argitalpenari egindako aldaketak?", "confirmations.discard_draft.post.cancel": "Zirriborroa berrekin", + "confirmations.discard_draft.post.message": "Jarraituz gero, idazten ari zaren sarrera bertan behera geratuko da.", "confirmations.discard_draft.post.title": "Zure argitalpenaren zirriborroa baztertu nahi duzu?", "confirmations.discard_edit_media.confirm": "Baztertu", "confirmations.discard_edit_media.message": "Multimediaren deskribapen edo aurrebistan gorde gabeko aldaketak daude, baztertu nahi dituzu?", @@ -413,8 +417,6 @@ "hints.profiles.see_more_followers": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_follows": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_posts": "Ikusi bidalketa gehiago {domain}-(e)n", - "hints.threads.replies_may_be_missing": "Baliteke beste zerbitzari batzuen erantzun batzuk ez erakustea.", - "hints.threads.see_more": "Ikusi erantzun gehiago {domain}-(e)n", "home.column_settings.show_quotes": "Erakutsi aipamenak", "home.column_settings.show_reblogs": "Erakutsi bultzadak", "home.column_settings.show_replies": "Erakutsi erantzunak", @@ -435,6 +437,7 @@ "ignore_notifications_modal.not_following_title": "Jarraitzen ez dituzun pertsonen jakinarazpenei ez ikusiarena egin?", "ignore_notifications_modal.private_mentions_title": "Eskatu gabeko aipamen pribatuen jakinarazpenei ez ikusiarena egin?", "info_button.label": "Laguntza", + "info_button.what_is_alt_text": "

Zer da Alt testua?

Alt testuak irudiak deskribatzeko aukera ematen du, ikusmen-urritasunak, banda-zabalera txikiko konexioak edo testuinguru gehigarria nahi duten pertsonentzat.

Alt testu argi, zehatz eta objektiboen bidez, guztion irisgarritasuna eta ulermena hobetu ditzakezu.

  • Hartu elementu garrantzitsuenak
  • Laburbildu irudietako testua
  • Erabili esaldien egitura erregularra
  • Baztertu informazio erredundantea.
  • Enfokatu joeretan eta funtsezko elementuetan irudi konplexuetan (diagrametan edo mapetan, adibidez)
", "interaction_modal.action.favourite": "Jarraitzeko, zure kontutik atsegindu behar duzu.", "interaction_modal.action.follow": "Jarraitzeko zure kontutik jarraitu behar duzu.", "interaction_modal.action.reply": "Jarraitzeko zure kontutik erantzun behar duzu.", @@ -903,6 +906,7 @@ "video.hide": "Ezkutatu bideoa", "video.pause": "Pausatu", "video.play": "Jo", + "video.skip_forward": "Jauzi aurrerantz", "video.unmute": "Soinua ezarri", "video.volume_down": "Bolumena jaitsi", "video.volume_up": "Bolumena Igo" diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index fadbb247007..c35687e0317 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}", "hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}", "hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}", - "hints.threads.replies_may_be_missing": "شاید پاسخ‌ها از دیگر کارسازها نباشند.", - "hints.threads.see_more": "دیدن پاسخ‌های بیش‌تر روی {domain}", "home.column_settings.show_quotes": "نمایش نقل‌قول‌ها", "home.column_settings.show_reblogs": "نمایش تقویت‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 7de3fc07ea9..36fcfd4e947 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Näytä lisää seuraajia palvelimella {domain}", "hints.profiles.see_more_follows": "Näytä lisää seurattavia palvelimella {domain}", "hints.profiles.see_more_posts": "Näytä lisää julkaisuja palvelimella {domain}", - "hints.threads.replies_may_be_missing": "Muiden palvelinten vastauksia saattaa puuttua.", - "hints.threads.see_more": "Näytä lisää vastauksia palvelimella {domain}", "home.column_settings.show_quotes": "Näytä lainaukset", "home.column_settings.show_reblogs": "Näytä tehostukset", "home.column_settings.show_replies": "Näytä vastaukset", @@ -847,6 +845,8 @@ "status.bookmark": "Lisää kirjanmerkki", "status.cancel_reblog_private": "Peru tehostus", "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa", + "status.context.load_new_replies": "Uusia vastauksia saatavilla", + "status.context.loading": "Tarkistetaan lisävastauksia", "status.continued_thread": "Jatkoi ketjua", "status.copy": "Kopioi linkki julkaisuun", "status.delete": "Poista", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index afd75afc42b..1bdccaeae86 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Sí fleiri fylgjarar á {domain}", "hints.profiles.see_more_follows": "Sí fleiri, ið viðkomandi fylgir, á {domain}", "hints.profiles.see_more_posts": "Sí fleiri postar á {domain}", - "hints.threads.replies_may_be_missing": "Svar frá øðrum ambætarum mangla møguliga.", - "hints.threads.see_more": "Sí fleiri svar á {domain}", "home.column_settings.show_quotes": "Vís siteringar", "home.column_settings.show_reblogs": "Vís lyft", "home.column_settings.show_replies": "Vís svar", @@ -847,6 +845,8 @@ "status.bookmark": "Goym", "status.cancel_reblog_private": "Strika stimbran", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", + "status.context.load_new_replies": "Nýggj svar tøk", + "status.context.loading": "Kanni um tað eru fleiri svar", "status.continued_thread": "Framhaldandi tráður", "status.copy": "Kopiera leinki til postin", "status.delete": "Strika", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 0fdd593859a..840b0e508d5 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.", - "hints.threads.see_more": "Afficher plus de réponses sur {domain}", "home.column_settings.show_quotes": "Afficher les citations", "home.column_settings.show_reblogs": "Afficher boosts", "home.column_settings.show_replies": "Afficher réponses", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 6f1bbae61c1..c0c9580a46d 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.", - "hints.threads.see_more": "Afficher plus de réponses sur {domain}", "home.column_settings.show_quotes": "Afficher les citations", "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 33c6d89d195..25f5c0cc01f 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Besjoch mear folgers op {domain}", "hints.profiles.see_more_follows": "Besjoch mear folge accounts op {domain}", "hints.profiles.see_more_posts": "Besjoch mear berjochten op {domain}", - "hints.threads.replies_may_be_missing": "Antwurden fan oare servers kinne ûntbrekke.", - "hints.threads.see_more": "Besjoch mear reaksjes op {domain}", "home.column_settings.show_quotes": "Sitaten toane", "home.column_settings.show_reblogs": "Boosts toane", "home.column_settings.show_replies": "Reaksjes toane", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 92d06a05cf1..2c0b709bdc3 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Féach ar a thuilleadh leantóirí ar {domain}", "hints.profiles.see_more_follows": "Féach tuilleadh seo a leanas ar {domain}", "hints.profiles.see_more_posts": "Féach ar a thuilleadh postálacha ar {domain}", - "hints.threads.replies_may_be_missing": "Seans go bhfuil freagraí ó fhreastalaithe eile in easnamh.", - "hints.threads.see_more": "Féach ar a thuilleadh freagraí ar {domain}", "home.column_settings.show_quotes": "Taispeáin Sleachta", "home.column_settings.show_reblogs": "Taispeáin moltaí", "home.column_settings.show_replies": "Taispeán freagraí", @@ -847,6 +845,8 @@ "status.bookmark": "Leabharmharcanna", "status.cancel_reblog_private": "Dímhol", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", + "status.context.load_new_replies": "Freagraí nua ar fáil", + "status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí", "status.continued_thread": "Snáithe ar lean", "status.copy": "Cóipeáil an nasc chuig an bpostáil", "status.delete": "Scrios", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 208e117037b..c783182cd5f 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Faic barrachd luchd-leantainn air {domain}", "hints.profiles.see_more_follows": "Faic barrachd a tha 'gan leantainn air {domain}", "hints.profiles.see_more_posts": "Faic barrachd phostaichean air {domain}", - "hints.threads.replies_may_be_missing": "Dh’fhaoidte gu bheil freagairtean o fhrithealaichean eile a dhìth.", - "hints.threads.see_more": "Faic barrachd fhreagairtean air {domain}", "home.column_settings.show_quotes": "Seall luaidhean", "home.column_settings.show_reblogs": "Seall na brosnachaidhean", "home.column_settings.show_replies": "Seall na freagairtean", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 7cb227215e4..6a8570c1d05 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Mira máis seguidoras en {domain}", "hints.profiles.see_more_follows": "Mira máis seguimentos en {domain}", "hints.profiles.see_more_posts": "Mira máis publicacións en {domain}", - "hints.threads.replies_may_be_missing": "Poderían faltar respostas desde outros servidores.", - "hints.threads.see_more": "Mira máis respostas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Amosar compartidos", "home.column_settings.show_replies": "Amosar respostas", @@ -847,6 +845,8 @@ "status.bookmark": "Marcar", "status.cancel_reblog_private": "Desfacer compartido", "status.cannot_reblog": "Esta publicación non pode ser promovida", + "status.context.load_new_replies": "Non hai respostas dispoñibles", + "status.context.loading": "Mirando se hai máis respostas", "status.continued_thread": "Continua co fío", "status.copy": "Copiar ligazón á publicación", "status.delete": "Eliminar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index cb5ffb52db4..715ec36f541 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "צפיה בעוד עוקבים משרת {domain}", "hints.profiles.see_more_follows": "צפיה בעוד נעקבים בשרת {domain}", "hints.profiles.see_more_posts": "צפיה בעוד פרסומים בשרת {domain}", - "hints.threads.replies_may_be_missing": "תגובות משרתים אחרים עלולות להיות חסרות.", - "hints.threads.see_more": "צפיה בעוד תגובות משרת {domain}", "home.column_settings.show_quotes": "הצגת ציטוטים", "home.column_settings.show_reblogs": "הצגת הדהודים", "home.column_settings.show_replies": "הצגת תגובות", @@ -847,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": "מחיקה", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index abd9b8ad116..06a8bf88465 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "További követők megtekintése itt: {domain}", "hints.profiles.see_more_follows": "További követések megtekintése itt: {domain}", "hints.profiles.see_more_posts": "További bejegyzések megtekintése itt: {domain}", - "hints.threads.replies_may_be_missing": "A más kiszolgálókról érkező válaszok lehet, hogy hiányoznak.", - "hints.threads.see_more": "További válaszok megtekintése itt: {domain}", "home.column_settings.show_quotes": "Idézetek megjelenítése", "home.column_settings.show_reblogs": "Megtolások megjelenítése", "home.column_settings.show_replies": "Válaszok megjelenítése", @@ -847,6 +845,8 @@ "status.bookmark": "Könyvjelzőzés", "status.cancel_reblog_private": "Megtolás visszavonása", "status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni", + "status.context.load_new_replies": "Új válaszok érhetőek el", + "status.context.loading": "További válaszok keresése", "status.continued_thread": "Folytatott szál", "status.copy": "Link másolása bejegyzésbe", "status.delete": "Törlés", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index f9deb2f859d..2edb77b7f39 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -404,8 +404,6 @@ "hints.profiles.see_more_followers": "Vider plus de sequitores sur {domain}", "hints.profiles.see_more_follows": "Vider plus de sequites sur {domain}", "hints.profiles.see_more_posts": "Vider plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Responsas de altere servitores pote mancar.", - "hints.threads.see_more": "Vider plus de responsas sur {domain}", "home.column_settings.show_reblogs": "Monstrar impulsos", "home.column_settings.show_replies": "Monstrar responsas", "home.hide_announcements": "Celar annuncios", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 69e549ea884..deb1cab89a4 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -383,8 +383,6 @@ "hints.profiles.see_more_followers": "Vidar plu multa sequanti sur {domain}", "hints.profiles.see_more_follows": "Vidar plu multa sequati sur {domain}", "hints.profiles.see_more_posts": "Vidar plu multa posti sur {domain}", - "hints.threads.replies_may_be_missing": "Respondi de altra servili forsan ne esas hike.", - "hints.threads.see_more": "Vidar plu multa demandi sur {domain}", "home.column_settings.show_reblogs": "Montrar repeti", "home.column_settings.show_replies": "Montrar respondi", "home.hide_announcements": "Celez anunci", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 52b3f5d97dd..54aec7cbee8 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Sjá fleiri fylgjendur á {domain}", "hints.profiles.see_more_follows": "Sjá fleiri sem þú fylgist með á {domain}", "hints.profiles.see_more_posts": "Sjá fleiri færslur á {domain}", - "hints.threads.replies_may_be_missing": "Svör af öðrum netþjónum gæti vantað.", - "hints.threads.see_more": "Sjá fleiri svör á {domain}", "home.column_settings.show_quotes": "Birta tilvitnanir", "home.column_settings.show_reblogs": "Sýna endurbirtingar", "home.column_settings.show_replies": "Birta svör", @@ -847,6 +845,8 @@ "status.bookmark": "Bókamerki", "status.cancel_reblog_private": "Taka úr endurbirtingu", "status.cannot_reblog": "Þessa færslu er ekki hægt að endurbirta", + "status.context.load_new_replies": "Ný svör hafa borist", + "status.context.loading": "Athuga með fleiri svör", "status.continued_thread": "Hélt samtali áfram", "status.copy": "Afrita tengil í færslu", "status.delete": "Eyða", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f39862e7cc5..5134b39e673 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Vedi altri seguaci su {domain}", "hints.profiles.see_more_follows": "Vedi altri profili seguiti su {domain}", "hints.profiles.see_more_posts": "Vedi altri post su {domain}", - "hints.threads.replies_may_be_missing": "Le risposte da altri server potrebbero essere mancanti.", - "hints.threads.see_more": "Vedi altre risposte su {domain}", "home.column_settings.show_quotes": "Mostra le citazioni", "home.column_settings.show_reblogs": "Mostra reblog", "home.column_settings.show_replies": "Mostra risposte", @@ -847,6 +845,8 @@ "status.bookmark": "Aggiungi segnalibro", "status.cancel_reblog_private": "Annulla reblog", "status.cannot_reblog": "Questo post non può essere condiviso", + "status.context.load_new_replies": "Nuove risposte disponibili", + "status.context.loading": "Controllo per altre risposte", "status.continued_thread": "Discussione continua", "status.copy": "Copia link al post", "status.delete": "Elimina", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ff12bf1dd18..59ed074012a 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "{domain} で正確な情報を見る", "hints.profiles.see_more_follows": "{domain} で正確な情報を見る", "hints.profiles.see_more_posts": "{domain} でその他の投稿を見る", - "hints.threads.replies_may_be_missing": "リモートの返信は表示されない場合があります。", - "hints.threads.see_more": "{domain} でその他の返信を見る", "home.column_settings.show_quotes": "引用を表示", "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 0c2faa120e1..0bc4665a253 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -297,8 +297,6 @@ "hashtag.follow": "Ḍfeṛ ahacṭag", "hashtag.mute": "Sgugem #{hashtag}", "hashtags.and_other": "…d {count, plural, one {}other {# nniḍen}}", - "hints.threads.replies_may_be_missing": "Tiririyin d-yusan deg iqeddacen nniḍen, yezmer ur d-ddant ara.", - "hints.threads.see_more": "Wali ugar n tririt deg {domain}", "home.column_settings.show_reblogs": "Ssken-d beṭṭu", "home.column_settings.show_replies": "Ssken-d tiririyin", "home.hide_announcements": "Ffer ulɣuyen", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index f0e981adb35..be0025fdb2f 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -1,6 +1,7 @@ { "about.blocks": "Модерацияланған серверлер", "about.contact": "Байланыс:", + "about.default_locale": "Әдепкі", "about.disclaimer": "Mastodon деген тегін, бастапқы коды ашық бағдарламалық жасақтама және Mastodon gGmbH-тің сауда маркасы.", "about.domain_blocks.no_reason_available": "Себеп қолжетімсіз", "about.domain_blocks.preamble": "Mastodon әдетте сізге Fediverse'тің кез келген серверінің қолданушыларының контентін көріп, олармен байланысуға мүмкіндік береді. Осы белгілі серверде жасалған ережеден тыс жағдайлар міне.", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 6c2cc7ea142..cfb3ba8c61d 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "{domain}에서 더 많은 팔로워 보기", "hints.profiles.see_more_follows": "{domain}에서 더 많은 팔로우 보기", "hints.profiles.see_more_posts": "{domain}에서 더 많은 게시물 보기", - "hints.threads.replies_may_be_missing": "다른 서버의 답글은 일부 누락되었을 수 있습니다.", - "hints.threads.see_more": "{domain}에서 더 많은 답글 보기", "home.column_settings.show_quotes": "인용 보기", "home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_replies": "답글 표시", diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json index 63110fda875..f628721f479 100644 --- a/app/javascript/mastodon/locales/ku.json +++ b/app/javascript/mastodon/locales/ku.json @@ -272,7 +272,6 @@ "hashtag.column_settings.tag_toggle": "Ji bo vê stûnê hin pêvekan tevlî bike", "hashtag.follow": "Hashtagê bişopîne", "hashtag.unfollow": "Hashtagê neşopîne", - "hints.threads.replies_may_be_missing": "Beriv ji rajekarên din dibe ku wendayî bin.", "home.column_settings.show_reblogs": "Bilindkirinan nîşan bike", "home.column_settings.show_replies": "Bersivan nîşan bide", "home.hide_announcements": "Reklaman veşêre", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 83ffe1834be..7c6ce24a8ff 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -368,8 +368,6 @@ "hints.profiles.see_more_followers": "Ve mas suivantes en {domain}", "hints.profiles.see_more_follows": "Ve mas segidos en {domain}", "hints.profiles.see_more_posts": "Ve mas puvlikasyones en {domain}", - "hints.threads.replies_may_be_missing": "Puede mankar repuestas de otros sirvidores.", - "hints.threads.see_more": "Ve mas repuestas en {domain}", "home.column_settings.show_reblogs": "Amostra repartajasyones", "home.column_settings.show_replies": "Amostra repuestas", "home.hide_announcements": "Eskonde pregones", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 944ab8d9649..ed5fa83171e 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -396,8 +396,6 @@ "hints.profiles.see_more_followers": "Žiūrėti daugiau sekėjų serveryje {domain}", "hints.profiles.see_more_follows": "Žiūrėti daugiau sekimų serveryje {domain}", "hints.profiles.see_more_posts": "Žiūrėti daugiau įrašų serveryje {domain}", - "hints.threads.replies_may_be_missing": "Atsakymai iš kitų serverių gali būti nepateikti.", - "hints.threads.see_more": "Žiūrėti daugiau atsakymų serveryje {domain}", "home.column_settings.show_reblogs": "Rodyti pakėlimus", "home.column_settings.show_replies": "Rodyti atsakymus", "home.hide_announcements": "Slėpti skelbimus", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 104528c0e13..ee3c143412f 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -386,8 +386,6 @@ "hints.profiles.see_more_followers": "Skatīt vairāk sekotāju {domain}", "hints.profiles.see_more_follows": "Skatīt vairāk sekojumu {domain}", "hints.profiles.see_more_posts": "Skatīt vairāk ierakstu {domain}", - "hints.threads.replies_may_be_missing": "Var trūkt atbilžu no citiem serveriem.", - "hints.threads.see_more": "Skatīt vairāk atbilžu {domain}", "home.column_settings.show_quotes": "Rādīt citātus", "home.column_settings.show_reblogs": "Rādīt pastiprinātos ierakstus", "home.column_settings.show_replies": "Rādīt atbildes", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index 48e1b75c2b0..e7e8d2c0cd1 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "佇 {domain} 看koh khah tsē跟tuè lí ê", "hints.profiles.see_more_follows": "佇 {domain} 看koh khah tsē lí跟tuè ê", "hints.profiles.see_more_posts": "佇 {domain} 看koh khah tsē ê PO文", - "hints.threads.replies_may_be_missing": "Tuì其他ê服侍器來ê回應可能有phah m̄見。", - "hints.threads.see_more": "佇 {domain} 看koh khah tsē ê回應", "home.column_settings.show_quotes": "顯示引用", "home.column_settings.show_reblogs": "顯示轉PO", "home.column_settings.show_replies": "顯示回應", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 90593ac2058..04ceb2591ba 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Bekijk meer volgers op {domain}", "hints.profiles.see_more_follows": "Bekijk meer gevolgde accounts op {domain}", "hints.profiles.see_more_posts": "Bekijk meer berichten op {domain}", - "hints.threads.replies_may_be_missing": "Antwoorden van andere servers kunnen ontbreken.", - "hints.threads.see_more": "Bekijk meer reacties op {domain}", "home.column_settings.show_quotes": "Citaten tonen", "home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_replies": "Reacties tonen", @@ -847,6 +845,8 @@ "status.bookmark": "Bladwijzer toevoegen", "status.cancel_reblog_private": "Niet langer boosten", "status.cannot_reblog": "Dit bericht kan niet geboost worden", + "status.context.load_new_replies": "Nieuwe reacties beschikbaar", + "status.context.loading": "Op nieuwe reacties aan het controleren", "status.continued_thread": "Vervolg van gesprek", "status.copy": "Link naar bericht kopiëren", "status.delete": "Verwijderen", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index b739208ab37..b5b97007c8c 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Sjå fleire fylgjarar på {domain}", "hints.profiles.see_more_follows": "Sjå fleire fylgjer på {domain}", "hints.profiles.see_more_posts": "Sjå fleire innlegg på {domain}", - "hints.threads.replies_may_be_missing": "Svar frå andre tenarar manglar kanskje.", - "hints.threads.see_more": "Sjå fleire svar på {domain}", "home.column_settings.show_quotes": "Vis sitat", "home.column_settings.show_reblogs": "Vis framhevingar", "home.column_settings.show_replies": "Vis svar", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index ca7c43e1e92..8ef9b6a4818 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -417,8 +417,6 @@ "hints.profiles.see_more_followers": "Se flere følgere på {domain}", "hints.profiles.see_more_follows": "Se flere som følger på {domain}", "hints.profiles.see_more_posts": "Se flere innlegg på {domain}", - "hints.threads.replies_may_be_missing": "Svar fra andre servere mangler kanskje.", - "hints.threads.see_more": "Se flere svar på {domain}", "home.column_settings.show_quotes": "Vis sitater", "home.column_settings.show_reblogs": "Vis fremhevinger", "home.column_settings.show_replies": "Vis svar", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 5236d246c0f..50756eebbf0 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -276,7 +276,6 @@ "hints.profiles.see_more_followers": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋਅਰ ਵੇਖੋ", "hints.profiles.see_more_follows": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋ ਨੂੰ ਵੇਖੋ", "hints.profiles.see_more_posts": "{domain} ਉੱਤੇ ਹੋਰ ਪੋਸਟਾਂ ਨੂੰ ਵੇਖੋ", - "hints.threads.see_more": "{domain} ਤੋਂ ਹੋਰ ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ", "home.column_settings.show_reblogs": "ਬੂਸਟਾਂ ਨੂੰ ਵੇਖੋ", "home.column_settings.show_replies": "ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ", "home.hide_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਓਹਲੇ ਕਰੋ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 1967a2333c0..bbe5789d2a4 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -383,8 +383,6 @@ "hints.profiles.see_more_followers": "Zobacz więcej obserwujących na {domain}", "hints.profiles.see_more_follows": "Zobacz więcej obserwowanych na {domain}", "hints.profiles.see_more_posts": "Zobacz więcej wpisów na {domain}", - "hints.threads.replies_may_be_missing": "Komentarze z innych serwerów mogą być niewidoczne.", - "hints.threads.see_more": "Zobacz więcej komentarzy na {domain}", "home.column_settings.show_reblogs": "Pokazuj podbicia", "home.column_settings.show_replies": "Pokazuj odpowiedzi", "home.hide_announcements": "Ukryj ogłoszenia", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index b437786b9b6..f5762701939 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -414,8 +414,6 @@ "hints.profiles.see_more_followers": "Ver mais seguidores no {domain}", "hints.profiles.see_more_follows": "Ver mais seguidores no {domain}", "hints.profiles.see_more_posts": "Ver mais publicações em {domain}", - "hints.threads.replies_may_be_missing": "Respostas de outros servidores podem estar faltando.", - "hints.threads.see_more": "Ver mais respostas no {domain}", "home.column_settings.show_reblogs": "Mostrar boosts", "home.column_settings.show_replies": "Mostrar respostas", "home.hide_announcements": "Ocultar comunicados", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 424e48ef2e3..6cc740f755f 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Ver mais seguidores em {domain}", "hints.profiles.see_more_follows": "Ver mais perfis seguidos em {domain}", "hints.profiles.see_more_posts": "Ver mais publicações em {domain}", - "hints.threads.replies_may_be_missing": "É possível que não estejam a ser mostradas todas as respostas de outros servidores.", - "hints.threads.see_more": "Ver mais respostas em {domain}", "home.column_settings.show_quotes": "Mostrar citações", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostas", @@ -847,6 +845,8 @@ "status.bookmark": "Guardar nos marcadores", "status.cancel_reblog_private": "Retirar impulso", "status.cannot_reblog": "Esta publicação não pode ser impulsionada", + "status.context.load_new_replies": "Novas respostas disponíveis", + "status.context.loading": "A verificar por mais respostas", "status.continued_thread": "Continuação da conversa", "status.copy": "Copiar hiperligação da publicação", "status.delete": "Eliminar", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index e2022b52d94..40d8341713f 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -45,7 +45,7 @@ "account.followers_counter": "{count, plural, one {{counter} подписчик} few {{counter} подписчика} other {{counter} подписчиков}}", "account.followers_you_know_counter": "{count, plural, one {{counter} ваш знакомый} other {{counter} ваших знакомых}}", "account.following": "Подписки", - "account.following_counter": "{count, plural, one {# подписка} many {# подписок} other {# подписки}}", + "account.following_counter": "{count, plural, one {{counter} подписка} few {{counter} подписки} many {{counter} подписок} other {{counter} подписок}}", "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", "account.follows_you": "Подписан(а) на вас", "account.go_to_profile": "Перейти к профилю", @@ -273,7 +273,7 @@ "domain_block_modal.they_cant_follow": "Пользователи с этого сервера не смогут подписаться на вас.", "domain_block_modal.they_wont_know": "Пользователи с этого сервера не будут знать, что вы их блокируете.", "domain_block_modal.title": "Заблокировать домен?", - "domain_block_modal.you_will_lose_num_followers": "Вы потеряете {followersCount, plural, one {{followersCountDisplay} подписчика} few {{followersCountDisplay} подписчика} other {{followersCountDisplay} подписчиков}} и {followingCount, plural, one {{followingCountDisplay} подписку} few {{followingCountDisplay} подписки} other {{followingCountDisplay} подписок}}.", + "domain_block_modal.you_will_lose_num_followers": "Вы потеряете {followersCount, plural, one {{followersCountDisplay} подписчика} few {{followersCountDisplay} подписчиков} other {{followersCountDisplay} подписчиков}} и {followingCount, plural, one {{followingCountDisplay} подписку} few {{followingCountDisplay} подписки} other {{followingCountDisplay} подписок}}.", "domain_block_modal.you_will_lose_relationships": "Вы потеряете все подписки и всех подписчиков с этого сервера.", "domain_block_modal.you_wont_see_posts": "Вы не будете видеть посты и уведомления от пользователей с этого сервера.", "domain_pill.activitypub_lets_connect": "Благодаря ему вы можете связываться и взаимодействовать не только с пользователями Mastodon, но и с пользователями других платформ.", @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Перейдите на {domain}, чтобы увидеть всех подписчиков", "hints.profiles.see_more_follows": "Перейдите на {domain}, чтобы увидеть все подписки", "hints.profiles.see_more_posts": "Перейдите на {domain}, чтобы увидеть все посты", - "hints.threads.replies_may_be_missing": "Некоторые ответы с других серверов могут здесь отсутствовать.", - "hints.threads.see_more": "Перейдите на {domain}, чтобы увидеть все ответы", "home.column_settings.show_quotes": "Показывать цитирования", "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index d893bc2a0d0..8dc3e597c11 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -1,6 +1,7 @@ { "about.blocks": "Serbidores moderados", "about.contact": "Cuntatu:", + "about.default_locale": "Predefinidu", "about.disclaimer": "Mastodon est software de còdighe lìberu e unu màrchiu de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Peruna resone a disponimentu", "about.domain_blocks.preamble": "Mastodon ti permitit de bìdere su cuntenutu de utentes de cale si siat àteru serbidore de su fediversu. Custas sunt etzetziones fatas in custu serbidore ispetzìficu.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Limitadu", "about.domain_blocks.suspended.explanation": "Perunu datu de custu serbidore at a èssere protzessadu, immagasinadu o cuncambiadu; est impossìbile duncas cale si siat interatzione o comunicatzione cun is utentes de custu serbidore.", "about.domain_blocks.suspended.title": "Suspèndidu", + "about.language_label": "Idioma", "about.not_available": "Custa informatzione no est istada posta a disponimentu in custu serbidore.", "about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}", "about.rules": "Règulas de su serbidore", @@ -19,13 +21,21 @@ "account.block_domain": "Bloca su domìniu {domain}", "account.block_short": "Bloca", "account.blocked": "Blocadu", + "account.blocking": "Blocadu", "account.cancel_follow_request": "Annulla sa sighidura", "account.copy": "Còpia su ligòngiu a su profilu", "account.direct": "Mèntova a @{name} in privadu", "account.disable_notifications": "Non mi notìfiches prus cando @{name} pùblichet messàgios", + "account.domain_blocking": "Blocamus su domìniu", "account.edit_profile": "Modìfica profilu", "account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios", "account.endorse": "Cussìgia in su profilu tuo", + "account.familiar_followers_many": "Sighidu dae {name1}, {name2} e {othersCount, plural,one {un'àtera persone chi connosches} other {àteras # persones chi connosches}}", + "account.familiar_followers_one": "Sighidu dae {name1}", + "account.familiar_followers_two": "Sighidu dae {name1} e {name2}", + "account.featured": "In evidèntzia", + "account.featured.accounts": "Profilos", + "account.featured.hashtags": "Etichetas", "account.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}", "account.featured_tags.last_status_never": "Peruna publicatzione", "account.follow": "Sighi", @@ -33,9 +43,11 @@ "account.followers": "Sighiduras", "account.followers.empty": "Nemos sighit ancora custa persone.", "account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}", + "account.followers_you_know_counter": "{counter} chi connosches", "account.following": "Sighende", "account.following_counter": "{count, plural, one {sighende a {counter}} other {sighende a {counter}}}", "account.follows.empty": "Custa persone non sighit ancora a nemos.", + "account.follows_you": "Ti sighit", "account.go_to_profile": "Bae a su profilu", "account.hide_reblogs": "Cua is cumpartziduras de @{name}", "account.in_memoriam": "In memoriam.", @@ -50,18 +62,22 @@ "account.mute_notifications_short": "Pone is notìficas a sa muda", "account.mute_short": "A sa muda", "account.muted": "A sa muda", + "account.muting": "A sa muda", "account.no_bio": "Peruna descritzione frunida.", "account.open_original_page": "Aberi sa pàgina originale", "account.posts": "Publicatziones", "account.posts_with_replies": "Publicatziones e rispostas", + "account.remove_from_followers": "Cantzella a {name} dae is sighiduras", "account.report": "Signala @{name}", "account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura", "account.requested_follow": "{name} at dimandadu de ti sighire", + "account.requests_to_follow_you": "Rechestas de sighidura", "account.share": "Cumpartzi su profilu de @{name}", "account.show_reblogs": "Ammustra is cumpartziduras de @{name}", "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "account.unblock": "Isbloca a @{name}", "account.unblock_domain": "Isbloca su domìniu {domain}", + "account.unblock_domain_short": "Isbloca", "account.unblock_short": "Isbloca", "account.unendorse": "Non cussiges in su profilu", "account.unfollow": "Non sigas prus", @@ -83,7 +99,22 @@ "alert.unexpected.message": "Ddoe est istada una faddina.", "alert.unexpected.title": "Oh!", "alt_text_badge.title": "Testu alternativu", + "alt_text_modal.add_alt_text": "Agiunghe testu alternativu", + "alt_text_modal.add_text_from_image": "Agiunghe testu dae un'immàgine", + "alt_text_modal.cancel": "Annulla", + "alt_text_modal.change_thumbnail": "Càmbia sa miniadura", + "alt_text_modal.done": "Fatu", "announcement.announcement": "Annùntziu", + "annual_report.summary.archetype.booster": "Semper a s'ùrtima", + "annual_report.summary.followers.followers": "sighiduras", + "annual_report.summary.followers.total": "{count} totale", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "aplicatzione prus impreada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "eticheta prus impreada", + "annual_report.summary.most_used_hashtag.none": "Peruna", + "annual_report.summary.new_posts.new_posts": "publicatziones noas", + "annual_report.summary.percentile.we_wont_tell_bernie": "No dd'amus a nàrrere a Bernie.", + "annual_report.summary.thanks": "Gràtzias de èssere parte de Mastodon!", "attachments_list.unprocessed": "(non protzessadu)", "audio.hide": "Cua s'àudio", "block_modal.remote_users_caveat": "Amus a pedire a su serbidore {domain} de rispetare sa detzisione tua. Nointames custu, su rispetu no est garantidu ca unos cantos serbidores diant pòdere gestire is blocos de manera diferente. Is publicatzione pùblicas diant pòdere ancora èssere visìbiles a is utentes chi no ant fatu s'atzessu.", @@ -107,6 +138,7 @@ "bundle_column_error.routing.body": "Impossìbile agatare sa pàgina rechesta. Seguru chi s'URL in sa barra de indiritzos est curretu?", "bundle_column_error.routing.title": "404", "bundle_modal_error.close": "Serra", + "bundle_modal_error.message": "Faddina in su carrigamentu de custu ischermu.", "bundle_modal_error.retry": "Torra·bi a proare", "closed_registrations.other_server_instructions": "Dae chi Mastodon est detzentralizadu, podes creare unu contu in un'àteru serbidore e interagire cun custu.", "closed_registrations_modal.description": "Sa creatzione de contos in {domain} no est possìbile in custu momentu, però tene in cunsideru chi non tenes bisòngiu de unu contu ispetzìficu in {domain} pro impreare Mastodon.", @@ -116,13 +148,16 @@ "column.blocks": "Persones blocadas", "column.bookmarks": "Sinnalibros", "column.community": "Lìnia de tempus locale", + "column.create_list": "Crea una lista", "column.direct": "Mentziones privadas", "column.directory": "Nàviga in is profilos", "column.domain_blocks": "Domìnios blocados", + "column.edit_list": "Modifica sa lista", "column.favourites": "Preferidos", "column.firehose": "Publicatziones in direta", "column.follow_requests": "Rechestas de sighidura", "column.home": "Printzipale", + "column.list_members": "Gesti is persones de sa lista", "column.lists": "Listas", "column.mutes": "Persones a sa muda", "column.notifications": "Notìficas", @@ -135,6 +170,7 @@ "column_header.pin": "Apica", "column_header.show_settings": "Ammustra is cunfiguratziones", "column_header.unpin": "Boga dae pitzu", + "column_search.cancel": "Annulla", "community.column_settings.local_only": "Isceti locale", "community.column_settings.media_only": "Isceti multimediale", "community.column_settings.remote_only": "Isceti remotu", @@ -152,6 +188,7 @@ "compose_form.poll.duration": "Longària de su sondàgiu", "compose_form.poll.multiple": "Sèberu mùltiplu", "compose_form.poll.option_placeholder": "Optzione {number}", + "compose_form.poll.single": "Sèberu ùnicu", "compose_form.poll.switch_to_multiple": "Muda su sondàgiu pro permìtere multi-optziones", "compose_form.poll.switch_to_single": "Muda su sondàgiu pro permìtere un'optzione isceti", "compose_form.poll.type": "Istile", @@ -169,6 +206,8 @@ "confirmations.delete_list.confirm": "Cantzella", "confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?", "confirmations.delete_list.title": "Cantzellare sa lista?", + "confirmations.discard_draft.confirm": "Iscarta e sighi", + "confirmations.discard_draft.edit.cancel": "Sighi cun s'editzione", "confirmations.discard_edit_media.confirm": "Iscarta", "confirmations.discard_edit_media.message": "Tenes modìficas non sarvadas a is descritziones o a is anteprimas de is cuntenutos, ddas boles iscartare su matessi?", "confirmations.logout.confirm": "Essi·nche", @@ -256,6 +295,7 @@ "explore.trending_links": "Noas", "explore.trending_statuses": "Publicatziones", "explore.trending_tags": "Etichetas", + "featured_carousel.slide": "{index} de {total}", "filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.", "filter_modal.added.expired_title": "Filtru iscadidu.", "filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru", @@ -294,8 +334,12 @@ "footer.privacy_policy": "Polìtica de riservadesa", "footer.source_code": "Ammustra su còdighe de orìgine", "footer.status": "Istadu", + "footer.terms_of_service": "Cunditziones de su servìtziu", "generic.saved": "Sarvadu", "getting_started.heading": "Comente cumintzare", + "hashtag.admin_moderation": "Aberi s'interfache de moderatzione pro #{name}", + "hashtag.browse": "Nàviga in is publicatziones de #{hashtag}", + "hashtag.browse_from_account": "Nàviga in is publicatziones de @{name} in #{hashtag}", "hashtag.column_header.tag_mode.all": "e {additional}", "hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.none": "sena {additional}", @@ -308,13 +352,14 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} partetzipante} other {{counter} partetzipantes}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe", + "hashtag.feature": "In evidèntzia in su profilu", "hashtag.follow": "Sighi su hashtag", + "hashtag.mute": "Pone #{hashtag} a sa muda", + "hashtag.unfeature": "No ddu pòngias in evidèntzia in su profilu", "hashtag.unfollow": "Non sigas prus s'eticheta", "hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}", "hints.profiles.posts_may_be_missing": "Podet èssere chi ammanchent tzertas publicatziones de custu profilu.", "hints.profiles.see_more_posts": "Bide prus publicatziones a {domain}", - "hints.threads.replies_may_be_missing": "Podet èssere chi ammanchent rispostas dae àteros serbidores.", - "hints.threads.see_more": "Bide prus rispostas a {domain}", "home.column_settings.show_reblogs": "Ammustra is cumpartziduras", "home.column_settings.show_replies": "Ammustra rispostas", "home.hide_announcements": "Cua annùntzios", @@ -326,9 +371,14 @@ "ignore_notifications_modal.filter_instead": "Opuru filtra", "ignore_notifications_modal.filter_to_act_users": "As a pòdere ancora atzetare, refudare o sinnalare a utentes", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrare agiudat a evitare possìbiles confusiones", + "info_button.label": "Agiudu", + "interaction_modal.go": "Sighi", + "interaction_modal.no_account_yet": "Non tenes galu perunu contu?", + "interaction_modal.on_another_server": "In un'àteru serbidore", "interaction_modal.on_this_server": "In custu serbidore", "interaction_modal.title.follow": "Sighi a {name}", "interaction_modal.title.reply": "Risponde a sa publicatzione de {name}", + "interaction_modal.username_prompt": "Pro es., {example}", "intervals.full.days": "{number, plural, one {# die} other {# dies}}", "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}", "intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}", @@ -341,6 +391,7 @@ "keyboard_shortcuts.direct": "pro abèrrere sa colunna de mèntovos privados", "keyboard_shortcuts.down": "Move in bàsciu in sa lista", "keyboard_shortcuts.enter": "Aberi una publicatzione", + "keyboard_shortcuts.favourite": "Publicatzione preferida", "keyboard_shortcuts.favourites": "Aberi sa lista de preferidos", "keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada", "keyboard_shortcuts.heading": "Incurtzaduras de tecladu", @@ -363,18 +414,25 @@ "keyboard_shortcuts.toggle_hidden": "Ammustra o cua su testu de is AC", "keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales", "keyboard_shortcuts.toot": "Cumintza a iscrìere una publicatzione noa", + "keyboard_shortcuts.translate": "pro tradùere una publicatzione", "keyboard_shortcuts.unfocus": "Essi de s'àrea de cumpositzione de testu o de chirca", "keyboard_shortcuts.up": "Move in susu in sa lista", "lightbox.close": "Serra", "lightbox.next": "Imbeniente", "lightbox.previous": "Pretzedente", + "lightbox.zoom_in": "Ismànnia finas a sa mannària atuale", "limited_account_hint.title": "Custu profilu est istadu cuadu dae sa moderatzione de {domain}.", + "link_preview.author": "Dae {name}", "link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "lists.delete": "Cantzella sa lista", "lists.edit": "Modìfica sa lista", + "lists.remove_member": "Cantzella", "lists.replies_policy.followed": "Cale si siat persone chi sighis", "lists.replies_policy.list": "Persones de sa lista", "lists.replies_policy.none": "Nemos", + "lists.save": "Sarva", + "lists.search": "Chirca", + "lists.show_replies_to": "Include rispostas dae gente de sa lista a", "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}", "loading_indicator.label": "Carrighende…", "media_gallery.hide": "Cua", @@ -389,8 +447,10 @@ "mute_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent a custa persone.", "mute_modal.you_wont_see_posts": "At a pòdere bìdere is publicatziones tuas, però tue no as a bìdere cussas suas.", "navigation_bar.about": "Informatziones", + "navigation_bar.account_settings": "Crae e seguresa", "navigation_bar.administration": "Amministratzione", "navigation_bar.advanced_interface": "Aberi s'interfache web avantzada", + "navigation_bar.automated_deletion": "Cantzelladura automàtica de publicatziones", "navigation_bar.blocks": "Persones blocadas", "navigation_bar.bookmarks": "Sinnalibros", "navigation_bar.direct": "Mentziones privadas", @@ -400,13 +460,18 @@ "navigation_bar.follow_requests": "Rechestas de sighidura", "navigation_bar.followed_tags": "Etichetas sighidas", "navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras", + "navigation_bar.import_export": "Importatzione e esportatzione", "navigation_bar.lists": "Listas", + "navigation_bar.live_feed_local": "Canale in direta (locale)", + "navigation_bar.live_feed_public": "Canale in direta (pùblicu)", "navigation_bar.logout": "Essi", "navigation_bar.moderation": "Moderatzione", + "navigation_bar.more": "Àteru", "navigation_bar.mutes": "Persones a sa muda", "navigation_bar.opened_in_classic_interface": "Publicatziones, contos e àteras pàginas ispetzìficas sunt abertas in manera predefinida in s'interfache web clàssica.", "navigation_bar.preferences": "Preferèntzias", "navigation_bar.search": "Chirca", + "navigation_bar.search_trends": "Chirca / in tendèntzia", "not_signed_in_indicator.not_signed_in": "Ti depes identificare pro atzèdere a custa resursa.", "notification.admin.report": "{name} at sinnaladu a {target}", "notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}", @@ -463,15 +528,19 @@ "notification_requests.minimize_banner": "Mìnima su bànner de notìficas filtradas", "notification_requests.notifications_from": "Notìficas dae {name}", "notification_requests.title": "Notìficas filtradas", + "notification_requests.view": "Mustra notìficas", "notifications.clear": "Lìmpia notìficas", "notifications.clear_confirmation": "Seguru chi boles isboidare in manera permanente totu is notìficas tuas?", + "notifications.clear_title": "Boles cantzellare is notìficas?", "notifications.column_settings.admin.report": "Informes noos:", + "notifications.column_settings.admin.sign_up": "Registros noos:", "notifications.column_settings.alert": "Notìficas de iscrivania", "notifications.column_settings.favourite": "Preferidos:", "notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias", "notifications.column_settings.filter_bar.category": "Barra de filtru lestru", "notifications.column_settings.follow": "Sighiduras noas:", "notifications.column_settings.follow_request": "Rechestas noas de sighidura:", + "notifications.column_settings.group": "Grupu", "notifications.column_settings.mention": "Mèntovos:", "notifications.column_settings.poll": "Resurtados de su sondàgiu:", "notifications.column_settings.push": "Notìficas push", @@ -495,6 +564,8 @@ "notifications.permission_denied": "Is notìficas de iscrivania non sunt a disponimentu pro neghe de rechestas de permissu chi sunt istadas dennegadas in antis", "notifications.permission_denied_alert": "Is notìficas de iscrivania non podent èssere abilitadas, ca su permissu de su navigadore est istadu dennegadu in antis", "notifications.permission_required": "Is notìficas de iscrivania no sunt a disponimentu ca ammancat su permissu rechèdidu.", + "notifications.policy.accept": "Atzeta", + "notifications.policy.accept_hint": "Mustra in is notìficas", "notifications.policy.filter_new_accounts.hint": "Creadu {days, plural, one {erisero} other {in is ùrtimas # dies}}", "notifications.policy.filter_new_accounts_title": "Contos noos", "notifications.policy.filter_not_followers_title": "Gente chi non ti sighit", @@ -503,8 +574,15 @@ "notifications_permission_banner.enable": "Abilita is notìficas de iscrivania", "notifications_permission_banner.how_to_control": "Pro retzire notìficas cando Mastodon no est abertu, abilita is notìficas de iscrivania. Podes controllare cun pretzisione is castas de interatziones chi ingendrant notìficas de iscrivania pro mèdiu de su butone {icon} in subra, cando sunt abilitadas.", "notifications_permission_banner.title": "Non ti perdas mai nudda", + "onboarding.follows.back": "A coa", + "onboarding.follows.done": "Fatu", + "onboarding.follows.search": "Chirca", + "onboarding.follows.title": "Sighi a gente pro cumintzare", "onboarding.profile.display_name": "Nòmine visìbile", "onboarding.profile.note": "Biografia", + "onboarding.profile.save_and_continue": "Sarva e sighi", + "onboarding.profile.title": "Cunfiguratzione de profilu", + "onboarding.profile.upload_avatar": "Càrriga una fotografia de profilu", "picture_in_picture.restore": "Torra·ddu a ue fiat", "poll.closed": "Serradu", "poll.refresh": "Atualiza", @@ -599,6 +677,7 @@ "search_results.hashtags": "Etichetas", "search_results.see_all": "Bide totu", "search_results.statuses": "Publicatziones", + "search_results.title": "Chirca \"{q}\"", "server_banner.about_active_users": "Gente chi at impreadu custu serbidore is ùrtimas 30 dies (Utentes cun Atividade a su Mese)", "server_banner.active_users": "utentes ativos", "server_banner.administered_by": "Amministradu dae:", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 9f9bd1590fe..438ca0b735a 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -406,8 +406,6 @@ "hints.profiles.see_more_followers": "{domain}හි තවත් අනුගාමිකයින් බලන්න", "hints.profiles.see_more_follows": "{domain}හි තවත් පහත ඒවා බලන්න.", "hints.profiles.see_more_posts": "{domain}හි තවත් සටහන් බලන්න", - "hints.threads.replies_may_be_missing": "අනෙකුත් සේවාදායකයන්ගෙන් ලැබෙන පිළිතුරු අස්ථානගත වී තිබිය හැක.", - "hints.threads.see_more": "{domain}හි තවත් පිළිතුරු බලන්න.", "home.column_settings.show_reblogs": "බූස්ට් පෙන්වන්න", "home.column_settings.show_replies": "පිළිතුරු පෙන්වන්න", "home.hide_announcements": "නිවේදන සඟවන්න", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 63fd556f79b..091ddf8ca24 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -365,8 +365,6 @@ "hints.profiles.see_more_followers": "Pozri viac nasledovateľov na {domain}", "hints.profiles.see_more_follows": "Pozri viac nasledovateľov na {domain}", "hints.profiles.see_more_posts": "Pozri viac príspevkov na {domain}", - "hints.threads.replies_may_be_missing": "Odpovede z ostatných serverov môžu chýbať.", - "hints.threads.see_more": "Pozri viac odpovedí na {domain}", "home.column_settings.show_reblogs": "Zobraziť zdieľania", "home.column_settings.show_replies": "Zobraziť odpovede", "home.hide_announcements": "Skryť oznámenia", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 7213af36662..c256318fee9 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -383,8 +383,6 @@ "hints.profiles.see_more_followers": "Pokaži več sledilcev na {domain}", "hints.profiles.see_more_follows": "Pokaži več sledenih ljudi na zbirališču {domain}", "hints.profiles.see_more_posts": "Pokaži več objav na {domain}", - "hints.threads.replies_may_be_missing": "Odgovori z drugih strežnikov morda manjkajo.", - "hints.threads.see_more": "Pokaži več odgovorov na {domain}", "home.column_settings.show_reblogs": "Pokaži izpostavitve", "home.column_settings.show_replies": "Pokaži odgovore", "home.hide_announcements": "Skrij obvestila", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index b5a7cb0e886..75c00681603 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -419,8 +419,6 @@ "hints.profiles.see_more_followers": "Shihni më tepër ndjekës në {domain}", "hints.profiles.see_more_follows": "Shihni më tepër ndjekje në {domain}", "hints.profiles.see_more_posts": "Shihni më tepër postime në {domain}", - "hints.threads.replies_may_be_missing": "Mund të mungojnë përgjigje nga shërbyes të tjerë.", - "hints.threads.see_more": "Shihni më tepër përgjigje në {domain}", "home.column_settings.show_quotes": "Shfaq thonjëza", "home.column_settings.show_reblogs": "Shfaq përforcime", "home.column_settings.show_replies": "Shfaq përgjigje", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 0808963e5ff..48f15b28b88 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Se fler följare på {domain}", "hints.profiles.see_more_follows": "Se fler följare på {domain}", "hints.profiles.see_more_posts": "Se fler inlägg på {domain}", - "hints.threads.replies_may_be_missing": "Det kan saknas svar från andra servrar.", - "hints.threads.see_more": "Se fler svar på {domain}", "home.column_settings.show_quotes": "Visa citat", "home.column_settings.show_reblogs": "Visa boostar", "home.column_settings.show_replies": "Visa svar", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 603c1ab61ff..03faba8a660 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -397,8 +397,6 @@ "hints.profiles.see_more_followers": "ดูผู้ติดตามเพิ่มเติมใน {domain}", "hints.profiles.see_more_follows": "ดูการติดตามเพิ่มเติมใน {domain}", "hints.profiles.see_more_posts": "ดูโพสต์เพิ่มเติมใน {domain}", - "hints.threads.replies_may_be_missing": "การตอบกลับจากเซิร์ฟเวอร์อื่น ๆ อาจขาดหายไป", - "hints.threads.see_more": "ดูการตอบกลับเพิ่มเติมใน {domain}", "home.column_settings.show_reblogs": "แสดงการดัน", "home.column_settings.show_replies": "แสดงการตอบกลับ", "home.hide_announcements": "ซ่อนประกาศ", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index c48ffa5fe24..cabee093148 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -371,8 +371,6 @@ "hints.profiles.see_more_followers": "o lukin e jan ni lon ma {domain}: ona li kute e jan ni.", "hints.profiles.see_more_follows": "o lukin e jan ni lon ma {domain}: jan ni li kute e ona.", "hints.profiles.see_more_posts": "o lukin e toki ante lon ma {domain}", - "hints.threads.replies_may_be_missing": "toki pi ma ante li weka lon ken.", - "hints.threads.see_more": "o lukin e toki ante lon ma {domain}", "home.column_settings.show_reblogs": "o lukin e pana toki", "home.hide_announcements": "o lukin ala e toki lawa suli", "home.pending_critical_update.body": "o sin e ilo Mastodon lon tenpo lili a!", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index a45acff11f5..c50ac3cbaa1 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "{domain} adresinde daha fazla takipçi gör", "hints.profiles.see_more_follows": "{domain} adresinde daha fazla takip edilen gör", "hints.profiles.see_more_posts": "{domain} adresinde daha fazla gönderi gör", - "hints.threads.replies_may_be_missing": "Diğer sunuculardan yanıtlar eksik olabilir.", - "hints.threads.see_more": "{domain} adresinde daha fazla yanıt gör", "home.column_settings.show_quotes": "Alıntıları göster", "home.column_settings.show_reblogs": "Yeniden paylaşımları göster", "home.column_settings.show_replies": "Yanıtları göster", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 56ff3444f45..2e118f3589e 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -395,8 +395,6 @@ "hints.profiles.see_more_followers": "Переглянути більше підписників на {domain}", "hints.profiles.see_more_follows": "Переглянути більше підписок на {domain}", "hints.profiles.see_more_posts": "Переглянути більше дописів на {domain}", - "hints.threads.replies_may_be_missing": "Відповіді з інших серверів можуть бути не показані.", - "hints.threads.see_more": "Переглянути більше відповідей на {domain}", "home.column_settings.show_reblogs": "Показувати поширення", "home.column_settings.show_replies": "Показувати відповіді", "home.hide_announcements": "Приховати оголошення", @@ -817,6 +815,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": "Видалити", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index ec6e7188f22..b5c8c3ec235 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Xem thêm người theo dõi ở {domain}", "hints.profiles.see_more_follows": "Xem thêm người mà người này theo dõi ở {domain}", "hints.profiles.see_more_posts": "Xem thêm tút ở {domain}", - "hints.threads.replies_may_be_missing": "Những trả lời từ máy chủ khác có thể không đầy đủ.", - "hints.threads.see_more": "Xem thêm ở {domain}", "home.column_settings.show_quotes": "Hiện những trích dẫn", "home.column_settings.show_reblogs": "Hiện những lượt đăng lại", "home.column_settings.show_replies": "Hiện những tút dạng trả lời", @@ -847,6 +845,8 @@ "status.bookmark": "Lưu", "status.cancel_reblog_private": "Bỏ đăng lại", "status.cannot_reblog": "Không thể đăng lại tút này", + "status.context.load_new_replies": "Có những trả lời mới", + "status.context.loading": "Kiểm tra nhiều trả lời hơn", "status.continued_thread": "Tiếp tục chủ đề", "status.copy": "Sao chép URL", "status.delete": "Xóa", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 8928f253b16..c6e0aa7f276 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -412,8 +412,6 @@ "hints.profiles.see_more_followers": "在 {domain} 查看更多关注者", "hints.profiles.see_more_follows": "在 {domain} 查看更多关注", "hints.profiles.see_more_posts": "在 {domain} 查看更多嘟文", - "hints.threads.replies_may_be_missing": "来自其它实例的回复可能没有完全显示。", - "hints.threads.see_more": "在 {domain} 查看更多回复", "home.column_settings.show_quotes": "显示引用", "home.column_settings.show_reblogs": "显示转嘟", "home.column_settings.show_replies": "显示回复", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 8fe9f71a69f..40eed25a495 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "於 {domain} 檢視更多跟隨者", "hints.profiles.see_more_follows": "於 {domain} 檢視更多正在跟隨", "hints.profiles.see_more_posts": "於 {domain} 檢視更多嘟文", - "hints.threads.replies_may_be_missing": "來自其他站點之回覆或有缺失。", - "hints.threads.see_more": "於 {domain} 檢視更多回覆", "home.column_settings.show_quotes": "顯示引用嘟文", "home.column_settings.show_reblogs": "顯示轉嘟", "home.column_settings.show_replies": "顯示回覆", @@ -847,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": "刪除", diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index e840429c41e..dcc71bdb843 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -4,12 +4,16 @@ import { Globals } from '@react-spring/web'; import { setupBrowserNotifications } from 'mastodon/actions/notifications'; import Mastodon from 'mastodon/containers/mastodon'; -import { isFeatureEnabled, me, reduceMotion } from 'mastodon/initial_state'; +import { me, reduceMotion } from 'mastodon/initial_state'; import * as perf from 'mastodon/performance'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; -import { isProduction, isDevelopment } from './utils/environment'; +import { + isProduction, + isDevelopment, + isModernEmojiEnabled, +} from './utils/environment'; function main() { perf.start('main()'); @@ -29,7 +33,7 @@ function main() { }); } - if (isFeatureEnabled('modern_emojis')) { + if (isModernEmojiEnabled()) { const { initializeEmoji } = await import('@/mastodon/features/emoji'); await initializeEmoji(); } diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 5ccd4d27e3f..457da5c7367 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -1,3 +1,5 @@ +import initialState from '../initial_state'; + export function isDevelopment() { if (typeof process !== 'undefined') return process.env.NODE_ENV === 'development'; @@ -9,3 +11,13 @@ export function isProduction() { return process.env.NODE_ENV === 'production'; else return import.meta.env.PROD; } + +export type Features = 'modern_emojis'; + +export function isFeatureEnabled(feature: Features) { + return initialState?.features.includes(feature) ?? false; +} + +export function isModernEmojiEnabled() { + return isFeatureEnabled('modern_emojis') && isDevelopment(); +} diff --git a/app/javascript/mastodon/utils/workers.ts b/app/javascript/mastodon/utils/workers.ts new file mode 100644 index 00000000000..02dd66d86e0 --- /dev/null +++ b/app/javascript/mastodon/utils/workers.ts @@ -0,0 +1,29 @@ +/** + * Loads Web Worker that is compatible with cross-origin scripts for CDNs. + * + * Returns null if the environment doesn't support web workers. + */ +export function loadWorker(url: string | URL, options: WorkerOptions = {}) { + if (!('Worker' in window)) { + return null; + } + + try { + // Check if the script origin and the window origin are the same. + const scriptUrl = new URL(import.meta.url); + if (location.origin === scriptUrl.origin) { + // Not cross-origin, can just load normally. + return new Worker(url, options); + } + } catch (err) { + // In case the URL parsing fails. + console.warn('Error instantiating Worker:', err); + } + + // Import the worker script from a same-origin Blob. + const contents = `import ${JSON.stringify(url)};`; + const blob = URL.createObjectURL( + new Blob([contents], { type: 'text/javascript' }), + ); + return new Worker(blob, options); +} diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 93b45e80188..64ee9acd052 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -116,6 +116,20 @@ class ActivityPub::Activity fetch_remote_original_status end + def quote_from_request_json(json) + quoted_status_uri = value_or_id(json['object']) + quoting_status_uri = value_or_id(json['instrument']) + return if quoting_status_uri.nil? || quoted_status_uri.nil? + + quoting_status = status_from_uri(quoting_status_uri) + return unless quoting_status.present? && quoting_status.quote.present? + + quoted_status = status_from_uri(quoted_status_uri) + return unless quoted_status.present? && quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status + + quoting_status.quote + end + def dereference_object! return unless @object.is_a?(String) @@ -143,6 +157,10 @@ class ActivityPub::Activity @follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? end + def quote_request_from_object + @quote_request_from_object ||= Quote.find_by(quoted_account: @account, activity_uri: object_uri) unless object_uri.nil? + end + def follow_from_object @follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? end diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 5126e23c6a9..144ba9645c5 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def perform return accept_follow_for_relay if relay_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? + return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? case @object['type'] when 'Follow' accept_embedded_follow + when 'QuoteRequest' + accept_embedded_quote_request end end @@ -31,6 +34,29 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow end + def accept_embedded_quote_request + approval_uri = value_or_id(first_of_value(@json['result'])) + return if approval_uri.nil? + + quote = quote_from_request_json(@object) + return unless quote.present? && quote.status.local? + + accept_quote!(quote) + end + + def accept_quote!(quote) + approval_uri = value_or_id(first_of_value(@json['result'])) + return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? + + # NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative + # as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies + # in case of an implementation bug + quote.update!(state: :accepted, approval_uri: approval_uri) + + DistributionWorker.perform_async(quote.status_id, { 'update' => true }) + ActivityPub::StatusUpdateDistributionWorker.perform_async(quote.status_id, { 'updated_at' => Time.now.utc.iso8601 }) + end + def accept_follow_for_relay relay.update!(state: :accepted) end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index 886dddb2355..3dafaba1882 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity return reject_follow_for_relay if relay_follow? return follow_request_from_object.reject! unless follow_request_from_object.nil? return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil? + return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil? case @object['type'] when 'Follow' reject_embedded_follow + when 'QuoteRequest' + reject_embedded_quote_request end end @@ -29,6 +32,20 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity relay.update!(state: :rejected) end + def reject_embedded_quote_request + quote = quote_from_request_json(@object) + return unless quote.present? && quote.status.local? + + reject_quote!(quoting_status.quote) + end + + def reject_quote!(quote) + return unless quote.quoted_account == @account && quote.status.local? + + # TODO: broadcast an update? + quote.reject! + end + def relay @relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil? end diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index bf5de722103..b9e1d3a62b2 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -12,9 +12,7 @@ module ActivityPub::CaseTransform when Hash then value.deep_transform_keys! { |key| camel_lower(key) } when Symbol then camel_lower(value.to_s).to_sym when String - camel_lower_cache[value] ||= if value.start_with?('_:') - "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" - elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) + camel_lower_cache[value] ||= if value.start_with?('_misskey') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) value else value.underscore.camelize(:lower) diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index c193df9bb65..a5f5103fffd 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -45,7 +45,9 @@ class EmojiFormatter i += 1 if inside_shortname && text[i] == ':' - inside_shortname = false + # https://github.com/rubocop/rubocop/issues/14383 + # False positive in line below, remove disable when resolved + inside_shortname = false # rubocop:disable Lint/UselessAssignment shortcode = text[(shortname_start_index + 1)..(i - 1)] char_after = text[i + 1] diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 674945c4039..5260a723b31 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -71,6 +71,8 @@ class StatusCacheHydrator payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id) payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id payload[:filtered] = mapped_applied_custom_filter(account_id, status) + # TODO: performance optimization by not loading `Account` twice + payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval] payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote] end diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb new file mode 100644 index 00000000000..7e7642209db --- /dev/null +++ b/app/models/concerns/status/interaction_policy_concern.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Status::InteractionPolicyConcern + extend ActiveSupport::Concern + + QUOTE_APPROVAL_POLICY_FLAGS = { + unknown: (1 << 0), + public: (1 << 1), + followers: (1 << 2), + followed: (1 << 3), + }.freeze + + def quote_policy_as_keys(kind) + case kind + when :automatic + policy = quote_approval_policy >> 16 + when :manual + policy = quote_approval_policy & 0xFFFF + end + + QUOTE_APPROVAL_POLICY_FLAGS.keys.select { |key| policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[key]) }.map(&:to_s) + end + + # Returns `:automatic`, `:manual`, `:unknown` or `:denied` + def quote_policy_for_account(other_account, preloaded_relations: {}) + return :denied if other_account.nil? + + following_author = nil + + # Post author is always allowed to quote themselves + return :automatic if account_id == other_account.id + + automatic_policy = quote_approval_policy >> 16 + manual_policy = quote_approval_policy & 0xFFFF + + # Checking for public policy first because it's less expensive than looking at mentions + return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + + # Mentioned users are always allowed to quote + if active_mentions.loaded? + return :automatic if active_mentions.any? { |mention| mention.account_id == other_account.id } + elsif active_mentions.exists?(account: other_account) + return :automatic + end + + if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + return :automatic if following_author + end + + # We don't know we are allowed by the automatic policy, considering the manual one + return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + + if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + return :manual if following_author + end + + return :unknown if (automatic_policy | manual_policy).anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:unknown]) + + :denied + end +end diff --git a/app/models/status.rb b/app/models/status.rb index e6e9450264b..51150bec49e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -43,16 +43,10 @@ class Status < ApplicationRecord include Status::SnapshotConcern include Status::ThreadingConcern include Status::Visibility + include Status::InteractionPolicyConcern MEDIA_ATTACHMENTS_LIMIT = 4 - QUOTE_APPROVAL_POLICY_FLAGS = { - unknown: (1 << 0), - public: (1 << 1), - followers: (1 << 2), - followed: (1 << 3), - }.freeze - rate_limit by: :account, family: :statuses self.discard_column = :deleted_at diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index c85b0170fd7..e64369b08be 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -90,14 +90,28 @@ class Trends::Statuses < Trends::Base def eligible?(status) status.created_at.past? && - status.public_visibility? && - status.account.discoverable? && - !status.account.silenced? && - !status.account.sensitized? && - status.spoiler_text.blank? && - !status.sensitive? && + opted_into_trends?(status) && + !sensitive_content?(status) && !status.reply? && - valid_locale?(status.language) + valid_locale?(status.language) && + (status.quote.nil? || trendable_quote?(status.quote)) + end + + def opted_into_trends?(status) + status.public_visibility? && + status.account.discoverable? && + !status.account.silenced? + end + + def sensitive_content?(status) + status.account.sensitized? || status.spoiler_text.present? || status.sensitive? + end + + def trendable_quote?(quote) + quote.acceptable? && + quote.quoted_status.present? && + opted_into_trends?(quote.quoted_status) && + !sensitive_content?(quote.quoted_status) end def calculate_scores(statuses, at_time) diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 540e266427f..d9bb7201c00 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -19,6 +19,11 @@ class StatusPolicy < ApplicationPolicy end end + # This is about requesting a quote post, not validating it + def quote? + record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied + end + def reblog? !requires_mention? && (!private? || owned?) && show? && !blocking_author? end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 7b29e6d69be..95a869658c3 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -30,6 +30,11 @@ 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_authorization, if: :quote_authorization? + def id ActivityPub::TagManager.instance.uri_for(object) end @@ -194,6 +199,24 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer object.preloadable_poll&.voters_count end + def quote? + object.quote&.present? + end + + def quote_authorization? + object.quote&.approval_uri.present? + end + + def quote + # TODO: handle inlining self-quotes + ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status) + end + + def quote_authorization + # TODO: approval of local quotes may work differently, perhaps? + object.quote.approval_uri + end + class MediaAttachmentSerializer < ActivityPub::Serializer context_extensions :blurhash, :focal_point diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 29e77e7d5b1..4ade8132111 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -32,6 +32,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_one :quote, key: :quote, serializer: REST::QuoteSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer + has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } def quote object.quote if object.quote&.acceptable? @@ -159,6 +160,14 @@ class REST::StatusSerializer < ActiveModel::Serializer object.active_mentions.to_a.sort_by(&:id) end + def quote_approval + { + automatic: object.quote_policy_as_keys(:automatic), + manual: object.quote_policy_as_keys(:manual), + current_user: object.quote_policy_for_account(current_user&.account), + } + end + private def relationships diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ac4b535ea9d..73e78f00478 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -96,13 +96,11 @@ class PostStatusService < BaseService # NOTE: for now this is only for convenience in testing, as we don't support the request flow nor serialize quotes in ActivityPub # we only support incoming quotes so far - status.quote = Quote.new(quoted_status: @quoted_status) - status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id }) - - # TODO: the following has yet to be implemented: - # - handle approval of local users (requires the interactionPolicy PR) - # - produce a QuoteAuthorization for quotes of local users - # - send a QuoteRequest for quotes of remote users + status.quote = Quote.create(quoted_status: @quoted_status, status: status) + if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote? + # TODO: produce a QuoteAuthorization + status.quote.accept! + end end def safeguard_mentions!(status) @@ -146,6 +144,7 @@ class PostStatusService < BaseService DistributionWorker.perform_async(@status.id) ActivityPub::DistributionWorker.perform_async(@status.id) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll + ActivityPub::QuoteRequestWorker.perform_async(@status.quote.id) if @status.quote&.quoted_status.present? && !@status.quote&.quoted_status&.local? end def validate_media! diff --git a/app/views/admin/accounts/_field.html.haml b/app/views/admin/accounts/_field.html.haml new file mode 100644 index 00000000000..ce8d80785e6 --- /dev/null +++ b/app/views/admin/accounts/_field.html.haml @@ -0,0 +1,9 @@ +-# locals: (field:, account:) +%dl + %dt.emojify{ title: field.name } + = prerender_custom_emojis(h(field.name), account.emojis) + %dd{ title: field.value, class: field_verified_class(field.verified?) } + - if field.verified? + %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } + = material_symbol 'check' + = prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index f148b9a0822..977967c58fb 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -7,25 +7,17 @@ = render 'application/card', account: @account -- account = @account -- fields = account.fields -- unless fields.empty? && account.note.blank? +- if @account.fields? || @account.note? .admin-account-bio - - unless fields.empty? + - if @account.fields? %div .account__header__fields - - fields.each do |field| - %dl - %dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis) - %dd{ title: field.value, class: custom_field_classes(field) } - - if field.verified? - %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } - = material_symbol 'check' - = prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis) + = render partial: 'field', collection: @account.fields, locals: { account: @account } - - if account.note.present? + - if @account.note? %div - .account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis) + .account__header__content.emojify + = prerender_custom_emojis(account_bio_format(@account), @account.emojis) = render 'admin/accounts/counters', account: @account diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index dd79695db7f..e02bd2b1773 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -21,6 +21,7 @@ .fields-group.fields-row__column.fields-row__column-6 = ff.input :default_privacy, collection: Status.selectable_visibilities, + selected: current_user.setting_default_privacy, hint: false, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, diff --git a/app/workers/activitypub/quote_request_worker.rb b/app/workers/activitypub/quote_request_worker.rb new file mode 100644 index 00000000000..de5e054f69f --- /dev/null +++ b/app/workers/activitypub/quote_request_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker + def perform(quote_id) + @quote = Quote.find(quote_id) + @account = @quote.account + + distribute! + rescue ActiveRecord::RecordNotFound + true + end + + protected + + def inboxes + @inboxes ||= [@quote.quoted_account&.inbox_url].compact + end + + def payload + @payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account)) + end +end diff --git a/app/workers/activitypub/status_update_distribution_worker.rb b/app/workers/activitypub/status_update_distribution_worker.rb index a79ede2bf61..7f70fcaecc6 100644 --- a/app/workers/activitypub/status_update_distribution_worker.rb +++ b/app/workers/activitypub/status_update_distribution_worker.rb @@ -17,10 +17,10 @@ class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWor def activity ActivityPub::ActivityPresenter.new( - id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join, + id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @options[:updated_at]&.to_datetime&.to_i || @status.edited_at.to_i].join, type: 'Update', actor: ActivityPub::TagManager.instance.uri_for(@status.account), - published: @status.edited_at, + published: @options[:updated_at]&.to_datetime || @status.edited_at, to: ActivityPub::TagManager.instance.to(@status), cc: ActivityPub::TagManager.instance.cc(@status), virtual_object: @status diff --git a/app/workers/mention_resolve_worker.rb b/app/workers/mention_resolve_worker.rb index 8c5938aeaf1..d14adb3cf31 100644 --- a/app/workers/mention_resolve_worker.rb +++ b/app/workers/mention_resolve_worker.rb @@ -22,11 +22,7 @@ class MentionResolveWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end private diff --git a/app/workers/redownload_avatar_worker.rb b/app/workers/redownload_avatar_worker.rb index df17b7718dc..c4c659f73e8 100644 --- a/app/workers/redownload_avatar_worker.rb +++ b/app/workers/redownload_avatar_worker.rb @@ -20,10 +20,6 @@ class RedownloadAvatarWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/redownload_header_worker.rb b/app/workers/redownload_header_worker.rb index 3b142ec5f98..2d600e29641 100644 --- a/app/workers/redownload_header_worker.rb +++ b/app/workers/redownload_header_worker.rb @@ -20,10 +20,6 @@ class RedownloadHeaderWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/redownload_media_worker.rb b/app/workers/redownload_media_worker.rb index 343caa32c23..5342ec0b2d4 100644 --- a/app/workers/redownload_media_worker.rb +++ b/app/workers/redownload_media_worker.rb @@ -20,10 +20,6 @@ class RedownloadMediaWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/remote_account_refresh_worker.rb b/app/workers/remote_account_refresh_worker.rb index 9632936b547..5a4cbdf7260 100644 --- a/app/workers/remote_account_refresh_worker.rb +++ b/app/workers/remote_account_refresh_worker.rb @@ -15,10 +15,6 @@ class RemoteAccountRefreshWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/config/environments/development.rb b/config/environments/development.rb index ca9e876e26b..79b491869cc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -19,6 +19,9 @@ Rails.application.configure do # Enable server timing. config.server_timing = true + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + config.asset_host = ENV['CDN_HOST'] if ENV['CDN_HOST'].present? + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? diff --git a/config/locales/en.yml b/config/locales/en.yml index 243634873b2..256b5bf846c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1873,6 +1873,7 @@ en: edited_at_html: Edited %{date} errors: in_reply_not_found: The post you are trying to reply to does not appear to exist. + quoted_status_not_found: The post you are trying to quote does not appear to exist. over_character_limit: character limit of %{max} exceeded pin_errors: direct: Posts that are only visible to mentioned users cannot be pinned diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index bb68494118f..a866debe453 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -319,6 +319,7 @@ fr-CA: create: Créer une annonce title: Nouvelle annonce preview: + disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur. explanation_html: 'L''e-mail sera envoyé à %{display_count} utilisateurs. Le texte suivant sera inclus :' title: Aperçu de la notification d'annonce publish: Publier diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6fcdb5b9722..c171a9ed731 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -319,6 +319,7 @@ fr: create: Créer une annonce title: Nouvelle annonce preview: + disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur. explanation_html: 'L''e-mail sera envoyé à %{display_count} utilisateurs. Le texte suivant sera inclus :' title: Aperçu de la notification d'annonce publish: Publier diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 971846789b6..35f79c67c01 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1393,7 +1393,7 @@ ru: featured_tags: add_new: Добавить errors: - limit: Вы уже добавили максимальное число хештегов + limit: Вы достигли максимального количества хештегов, которые можно рекомендовать в профиле hint_html: "Рекомендуйте самые важные для вас хештеги в своём профиле. Это отличный инструмент для того, чтобы держать подписчиков в курсе ваших долгосрочных проектов и творческих работ. Рекомендации хештегов заметны в вашем профиле и предоставляют быстрый доступ к вашим постам." filters: contexts: @@ -1477,56 +1477,56 @@ ru: other: Проверьте введённые вами данные! Далее по странице вы можете увидеть %{count} сообщений об ошибке imports: errors: - empty: Пустой CSV-файл - incompatible_type: Несовместимость с выбранным типом импорта - invalid_csv_file: 'Неверный файл CSV. Ошибка: %{error}' + empty: Файл CSV пуст + incompatible_type: Несовместим с выбранным типом данных для импорта + invalid_csv_file: 'Ошибка при чтении файла CSV: %{error}' over_rows_processing_limit: содержит более %{count} строк too_large: Файл слишком большой failures: Ошибки - imported: Импортирован - mismatched_types_warning: Возможно, вы выбрали неверный тип для этого импорта, пожалуйста, перепроверьте. + imported: Импортировано + mismatched_types_warning: По-видимому, вы выбрали неверный тип данных для импорта. Проверьте всё внимательно! modes: merge: Объединить - merge_long: Сохранить имеющиеся данные и добавить новые. - overwrite: Перезаписать - overwrite_long: Перезаписать имеющиеся данные новыми. + merge_long: Добавить новые данные к уже имеющимся + overwrite: Заменить + overwrite_long: Заменить имеющиеся данные новыми overwrite_preambles: blocking_html: - few: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунта, из файла %{filename}. - many: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунов, из файла %{filename}. - one: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунт, из файла %{filename}. - other: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунтов, из файла %{filename}. + few: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. + many: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. + one: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователя. + other: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. bookmarks_html: - few: Вы собираетесь заменить свои закладки, в которых сейчас %{count} поста, из файла %{filename}. - many: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. - one: Вы собираетесь заменить свои закладки, в которых сейчас %{count} пост, из файла %{filename}. - other: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. + few: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} поста. + many: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} постов. + one: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} пост. + other: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} постов. domain_blocking_html: - few: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домена, из файла %{filename}. - many: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. - one: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домен, из файла %{filename}. - other: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. + few: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} домена. + many: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} доменов. + one: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} домен. + other: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} доменов. following_html: - few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename} и отписаться от всех прочих. - many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. - one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename} и отписаться от всех прочих. - other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. + few: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. + many: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. + one: Вы собираетесь подписаться на %{count} пользователя из файла %{filename} и отписаться от всех прочих. + other: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. lists_html: - few: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунта. - many: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. - one: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будет добавлен %{count} аккаунт. - other: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. + few: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователя. + many: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователей. + one: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будет добавлен %{count} пользователь. + other: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователей. muting_html: - few: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. - many: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунта из файла %{filename}. - other: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. + many: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. + one: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователя. + other: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. preambles: blocking_html: - few: Вы собираетесь заблокировать %{count} аккаунта из файла %{filename}. - many: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь заблокировать %{count} аккаунт из файла %{filename}. - other: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. + many: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. + one: Вы собираетесь заблокировать %{count} пользователя из файла %{filename}. + other: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. bookmarks_html: few: Вы собираетесь добавить %{count} поста из файла %{filename} в свои закладки. many: Вы собираетесь добавить %{count} постов из файла %{filename} в свои закладки. @@ -1538,52 +1538,52 @@ ru: one: Вы собираетесь заблокировать %{count} домен из файла %{filename}. other: Вы собираетесь заблокировать %{count} доменов из файла %{filename}. following_html: - few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename}. - many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename}. - other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. + many: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. + one: Вы собираетесь подписаться на %{count} пользователя из файла %{filename}. + other: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. lists_html: - few: Вы собираетесь добавить %{count} аккаунта из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - many: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - one: Вы собираетесь добавить %{count} аккаунт из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - other: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + few: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + many: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + one: Вы собираетесь добавить %{count} пользователя из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + other: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. muting_html: - few: Вы собираетесь начать игнорировать %{count} аккаунта из файла %{filename}. - many: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь начать игнорировать %{count} аккаунт из файла %{filename}. - other: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. - preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Вашу учётную запись на этом узле из файлов, экспортированных с другого узла. - recent_imports: Недавно импортированное + few: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + many: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + one: Вы собираетесь игнорировать %{count} пользователя из файла %{filename}. + other: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + preface: Вы можете перенести прежде экспортированные с другого сервера данные, такие как блокируемые вами пользователи и ваши подписки. + recent_imports: История импорта states: - finished: Готово - in_progress: В процессе - scheduled: Запланировано - unconfirmed: Неподтвержденный - status: Статус - success: Ваши данные были успешно загружены и будут обработаны с должной скоростью - time_started: Началось в + finished: Завершён + in_progress: Выполняется + scheduled: Запланирован + unconfirmed: Не подтверждён + status: Состояние + success: Ваши данные были загружены и в скором времени будут обработаны + time_started: Начат titles: - blocking: Импорт заблокированных аккаунтов + blocking: Импорт списка заблокированных пользователей bookmarks: Импорт закладок - domain_blocking: Импорт заблокированных доменов - following: Импорт последующих аккаунтов - lists: Импортировать список - muting: Импорт отключенных аккаунтов + domain_blocking: Импорт списка заблокированных доменов + following: Импорт подписок + lists: Импорт списков + muting: Импорт списка игнорируемых пользователей type: Тип импорта type_groups: constructive: Подписки и закладки - destructive: Блокировки и игнорируемые + destructive: Чёрный список types: - blocking: Список блокировки + blocking: Заблокированные пользователи bookmarks: Закладки - domain_blocking: Список доменных блокировок + domain_blocking: Заблокированные домены following: Подписки lists: Списки - muting: Список глушения + muting: Игнорируемые пользователи upload: Загрузить invites: delete: Удалить - expired: Истекло + expired: Срок действия истёк expires_in: '1800': 30 минут '21600': 6 часов @@ -1592,33 +1592,32 @@ ru: '604800': 1 неделя '86400': 1 день expires_in_prompt: Бессрочно - generate: Сгенерировать + generate: Создать ссылку invalid: Это приглашение недействительно - invited_by: 'Вас пригласил(а):' + invited_by: 'Вы были приглашены этим пользователем:' max_uses: few: "%{count} раза" many: "%{count} раз" one: "%{count} раз" - other: "%{count} раза" + other: "%{count} раз" max_uses_prompt: Без ограничения - prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу. + prompt: Создавайте приглашения и делитесь ими с другими людьми, чтобы они могли зарегистрироваться на этом сервере table: expires_at: Истекает - uses: Исп. - title: Пригласить людей + uses: Регистрации + title: Приглашения lists: errors: - limit: Вы достигли максимального количества пользователей + limit: Вы достигли максимального количества списков login_activities: authentication_methods: - otp: приложение двухфакторной аутентификации - password: пароль - sign_in_token: код безопасности электронной почты - webauthn: ключи безопасности - description_html: Если вы видите неопознанное действие, смените пароль и/или включите двухфакторную авторизацию. - empty: Нет доступной истории входов - failed_sign_in_html: Неудачная попытка входа используя %{method} через %{browser} (%{ip}) - successful_sign_in_html: Успешный вход используя %{method} через %{browser} (%{ip}) + otp: приложения двухфакторной аутентификации + password: пароля + webauthn: электронного ключа + description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию. + empty: История входов отсутствует + failed_sign_in_html: Неудачная попытка входа при помощи %{method} с IP-адреса %{ip} (%{browser}) + successful_sign_in_html: Вход при помощи %{method} с IP-адреса %{ip} (%{browser}) title: История входов mail_subscriptions: unsubscribe: diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index cc222cc85ee..fd32c087440 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -231,8 +231,8 @@ el: setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF setting_boost_modal: Επιβεβαίωση πριν την προώθηση - setting_default_language: Γλώσσα δημοσιεύσεων - setting_default_privacy: Ιδιωτικότητα δημοσιεύσεων + setting_default_language: Γλώσσα κατά την ανάρτηση + setting_default_privacy: Ιδιωτικότητα αναρτήσεων setting_default_quote_policy: Ποιος μπορεί να παραθέσει setting_default_sensitive: Σημείωση όλων των πολυμέσων ως ευαίσθητου περιεχομένου setting_delete_modal: Επιβεβαίωση πριν τη διαγραφή ενός τουτ diff --git a/docker-compose.yml b/docker-compose.yml index e5d94b390f4..8fe42fa0c9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.1 + image: ghcr.io/mastodon/mastodon:v4.4.2 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.4.1 + image: ghcr.io/mastodon/mastodon-streaming:v4.4.2 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.1 + image: ghcr.io/mastodon/mastodon:v4.4.2 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb deleted file mode 100644 index 1e01709262b..00000000000 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::Web::PushSubscriptionsController do - render_views - - let(:user) { Fabricate(:user) } - - let(:create_payload) do - { - subscription: { - endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', - keys: { - p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', - auth: 'eH_C8rq2raXqlcBVDa1gLg==', - }, - standard: standard, - }, - } - end - - let(:alerts_payload) do - { - data: { - policy: 'all', - - alerts: { - follow: true, - follow_request: false, - favourite: false, - reblog: true, - mention: false, - poll: true, - status: false, - }, - }, - } - end - let(:standard) { '1' } - - before do - sign_in(user) - - stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200) - end - - describe 'POST #create' do - it 'saves push subscriptions' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - - user.reload - - expect(created_push_subscription) - .to have_attributes( - endpoint: eq(create_payload[:subscription][:endpoint]), - key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), - key_auth: eq(create_payload[:subscription][:keys][:auth]) - ) - .and be_standard - expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription) - end - - context 'when standard is provided as false value' do - let(:standard) { '0' } - - it 'saves push subscription with standard as false' do - post :create, format: :json, params: create_payload - - expect(created_push_subscription) - .to_not be_standard - end - end - - context 'with a user who has a session with a prior subscription' do - let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) } - - it 'destroys prior subscription when creating new one' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - expect { prior_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with initial data' do - it 'saves alert settings' do - post :create, format: :json, params: create_payload.merge(alerts_payload) - - expect(response).to have_http_status(200) - - expect(created_push_subscription.data['policy']).to eq 'all' - - %w(follow follow_request favourite reblog mention poll status).each do |type| - expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) - end - end - end - end - - describe 'PUT #update' do - it 'changes alert settings' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - - alerts_payload[:id] = created_push_subscription.id - - put :update, format: :json, params: alerts_payload - - expect(created_push_subscription.data['policy']).to eq 'all' - - %w(follow follow_request favourite reblog mention poll status).each do |type| - expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) - end - end - end - - def created_push_subscription - Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - end -end diff --git a/spec/controllers/concerns/accountable_concern_spec.rb b/spec/controllers/concerns/accountable_concern_spec.rb index cd06d872bb5..e68090fdc2f 100644 --- a/spec/controllers/concerns/accountable_concern_spec.rb +++ b/spec/controllers/concerns/accountable_concern_spec.rb @@ -6,6 +6,7 @@ RSpec.describe AccountableConcern do let(:hoge_class) do Class.new do include AccountableConcern + attr_reader :current_account def initialize(current_account) diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb index c3fbff4e8bb..a8f6d99f032 100644 --- a/spec/helpers/home_helper_spec.rb +++ b/spec/helpers/home_helper_spec.rb @@ -75,23 +75,19 @@ RSpec.describe HomeHelper do end end - describe 'custom_field_classes' do - context 'with a verified field' do - let(:field) { instance_double(Account::Field, verified?: true) } + describe 'field_verified_class' do + subject { helper.field_verified_class(verified) } - it 'returns verified string' do - result = helper.custom_field_classes(field) - expect(result).to eq 'verified' - end + context 'with a verified field' do + let(:verified) { true } + + it { is_expected.to eq('verified') } end context 'with a non-verified field' do - let(:field) { instance_double(Account::Field, verified?: false) } + let(:verified) { false } - it 'returns verified string' do - result = helper.custom_field_classes(field) - expect(result).to eq 'emojify' - end + it { is_expected.to eq('emojify') } end end diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 6d7c05e6165..615287389c3 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Accept do - let(:sender) { Fabricate(:account) } + let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } describe '#perform' do @@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do end end end + + context 'with a QuoteRequest' do + let(:status) { Fabricate(:status, account: recipient) } + let(:quoted_status) { Fabricate(:status, account: sender) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status) } + let(:approval_uri) { "https://#{sender.domain}/approvals/1" } + + let(:json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest', + }, + ], + id: 'foo', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: { + id: quote.activity_uri, + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(recipient), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: ActivityPub::TagManager.instance.uri_for(status), + }, + result: approval_uri, + }.with_indifferent_access + end + + it 'marks the quote as approved and distribute an update' do + expect { subject.perform } + .to change { quote.reload.accepted? }.from(false).to(true) + .and change { quote.reload.approval_uri }.to(approval_uri) + expect(DistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) }) + end + + context 'when the quoted status is not from the sender of the Accept' do + let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when the quoting status is from an unrelated user' do + let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when the QuoteRequest is referenced by its identifier' do + let(:json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest', + }, + ], + id: 'foo', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: quote.activity_uri, + result: approval_uri, + }.with_indifferent_access + end + + it 'marks the quote as approved and distribute an update' do + expect { subject.perform } + .to change { quote.reload.accepted? }.from(false).to(true) + .and change { quote.reload.approval_uri }.to(approval_uri) + expect(DistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) }) + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + end + end end end diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb index 1afb0cd4033..ee8557f1239 100644 --- a/spec/lib/activitypub/activity/reject_spec.rb +++ b/spec/lib/activitypub/activity/reject_spec.rb @@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do expect(relay.reload.rejected?).to be true end end + + context 'with a QuoteRequest' do + let(:status) { Fabricate(:status, account: recipient) } + let(:quoted_status) { Fabricate(:status, account: sender) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') } + let(:approval_uri) { "https://#{sender.domain}/approvals/1" } + + let(:object_json) do + { + id: 'https://abc-123/456', + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(recipient), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: ActivityPub::TagManager.instance.uri_for(status), + }.with_indifferent_access + end + + it 'marks the quote as rejected' do + expect { subject.perform } + .to change { quote.reload.rejected? }.from(false).to(true) + end + end end end diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index e56393da1dd..f450997976a 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do end end + context 'when handling a status with a quote policy', feature: :outgoing_quotes do + let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + + before do + account.follow!(status.account) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + end + end + context 'when handling a filtered status' do let(:status) { Fabricate(:status, text: 'this toot is about that banned word') } diff --git a/spec/models/concerns/status/interaction_policy_concern_spec.rb b/spec/models/concerns/status/interaction_policy_concern_spec.rb new file mode 100644 index 00000000000..af42f2bba3d --- /dev/null +++ b/spec/models/concerns/status/interaction_policy_concern_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Status::InteractionPolicyConcern do + let(:status) { Fabricate(:status, quote_approval_policy: (0b0101 << 16) | 0b0010) } + + describe '#quote_policy_as_keys' do + it 'returns the expected values' do + expect(status.quote_policy_as_keys(:automatic)).to eq ['unknown', 'followers'] + expect(status.quote_policy_as_keys(:manual)).to eq ['public'] + end + end + + describe '#quote_policy_for_account' do + let(:account) { Fabricate(:account) } + + context 'when the account is not following the user' do + it 'returns :manual because of the public entry in the manual policy' do + expect(status.quote_policy_for_account(account)).to eq :manual + end + end + + context 'when the account is following the user' do + before do + account.follow!(status.account) + end + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(status.quote_policy_for_account(account)).to eq :automatic + end + end + + context 'when the account falls into the unknown bucket' do + let(:status) { Fabricate(:status, quote_approval_policy: (0b0001 << 16) | 0b0100) } + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(status.quote_policy_for_account(account)).to eq :unknown + end + end + end +end diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb index 39839010427..54b227dad0c 100644 --- a/spec/models/trends/statuses_spec.rb +++ b/spec/models/trends/statuses_spec.rb @@ -111,12 +111,16 @@ RSpec.describe Trends::Statuses do let!(:yesterday) { today - 1.day } let!(:status_foo) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: yesterday) } - let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } + let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today, quote: Quote.new(state: :accepted, quoted_status: status_foo)) } let!(:status_baz) { Fabricate(:status, text: 'Baz', language: 'en', trendable: true, created_at: today) } + let!(:untrendable) { Fabricate(:status, text: 'Untrendable', language: 'en', trendable: true, visibility: :unlisted) } + let!(:untrendable_quote) { Fabricate(:status, text: 'Untrendable quote!', language: 'en', trendable: true, created_at: today, quote: Quote.new(state: :accepted, quoted_status: untrendable)) } before do default_threshold_value.times { reblog(status_foo, today) } default_threshold_value.times { reblog(status_bar, today) } + default_threshold_value.times { reblog(untrendable, today) } + default_threshold_value.times { reblog(untrendable_quote, today) } (default_threshold_value - 1).times { reblog(status_baz, today) } end @@ -129,7 +133,7 @@ RSpec.describe Trends::Statuses do results = subject.query.limit(10).to_a expect(results).to eq [status_bar, status_foo] - expect(results).to_not include(status_baz) + expect(results).to_not include(status_baz, untrendable, untrendable_quote) end end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 69c0bad026b..63622970455 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -86,6 +86,92 @@ RSpec.describe StatusPolicy, type: :model do end end + context 'with the permission of quote?' do + permissions :quote? do + it 'grants access when direct and account is viewer' do + status.visibility = :direct + + expect(subject).to permit(status.account, status) + end + + it 'grants access when direct and viewer is mentioned' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: alice)] + + expect(subject).to permit(alice, status) + end + + it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + status.active_mentions.load + + expect(subject).to permit(bob, status) + end + + it 'denies access when direct and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :direct + + expect(subject).to_not permit(viewer, status) + end + + it 'denies access when private and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :private + + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when private and viewer is mentioned' do + status.visibility = :private + status.mentions = [Fabricate(:mention, account: bob)] + + expect(subject).to permit(bob, status) + end + + it 'denies access when private and non-viewer is mentioned' do + viewer = Fabricate(:account) + status.visibility = :private + status.mentions = [Fabricate(:mention, account: bob)] + + expect(subject).to_not permit(viewer, status) + end + + it 'denies access when private and account is following viewer' do + follow = Fabricate(:follow) + status.visibility = :private + status.account = follow.target_account + + expect(subject).to_not permit(follow.account, status) + end + + it 'denies access when public but policy does not allow anyone' do + viewer = Fabricate(:account) + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when public and policy allows everyone' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] + viewer = Fabricate(:account) + expect(subject).to permit(viewer, status) + end + + it 'denies access when public and policy allows followers but viewer is not one' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + viewer = Fabricate(:account) + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when public and policy allows followers and viewer is one' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + viewer = Fabricate(:account) + viewer.follow!(status.account) + expect(subject).to permit(viewer, status) + end + end + end + context 'with the permission of reblog?' do permissions :reblog? do it 'denies access when private' do diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb index 359de9d95c0..bccbda6fa23 100644 --- a/spec/requests/api/v1/push/subscriptions_spec.rb +++ b/spec/requests/api/v1/push/subscriptions_spec.rb @@ -166,17 +166,30 @@ RSpec.describe 'API V1 Push Subscriptions' do describe 'GET /api/v1/push/subscription' do subject { get '/api/v1/push/subscription', headers: headers } - before { create_subscription_with_token } + context 'with a subscription' do + before { create_subscription_with_token } - it 'shows subscription details' do - subject + it 'shows subscription details' do + subject - expect(response) - .to have_http_status(200) - expect(response.content_type) - .to start_with('application/json') - expect(response.parsed_body) - .to include(endpoint: endpoint) + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to include(endpoint: endpoint) + end + end + + context 'without a subscription' do + it 'returns not found' do + subject + + expect(response) + .to have_http_status(404) + expect(response.content_type) + .to start_with('application/json') + end end end diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 285fa936552..ac15ae24623 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do end end + context 'with a self-quote post', feature: :outgoing_quotes do + let(:quoted_status) { Fabricate(:status, account: user.account) } + let(:params) do + { + status: 'Hello world, this is a self-quote', + quoted_status_id: quoted_status.id, + } + end + + it 'returns a quote post, as well as rate limit headers', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote]).to be_present + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + context 'with a safeguard' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 42545b3d6e2..91d7b85bc58 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -3,6 +3,38 @@ require 'rails_helper' RSpec.describe 'API Web Push Subscriptions' do + let(:create_payload) do + { + subscription: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + standard: standard, + }, + } + end + + let(:alerts_payload) do + { + data: { + policy: 'all', + + alerts: { + follow: true, + follow_request: false, + favourite: false, + reblog: true, + mention: false, + poll: true, + status: false, + }, + }, + } + end + let(:standard) { '1' } + describe 'DELETE /api/web/push_subscriptions/:id' do subject { delete api_web_push_subscription_path(token) } @@ -54,7 +86,9 @@ RSpec.describe 'API Web Push Subscriptions' do end describe 'POST /api/web/push_subscriptions' do - before { sign_in Fabricate :user } + before { sign_in(user) } + + let(:user) { Fabricate :user } it 'gracefully handles invalid nested params' do post api_web_push_subscriptions_path, params: { subscription: 'invalid' } @@ -62,6 +96,69 @@ RSpec.describe 'API Web Push Subscriptions' do expect(response) .to have_http_status(400) end + + it 'saves push subscriptions with valid params' do + post api_web_push_subscriptions_path, params: create_payload + expect(response) + .to have_http_status(200) + + expect(created_push_subscription) + .to have_attributes( + endpoint: eq(create_payload[:subscription][:endpoint]), + key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), + key_auth: eq(create_payload[:subscription][:keys][:auth]) + ) + .and be_standard + expect(user.session_activations.first.web_push_subscription) + .to eq(created_push_subscription) + end + + context 'when standard is provided as false value' do + let(:standard) { '0' } + + it 'saves push subscription with standard as false' do + post api_web_push_subscriptions_path, params: create_payload + + expect(created_push_subscription) + .to_not be_standard + end + end + + context 'with a user who has a session with a prior subscription' do + before do + # Trigger creation of a `SessionActivation` for the user so that the + # prior_subscription setup and verification works as expected + get about_path + end + + let!(:prior_subscription) { Fabricate(:web_push_subscription, user:, session_activation: user.session_activations.last) } + + it 'destroys prior subscription when creating new one' do + post api_web_push_subscriptions_path, params: create_payload + + expect(response) + .to have_http_status(200) + expect { prior_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with initial data' do + it 'saves alert settings' do + post api_web_push_subscriptions_path, params: create_payload.merge(alerts_payload) + + expect(response) + .to have_http_status(200) + + expect(created_push_subscription.data['policy']) + .to eq 'all' + + alert_types.each do |type| + expect(created_push_subscription.data['alerts'][type]) + .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) + end + end + end end describe 'PUT /api/web/push_subscriptions' do @@ -75,5 +172,30 @@ RSpec.describe 'API Web Push Subscriptions' do expect(response) .to have_http_status(400) end + + it 'changes existing alert settings' do + # Create record this way to correctly associate a `SessionActivation` + # during full POST->create cycle + post api_web_push_subscriptions_path params: create_payload + expect(response) + .to have_http_status(200) + + put api_web_push_subscription_path(created_push_subscription), params: alerts_payload + expect(created_push_subscription.data['policy']) + .to eq 'all' + alert_types.each do |type| + expect(created_push_subscription.data['alerts'][type]) + .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) + end + end + end + + def created_push_subscription + Web::PushSubscription + .find_by(endpoint: create_payload[:subscription][:endpoint]) + end + + def alert_types + Notification::LEGACY_TYPE_CLASS_MAP.values.map(&:to_s) end end diff --git a/spec/routing/accounts_routing_spec.rb b/spec/routing/accounts_routing_spec.rb index 8ff711a681e..bb0bf082bde 100644 --- a/spec/routing/accounts_routing_spec.rb +++ b/spec/routing/accounts_routing_spec.rb @@ -49,6 +49,7 @@ RSpec.describe 'Routes under accounts/' do context 'with local username encoded at' do include RSpec::Rails::RequestExampleGroup + let(:username) { 'alice' } it 'routes /%40:username' do @@ -140,6 +141,7 @@ RSpec.describe 'Routes under accounts/' do context 'with remote username encoded at' do include RSpec::Rails::RequestExampleGroup + let(:username) { 'alice%40example.com' } let(:username_decoded) { 'alice@example.com' } diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index a6976193b20..d1af3f068f5 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -41,4 +41,20 @@ RSpec.describe ActivityPub::NoteSerializer do .and(not_include(reply_by_other_first.uri)) # Replies from others .and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility end + + context 'with a quote' do + let(:quoted_status) { Fabricate(:status) } + let(:approval_uri) { 'https://example.com/foo/bar' } + let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, approval_uri: approval_uri) } + + it 'has the expected shape' do + expect(subject).to include({ + 'type' => 'Note', + 'quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + 'quoteUri' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + '_misskey_quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + 'quoteAuthorization' => approval_uri, + }) + end + end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 8836b9e0a63..7e47506a9f7 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -291,6 +291,14 @@ RSpec.describe PostStatusService do ) end + it 'correctly requests a quote for remote posts' do + account = Fabricate(:account) + quoted_status = Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) + + expect { subject.call(account, text: 'test', quoted_status: quoted_status) } + .to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker) + end + it 'returns existing status when used twice with idempotency key' do account = Fabricate(:account) status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') diff --git a/spec/validators/existing_username_validator_spec.rb b/spec/validators/existing_username_validator_spec.rb index 25ecb1fbcde..ab5be524534 100644 --- a/spec/validators/existing_username_validator_spec.rb +++ b/spec/validators/existing_username_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe ExistingUsernameValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :contact, :friends def self.name diff --git a/spec/validators/language_validator_spec.rb b/spec/validators/language_validator_spec.rb index 19e55f34672..d19b33f27f8 100644 --- a/spec/validators/language_validator_spec.rb +++ b/spec/validators/language_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe LanguageValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :locale validates :locale, language: true diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index ad1092109db..67a2921885e 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe UnreservedUsernameValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :username validates_with UnreservedUsernameValidator diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index 2297dddaa01..55c0347d18e 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe URLValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :profile validates :profile, url: true diff --git a/spec/workers/activitypub/quote_request_worker_spec.rb b/spec/workers/activitypub/quote_request_worker_spec.rb new file mode 100644 index 00000000000..3d0131baaad --- /dev/null +++ b/spec/workers/activitypub/quote_request_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::QuoteRequestWorker do + subject { described_class.new } + + let(:quoted_account) { Fabricate(:account, inbox_url: 'http://example.com', domain: 'example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:status) { Fabricate(:status, text: 'foo') } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'TODO') } # TODO: activity URI + + describe '#perform' do + it 'sends the expected QuoteRequest activity' do + subject.perform(quote.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_object_shape, quote.account_id, 'http://example.com', {}) + end + + def match_object_shape + match_json_values( + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(quote.account), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: anything # TODO: inline post in request? + ) + end + end +end diff --git a/spec/workers/activitypub/status_update_distribution_worker_spec.rb b/spec/workers/activitypub/status_update_distribution_worker_spec.rb index e9a70d11d19..58d11db41cb 100644 --- a/spec/workers/activitypub/status_update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/status_update_distribution_worker_spec.rb @@ -9,36 +9,64 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') } describe '#perform' do - before do - follower.follow!(status.account) - - status.snapshot! - status.text = 'bar' - status.edited_at = Time.now.utc - status.snapshot! - status.save! - end - - context 'with public status' do + context 'with an explicitly edited status' do before do - status.update(visibility: :public) + follower.follow!(status.account) + + status.snapshot! + status.text = 'bar' + status.edited_at = Time.now.utc + status.snapshot! + status.save! end - it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + expect { subject.perform(status.id) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + expect { subject.perform(status.id) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) end end end - context 'with private status' do + context 'with an implicitly edited status' do before do - status.update(visibility: :private) + follower.follow!(status.account) end - it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) end end end diff --git a/vite.config.mts b/vite.config.mts index 7f93157b7e1..30c0741aaa6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -65,6 +65,11 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { // but it needs to be scoped to the whole domain 'Service-Worker-Allowed': '/', }, + hmr: { + // Forcing the protocol to be insecure helps if you are proxying your dev server with SSL, + // because Vite still tries to connect to localhost. + protocol: 'ws', + }, port: 3036, }, build: { @@ -184,29 +189,30 @@ async function findEntrypoints() { const entrypoints: Record = {}; // First, JS entrypoints - const jsEntrypoints = await readdir(path.resolve(jsRoot, 'entrypoints'), { + const jsEntrypointsDir = path.resolve(jsRoot, 'entrypoints'); + const jsEntrypoints = await readdir(jsEntrypointsDir, { withFileTypes: true, }); const jsExtTest = /\.[jt]sx?$/; for (const file of jsEntrypoints) { if (file.isFile() && jsExtTest.test(file.name)) { entrypoints[file.name.replace(jsExtTest, '')] = path.resolve( - file.parentPath, + jsEntrypointsDir, file.name, ); } } // Next, SCSS entrypoints - const scssEntrypoints = await readdir( - path.resolve(jsRoot, 'styles/entrypoints'), - { withFileTypes: true }, - ); + const scssEntrypointsDir = path.resolve(jsRoot, 'styles/entrypoints'); + const scssEntrypoints = await readdir(scssEntrypointsDir, { + withFileTypes: true, + }); const scssExtTest = /\.s?css$/; for (const file of scssEntrypoints) { if (file.isFile() && scssExtTest.test(file.name)) { entrypoints[file.name.replace(scssExtTest, '')] = path.resolve( - file.parentPath, + scssEntrypointsDir, file.name, ); } diff --git a/yarn.lock b/yarn.lock index 67739e74d4c..2c1faaa91a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -73,39 +73,39 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/core@npm:7.27.4" +"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/core@npm:7.28.0" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.3" + "@babel/generator": "npm:^7.28.0" "@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.4" - "@babel/parser": "npm:^7.27.4" + "@babel/helpers": "npm:^7.27.6" + "@babel/parser": "npm:^7.28.0" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.27.4" - "@babel/types": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/d2d17b106a8d91d3eda754bb3f26b53a12eb7646df73c2b2d2e9b08d90529186bc69e3823f70a96ec6e5719dc2372fb54e14ad499da47ceeb172d2f7008787b5 + checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 languageName: node linkType: hard -"@babel/generator@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/generator@npm:7.27.3" +"@babel/generator@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/generator@npm:7.28.0" dependencies: - "@babel/parser": "npm:^7.27.3" - "@babel/types": "npm:^7.27.3" - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" + "@babel/parser": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10c0/341622e17c61d008fc746b655ab95ef7febb543df8efb4148f57cf06e60ade1abe091ed7d6811df17b064d04d64f69bb7f35ab0654137116d55c54a73145a61a + checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 languageName: node linkType: hard @@ -176,6 +176,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-globals@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/helper-globals@npm:7.28.0" + checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232 + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" @@ -293,24 +300,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/helpers@npm:7.27.4" +"@babel/helpers@npm:^7.27.6": + version: 7.27.6 + resolution: "@babel/helpers@npm:7.27.6" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.3" - checksum: 10c0/3463551420926b3f403c1a30d66ac67bba5c4f73539a8ccb71544da129c4709ac37c57fac740ed8a261b3e6bbbf353b05e03b36ea1a6bf1081604b2a94ca43c1 + "@babel/types": "npm:^7.27.6" + checksum: 10c0/448bac96ef8b0f21f2294a826df9de6bf4026fd023f8a6bb6c782fe3e61946801ca24381490b8e58d861fee75cd695a1882921afbf1f53b0275ee68c938bd6d3 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3, @babel/parser@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/parser@npm:7.27.4" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/parser@npm:7.28.0" dependencies: - "@babel/types": "npm:^7.27.3" + "@babel/types": "npm:^7.28.0" bin: parser: ./bin/babel-parser.js - checksum: 10c0/d1bf17e7508585235e2a76594ba81828e48851877112bb8abbecd7161a31fb66654e993e458ddaedb18a3d5fa31970e5f3feca5ae2900f51e6d8d3d35da70dbf + checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 languageName: node linkType: hard @@ -1157,28 +1164,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/traverse@npm:7.27.4" +"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/traverse@npm:7.28.0" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.3" - "@babel/parser": "npm:^7.27.4" + "@babel/generator": "npm:^7.28.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.0" "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.3" + "@babel/types": "npm:^7.28.0" debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10c0/6de8aa2a0637a6ee6d205bf48b9e923928a02415771fdec60085ed754dcdf605e450bb3315c2552fa51c31a4662275b45d5ae4ad527ce55a7db9acebdbbbb8ed + checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.4.4": - version: 7.27.3 - resolution: "@babel/types@npm:7.27.3" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0, @babel/types@npm:^7.4.4": + version: 7.28.1 + resolution: "@babel/types@npm:7.28.1" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10c0/bafdfc98e722a6b91a783b6f24388f478fd775f0c0652e92220e08be2cc33e02d42088542f1953ac5e5ece2ac052172b3dadedf12bec9aae57899e92fb9a9757 + checksum: 10c0/5e99b346c11ee42ffb0cadc28159fe0b184d865a2cc1593df79b199772a534f6453969b4942aa5e4a55a3081863096e1cc3fc1c724d826926dc787cf229b845d languageName: node linkType: hard @@ -2542,14 +2549,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.5 - resolution: "@jridgewell/gen-mapping@npm:0.3.5" +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.12 + resolution: "@jridgewell/gen-mapping@npm:0.3.12" dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb + checksum: 10c0/32f771ae2467e4d440be609581f7338d786d3d621bac3469e943b9d6d116c23c4becb36f84898a92bbf2f3c0511365c54a945a3b86a83141547a2a360a5ec0c7 languageName: node linkType: hard @@ -2560,13 +2566,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.6 resolution: "@jridgewell/source-map@npm:0.3.6" @@ -2577,20 +2576,20 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.29 + resolution: "@jridgewell/trace-mapping@npm:0.3.29" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + checksum: 10c0/fb547ba31658c4d74eb17e7389f4908bf7c44cef47acb4c5baa57289daf68e6fe53c639f41f751b3923aca67010501264f70e7b49978ad1f040294b22c37b333 languageName: node linkType: hard @@ -3211,10 +3210,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.19": - version: 1.0.0-beta.19 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.19" - checksum: 10c0/e4205df56e6231a347ac601d044af365639741d51b5bea4e91ecc37e19e9777cb79d1daa924b8709ddf1f743ed6922e4e68e2445126434c4d420d9f4416f4feb +"@rolldown/pluginutils@npm:1.0.0-beta.27": + version: 1.0.0-beta.27 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" + checksum: 10c0/9658f235b345201d4f6bfb1f32da9754ca164f892d1cb68154fe5f53c1df42bd675ecd409836dff46884a7847d6c00bdc38af870f7c81e05bba5c2645eb4ab9c languageName: node linkType: hard @@ -4735,18 +4734,18 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^4.2.1": - version: 4.6.0 - resolution: "@vitejs/plugin-react@npm:4.6.0" + version: 4.7.0 + resolution: "@vitejs/plugin-react@npm:4.7.0" dependencies: - "@babel/core": "npm:^7.27.4" + "@babel/core": "npm:^7.28.0" "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" - "@rolldown/pluginutils": "npm:1.0.0-beta.19" + "@rolldown/pluginutils": "npm:1.0.0-beta.27" "@types/babel__core": "npm:^7.20.5" react-refresh: "npm:^0.17.0" peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - checksum: 10c0/73b8f271978a0337debb255afd1667f49c2018c118962a8613120383375c4038255a5315cee2ef210dc7fd07cd30d5b12271077ad47db29980f8156b8a49be2c + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/692f23960972879485d647713663ec299c478222c96567d60285acf7c7dc5c178e71abfe9d2eefddef1eeb01514dacbc2ed68aad84628debf9c7116134734253 languageName: node linkType: hard