From 37cec638dfd7befe756d540abef0d6960e30d3d7 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 23 Sep 2025 15:40:04 +0200 Subject: [PATCH 01/58] Update to puma 7 (#36238) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- config/puma.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 5dd5d5bf26..126d73f9ca 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.2.0', '< 3.5.0' gem 'propshaft' -gem 'puma', '~> 6.3' +gem 'puma', '~> 7.0' gem 'rails', '~> 8.0' gem 'thor', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index b3364ba38d..64ef3057d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -637,7 +637,7 @@ GEM date stringio public_suffix (6.0.2) - puma (6.6.1) + puma (7.0.3) nio4r (~> 2.0) pundit (2.5.1) activesupport (>= 3.0.0) @@ -1052,7 +1052,7 @@ DEPENDENCIES prometheus_exporter (~> 2.2) propshaft public_suffix (~> 6.0) - puma (~> 6.3) + puma (~> 7.0) pundit (~> 2.3) rack-attack (~> 6.6) rack-cors diff --git a/config/puma.rb b/config/puma.rb index 16c481a2ae..d34c14b425 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -27,7 +27,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' end end - on_worker_boot do + before_worker_boot do # Ruby process metrics (memory, GC, etc) PrometheusExporter::Instrumentation::Process.start(type: 'puma') @@ -44,7 +44,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' end end -on_worker_boot do +before_worker_boot do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.establish_connection end From 6cbc857ee01eab85c469a42143f84cccd5e247bc Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 24 Sep 2025 10:57:18 +0200 Subject: [PATCH 02/58] Fix unfortunate action button wrapping in admin area (#36247) --- app/javascript/styles/mastodon/admin.scss | 19 ++++++++++++++----- .../announcements/_announcement.html.haml | 2 +- app/views/admin/roles/_role.html.haml | 2 +- app/views/admin/rules/_rule.html.haml | 2 +- .../warning_presets/_warning_preset.html.haml | 2 +- app/views/admin/webhooks/_webhook.html.haml | 2 +- app/views/filters/_filter.html.haml | 6 +++--- .../authorized_applications/index.html.haml | 2 +- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 2f3b96411d..f7432d5083 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -163,7 +163,7 @@ $content-width: 840px; flex: 1 1 auto; } - @media screen and (max-width: $content-width + $sidebar-width) { + @media screen and (max-width: ($content-width + $sidebar-width)) { .sidebar-wrapper--empty { display: none; } @@ -1065,6 +1065,17 @@ a.name-tag, } } + &__action-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + &:not(.no-wrap) { + flex-wrap: wrap; + } + } + &__meta { padding: 0 15px; color: $dark-text-color; @@ -1081,10 +1092,8 @@ a.name-tag, } } - &__action-bar { - display: flex; - justify-content: space-between; - align-items: center; + &__actions { + margin-inline-start: auto; } &__permissions { diff --git a/app/views/admin/announcements/_announcement.html.haml b/app/views/admin/announcements/_announcement.html.haml index 87ae97cf48..5944b0b295 100644 --- a/app/views/admin/announcements/_announcement.html.haml +++ b/app/views/admin/announcements/_announcement.html.haml @@ -9,7 +9,7 @@ - else = l(announcement.created_at) - %div + .announcements-list__item__actions - if can?(:distribute, announcement) = table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement) - if can?(:update, announcement) diff --git a/app/views/admin/roles/_role.html.haml b/app/views/admin/roles/_role.html.haml index 085bdbd156..ddaca5d8a9 100644 --- a/app/views/admin/roles/_role.html.haml +++ b/app/views/admin/roles/_role.html.haml @@ -26,5 +26,5 @@ = link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id) · %abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size) - %div + .announcements-list__item__actions = table_link_to 'edit', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role) diff --git a/app/views/admin/rules/_rule.html.haml b/app/views/admin/rules/_rule.html.haml index 7d84534d59..d79c1dfa6c 100644 --- a/app/views/admin/rules/_rule.html.haml +++ b/app/views/admin/rules/_rule.html.haml @@ -3,7 +3,7 @@ #{rule_counter + 1}. = truncate(rule.text) - .announcements-list__item__action-bar + .announcements-list__item__action-bar.no-wrap .announcements-list__item__meta = rule.hint diff --git a/app/views/admin/warning_presets/_warning_preset.html.haml b/app/views/admin/warning_presets/_warning_preset.html.haml index 2cc056420f..6488c3a554 100644 --- a/app/views/admin/warning_presets/_warning_preset.html.haml +++ b/app/views/admin/warning_presets/_warning_preset.html.haml @@ -6,5 +6,5 @@ .announcements-list__item__meta = truncate(warning_preset.text) - %div + .announcements-list__item__actions = table_link_to 'delete', t('admin.warning_presets.delete'), admin_warning_preset_path(warning_preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, warning_preset) diff --git a/app/views/admin/webhooks/_webhook.html.haml b/app/views/admin/webhooks/_webhook.html.haml index dca5abeb77..6159d97820 100644 --- a/app/views/admin/webhooks/_webhook.html.haml +++ b/app/views/admin/webhooks/_webhook.html.haml @@ -14,6 +14,6 @@ %abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size) - %div + .announcements-list__item__actions = table_link_to 'edit', t('admin.webhooks.edit'), edit_admin_webhook_path(webhook) if can?(:update, webhook) = table_link_to 'delete', t('admin.webhooks.delete'), admin_webhook_path(webhook), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, webhook) diff --git a/app/views/filters/_filter.html.haml b/app/views/filters/_filter.html.haml index a544ac3a75..15326f3006 100644 --- a/app/views/filters/_filter.html.haml +++ b/app/views/filters/_filter.html.haml @@ -32,10 +32,10 @@ .permissions-list__item__text__type = t('filters.index.statuses_long', count: filter.statuses.size) - .announcements-list__item__action-bar - .announcements-list__item__meta + .filters-list__item__action-bar + .filters-list__item__meta = t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ')) - %div + .filters-list__item__actions = table_link_to 'edit', t('filters.edit.title'), edit_filter_path(filter) = table_link_to 'close', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 3745ed219f..b28302a93f 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -28,7 +28,7 @@ = t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date)) - unless application.superapp? || current_account.unavailable? - %div + .announcements-list__item__actions = table_link_to 'close', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } .announcements-list__item__permissions From e1f7847b64f8e963ad472a5fea91428cfd858910 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Sep 2025 10:58:08 +0200 Subject: [PATCH 03/58] Remove the `outgoing_quotes` feature flag, making the feature unconditional (#36130) --- .../interaction_policies_controller.rb | 5 -- app/controllers/api/v1/statuses_controller.rb | 2 - .../api/interaction_policies_concern.rb | 2 - .../status/boost_button.stories.tsx | 16 +---- .../components/status/boost_button.tsx | 70 +------------------ .../components/status_action_bar/index.jsx | 3 +- .../mastodon/containers/status_container.jsx | 6 +- .../compose/components/visibility_button.tsx | 5 -- .../features/keyboard_shortcuts/index.jsx | 11 ++- .../features/status/components/action_bar.jsx | 3 +- app/javascript/mastodon/utils/environment.ts | 6 +- app/lib/activitypub/activity/quote_request.rb | 2 +- .../activitypub/note_serializer.rb | 2 +- .../rest/shallow_status_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 2 +- lib/mastodon/version.rb | 2 +- .../activity/quote_request_spec.rb | 2 +- spec/lib/status_cache_hydrator_spec.rb | 2 +- .../v1/statuses/interaction_policies_spec.rb | 2 +- spec/requests/api/v1/statuses_spec.rb | 12 ++-- .../activitypub/note_serializer_spec.rb | 2 +- 21 files changed, 27 insertions(+), 132 deletions(-) diff --git a/app/controllers/api/v1/statuses/interaction_policies_controller.rb b/app/controllers/api/v1/statuses/interaction_policies_controller.rb index b8ec4fe140..6e2745806d 100644 --- a/app/controllers/api/v1/statuses/interaction_policies_controller.rb +++ b/app/controllers/api/v1/statuses/interaction_policies_controller.rb @@ -4,7 +4,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base include Api::InteractionPoliciesConcern before_action -> { doorkeeper_authorize! :write, :'write:statuses' } - before_action -> { check_feature_enabled } def update authorize @status, :update? @@ -22,10 +21,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base params.permit(:quote_approval_policy) end - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled? - end - def broadcast_updates! DistributionWorker.perform_async(@status.id, { 'update' => true }) ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 }) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 93dbd8f9d1..6c4e7619b7 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -157,8 +157,6 @@ class Api::V1::StatusesController < Api::BaseController 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 diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb index 5b63705a9b..f1e1480c0c 100644 --- a/app/controllers/concerns/api/interaction_policies_concern.rb +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -4,8 +4,6 @@ module Api::InteractionPoliciesConcern extend ActiveSupport::Concern def quote_approval_policy - return nil unless Mastodon::Feature.outgoing_quotes_enabled? - case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy when 'public' Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 diff --git a/app/javascript/mastodon/components/status/boost_button.stories.tsx b/app/javascript/mastodon/components/status/boost_button.stories.tsx index e81d334a93..402695a829 100644 --- a/app/javascript/mastodon/components/status/boost_button.stories.tsx +++ b/app/javascript/mastodon/components/status/boost_button.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import type { StatusVisibility } from '@/mastodon/api_types/statuses'; import { statusFactoryState } from '@/testing/factories'; -import { LegacyReblogButton, StatusBoostButton } from './boost_button'; +import { BoostButton } from './boost_button'; interface StoryProps { visibility: StatusVisibility; @@ -38,10 +38,7 @@ const meta = { }, }, render: (args) => ( - 0} - /> + 0} /> ), } satisfies Meta; @@ -78,12 +75,3 @@ export const Mine: Story = { }, }, }; - -export const Legacy: Story = { - render: (args) => ( - 0} - /> - ), -}; diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx index 49bdc953e1..337eca5071 100644 --- a/app/javascript/mastodon/components/status/boost_button.tsx +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react'; +import type { FC, KeyboardEvent, MouseEvent } from 'react'; import { useIntl } from 'react-intl'; @@ -11,7 +11,6 @@ import { openModal } from '@/mastodon/actions/modal'; import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu'; import type { Status } from '@/mastodon/models/status'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; -import { isFeatureEnabled } from '@/mastodon/utils/environment'; import type { SomeRequired } from '@/mastodon/utils/types'; import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; @@ -47,10 +46,7 @@ interface ReblogButtonProps { type ActionMenuItemWithIcon = SomeRequired; -export const StatusBoostButton: FC = ({ - status, - counters, -}) => { +export const BoostButton: FC = ({ status, counters }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const statusState = useAppSelector((state) => @@ -192,65 +188,3 @@ const ReblogMenuItem: FC = ({ ); }; - -// Legacy helpers - -// Switch between the legacy and new reblog button based on feature flag. -export const BoostButton: FC = (props) => { - if (isFeatureEnabled('outgoing_quotes')) { - return ; - } - return ; -}; - -export const LegacyReblogButton: FC = ({ - status, - counters, -}) => { - const intl = useIntl(); - const statusState = useAppSelector((state) => - selectStatusState(state, status), - ); - - const { title, meta, iconComponent, disabled } = useMemo( - () => boostItemState(statusState), - [statusState], - ); - - const dispatch = useAppDispatch(); - const handleClick: MouseEventHandler = useCallback( - (event) => { - if (statusState.isLoggedIn) { - dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); - } else { - dispatch( - openModal({ - modalType: 'INTERACTION', - modalProps: { - accountId: status.getIn(['account', 'id']), - url: status.get('uri'), - }, - }), - ); - } - }, - [dispatch, status, statusState.isLoggedIn], - ); - - return ( - - ); -}; diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index 0e72a8cefe..3e82912ab1 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -23,7 +23,6 @@ import { Dropdown } from 'mastodon/components/dropdown_menu'; import { me } from '../../initial_state'; import { IconButton } from '../icon_button'; -import { isFeatureEnabled } from '../../utils/environment'; import { BoostButton } from '../status/boost_button'; import { RemoveQuoteHint } from './remove_quote_hint'; @@ -281,7 +280,7 @@ class StatusActionBar extends ImmutablePureComponent { if (writtenByMe || withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { + if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index ba280ffd23..baf4157f96 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -47,8 +47,6 @@ import Status from '../components/status'; import { deleteModal } from '../initial_state'; import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; -import { isFeatureEnabled } from 'mastodon/utils/environment'; - const makeMapStateToProps = () => { const getStatus = makeGetStatus(); const getPictureInPicture = makeGetPictureInPicture(); @@ -81,9 +79,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ }, onQuote (status) { - if (isFeatureEnabled('outgoing_quotes')) { - dispatch(quoteComposeById(status.get('id'))); - } + dispatch(quoteComposeById(status.get('id'))); }, onFavourite (status) { diff --git a/app/javascript/mastodon/features/compose/components/visibility_button.tsx b/app/javascript/mastodon/features/compose/components/visibility_button.tsx index fadb896b5e..1ea504ab1a 100644 --- a/app/javascript/mastodon/features/compose/components/visibility_button.tsx +++ b/app/javascript/mastodon/features/compose/components/visibility_button.tsx @@ -12,14 +12,12 @@ import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes'; import type { StatusVisibility } from '@/mastodon/api_types/statuses'; import { Icon } from '@/mastodon/components/icon'; import { useAppSelector, useAppDispatch } from '@/mastodon/store'; -import { isFeatureEnabled } from '@/mastodon/utils/environment'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import type { VisibilityModalCallback } from '../../ui/components/visibility_modal'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import { messages as privacyMessages } from './privacy_dropdown'; @@ -43,9 +41,6 @@ interface PrivacyDropdownProps { } export const VisibilityButton: FC = (props) => { - if (!isFeatureEnabled('outgoing_quotes')) { - return ; - } return ; }; diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx index a1f14f14f6..01a4f0e1fd 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx @@ -9,7 +9,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import { isFeatureEnabled } from 'mastodon/utils/environment'; const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, @@ -63,12 +62,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { b - {isFeatureEnabled('outgoing_quotes') && ( - - q - - - )} + + q + + enter, o diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index fa9d6497ae..6156cf1916 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -19,7 +19,6 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/ import { IconButton } from '../../../components/icon_button'; import { Dropdown } from 'mastodon/components/dropdown_menu'; import { me } from '../../../initial_state'; -import { isFeatureEnabled } from '@/mastodon/utils/environment'; import { BoostButton } from '@/mastodon/components/status/boost_button'; const messages = defineMessages({ @@ -237,7 +236,7 @@ class ActionBar extends PureComponent { } menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { + if (!['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index fc4448740f..2d544417e3 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,11 +12,7 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = - | 'modern_emojis' - | 'outgoing_quotes' - | 'fasp' - | 'http_message_signatures'; +export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures'; export function isFeatureEnabled(feature: Features) { return initialState?.features.includes(feature) ?? false; diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 7b49acd119..6d386f45dc 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -9,7 +9,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity quoted_status = status_from_uri(object_uri) return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? - if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote? + if StatusPolicy.new(@account, quoted_status).quote? accept_quote_request!(quoted_status) else reject_quote_request!(quoted_status) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index ab4743bab4..4c5d3f4cf8 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -36,7 +36,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :quote, key: :quote_uri, if: :quote? attribute :quote_authorization, if: :quote_authorization? - attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } + attribute :interaction_policy def id ActivityPub::TagManager.instance.uri_for(object) diff --git a/app/serializers/rest/shallow_status_serializer.rb b/app/serializers/rest/shallow_status_serializer.rb index d82ac32621..0b951f6caa 100644 --- a/app/serializers/rest/shallow_status_serializer.rb +++ b/app/serializers/rest/shallow_status_serializer.rb @@ -6,5 +6,5 @@ class REST::ShallowStatusSerializer < REST::StatusSerializer # It looks like redefining one `has_one` requires redefining all inherited ones 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? } + has_one :quote_approval end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 1e267df82a..a06ddc6861 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -34,7 +34,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? } + has_one :quote_approval def quote object.quote if object.quote&.acceptable? diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index a6bbfcd24d..043f22b28e 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: Mastodon::Feature.outgoing_quotes_enabled? ? 7 : 6, + mastodon: 7, } end diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index 64627cbdfb..aae4ce0338 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do +RSpec.describe ActivityPub::Activity::QuoteRequest do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } let(:quoted_post) { Fabricate(:status, account: recipient) } diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index f450997976..085866ef1d 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -28,7 +28,7 @@ RSpec.describe StatusCacheHydrator do end end - context 'when handling a status with a quote policy', feature: :outgoing_quotes do + context 'when handling a status with a quote policy' do let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } before do diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index cdc33e40d7..aa447de17f 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Interaction policies', feature: :outgoing_quotes do +RSpec.describe 'Interaction policies' do let(:user) { Fabricate(:user) } let(:scopes) { 'write:statuses' } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index eb3e8aed5b..249abc2440 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -158,7 +158,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'without a quote policy', feature: :outgoing_quotes do + context 'without a quote policy' do let(:user) do Fabricate(:user, settings: { default_quote_policy: 'followers' }) end @@ -180,7 +180,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'without a quote policy and the user defaults to nobody', feature: :outgoing_quotes do + context 'without a quote policy and the user defaults to nobody' do let(:user) do Fabricate(:user, settings: { default_quote_policy: 'nobody' }) end @@ -202,7 +202,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a quote policy', feature: :outgoing_quotes do + context 'with a quote policy' do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -227,7 +227,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a self-quote post', feature: :outgoing_quotes do + context 'with a self-quote post' do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -248,7 +248,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a self-quote post and a CW but no text', feature: :outgoing_quotes do + context 'with a self-quote post and a CW but no text' do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -420,7 +420,7 @@ RSpec.describe '/api/v1/statuses' do context 'when updating only the quote policy' do let(:params) { { status: status.text, quote_approval_policy: 'public' } } - it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do + it 'updates the status', :aggregate_failures do expect { subject } .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 336f394337..04179e9bf4 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -58,7 +58,7 @@ RSpec.describe ActivityPub::NoteSerializer do end end - context 'with a quote policy', feature: :outgoing_quotes do + context 'with a quote policy' do let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } it 'has the expected shape' do From 23a69e3bd73b675d313c85853da412632180dc6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:18:50 +0200 Subject: [PATCH 04/58] New Crowdin Translations (automated) (#36246) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/cy.json | 2 ++ app/javascript/mastodon/locales/de.json | 2 +- app/javascript/mastodon/locales/et.json | 2 ++ app/javascript/mastodon/locales/ga.json | 2 ++ app/javascript/mastodon/locales/ja.json | 11 ++++++----- app/javascript/mastodon/locales/tr.json | 2 ++ config/locales/ja.yml | 6 ++++++ 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 4c24eaa387..6d8129e703 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -864,6 +864,7 @@ "status.cancel_reblog_private": "Dadhybu", "status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn", "status.cannot_reblog": "Does dim modd hybu'r postiad hwn", + "status.contains_quote": "Yn cynnwys dyfyniad", "status.context.load_new_replies": "Mae atebion newydd ar gael", "status.context.loading": "Yn chwilio am fwy o atebion", "status.continued_thread": "Edefyn parhaus", @@ -903,6 +904,7 @@ "status.quote_error.revoked": "Postiad wedi'i ddileu gan yr awdur", "status.quote_followers_only": "Dim ond dilynwyr all ddyfynnu'r postiad hwn", "status.quote_manual_review": "Bydd yr awdur yn ei adolygu ei hyn", + "status.quote_noun": "Dyfynnu", "status.quote_policy_change": "Newid pwy all ddyfynnu", "status.quote_post_author": "Wedi dyfynnu postiad gan @{name}", "status.quote_private": "Does dim modd dyfynnu postiadau preifat", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 0e18f9a79a..7ceb4509dc 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -864,7 +864,7 @@ "status.cancel_reblog_private": "Beitrag nicht mehr teilen", "status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", - "status.contains_quote": "Beinhaltet Zitat", + "status.contains_quote": "Enthält Zitat", "status.context.load_new_replies": "Neue Antworten verfügbar", "status.context.loading": "Weitere Antworten werden abgerufen", "status.continued_thread": "Fortgeführter Thread", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 791736f131..4aaeb51421 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -864,6 +864,7 @@ "status.cancel_reblog_private": "Lõpeta jagamine", "status.cannot_quote": "Sul pole õigust seda postitust tsiteerida", "status.cannot_reblog": "Seda postitust ei saa jagada", + "status.contains_quote": "Sisaldab tsitaati", "status.context.load_new_replies": "Leidub uusi vastuseid", "status.context.loading": "Kontrollin täiendavate vastuste olemasolu", "status.continued_thread": "Jätkatud lõim", @@ -903,6 +904,7 @@ "status.quote_error.revoked": "Autor on postituse eemaldanud", "status.quote_followers_only": "Vaid jälgijad saavad seda postitust tsiteerida", "status.quote_manual_review": "Autor vaatab selle üle", + "status.quote_noun": "Tsitaat", "status.quote_policy_change": "Muuda neid, kes võivad tsiteerida", "status.quote_post_author": "Tsiteeris kasutaja @{name} postitust", "status.quote_private": "Otsepostituste tsiteerimine pole võimalik", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index f1641450df..4ede71c487 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -864,6 +864,7 @@ "status.cancel_reblog_private": "Dímhol", "status.cannot_quote": "Ní cheadaítear duit an post seo a lua", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", + "status.contains_quote": "Tá luachan ann", "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", @@ -903,6 +904,7 @@ "status.quote_error.revoked": "Post bainte ag an údar", "status.quote_followers_only": "Ní féidir ach le leantóirí an post seo a lua", "status.quote_manual_review": "Déanfaidh an t-údar athbhreithniú de láimh", + "status.quote_noun": "Luachan", "status.quote_policy_change": "Athraigh cé a fhéadann luachan a thabhairt", "status.quote_post_author": "Luaigh mé post le @{name}", "status.quote_private": "Ní féidir poist phríobháideacha a lua", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 8cd1bafa4a..ba42000ec7 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -642,21 +642,21 @@ "notifications.column_settings.alert": "デスクトップ通知", "notifications.column_settings.favourite": "お気に入り", "notifications.column_settings.filter_bar.advanced": "すべてのカテゴリを表示", - "notifications.column_settings.filter_bar.category": "クイックフィルターバー:", + "notifications.column_settings.filter_bar.category": "クイックフィルターバー", "notifications.column_settings.follow": "新しいフォロワー", - "notifications.column_settings.follow_request": "新しいフォローリクエスト:", + "notifications.column_settings.follow_request": "新しいフォローリクエスト", "notifications.column_settings.group": "グループ", "notifications.column_settings.mention": "返信", "notifications.column_settings.poll": "アンケート結果", "notifications.column_settings.push": "プッシュ通知", "notifications.column_settings.quote": "引用", - "notifications.column_settings.reblog": "ブースト:", + "notifications.column_settings.reblog": "ブースト", "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", "notifications.column_settings.status": "新しい投稿", - "notifications.column_settings.unread_notifications.category": "未読の通知:", + "notifications.column_settings.unread_notifications.category": "未読の通知", "notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示", - "notifications.column_settings.update": "編集:", + "notifications.column_settings.update": "編集", "notifications.filter.all": "すべて", "notifications.filter.boosts": "ブースト", "notifications.filter.favourites": "お気に入り", @@ -875,6 +875,7 @@ "status.quote.cancel": "引用をキャンセル", "status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています", "status.quote_error.pending_approval": "承認待ちの投稿", + "status.quote_noun": "引用", "status.quotes": "{count, plural, other {引用}}", "status.read_more": "もっと見る", "status.reblog": "ブースト", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index d4f5993b85..9d61ed2053 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -864,6 +864,7 @@ "status.cancel_reblog_private": "Yeniden paylaşımı geri al", "status.cannot_quote": "Bu gönderiyi alıntılamaya izniniz yok", "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz", + "status.contains_quote": "Alıntı içeriyor", "status.context.load_new_replies": "Yeni yanıtlar mevcut", "status.context.loading": "Daha fazla yanıt için kontrol ediliyor", "status.continued_thread": "Devam eden akış", @@ -903,6 +904,7 @@ "status.quote_error.revoked": "Gönderi yazarı tarafından kaldırıldı", "status.quote_followers_only": "Sadece takipçiler bu gönderiyi alıntılayabilir", "status.quote_manual_review": "Yazar manuel olarak gözden geçirecek", + "status.quote_noun": "Alıntı", "status.quote_policy_change": "Kimin alıntı yapabileceğini değiştirin", "status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı", "status.quote_private": "Özel gönderiler alıntılanamaz", diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 28502bdd9c..184ab506d6 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -572,6 +572,7 @@ ja: title: モデレーション moderation_notes: create: モデレーションノートを追加 + title: モデレーションメモ private_comment: コメント (非公開) public_comment: コメント (公開) purge: パージ @@ -1067,14 +1068,18 @@ ja: trending: トレンド username_blocks: add_new: ルールを作成 + block_registrations: 登録拒否 comparison: contains: 含む equals: 一致 + contains_html: "%{string}を含む" delete: 削除 edit: title: ユーザー名ルールの編集 + matches_exactly_html: "%{string}に一致" new: create: ルールを作成 + title: ユーザー名ルール warning_presets: add_new: 追加 delete: 削除 @@ -1679,6 +1684,7 @@ ja: self_vote: 自分のアンケートには解答できません too_few_options: は複数必要です too_many_options: は%{max}個までです + vote: 投票 preferences: other: その他 posting_defaults: デフォルトの投稿設定 From 059bf1e980270deb4adc65bed0d81b74ae5873db Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 24 Sep 2025 11:27:33 +0200 Subject: [PATCH 05/58] Highlight newly added replies in thread view (#36237) --- app/javascript/mastodon/components/status.jsx | 2 ++ .../mastodon/features/status/index.jsx | 18 +++++++++++++++++- app/javascript/styles/mastodon/components.scss | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 8664320abe..196da7c99a 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -118,6 +118,7 @@ class Status extends ImmutablePureComponent { unread: PropTypes.bool, showThread: PropTypes.bool, isQuotedPost: PropTypes.bool, + shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, cacheMediaWidth: PropTypes.func, @@ -567,6 +568,7 @@ class Status extends ImmutablePureComponent { 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'status--is-quote': isQuotedPost, 'status--has-quote': !!status.get('quote'), + 'status--highlighted-entry': this.props.shouldHighlightOnMount, }) } data-id={status.get('id')} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 8bab174f67..fb8f3d81d9 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { withRouter } from 'react-router-dom'; +import { difference } from 'lodash'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -150,6 +151,11 @@ class Status extends ImmutablePureComponent { fullscreen: false, showMedia: defaultMediaVisibility(this.props.status), loadedStatusId: undefined, + /** + * Holds the ids of newly added replies, excluding the initial load. + * Used to highlight newly added replies in the UI + */ + newRepliesIds: [], }; UNSAFE_componentWillMount () { @@ -462,6 +468,7 @@ class Status extends ImmutablePureComponent { previousId={i > 0 ? list[i - 1] : undefined} nextId={list[i + 1] || (ancestors && statusId)} rootId={statusId} + shouldHighlightOnMount={this.state.newRepliesIds.includes(id)} /> )); } @@ -495,11 +502,20 @@ class Status extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { status, ancestorsIds } = this.props; + const { status, ancestorsIds, descendantsIds } = this.props; if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) { this._scrollStatusIntoView(); } + + // Only highlight replies after the initial load + if (prevProps.descendantsIds.length) { + const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); + + if (newRepliesIds.length) { + this.setState({newRepliesIds}); + } + } } componentWillUnmount () { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index acfc906dc6..079985c404 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1597,6 +1597,15 @@ } } } + + .no-reduce-motion &--highlighted-entry::before { + content: ''; + position: absolute; + inset: 0; + background: rgb(from $ui-highlight-color r g b / 20%); + opacity: 0; + animation: fade 0.7s reverse both 0.3s; + } } .status__relative-time { From 29d9f81e427fa86489d3f084710dc9a31b9f0e3b Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 24 Sep 2025 11:37:11 +0200 Subject: [PATCH 06/58] Fix missed event handler (#36248) --- .../features/direct_timeline/components/conversation.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index dc1461e5b4..bb0815087b 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -136,7 +136,7 @@ export const Conversation = ({ conversation, scrollKey }) => { {unread && } -
+
{names} }} />
From 3a81ee8f5b2f23bb0942b4918db0cacdd432a61a Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 24 Sep 2025 11:54:07 +0200 Subject: [PATCH 07/58] Implement new design for "Refetch all" (#36172) --- .../components/alert/alert.stories.tsx | 15 ++ .../mastodon/components/alert/index.tsx | 19 +- .../components/exit_animation_wrapper.tsx | 53 ++++++ .../status/components/refresh_controller.tsx | 166 +++++++++++++----- .../mastodon/features/status/index.jsx | 2 +- app/javascript/mastodon/locales/en.json | 9 +- .../styles/mastodon/components.scss | 40 ++++- 7 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 app/javascript/mastodon/components/exit_animation_wrapper.tsx diff --git a/app/javascript/mastodon/components/alert/alert.stories.tsx b/app/javascript/mastodon/components/alert/alert.stories.tsx index 4d5f8acb65..f12f06751d 100644 --- a/app/javascript/mastodon/components/alert/alert.stories.tsx +++ b/app/javascript/mastodon/components/alert/alert.stories.tsx @@ -8,6 +8,7 @@ const meta = { component: Alert, args: { isActive: true, + isLoading: false, animateFrom: 'side', title: '', message: '', @@ -20,6 +21,12 @@ const meta = { type: 'boolean', description: 'Animate to the active (displayed) state of the alert', }, + isLoading: { + control: 'boolean', + type: 'boolean', + description: + 'Display a loading indicator in the alert, replacing the dismiss button if present', + }, animateFrom: { control: 'radio', type: 'string', @@ -108,3 +115,11 @@ export const InSizedContainer: Story = { ), }; + +export const WithLoadingIndicator: Story = { + args: { + ...WithDismissButton.args, + isLoading: true, + }, + render: InSizedContainer.render, +}; diff --git a/app/javascript/mastodon/components/alert/index.tsx b/app/javascript/mastodon/components/alert/index.tsx index 1009e77524..72fee0a4a3 100644 --- a/app/javascript/mastodon/components/alert/index.tsx +++ b/app/javascript/mastodon/components/alert/index.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl'; import classNames from 'classnames'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { IconButton } from '../icon_button'; @@ -10,21 +11,23 @@ import { IconButton } from '../icon_button'; * Snackbar/Toast-style notification component. */ export const Alert: React.FC<{ - isActive?: boolean; - animateFrom?: 'side' | 'below'; title?: string; message: string; action?: string; onActionClick?: () => void; onDismiss?: () => void; + isActive?: boolean; + isLoading?: boolean; + animateFrom?: 'side' | 'below'; }> = ({ - isActive, - animateFrom = 'side', title, message, action, onActionClick, onDismiss, + isActive, + isLoading, + animateFrom = 'side', }) => { const intl = useIntl(); @@ -51,7 +54,13 @@ export const Alert: React.FC<{ )} - {onDismiss && ( + {isLoading && ( + + + + )} + + {onDismiss && !isLoading && ( React.ReactNode; +}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => { + const [delayedIsActive, setDelayedIsActive] = useState(false); + + useEffect(() => { + if (isActive && !withEntryDelay) { + setDelayedIsActive(true); + + return () => ''; + } else { + const timeout = setTimeout(() => { + setDelayedIsActive(isActive); + }, delayMs); + + return () => { + clearTimeout(timeout); + }; + } + }, [isActive, delayMs, withEntryDelay]); + + if (!isActive && !delayedIsActive) { + return null; + } + + return children(isActive && delayedIsActive); +}; diff --git a/app/javascript/mastodon/features/status/components/refresh_controller.tsx b/app/javascript/mastodon/features/status/components/refresh_controller.tsx index 9788b2849f..34faaf1d5d 100644 --- a/app/javascript/mastodon/features/status/components/refresh_controller.tsx +++ b/app/javascript/mastodon/features/status/components/refresh_controller.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; -import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; import { fetchContext, @@ -8,31 +8,80 @@ import { } from 'mastodon/actions/statuses'; import type { AsyncRefreshHeader } from 'mastodon/api'; import { apiGetAsyncRefresh } from 'mastodon/api/async_refreshes'; +import { Alert } from 'mastodon/components/alert'; +import { ExitAnimationWrapper } from 'mastodon/components/exit_animation_wrapper'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; +const AnimatedAlert: React.FC< + React.ComponentPropsWithoutRef & { withEntryDelay?: boolean } +> = ({ isActive = false, withEntryDelay, ...props }) => ( + + {(delayedIsActive) => } + +); + const messages = defineMessages({ - loading: { + moreFound: { + id: 'status.context.more_replies_found', + defaultMessage: 'More replies found', + }, + show: { + id: 'status.context.show', + defaultMessage: 'Show', + }, + loadingInitial: { id: 'status.context.loading', - defaultMessage: 'Checking for more replies', + defaultMessage: 'Loading', + }, + loadingMore: { + id: 'status.context.loading_more', + defaultMessage: 'Loading more replies', + }, + success: { + id: 'status.context.loading_success', + defaultMessage: 'All replies loaded', + }, + error: { + id: 'status.context.loading_error', + defaultMessage: "Couldn't load new replies", + }, + retry: { + id: 'status.context.retry', + defaultMessage: 'Retry', }, }); +type LoadingState = + | 'idle' + | 'more-available' + | 'loading-initial' + | 'loading-more' + | 'success' + | 'error'; + export const RefreshController: React.FC<{ statusId: string; }> = ({ statusId }) => { const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); - const autoRefresh = useAppSelector( - (state) => - !state.contexts.replies[statusId] || - state.contexts.replies[statusId].length === 0, + const currentReplyCount = useAppSelector( + (state) => state.contexts.replies[statusId]?.length ?? 0, ); + const autoRefresh = !currentReplyCount; const dispatch = useAppDispatch(); const intl = useIntl(); - const [ready, setReady] = useState(false); - const [loading, setLoading] = useState(false); + + const [loadingState, setLoadingState] = useState( + refresh && autoRefresh ? 'loading-initial' : 'idle', + ); + + const [wasDismissed, setWasDismissed] = useState(false); + const dismissPrompt = useCallback(() => { + setWasDismissed(true); + setLoadingState('idle'); + }, []); useEffect(() => { let timeoutId: ReturnType; @@ -45,67 +94,104 @@ export const RefreshController: React.FC<{ if (result.async_refresh.result_count > 0) { if (autoRefresh) { - void dispatch(fetchContext({ statusId })); - return ''; + void dispatch(fetchContext({ statusId })).then(() => { + setLoadingState('idle'); + }); + } else { + setLoadingState('more-available'); } - - setReady(true); + } else { + setLoadingState('idle'); } } else { scheduleRefresh(refresh); } - - return ''; }); }, refresh.retry * 1000); }; - if (refresh) { + if (refresh && !wasDismissed) { scheduleRefresh(refresh); + setLoadingState('loading-initial'); } return () => { clearTimeout(timeoutId); }; - }, [dispatch, setReady, statusId, refresh, autoRefresh]); + }, [dispatch, statusId, refresh, autoRefresh, wasDismissed]); + + useEffect(() => { + // Hide success message after a short delay + if (loadingState === 'success') { + const timeoutId = setTimeout(() => { + setLoadingState('idle'); + }, 3000); + + return () => { + clearTimeout(timeoutId); + }; + } + return () => ''; + }, [loadingState]); const handleClick = useCallback(() => { - setLoading(true); - setReady(false); + setLoadingState('loading-more'); dispatch(fetchContext({ statusId })) .then(() => { - setLoading(false); + setLoadingState('success'); return ''; }) .catch(() => { - setLoading(false); + setLoadingState('error'); }); - }, [dispatch, setReady, statusId]); + }, [dispatch, statusId]); - if (ready && !loading) { + if (loadingState === 'loading-initial') { return ( - +
+ +
); } - if (!refresh && !loading) { - return null; - } - return ( -
- +
+ + + +
); }; diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index fb8f3d81d9..404faf609e 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -648,8 +648,8 @@ class Status extends ImmutablePureComponent {
- {remoteHint} {descendants} + {remoteHint} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 7721cc36d3..f949c30339 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -865,8 +865,13 @@ "status.cannot_quote": "You are not allowed to quote this post", "status.cannot_reblog": "This post cannot be boosted", "status.contains_quote": "Contains quote", - "status.context.load_new_replies": "New replies available", - "status.context.loading": "Checking for more replies", + "status.context.loading": "Loading more replies", + "status.context.loading_error": "Couldn't load new replies", + "status.context.loading_more": "Loading more replies", + "status.context.loading_success": "All replies loaded", + "status.context.more_replies_found": "More replies found", + "status.context.retry": "Retry", + "status.context.show": "Show", "status.continued_thread": "Continued thread", "status.copy": "Copy link to post", "status.delete": "Delete", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 079985c404..d893a35836 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2969,7 +2969,6 @@ a.account__display-name { flex: 1 1 auto; flex-direction: row; justify-content: flex-start; - overflow-x: auto; position: relative; &.unscrollable { @@ -3145,6 +3144,29 @@ a.account__display-name { } } +.column__alert { + position: sticky; + bottom: 1rem; + z-index: 10; + box-sizing: border-box; + display: grid; + width: 100%; + max-width: 360px; + padding-inline: 10px; + margin-top: 1rem; + margin-inline: auto; + + @media (max-width: #{$mobile-menu-breakpoint - 1}) { + bottom: 4rem; + } + + & > * { + // Make all nested alerts occupy the same space + // rather than stack + grid-area: 1 / 1; + } +} + .ui { --mobile-bottom-nav-height: 55px; --last-content-item-border-width: 2px; @@ -3185,7 +3207,6 @@ a.account__display-name { .column, .drawer { flex: 1 1 100%; - overflow: hidden; } @media screen and (width > $mobile-breakpoint) { @@ -10397,6 +10418,21 @@ noscript { } } +.notification-bar__loading-indicator { + --spinner-size: 22px; + + position: relative; + height: var(--spinner-size); + width: var(--spinner-size); + margin-inline-start: 2px; + + svg { + color: $white; + height: var(--spinner-size); + width: var(--spinner-size); + } +} + .hashtag-header { border-bottom: 1px solid var(--background-border-color); padding: 15px; From 28be5a199f685b9fb26742c4d78ddca53df05d57 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Sep 2025 12:58:52 +0200 Subject: [PATCH 08/58] Fix Private Messages self-quoting private posts being changed to followers-only (#36249) --- app/services/post_status_service.rb | 2 +- spec/services/post_status_service_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index e6d0c40730..c55a54df37 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -69,7 +69,7 @@ class PostStatusService < BaseService @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? && @quoted_status.blank? @visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? - @visibility = :private if @quoted_status&.private_visibility? + @visibility = :private if @quoted_status&.private_visibility? && %i(public unlisted).include?(@visibility&.to_sym) @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index c434d0cb6e..96289cdeee 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -321,6 +321,14 @@ RSpec.describe PostStatusService do expect(status).to be_private_visibility end + it 'correctly preserves visibility for private mentions self-quoting private posts' do + account = Fabricate(:account) + quoted_status = Fabricate(:status, account: account, visibility: :private) + + status = subject.call(account, text: 'test', quoted_status: quoted_status, visibility: 'direct') + expect(status).to be_direct_visibility + end + it 'returns existing status when used twice with idempotency key' do account = Fabricate(:account) status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') From df72a2dbbec8173515568c02427076ebff5c2297 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Sep 2025 22:37:20 +0200 Subject: [PATCH 09/58] Fix newly-highlighted replies not being interactable (#36256) --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d893a35836..b390a8a8e5 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1605,6 +1605,7 @@ background: rgb(from $ui-highlight-color r g b / 20%); opacity: 0; animation: fade 0.7s reverse both 0.3s; + pointer-events: none; } } From cc54b33720cc24906ebd4b3b909a1983913c5df5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:52:37 +0200 Subject: [PATCH 10/58] chore(deps): update dependency typescript to ~5.9.0 (#36212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: diondiondion --- .../mastodon/components/blurhash.tsx | 7 +++- .../mastodon/components/hashtag_bar.tsx | 2 +- .../components/embedded_status_content.tsx | 2 +- .../mastodon/features/onboarding/profile.tsx | 4 +- .../features/ui/components/embed_modal.tsx | 1 + app/javascript/mastodon/hooks/useLinks.ts | 2 +- eslint.config.mjs | 1 - package.json | 2 +- streaming/package.json | 2 +- yarn.lock | 40 +++++-------------- 10 files changed, 22 insertions(+), 41 deletions(-) diff --git a/app/javascript/mastodon/components/blurhash.tsx b/app/javascript/mastodon/components/blurhash.tsx index 8e2a8af23e..b7331755b7 100644 --- a/app/javascript/mastodon/components/blurhash.tsx +++ b/app/javascript/mastodon/components/blurhash.tsx @@ -30,9 +30,12 @@ const Blurhash: React.FC = ({ try { const pixels = decode(hash, width, height); const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); + const imageData = ctx?.createImageData(width, height); + imageData?.data.set(pixels); - ctx?.putImageData(imageData, 0, 0); + if (imageData) { + ctx?.putImageData(imageData, 0, 0); + } } catch (err) { console.error('Blurhash decoding failure', { err, hash }); } diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx index 4f88385bef..f6b72d4340 100644 --- a/app/javascript/mastodon/components/hashtag_bar.tsx +++ b/app/javascript/mastodon/components/hashtag_bar.tsx @@ -33,7 +33,7 @@ function isNodeLinkHashtag(element: Node): element is HTMLLinkElement { return ( element instanceof HTMLAnchorElement && // it may be a starting with a hashtag - (element.textContent?.[0] === '#' || + (element.textContent.startsWith('#') || // or a # element.previousSibling?.textContent?.[ element.previousSibling.textContent.length - 1 diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 1a38be536b..855e160fac 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -64,7 +64,7 @@ export const EmbeddedStatusContent: React.FC<{ link.setAttribute('title', `@${mention.get('acct')}`); link.setAttribute('href', `/@${mention.get('acct')}`); } else if ( - link.textContent?.[0] === '#' || + link.textContent.startsWith('#') || link.previousSibling?.textContent?.endsWith('#') ) { link.addEventListener( diff --git a/app/javascript/mastodon/features/onboarding/profile.tsx b/app/javascript/mastodon/features/onboarding/profile.tsx index d9b394acfb..7e725e97cc 100644 --- a/app/javascript/mastodon/features/onboarding/profile.tsx +++ b/app/javascript/mastodon/features/onboarding/profile.tsx @@ -54,9 +54,7 @@ export const Profile: React.FC<{ me ? state.accounts.get(me) : undefined, ); const [displayName, setDisplayName] = useState(account?.display_name ?? ''); - const [note, setNote] = useState( - account ? (unescapeHTML(account.note) ?? '') : '', - ); + const [note, setNote] = useState(account ? unescapeHTML(account.note) : ''); const [avatar, setAvatar] = useState(); const [header, setHeader] = useState(); const [discoverable, setDiscoverable] = useState( diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.tsx b/app/javascript/mastodon/features/ui/components/embed_modal.tsx index b78d5b64c4..0290b01d2f 100644 --- a/app/javascript/mastodon/features/ui/components/embed_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/embed_modal.tsx @@ -36,6 +36,7 @@ const EmbedModal: React.FC<{ } iframeDocument.open(); + // eslint-disable-next-line @typescript-eslint/no-deprecated iframeDocument.write(data.html); iframeDocument.close(); diff --git a/app/javascript/mastodon/hooks/useLinks.ts b/app/javascript/mastodon/hooks/useLinks.ts index 160fe18503..00e1dd9bb4 100644 --- a/app/javascript/mastodon/hooks/useLinks.ts +++ b/app/javascript/mastodon/hooks/useLinks.ts @@ -12,7 +12,7 @@ const isMentionClick = (element: HTMLAnchorElement) => !element.classList.contains('hashtag'); const isHashtagClick = (element: HTMLAnchorElement) => - element.textContent?.[0] === '#' || + element.textContent.startsWith('#') || element.previousSibling?.textContent?.endsWith('#'); export const useLinks = (skipHashtags?: boolean) => { diff --git a/eslint.config.mjs b/eslint.config.mjs index 43aabc5110..883ddf5fce 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,6 @@ import path from 'node:path'; import js from '@eslint/js'; import { globalIgnores } from 'eslint/config'; import formatjs from 'eslint-plugin-formatjs'; -// @ts-expect-error -- No typings import importPlugin from 'eslint-plugin-import'; import jsdoc from 'eslint-plugin-jsdoc'; import jsxA11Y from 'eslint-plugin-jsx-a11y'; diff --git a/package.json b/package.json index fcc74dc83f..e38bc68e37 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ "stylelint": "^16.19.1", "stylelint-config-prettier-scss": "^1.0.0", "stylelint-config-standard-scss": "^15.0.1", - "typescript": "~5.7.3", + "typescript": "~5.9.0", "typescript-eslint": "^8.29.1", "vitest": "^3.2.4" }, diff --git a/streaming/package.json b/streaming/package.json index 40a737a61d..05acc58031 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -39,7 +39,7 @@ "@types/ws": "^8.5.9", "globals": "^16.0.0", "pino-pretty": "^13.0.0", - "typescript": "~5.7.3", + "typescript": "~5.9.0", "typescript-eslint": "^8.28.0" }, "optionalDependencies": { diff --git a/yarn.lock b/yarn.lock index 384a553387..9a5ac444be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2859,7 +2859,7 @@ __metadata: tesseract.js: "npm:^6.0.0" tiny-queue: "npm:^0.2.1" twitter-text: "npm:3.1.0" - typescript: "npm:~5.7.3" + typescript: "npm:~5.9.0" typescript-eslint: "npm:^8.29.1" use-debounce: "npm:^10.0.0" vite: "npm:^7.1.1" @@ -2907,7 +2907,7 @@ __metadata: pino-http: "npm:^10.0.0" pino-pretty: "npm:^13.0.0" prom-client: "npm:^15.0.0" - typescript: "npm:~5.7.3" + typescript: "npm:~5.9.0" typescript-eslint: "npm:^8.28.0" utf-8-validate: "npm:^6.0.3" uuid: "npm:^11.0.0" @@ -13459,43 +13459,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.6.0": - version: 5.8.2 - resolution: "typescript@npm:5.8.2" +"typescript@npm:^5.6.0, typescript@npm:~5.9.0": + version: 5.9.2 + resolution: "typescript@npm:5.9.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6 + checksum: 10c0/cd635d50f02d6cf98ed42de2f76289701c1ec587a363369255f01ed15aaf22be0813226bff3c53e99d971f9b540e0b3cc7583dbe05faded49b1b0bed2f638a18 languageName: node linkType: hard -"typescript@npm:~5.7.3": - version: 5.7.3 - resolution: "typescript@npm:5.7.3" +"typescript@patch:typescript@npm%3A^5.6.0#optional!builtin, typescript@patch:typescript@npm%3A~5.9.0#optional!builtin": + version: 5.9.2 + resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.6.0#optional!builtin": - version: 5.8.2 - resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A~5.7.3#optional!builtin": - version: 5.7.3 - resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/6fd7e0ed3bf23a81246878c613423730c40e8bdbfec4c6e4d7bf1b847cbb39076e56ad5f50aa9d7ebd89877999abaee216002d3f2818885e41c907caaa192cc4 + checksum: 10c0/34d2a8e23eb8e0d1875072064d5e1d9c102e0bdce56a10a25c0b917b8aa9001a9cf5c225df12497e99da107dc379360bc138163c66b55b95f5b105b50578067e languageName: node linkType: hard From e0f7aedf419ab4cb136ec36036c8c0c68dbef2ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:54:07 +0000 Subject: [PATCH 11/58] chore(deps): update dependency propshaft to v1.3.1 (#36241) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 64ef3057d8..27a70b2460 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -629,7 +629,7 @@ GEM prism (1.4.0) prometheus_exporter (2.3.0) webrick - propshaft (1.2.1) + propshaft (1.3.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -643,7 +643,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.16) + rack (3.2.1) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (3.0.0) From 33fd8c774b923ced52a2b79f8e7b83e6f609fb78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:54:41 +0000 Subject: [PATCH 12/58] chore(deps): update dependency webauthn to v3.4.2 (#36243) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 27a70b2460..d4486681d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM playwright-ruby-client (>= 1.16.0) case_transform (0.2) activesupport - cbor (0.5.9.8) + cbor (0.5.10.1) cgi (0.4.2) charlock_holmes (0.7.9) chewy (7.6.0) @@ -814,8 +814,8 @@ GEM rubyzip (3.1.0) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) - safety_net_attestation (0.4.0) - jwt (~> 2.0) + safety_net_attestation (0.5.0) + jwt (>= 2.0, < 4.0) sanitize (7.0.0) crass (~> 1.0.2) nokogiri (>= 1.16.8) @@ -918,13 +918,13 @@ GEM zeitwerk (~> 2.2) warden (1.2.9) rack (>= 2.0.9) - webauthn (3.4.1) + webauthn (3.4.2) android_key_attestation (~> 0.3.0) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) openssl (>= 2.2) - safety_net_attestation (~> 0.4.0) + safety_net_attestation (~> 0.5.0) tpm-key_attestation (~> 0.14.0) webfinger (2.1.3) activesupport From 0798d0c95a41deea018114e00a32d01832a544fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:54:58 +0200 Subject: [PATCH 13/58] chore(deps): update dependency pundit to v2.5.2 (#36251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d4486681d2..2d38065acc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -639,7 +639,7 @@ GEM public_suffix (6.0.2) puma (7.0.3) nio4r (~> 2.0) - pundit (2.5.1) + pundit (2.5.2) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) From fda3589498d47835bfa68e3b918300c06473f706 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:55:09 +0200 Subject: [PATCH 14/58] fix(deps): update dependency sass to v1.93.2 (#36231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9a5ac444be..6fd58db304 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12018,8 +12018,8 @@ __metadata: linkType: hard "sass@npm:^1.62.1": - version: 1.93.0 - resolution: "sass@npm:1.93.0" + version: 1.93.2 + resolution: "sass@npm:1.93.2" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -12030,7 +12030,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/51dcb4e65a69f97b4c200ee154ca45f81b748a45f8ef0ec3236b774bb143590a9304038e9ab09f809f734d4edb3add96a0a690b2e8451ff66b9f57c469b2685e + checksum: 10c0/5a19f12dbe8c142e40c1e0473d1e624898242b1c21010301e169b528be8c580df6356329c798522b525eb11eda4b04b9b77422badc55c47889600f8477201d2b languageName: node linkType: hard From 8fac87d77c06e0541392ea0dc574507a05738786 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:55:46 +0000 Subject: [PATCH 15/58] chore(deps): update dependency rails to v8.0.3 (#36230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 110 ++++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2d38065acc..390debbf5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,29 +10,29 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (8.0.2.1) - actionpack (= 8.0.2.1) - activesupport (= 8.0.2.1) + actioncable (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2.1) - actionpack (= 8.0.2.1) - activejob (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionmailbox (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) mail (>= 2.8.0) - actionmailer (8.0.2.1) - actionpack (= 8.0.2.1) - actionview (= 8.0.2.1) - activejob (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionmailer (8.0.3) + actionpack (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activesupport (= 8.0.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2.1) - actionview (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionpack (8.0.3) + actionview (= 8.0.3) + activesupport (= 8.0.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -40,15 +40,15 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2.1) - actionpack (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + actiontext (8.0.3) + actionpack (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2.1) - activesupport (= 8.0.2.1) + actionview (8.0.3) + activesupport (= 8.0.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -58,22 +58,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.2.1) - activesupport (= 8.0.2.1) + activejob (8.0.3) + activesupport (= 8.0.3) globalid (>= 0.3.6) - activemodel (8.0.2.1) - activesupport (= 8.0.2.1) - activerecord (8.0.2.1) - activemodel (= 8.0.2.1) - activesupport (= 8.0.2.1) + activemodel (8.0.3) + activesupport (= 8.0.3) + activerecord (8.0.3) + activemodel (= 8.0.3) + activesupport (= 8.0.3) timeout (>= 0.4.0) - activestorage (8.0.2.1) - actionpack (= 8.0.2.1) - activejob (= 8.0.2.1) - activerecord (= 8.0.2.1) - activesupport (= 8.0.2.1) + activestorage (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activesupport (= 8.0.3) marcel (~> 1.0) - activesupport (8.0.2.1) + activesupport (8.0.3) base64 benchmark (>= 0.3) bigdecimal @@ -447,7 +447,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.9) + net-imap (0.5.10) date net-protocol net-ldap (0.20.0) @@ -669,20 +669,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.2.1) - actioncable (= 8.0.2.1) - actionmailbox (= 8.0.2.1) - actionmailer (= 8.0.2.1) - actionpack (= 8.0.2.1) - actiontext (= 8.0.2.1) - actionview (= 8.0.2.1) - activejob (= 8.0.2.1) - activemodel (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + rails (8.0.3) + actioncable (= 8.0.3) + actionmailbox (= 8.0.3) + actionmailer (= 8.0.3) + actionpack (= 8.0.3) + actiontext (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activemodel (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) bundler (>= 1.15.0) - railties (= 8.0.2.1) + railties (= 8.0.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -693,13 +693,14 @@ GEM rails-i18n (8.0.2) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.2.1) - actionpack (= 8.0.2.1) - activesupport (= 8.0.2.1) + railties (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) @@ -879,6 +880,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) From 85213dab471544114507345a1aeb182a34b138ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:56:33 +0000 Subject: [PATCH 16/58] chore(deps): update yarn to v4.10.3 (#36178) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- streaming/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e38bc68e37..2d0fa230cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/mastodon", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.9.4", + "packageManager": "yarn@4.10.3", "engines": { "node": ">=20" }, diff --git a/streaming/package.json b/streaming/package.json index 05acc58031..b77cfc02ad 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/streaming", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.9.4", + "packageManager": "yarn@4.10.3", "engines": { "node": ">=20" }, From 52d5e628a42023302f2d334a325307c0fe2aa87c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:57:47 +0000 Subject: [PATCH 17/58] chore(deps): update dependency hiredis-client to v0.26.0 (#36233) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 390debbf5d..ba724ac82b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -300,8 +300,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.25.3) - redis-client (= 0.25.3) + hiredis-client (0.26.0) + redis-client (= 0.26.0) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -720,7 +720,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.25.3) + redis-client (0.26.0) connection_pool regexp_parser (2.11.2) reline (0.6.2) From 507e6dc47317af84606ab81ba101f7a13aa58960 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:58:20 +0000 Subject: [PATCH 18/58] fix(deps): update dependency ioredis to v5.8.0 (#36234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6fd58db304..02f5f7dc5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2595,10 +2595,10 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:^1.3.0": - version: 1.3.0 - resolution: "@ioredis/commands@npm:1.3.0" - checksum: 10c0/5ab990a8f69c20daf3d7d64307aa9f13ee727c92ab4c7664a6943bb500227667a0c368892e9c4913f06416377db47dba78d58627fe723da476d25f2c04a6d5aa +"@ioredis/commands@npm:1.4.0": + version: 1.4.0 + resolution: "@ioredis/commands@npm:1.4.0" + checksum: 10c0/99afe21fba794f84a2b84cceabcc370a7622e7b8b97a6589456c07c9fa62a15d54c5546f6f7214fb9a2458b1fa87579d5c531aaf48e06cc9be156d5923892c8d languageName: node linkType: hard @@ -8364,10 +8364,10 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.7.0 - resolution: "ioredis@npm:5.7.0" + version: 5.8.0 + resolution: "ioredis@npm:5.8.0" dependencies: - "@ioredis/commands": "npm:^1.3.0" + "@ioredis/commands": "npm:1.4.0" cluster-key-slot: "npm:^1.1.0" debug: "npm:^4.3.4" denque: "npm:^2.1.0" @@ -8376,7 +8376,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/c63c521a953bfaf29f8c8871b122af38e439328336fa238f83bfbb066556f64daf69ed7a4ec01fc7b9ee1f0862059dd188b8c684150125d362d36642399b30ee + checksum: 10c0/66fad6283c6d9052b4aa0987d592c1bf6c9471304eb0edf0c9d18024b1b38028adf29c05f1cf114b90f5bdb516576f897a654946e8c29568f404ac33cd3b9d19 languageName: node linkType: hard From d2bdb03da0decfec96acc47a2ab97f744c45b2ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:53:58 +0200 Subject: [PATCH 19/58] New Crowdin Translations (automated) (#36258) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/ar.json | 2 -- app/javascript/mastodon/locales/az.json | 2 -- app/javascript/mastodon/locales/be.json | 2 -- app/javascript/mastodon/locales/bg.json | 2 -- app/javascript/mastodon/locales/br.json | 2 -- app/javascript/mastodon/locales/ca.json | 2 -- app/javascript/mastodon/locales/cs.json | 2 -- app/javascript/mastodon/locales/cy.json | 2 -- app/javascript/mastodon/locales/da.json | 9 +++++++-- app/javascript/mastodon/locales/de.json | 9 +++++++-- app/javascript/mastodon/locales/el.json | 9 +++++++-- app/javascript/mastodon/locales/eo.json | 2 -- app/javascript/mastodon/locales/es-AR.json | 9 +++++++-- app/javascript/mastodon/locales/es-MX.json | 9 +++++++-- app/javascript/mastodon/locales/es.json | 9 +++++++-- app/javascript/mastodon/locales/et.json | 2 -- app/javascript/mastodon/locales/fa.json | 2 -- app/javascript/mastodon/locales/fi.json | 9 +++++++-- app/javascript/mastodon/locales/fo.json | 2 -- app/javascript/mastodon/locales/fr-CA.json | 2 -- app/javascript/mastodon/locales/fr.json | 2 -- app/javascript/mastodon/locales/fy.json | 2 -- app/javascript/mastodon/locales/ga.json | 2 -- app/javascript/mastodon/locales/gd.json | 2 -- app/javascript/mastodon/locales/gl.json | 11 ++++++++--- app/javascript/mastodon/locales/he.json | 9 +++++++-- app/javascript/mastodon/locales/hu.json | 2 -- app/javascript/mastodon/locales/ia.json | 13 +++++++++++-- app/javascript/mastodon/locales/is.json | 2 -- app/javascript/mastodon/locales/it.json | 11 +++++++++-- app/javascript/mastodon/locales/ja.json | 1 - app/javascript/mastodon/locales/kab.json | 1 - app/javascript/mastodon/locales/ko.json | 2 -- app/javascript/mastodon/locales/lt.json | 2 -- app/javascript/mastodon/locales/lv.json | 1 - app/javascript/mastodon/locales/nan.json | 2 -- app/javascript/mastodon/locales/nl.json | 2 -- app/javascript/mastodon/locales/nn.json | 2 -- app/javascript/mastodon/locales/pl.json | 2 -- app/javascript/mastodon/locales/pt-BR.json | 2 -- app/javascript/mastodon/locales/pt-PT.json | 11 +++++++++-- app/javascript/mastodon/locales/ru.json | 2 -- app/javascript/mastodon/locales/sv.json | 2 -- app/javascript/mastodon/locales/tr.json | 9 +++++++-- app/javascript/mastodon/locales/uk.json | 2 -- app/javascript/mastodon/locales/vi.json | 9 +++++++-- app/javascript/mastodon/locales/zh-CN.json | 11 ++++++++--- app/javascript/mastodon/locales/zh-TW.json | 9 +++++++-- config/locales/da.yml | 2 +- 49 files changed, 123 insertions(+), 96 deletions(-) diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 3af126d5e7..eb4933e188 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -861,8 +861,6 @@ "status.cancel_reblog_private": "إلغاء إعادة النشر", "status.cannot_quote": "غير مصرح لك باقتباس هذا المنشور", "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/az.json b/app/javascript/mastodon/locales/az.json index be7541011e..4c7a61746f 100644 --- a/app/javascript/mastodon/locales/az.json +++ b/app/javascript/mastodon/locales/az.json @@ -829,8 +829,6 @@ "status.bookmark": "Əlfəcin", "status.cancel_reblog_private": "Təkrar paylaşımı geri al", "status.cannot_reblog": "Bu göndəriş təkrar paylaşıla bilməz", - "status.context.load_new_replies": "Yeni cavablar mövcuddur", - "status.context.loading": "Daha çox cavab yoxlanılır", "status.continued_thread": "Davam edən mövzu", "status.copy": "Göndəriş keçidini kopyala", "status.delete": "Sil", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index bf4f493831..c61938faf5 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Прыбраць", "status.cannot_quote": "Вы не маеце дазвол цытаваць гэты допіс", "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/bg.json b/app/javascript/mastodon/locales/bg.json index 618ce28c61..b148994b55 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -832,8 +832,6 @@ "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/br.json b/app/javascript/mastodon/locales/br.json index 0f2f63c9ce..5a4828daf5 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -653,8 +653,6 @@ "status.bookmark": "Ouzhpennañ d'ar sinedoù", "status.cancel_reblog_private": "Nac'hañ ar skignadenn", "status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet", - "status.context.load_new_replies": "Respontoù nevez zo", - "status.context.loading": "O kerc'hat muioc'h a respontoù", "status.copy": "Eilañ liamm ar c'hannad", "status.delete": "Dilemel", "status.delete.success": "Embannadur dilamet", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 5f3d6913ab..ee6e5050a8 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -844,8 +844,6 @@ "status.bookmark": "Marca", "status.cancel_reblog_private": "Desfés l'impuls", "status.cannot_reblog": "No es pot impulsar aquest tut", - "status.context.load_new_replies": "Hi ha respostes noves", - "status.context.loading": "Comprovació de més respostes", "status.continued_thread": "Continuació del fil", "status.copy": "Copia l'enllaç al tut", "status.delete": "Elimina", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index d1aaffe2f2..3b4138cbc8 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", "status.contains_quote": "Obsahuje citaci", - "status.context.load_new_replies": "K dispozici jsou nové odpovědi", - "status.context.loading": "Hledání dalších odpovědí", "status.continued_thread": "Pokračuje ve vlákně", "status.copy": "Zkopírovat odkaz na příspěvek", "status.delete": "Smazat", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 6d8129e703..851ec1d559 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn", "status.cannot_reblog": "Does dim modd hybu'r postiad hwn", "status.contains_quote": "Yn cynnwys dyfyniad", - "status.context.load_new_replies": "Mae atebion newydd ar gael", - "status.context.loading": "Yn chwilio am fwy o atebion", "status.continued_thread": "Edefyn parhaus", "status.copy": "Copïo dolen i'r post", "status.delete": "Dileu", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 9521401bca..b22190881f 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Du har ikke tilladelse til at citere dette indlæg", "status.cannot_reblog": "Dette indlæg kan ikke fremhæves", "status.contains_quote": "Indeholder citat", - "status.context.load_new_replies": "Nye svar tilgængelige", - "status.context.loading": "Tjekker for flere svar", + "status.context.loading": "Indlæser flere svar", + "status.context.loading_error": "Kunne ikke indlæse nye svar", + "status.context.loading_more": "Indlæser flere svar", + "status.context.loading_success": "Alle svar indlæst", + "status.context.more_replies_found": "Flere svar fundet", + "status.context.retry": "Prøv igen", + "status.context.show": "Vis", "status.continued_thread": "Fortsat tråd", "status.copy": "Kopiér link til indlæg", "status.delete": "Slet", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 7ceb4509dc..910ed0c19a 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", "status.contains_quote": "Enthält Zitat", - "status.context.load_new_replies": "Neue Antworten verfügbar", - "status.context.loading": "Weitere Antworten werden abgerufen", + "status.context.loading": "Weitere Antworten werden geladen", + "status.context.loading_error": "Neue Antworten konnten nicht geladen werden", + "status.context.loading_more": "Weitere Antworten werden geladen", + "status.context.loading_success": "Alle Antworten geladen", + "status.context.more_replies_found": "Weitere Antworten verfügbar", + "status.context.retry": "Wiederholen", + "status.context.show": "Anzeigen", "status.continued_thread": "Fortgeführter Thread", "status.copy": "Link zum Beitrag kopieren", "status.delete": "Beitrag löschen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index feefaabeea..9b5479feeb 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Δε σας επιτρέπετε να παραθέσετε αυτή την ανάρτηση", "status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί", "status.contains_quote": "Περιέχει παράθεση", - "status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες", - "status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις", + "status.context.loading": "Φόρτωση περισσότερων απαντήσεων", + "status.context.loading_error": "Αδυναμία φόρτωσης νέων απαντήσεων", + "status.context.loading_more": "Φόρτωση περισσότερων απαντήσεων", + "status.context.loading_success": "Όλες οι απαντήσεις φορτώθηκαν", + "status.context.more_replies_found": "Βρέθηκαν περισσότερες απαντήσεις", + "status.context.retry": "Επανάληψη", + "status.context.show": "Εμφάνιση", "status.continued_thread": "Συνεχιζόμενο νήματος", "status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.delete": "Διαγραφή", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 34967d20b0..c1d650e3a1 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -849,8 +849,6 @@ "status.cancel_reblog_private": "Ne plu diskonigi", "status.cannot_quote": "Vi ne rajtas citi ĉi tiun afiŝon", "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", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 3f582452d5..3bd5172e13 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -865,8 +865,13 @@ "status.cannot_quote": "No te es permitido citar este mensaje", "status.cannot_reblog": "No se puede adherir a este mensaje", "status.contains_quote": "Contiene cita", - "status.context.load_new_replies": "Hay nuevas respuestas", - "status.context.loading": "Buscando más respuestas", + "status.context.loading": "Cargando más respuestas", + "status.context.loading_error": "No se pudieron cargar nuevas respuestas", + "status.context.loading_more": "Cargando más respuestas", + "status.context.loading_success": "Se cargaron todas las respuestas", + "status.context.more_replies_found": "Se encontraron más respuestas", + "status.context.retry": "Reintentar", + "status.context.show": "Mostrar", "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 6b48c21d36..9c37456a3e 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -865,8 +865,13 @@ "status.cannot_quote": "No está permitido citar esta publicación", "status.cannot_reblog": "Esta publicación no puede ser impulsada", "status.contains_quote": "Contiene cita", - "status.context.load_new_replies": "Nuevas respuestas disponibles", - "status.context.loading": "Comprobando si hay más respuestas", + "status.context.loading": "Cargando más respuestas", + "status.context.loading_error": "No se pudieron cargar nuevas respuestas", + "status.context.loading_more": "Cargando más respuestas", + "status.context.loading_success": "Todas las respuestas cargadas", + "status.context.more_replies_found": "Se han encontrado más respuestas", + "status.context.retry": "Reintentar", + "status.context.show": "Mostrar", "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 f84cb41511..67eda71cf9 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -865,8 +865,13 @@ "status.cannot_quote": "No tienes permiso para citar esta publicación", "status.cannot_reblog": "Esta publicación no se puede impulsar", "status.contains_quote": "Contiene cita", - "status.context.load_new_replies": "Hay nuevas respuestas", - "status.context.loading": "Buscando más respuestas", + "status.context.loading": "Cargando más respuestas", + "status.context.loading_error": "No se pudieron cargar nuevas respuestas", + "status.context.loading_more": "Cargando más respuestas", + "status.context.loading_success": "Se cargaron todas las respuestas", + "status.context.more_replies_found": "Se encontraron más respuestas", + "status.context.retry": "Reintentar", + "status.context.show": "Mostrar", "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 4aaeb51421..b57383da23 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Sul pole õigust seda postitust tsiteerida", "status.cannot_reblog": "Seda postitust ei saa jagada", "status.contains_quote": "Sisaldab tsitaati", - "status.context.load_new_replies": "Leidub uusi vastuseid", - "status.context.loading": "Kontrollin täiendavate vastuste olemasolu", "status.continued_thread": "Jätkatud lõim", "status.copy": "Kopeeri postituse link", "status.delete": "Kustuta", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index b2b0a6c2de..64e3ec83d1 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -842,8 +842,6 @@ "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/fi.json b/app/javascript/mastodon/locales/fi.json index 8bf69ab31d..aad79c9fe9 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Sinulla ei ole oikeutta lainata tätä julkaisua", "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa", "status.contains_quote": "Sisältää lainauksen", - "status.context.load_new_replies": "Uusia vastauksia saatavilla", - "status.context.loading": "Tarkistetaan lisävastauksia", + "status.context.loading": "Ladataan lisää vastauksia", + "status.context.loading_error": "Ei voitu ladata lisää vastauksia", + "status.context.loading_more": "Ladataan lisää vastauksia", + "status.context.loading_success": "Kaikki vastaukset ladattu", + "status.context.more_replies_found": "Löytyi lisää vastauksia", + "status.context.retry": "Yritä uudelleen", + "status.context.show": "Näytä", "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 a8db9baeca..348de34eb1 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", "status.contains_quote": "Inniheldur sitat", - "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 2bc6195c55..742d502fd5 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Débooster", "status.cannot_quote": "Vous n'êtes pas autorisé à citer ce message", "status.cannot_reblog": "Cette publication ne peut pas être boostée", - "status.context.load_new_replies": "Nouvelles réponses disponibles", - "status.context.loading": "Vérification de plus de réponses", "status.continued_thread": "Suite du fil", "status.copy": "Copier un lien vers cette publication", "status.delete": "Supprimer", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index e579dbe09b..38a1215fd4 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Annuler le partage", "status.cannot_quote": "Vous n'êtes pas autorisé à citer ce message", "status.cannot_reblog": "Ce message ne peut pas être partagé", - "status.context.load_new_replies": "Nouvelles réponses disponibles", - "status.context.loading": "Vérification de plus de réponses", "status.continued_thread": "Suite du fil", "status.copy": "Copier le lien vers le message", "status.delete": "Supprimer", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 31b5196f3e..d0da2af6bb 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -834,8 +834,6 @@ "status.bookmark": "Blêdwizer tafoegje", "status.cancel_reblog_private": "Net langer booste", "status.cannot_reblog": "Dit berjocht kin net boost wurde", - "status.context.load_new_replies": "Nije reaksjes beskikber", - "status.context.loading": "Op nije reaksjes oan it kontrolearjen", "status.continued_thread": "Ferfolgje it petear", "status.copy": "Copy link to status", "status.delete": "Fuortsmite", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 4ede71c487..2277128032 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Ní cheadaítear duit an post seo a lua", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", "status.contains_quote": "Tá luachan ann", - "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 2b2efe733c..0b582c6fda 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Na brosnaich tuilleadh", "status.cannot_quote": "Chan fhaod thu am post seo a luaidh", "status.cannot_reblog": "Cha ghabh am post seo brosnachadh", - "status.context.load_new_replies": "Tha freagairt no dhà ùr ri fhaighinn", - "status.context.loading": "A’ toirt sùil airson barrachd fhreagairtean", "status.continued_thread": "Pàirt de shnàithlean", "status.copy": "Dèan lethbhreac dhen cheangal dhan phost", "status.delete": "Sguab às", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 2d37337a0c..c724e3d9cf 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -851,7 +851,7 @@ "server_banner.is_one_of_many": "{domain} é un dos moitos servidores Mastodon independentes que podes usar para participar do Fediverso.", "server_banner.server_stats": "Estatísticas do servidor:", "sign_in_banner.create_account": "Crear conta", - "sign_in_banner.follow_anyone": "Sigue a quen queiras no Fediverso e le as publicacións en orde cronolóxica. Sen algoritmos, publicidade nin titulares engañosos.", + "sign_in_banner.follow_anyone": "Sigue a quen queiras no Fediverso e le as publicacións en orde cronolóxica. Sen algoritmos, publicidade nin titulares enganosos.", "sign_in_banner.mastodon_is": "Mastodon é o mellor xeito de estar ao día do que acontece.", "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Acceder ou Crear conta", @@ -865,8 +865,13 @@ "status.cannot_quote": "Non tes permiso para citar esta publicación", "status.cannot_reblog": "Esta publicación non pode ser promovida", "status.contains_quote": "Contén unha cita", - "status.context.load_new_replies": "Non hai respostas dispoñibles", - "status.context.loading": "Mirando se hai máis respostas", + "status.context.loading": "Cargando máis respostas", + "status.context.loading_error": "Non se puideron mostrar novas respostas", + "status.context.loading_more": "Cargando máis respostas", + "status.context.loading_success": "Móstranse todas as respostas", + "status.context.more_replies_found": "Existen máis respostas", + "status.context.retry": "Volver tentar", + "status.context.show": "Mostrar", "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 bcba5e0b7c..6adcc4b406 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -865,8 +865,13 @@ "status.cannot_quote": "אין לך הרשאה לצטט את ההודעה הזו", "status.cannot_reblog": "לא ניתן להדהד חצרוץ זה", "status.contains_quote": "הודעה מכילה ציטוט", - "status.context.load_new_replies": "הגיעו תגובות חדשות", - "status.context.loading": "מחפש תגובות חדשות", + "status.context.loading": "נטענות תשובות נוספות", + "status.context.loading_error": "טעינת תשובות נוספות נכשלה", + "status.context.loading_more": "נטענות תשובות נוספות", + "status.context.loading_success": "כל התשובות נטענו", + "status.context.more_replies_found": "תשובות נוספות נמצאו", + "status.context.retry": "נסה שוב", + "status.context.show": "הצג", "status.continued_thread": "שרשור מתמשך", "status.copy": "העתק/י קישור להודעה זו", "status.delete": "מחיקה", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 83fb3e60fe..80693f586e 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Nem idézheted ezt a bejegyzést", "status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni", "status.contains_quote": "Idézést tartalmaz", - "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 8291e8ad5f..9b3c8f8ed4 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -864,8 +864,14 @@ "status.cancel_reblog_private": "Disfacer impulso", "status.cannot_quote": "Tu non es autorisate a citar iste message", "status.cannot_reblog": "Iste message non pote esser impulsate", - "status.context.load_new_replies": "Nove responsas disponibile", - "status.context.loading": "Cercante plus responsas", + "status.contains_quote": "Contine un citation", + "status.context.loading": "Cargante plus responsas", + "status.context.loading_error": "Non poteva cargar nove responsas", + "status.context.loading_more": "Cargante plus responsas", + "status.context.loading_success": "Tote le responsas cargate", + "status.context.more_replies_found": "Plus responsas trovate", + "status.context.retry": "Tentar de novo", + "status.context.show": "Monstrar", "status.continued_thread": "Continuation del discussion", "status.copy": "Copiar ligamine a message", "status.delete": "Deler", @@ -895,12 +901,15 @@ "status.quote": "Citar", "status.quote.cancel": "Cancellar le citation", "status.quote_error.filtered": "Celate a causa de un de tu filtros", + "status.quote_error.limited_account_hint.action": "Monstrar in omne caso", + "status.quote_error.limited_account_hint.title": "Iste conto ha essite celate per le moderatores de {domain}.", "status.quote_error.not_available": "Message indisponibile", "status.quote_error.pending_approval": "Message pendente", "status.quote_error.pending_approval_popout.body": "Sur Mastodon, tu pote controlar si on pote citar te. Iste message attende ora le approbation del autor original.", "status.quote_error.revoked": "Message removite per le autor", "status.quote_followers_only": "Solmente sequitores pote citar iste message", "status.quote_manual_review": "Le autor lo examinara manualmente", + "status.quote_noun": "Citation", "status.quote_policy_change": "Cambiar qui pote citar", "status.quote_post_author": "Ha citate un message de @{name}", "status.quote_private": "Le messages private non pote esser citate", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index f380a4b4a5..92bae01531 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Taka úr endurbirtingu", "status.cannot_quote": "Þú hefur ekki heimild til að vitna í þessa færslu", "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 448b51944b..3dde3f5775 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -864,8 +864,14 @@ "status.cancel_reblog_private": "Annulla reblog", "status.cannot_quote": "Non ti è consentito citare questo post", "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.contains_quote": "Contiene una citazione", + "status.context.loading": "Caricamento di altre risposte", + "status.context.loading_error": "Impossibile caricare nuove risposte", + "status.context.loading_more": "Caricamento di altre risposte", + "status.context.loading_success": "Tutte le risposte caricate", + "status.context.more_replies_found": "Sono state trovate altre risposte", + "status.context.retry": "Riprova", + "status.context.show": "Mostra", "status.continued_thread": "Discussione continua", "status.copy": "Copia link al post", "status.delete": "Elimina", @@ -903,6 +909,7 @@ "status.quote_error.revoked": "Post rimosso dall'autore", "status.quote_followers_only": "Solo i seguaci possono citare questo post", "status.quote_manual_review": "L'autore esaminerà manualmente", + "status.quote_noun": "Citazione", "status.quote_policy_change": "Cambia chi può citare", "status.quote_post_author": "Citato un post di @{name}", "status.quote_private": "I post privati non possono essere citati", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ba42000ec7..22377dc786 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -844,7 +844,6 @@ "status.cancel_reblog_private": "ブースト解除", "status.cannot_quote": "この投稿は引用できません", "status.cannot_reblog": "この投稿はブーストできません", - "status.context.load_new_replies": "新しい返信があります", "status.continued_thread": "続きのスレッド", "status.copy": "投稿へのリンクをコピー", "status.delete": "削除", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index c484809957..f14f3b5c70 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -639,7 +639,6 @@ "status.bookmark": "Creḍ", "status.cancel_reblog_private": "Sefsex beṭṭu", "status.cannot_reblog": "Tasuffeɣt-a ur tezmir ara ad tettwabḍu tikelt-nniḍen", - "status.context.load_new_replies": "Llant tririyin timaynutin", "status.continued_thread": "Asqerdec yettkemmil", "status.copy": "Nɣel assaɣ ɣer tasuffeɣt", "status.delete": "Kkes", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 40c9ae0004..89137643f3 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -861,8 +861,6 @@ "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/lt.json b/app/javascript/mastodon/locales/lt.json index 6276bb8038..5f4d357f0d 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -784,8 +784,6 @@ "status.bookmark": "Pridėti į žymės", "status.cancel_reblog_private": "Nebepasidalinti", "status.cannot_reblog": "Šis įrašas negali būti pakeltas.", - "status.context.load_new_replies": "Yra naujų atsakymų", - "status.context.loading": "Tikrinama dėl daugiau atsakymų", "status.continued_thread": "Tęsiama gijoje", "status.copy": "Kopijuoti nuorodą į įrašą", "status.delete": "Ištrinti", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index d80b8daf20..ff532bb295 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -725,7 +725,6 @@ "status.bookmark": "Grāmatzīme", "status.cancel_reblog_private": "Nepastiprināt", "status.cannot_reblog": "Šo ierakstu nevar pastiprināt", - "status.context.loading": "Pārbauda, vai ir vairāk atbilžu", "status.continued_thread": "Turpināts pavediens", "status.copy": "Ievietot ieraksta saiti starpliktuvē", "status.delete": "Dzēst", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index 9bce13a8da..b070566564 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "取消轉送", "status.cannot_quote": "Lí bô允准引用tsit篇PO文。", "status.cannot_reblog": "Tsit篇PO文bē當轉送", - "status.context.load_new_replies": "有新ê回應", - "status.context.loading": "Leh檢查其他ê回應", "status.continued_thread": "接續ê討論線", "status.copy": "Khóo-pih PO文ê連結", "status.delete": "Thâi掉", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index f61f7469d5..894a93b5fb 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -865,8 +865,6 @@ "status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren", "status.cannot_reblog": "Dit bericht kan niet geboost worden", "status.contains_quote": "Bevat citaat", - "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 caab03e7cd..fe2c5fc925 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -864,8 +864,6 @@ "status.cancel_reblog_private": "Opphev framheving", "status.cannot_quote": "Du har ikkje løyve til å sitera dette innlegget", "status.cannot_reblog": "Du kan ikkje framheva dette innlegget", - "status.context.load_new_replies": "Nye svar finst", - "status.context.loading": "Ser etter fleire svar", "status.continued_thread": "Framhald til tråden", "status.copy": "Kopier lenke til status", "status.delete": "Slett", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index ab1062ed8b..2fb0a6f1bd 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -849,8 +849,6 @@ "status.bookmark": "Dodaj zakładkę", "status.cancel_reblog_private": "Cofnij podbicie", "status.cannot_reblog": "Ten wpis nie może zostać podbity", - "status.context.load_new_replies": "Dostępne są nowe odpowiedzi", - "status.context.loading": "Sprawdzanie kolejnych odpowiedzi", "status.continued_thread": "Ciąg dalszy wątku", "status.copy": "Skopiuj odnośnik do wpisu", "status.delete": "Usuń", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index cc46fb4304..e550b7463a 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -855,8 +855,6 @@ "status.bookmark": "Salvar", "status.cancel_reblog_private": "Desfazer boost", "status.cannot_reblog": "Este toot não pode receber boost", - "status.context.load_new_replies": "Novas respostas disponíveis", - "status.context.loading": "Verificando mais respostas", "status.continued_thread": "Continuação da conversa", "status.copy": "Copiar link", "status.delete": "Excluir", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index af7c70b9fb..8167b9b49e 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -864,8 +864,14 @@ "status.cancel_reblog_private": "Retirar impulso", "status.cannot_quote": "Não lhe é permitido citar esta publicação", "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.contains_quote": "Contém citação", + "status.context.loading": "A carregar mais respostas", + "status.context.loading_error": "Não foi possível carregar novas respostas", + "status.context.loading_more": "A carregar mais respostas", + "status.context.loading_success": "Todas as respostas carregadas", + "status.context.more_replies_found": "Foram encontradas mais respostas", + "status.context.retry": "Repetir", + "status.context.show": "Mostrar", "status.continued_thread": "Continuação da conversa", "status.copy": "Copiar hiperligação da publicação", "status.delete": "Eliminar", @@ -903,6 +909,7 @@ "status.quote_error.revoked": "Publicação removida pelo autor", "status.quote_followers_only": "Apenas seguidores podem citar esta publicação", "status.quote_manual_review": "O autor vai proceder a uma revisão manual", + "status.quote_noun": "Citação", "status.quote_policy_change": "Alterar quem pode citar", "status.quote_post_author": "Citou uma publicação de @{name}", "status.quote_private": "Publicações privadas não podem ser citadas", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index a22268bb62..1416198a42 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -863,8 +863,6 @@ "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/sv.json b/app/javascript/mastodon/locales/sv.json index 3bbe6434ed..e56c9da326 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -843,8 +843,6 @@ "status.bookmark": "Bokmärk", "status.cancel_reblog_private": "Sluta boosta", "status.cannot_reblog": "Detta inlägg kan inte boostas", - "status.context.load_new_replies": "Nya svar finns", - "status.context.loading": "Letar efter fler svar", "status.continued_thread": "Fortsatt tråd", "status.copy": "Kopiera inläggslänk", "status.delete": "Radera", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 9d61ed2053..cc94d324d7 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Bu gönderiyi alıntılamaya izniniz yok", "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz", "status.contains_quote": "Alıntı içeriyor", - "status.context.load_new_replies": "Yeni yanıtlar mevcut", - "status.context.loading": "Daha fazla yanıt için kontrol ediliyor", + "status.context.loading": "Daha fazla yanıt yükleniyor", + "status.context.loading_error": "Yeni yanıtlar yüklenemiyor", + "status.context.loading_more": "Daha fazla yanıt yükleniyor", + "status.context.loading_success": "Tüm yanıtlar yüklendi", + "status.context.more_replies_found": "Daha fazla yanıt bulundu", + "status.context.retry": "Yeniden dene", + "status.context.show": "Göster", "status.continued_thread": "Devam eden akış", "status.copy": "Gönderi bağlantısını kopyala", "status.delete": "Sil", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 913ef41d74..02370341df 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -814,8 +814,6 @@ "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 99a5dc9051..c4e21bbe7e 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -865,8 +865,13 @@ "status.cannot_quote": "Bạn không được phép trích dẫn tút này", "status.cannot_reblog": "Không thể đăng lại tút này", "status.contains_quote": "Chứa trích dẫn", - "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.context.loading": "Tải thêm các trả lời", + "status.context.loading_error": "Không thể tải những trả lời mới", + "status.context.loading_more": "Tải thêm các trả lời", + "status.context.loading_success": "Đã tải toàn bộ trả lời", + "status.context.more_replies_found": "Có trả lời mới", + "status.context.retry": "Thử lại", + "status.context.show": "Hiệ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 c69e72eced..1c52a6334f 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -862,11 +862,16 @@ "status.block": "屏蔽 @{name}", "status.bookmark": "添加到书签", "status.cancel_reblog_private": "取消转嘟", - "status.cannot_quote": "你无法引用此嘟文", + "status.cannot_quote": "你无权引用这条嘟文", "status.cannot_reblog": "不能转嘟这条嘟文", "status.contains_quote": "包含引用", - "status.context.load_new_replies": "有新回复", - "status.context.loading": "正在检查更多回复", + "status.context.loading": "正在加载更多回复", + "status.context.loading_error": "无法加载新回复", + "status.context.loading_more": "正在加载更多回复", + "status.context.loading_success": "已加载所有回复", + "status.context.more_replies_found": "已找到更多回复", + "status.context.retry": "重试", + "status.context.show": "显示", "status.continued_thread": "上接嘟文串", "status.copy": "复制嘟文链接", "status.delete": "删除", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index e4f9dcea29..02edd5f452 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -865,8 +865,13 @@ "status.cannot_quote": "您不被允許引用此嘟文", "status.cannot_reblog": "這則嘟文無法被轉嘟", "status.contains_quote": "包含引用嘟文", - "status.context.load_new_replies": "有新回嘟", - "status.context.loading": "正在檢查更多回嘟", + "status.context.loading": "讀取更多回嘟", + "status.context.loading_error": "無法讀取新回嘟", + "status.context.loading_more": "讀取更多回嘟", + "status.context.loading_success": "已讀取所有回嘟", + "status.context.more_replies_found": "已有更多回嘟", + "status.context.retry": "再試一次", + "status.context.show": "顯示", "status.continued_thread": "接續討論串", "status.copy": "複製嘟文連結", "status.delete": "刪除", diff --git a/config/locales/da.yml b/config/locales/da.yml index 9f15c2a43f..8ad3ccb138 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1634,7 +1634,7 @@ da: not_found: kunne ikke findes on_cooldown: Du er på nedkøling followers_count: Følgere på flytningstidspunktet - incoming_migrations: Flytter fra en anden konto + incoming_migrations: Flytning fra en anden konto incoming_migrations_html: For at flytte fra en anden konto til denne skal der først oprettes et kontoalias. moved_msg: Din konto omdirigeres nu til %{acct} og dine følgere overflyttes. not_redirecting: Din konto omdirigerer pt. ikke til nogen anden konto. From 719b2de3c35e947f45626c86bf730b6cbc6a1477 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Thu, 25 Sep 2025 10:54:02 +0200 Subject: [PATCH 20/58] Update `uuid` package to latest version (#36259) --- streaming/package.json | 3 +-- yarn.lock | 20 ++++++-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/streaming/package.json b/streaming/package.json index b77cfc02ad..7df035d6e7 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -27,7 +27,7 @@ "pino": "^9.0.0", "pino-http": "^10.0.0", "prom-client": "^15.0.0", - "uuid": "^11.0.0", + "uuid": "^13.0.0", "ws": "^8.12.1" }, "devDependencies": { @@ -35,7 +35,6 @@ "@types/cors": "^2.8.16", "@types/express": "^4.17.17", "@types/pg": "^8.6.6", - "@types/uuid": "^10.0.0", "@types/ws": "^8.5.9", "globals": "^16.0.0", "pino-pretty": "^13.0.0", diff --git a/yarn.lock b/yarn.lock index 02f5f7dc5b..1ca3bec11e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2892,7 +2892,6 @@ __metadata: "@types/cors": "npm:^2.8.16" "@types/express": "npm:^4.17.17" "@types/pg": "npm:^8.6.6" - "@types/uuid": "npm:^10.0.0" "@types/ws": "npm:^8.5.9" bufferutil: "npm:^4.0.7" cors: "npm:^2.8.5" @@ -2910,7 +2909,7 @@ __metadata: typescript: "npm:~5.9.0" typescript-eslint: "npm:^8.28.0" utf-8-validate: "npm:^6.0.3" - uuid: "npm:^11.0.0" + uuid: "npm:^13.0.0" ws: "npm:^8.12.1" dependenciesMeta: bufferutil: @@ -4488,13 +4487,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^10.0.0": - version: 10.0.0 - resolution: "@types/uuid@npm:10.0.0" - checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60 - languageName: node - linkType: hard - "@types/warning@npm:^3.0.0": version: 3.0.2 resolution: "@types/warning@npm:3.0.2" @@ -13791,12 +13783,12 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.0": - version: 11.1.0 - resolution: "uuid@npm:11.1.0" +"uuid@npm:^13.0.0": + version: 13.0.0 + resolution: "uuid@npm:13.0.0" bin: - uuid: dist/esm/bin/uuid - checksum: 10c0/34aa51b9874ae398c2b799c88a127701408cd581ee89ec3baa53509dd8728cbb25826f2a038f9465f8b7be446f0fbf11558862965b18d21c993684297628d4d3 + uuid: dist-node/bin/uuid + checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67 languageName: node linkType: hard From 66686994c1c9a079d3a854f0c2ef5baae4cf16c0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Sep 2025 11:25:00 +0200 Subject: [PATCH 21/58] Fix not being able to author quotes with CW but no text (#36153) Co-authored-by: diondiondion --- app/javascript/mastodon/actions/compose.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 96c0c43c74..ccb69f0a3d 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -194,8 +194,10 @@ export function submitCompose(successCallback) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); const statusId = getState().getIn(['compose', 'id'], null); + const hasQuote = !!getState().getIn(['compose', 'quoted_status_id']); + const spoiler_text = getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : ''; - if ((!status || !status.length) && media.size === 0) { + if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) { return; } @@ -227,11 +229,11 @@ export function submitCompose(successCallback) { method: statusId === null ? 'post' : 'put', data: { status, + spoiler_text, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), media_attributes, sensitive: getState().getIn(['compose', 'sensitive']), - spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: visibility, poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), From 6d2493ca7c39a5d60edd7109de0c24256f8fff8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:03:53 +0200 Subject: [PATCH 22/58] chore(deps): update dependency puma to v7.0.4 (#36240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba724ac82b..b4d58cbb69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -637,7 +637,7 @@ GEM date stringio public_suffix (6.0.2) - puma (7.0.3) + puma (7.0.4) nio4r (~> 2.0) pundit (2.5.2) activesupport (>= 3.0.0) From d801cf8e59edaea24cf70f1f62aa4e1ff8d1dcbd Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 25 Sep 2025 14:26:50 +0200 Subject: [PATCH 23/58] Replace `react-router-scroll-4` with inlined implementation (#36253) --- app/javascript/mastodon/components/router.tsx | 5 +- .../mastodon/components/scrollable_list.jsx | 2 +- .../mastodon/containers/mastodon.jsx | 9 +- .../mastodon/containers/scroll_container.js | 18 --- .../default_should_update_scroll.tsx | 25 ++++ .../containers/scroll_container/index.tsx | 62 ++++++++ .../scroll_container/scroll_context.tsx | 141 ++++++++++++++++++ .../scroll_container/state_storage.ts | 46 ++++++ .../mastodon/features/directory/index.tsx | 3 +- .../mastodon/features/status/index.jsx | 6 +- package.json | 2 +- yarn.lock | 55 ++----- 12 files changed, 302 insertions(+), 72 deletions(-) delete mode 100644 app/javascript/mastodon/containers/scroll_container.js create mode 100644 app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx create mode 100644 app/javascript/mastodon/containers/scroll_container/index.tsx create mode 100644 app/javascript/mastodon/containers/scroll_container/scroll_context.tsx create mode 100644 app/javascript/mastodon/containers/scroll_container/state_storage.ts diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx index 815b4b59ab..1dc1d45083 100644 --- a/app/javascript/mastodon/components/router.tsx +++ b/app/javascript/mastodon/components/router.tsx @@ -1,6 +1,7 @@ import type { PropsWithChildren } from 'react'; import type React from 'react'; +import type { useLocation } from 'react-router'; import { Router as OriginalRouter, useHistory } from 'react-router'; import type { @@ -18,7 +19,9 @@ interface MastodonLocationState { mastodonModalKey?: string; } -type LocationState = MastodonLocationState | null | undefined; +export type LocationState = MastodonLocationState | null | undefined; + +export type MastodonLocation = ReturnType>; type HistoryPath = Path | LocationDescriptor; diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx index 22ec18afa9..47b6235c9e 100644 --- a/app/javascript/mastodon/components/scrollable_list.jsx +++ b/app/javascript/mastodon/components/scrollable_list.jsx @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { supportsPassiveEvents } from 'detect-passive-events'; import { throttle } from 'lodash'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 8dcda3b0a9..086a7681c4 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -5,7 +5,6 @@ import { Route } from 'react-router-dom'; import { Provider as ReduxProvider } from 'react-redux'; -import { ScrollContext } from 'react-router-scroll-4'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { hydrateStore } from 'mastodon/actions/store'; @@ -20,6 +19,8 @@ import { store } from 'mastodon/store'; import { isProduction } from 'mastodon/utils/environment'; import { BodyScrollLock } from 'mastodon/features/ui/components/body_scroll_lock'; +import { ScrollContext } from './scroll_container/scroll_context'; + const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`; const hydrateAction = hydrateStore(initialState); @@ -45,10 +46,6 @@ export default class Mastodon extends PureComponent { } } - shouldUpdateScroll (prevRouterProps, { location }) { - return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey); - } - render () { return ( @@ -56,7 +53,7 @@ export default class Mastodon extends PureComponent { - + diff --git a/app/javascript/mastodon/containers/scroll_container.js b/app/javascript/mastodon/containers/scroll_container.js deleted file mode 100644 index d21ff63687..0000000000 --- a/app/javascript/mastodon/containers/scroll_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4'; - -// ScrollContainer is used to automatically scroll to the top when pushing a -// new history state and remembering the scroll position when going back. -// There are a few things we need to do differently, though. -const defaultShouldUpdateScroll = (prevRouterProps, { location }) => { - // If the change is caused by opening a modal, do not scroll to top - return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey); -}; - -export default -class ScrollContainer extends OriginalScrollContainer { - - static defaultProps = { - shouldUpdateScroll: defaultShouldUpdateScroll, - }; - -} diff --git a/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx new file mode 100644 index 0000000000..b8726a1a75 --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx @@ -0,0 +1,25 @@ +import type { MastodonLocation } from 'mastodon/components/router'; + +export type ShouldUpdateScrollFn = ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, +) => boolean; + +/** + * ScrollBehavior will automatically scroll to the top on navigations + * or restore saved scroll positions, but on some location changes we + * need to prevent this. + */ + +export const defaultShouldUpdateScroll: ShouldUpdateScrollFn = ( + prevLocation, + location, +) => { + // If the change is caused by opening a modal, do not scroll to top + const shouldUpdateScroll = !( + location.state?.mastodonModalKey && + location.state.mastodonModalKey !== prevLocation?.state?.mastodonModalKey + ); + + return shouldUpdateScroll; +}; diff --git a/app/javascript/mastodon/containers/scroll_container/index.tsx b/app/javascript/mastodon/containers/scroll_container/index.tsx new file mode 100644 index 0000000000..e7d2726715 --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/index.tsx @@ -0,0 +1,62 @@ +import React, { useContext, useEffect, useRef } from 'react'; + +import { defaultShouldUpdateScroll } from './default_should_update_scroll'; +import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; +import { ScrollBehaviorContext } from './scroll_context'; + +interface ScrollContainerProps { + /** + * This key must be static for the element & not change + * while the component is mounted. + */ + scrollKey: string; + shouldUpdateScroll?: ShouldUpdateScrollFn; + children: React.ReactElement; +} + +/** + * `ScrollContainer` is used to manage the scroll position of elements on the page + * that can be scrolled independently of the page body. + * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ + */ + +export const ScrollContainer: React.FC = ({ + children, + scrollKey, + shouldUpdateScroll = defaultShouldUpdateScroll, +}) => { + const scrollBehaviorContext = useContext(ScrollBehaviorContext); + + const containerRef = useRef(); + + /** + * Register/unregister scrollable element with ScrollBehavior + */ + useEffect(() => { + if (!scrollBehaviorContext || !containerRef.current) { + return; + } + + scrollBehaviorContext.registerElement( + scrollKey, + containerRef.current, + (prevLocation, location) => { + // Hack to allow accessing scrollBehavior._stateStorage + return shouldUpdateScroll.call( + scrollBehaviorContext.scrollBehavior, + prevLocation, + location, + ); + }, + ); + + return () => { + scrollBehaviorContext.unregisterElement(scrollKey); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return React.Children.only( + React.cloneElement(children, { ref: containerRef }), + ); +}; diff --git a/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx new file mode 100644 index 0000000000..a7eb780800 --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import { useLocation, useHistory } from 'react-router-dom'; + +import type { LocationBase } from 'scroll-behavior'; +import ScrollBehavior from 'scroll-behavior'; + +import type { + LocationState, + MastodonLocation, +} from 'mastodon/components/router'; +import { usePrevious } from 'mastodon/hooks/usePrevious'; + +import { defaultShouldUpdateScroll } from './default_should_update_scroll'; +import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; +import { SessionStorage } from './state_storage'; + +type ScrollBehaviorInstance = InstanceType< + typeof ScrollBehavior +>; + +export interface ScrollBehaviorContextType { + registerElement: ( + key: string, + element: HTMLElement, + shouldUpdateScroll: ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, + ) => boolean, + ) => void; + unregisterElement: (key: string) => void; + scrollBehavior?: ScrollBehaviorInstance; +} + +export const ScrollBehaviorContext = + React.createContext(null); + +interface ScrollContextProps { + shouldUpdateScroll?: ShouldUpdateScrollFn; + children: React.ReactElement; +} + +/** + * A top-level wrapper that provides the app with an instance of the + * ScrollBehavior object. scroll-behavior is a library for managing the + * scroll position of a single-page app in the same way the browser would + * normally do for a multi-page app. This means it'll scroll back to top + * when navigating to a new page, and will restore the scroll position + * when navigating e.g. using `history.back`. + * The library keeps a record of scroll positions in session storage. + * + * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ + */ + +export const ScrollContext: React.FC = ({ + children, + shouldUpdateScroll = defaultShouldUpdateScroll, +}) => { + const location = useLocation(); + const history = useHistory(); + + /** + * Keep the current location in a mutable ref so that ScrollBehavior's + * `getCurrentLocation` can access it without having to recreate the + * whole ScrollBehavior object + */ + const currentLocationRef = useRef(location); + useEffect(() => { + currentLocationRef.current = location; + }, [location]); + + /** + * Initialise ScrollBehavior object once – using state rather + * than a ref to simplify the types and ensure it's defined immediately. + */ + const [scrollBehavior] = useState( + (): ScrollBehaviorInstance => + new ScrollBehavior({ + addNavigationListener: history.listen.bind(history), + stateStorage: new SessionStorage(), + getCurrentLocation: () => + currentLocationRef.current as unknown as LocationBase, + shouldUpdateScroll: ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, + ) => + // Hack to allow accessing scrollBehavior._stateStorage + shouldUpdateScroll.call( + scrollBehavior, + prevLocationContext, + locationContext, + ), + }), + ); + + /** + * Handle scroll update when location changes + */ + const prevLocation = usePrevious(location) ?? null; + useEffect(() => { + scrollBehavior.updateScroll(prevLocation, location); + }, [location, prevLocation, scrollBehavior]); + + /** + * Stop Scrollbehavior on unmount + */ + useEffect(() => { + return () => { + scrollBehavior.stop(); + }; + }, [scrollBehavior]); + + /** + * Provide the app with a way to register separately scrollable + * elements to also be tracked by ScrollBehavior. (By default + * ScrollBehavior only handles scrolling on the main document body.) + */ + const contextValue = useMemo( + () => ({ + registerElement: (key, element, shouldUpdateScroll) => { + scrollBehavior.registerElement( + key, + element, + shouldUpdateScroll, + location, + ); + }, + unregisterElement: (key) => { + scrollBehavior.unregisterElement(key); + }, + scrollBehavior, + }), + [location, scrollBehavior], + ); + + return ( + + {React.Children.only(children)} + + ); +}; diff --git a/app/javascript/mastodon/containers/scroll_container/state_storage.ts b/app/javascript/mastodon/containers/scroll_container/state_storage.ts new file mode 100644 index 0000000000..fe8a208aae --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/state_storage.ts @@ -0,0 +1,46 @@ +import type { LocationBase, ScrollPosition } from 'scroll-behavior'; + +const STATE_KEY_PREFIX = '@@scroll|'; + +interface LocationBaseWithKey extends LocationBase { + key?: string; +} + +/** + * This module is part of our port of https://github.com/ytase/react-router-scroll/ + * and handles storing scroll positions in SessionStorage. + * Stored positions (`[x, y]`) are keyed by the location key and an optional + * `scrollKey` that's used for to track separately scrollable elements other + * than the document body. + */ + +export class SessionStorage { + read( + location: LocationBaseWithKey, + key: string | null, + ): ScrollPosition | null { + const stateKey = this.getStateKey(location, key); + + try { + const value = sessionStorage.getItem(stateKey); + return value ? (JSON.parse(value) as ScrollPosition) : null; + } catch { + return null; + } + } + + save(location: LocationBaseWithKey, key: string | null, value: unknown) { + const stateKey = this.getStateKey(location, key); + const storedValue = JSON.stringify(value); + + try { + sessionStorage.setItem(stateKey, storedValue); + } catch {} + } + + getStateKey(location: LocationBaseWithKey, key: string | null) { + const locationKey = location.key; + const stateKeyBase = `${STATE_KEY_PREFIX}${locationKey}`; + return key == null ? stateKeyBase : `${stateKeyBase}|${key}`; + } +} diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx index a29febcd1a..0fe140b4eb 100644 --- a/app/javascript/mastodon/features/directory/index.tsx +++ b/app/javascript/mastodon/features/directory/index.tsx @@ -21,7 +21,7 @@ import { ColumnHeader } from 'mastodon/components/column_header'; import { LoadMore } from 'mastodon/components/load_more'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { RadioButton } from 'mastodon/components/radio_button'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import { useSearchParam } from 'mastodon/hooks/useSearchParam'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; @@ -206,7 +206,6 @@ export const Directory: React.FC<{ /> {multiColumn && !pinned ? ( - // @ts-expect-error ScrollContainer is not properly typed yet {scrollableArea} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 404faf609e..2ceff2577f 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -16,7 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac import { Hotkeys } from 'mastodon/components/hotkeys'; import { Icon } from 'mastodon/components/icon'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -526,9 +526,9 @@ class Status extends ImmutablePureComponent { this.setState({ fullscreen: isFullscreen() }); }; - shouldUpdateScroll = (prevRouterProps, { location }) => { + shouldUpdateScroll = (prevLocation, location) => { // Do not change scroll when opening a modal - if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) { + if (location.state?.mastodonModalKey !== prevLocation?.state?.mastodonModalKey) { return false; } diff --git a/package.json b/package.json index 2d0fa230cd..0fd14de656 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "react-redux-loading-bar": "^5.0.8", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", - "react-router-scroll-4": "^1.0.0-beta.1", "react-select": "^5.7.3", "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.14.0", @@ -111,6 +110,7 @@ "rollup-plugin-gzip": "^4.1.1", "rollup-plugin-visualizer": "^6.0.3", "sass": "^1.62.1", + "scroll-behavior": "^0.11.0", "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", "substring-trie": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 1ca3bec11e..beca808c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2836,7 +2836,6 @@ __metadata: react-redux-loading-bar: "npm:^5.0.8" react-router: "npm:^5.3.4" react-router-dom: "npm:^5.3.4" - react-router-scroll-4: "npm:^1.0.0-beta.1" react-select: "npm:^5.7.3" react-sparklines: "npm:^1.7.0" react-swipeable-views: "npm:^0.14.0" @@ -2849,6 +2848,7 @@ __metadata: rollup-plugin-gzip: "npm:^4.1.1" rollup-plugin-visualizer: "npm:^6.0.3" sass: "npm:^1.62.1" + scroll-behavior: "npm:^0.11.0" stacktrace-js: "npm:^2.0.2" storybook: "npm:^9.1.1" stringz: "npm:^2.1.0" @@ -6478,16 +6478,7 @@ __metadata: languageName: node linkType: hard -"dom-helpers@npm:^3.4.0": - version: 3.4.0 - resolution: "dom-helpers@npm:3.4.0" - dependencies: - "@babel/runtime": "npm:^7.1.2" - checksum: 10c0/1d2d3e4eadac2c4f4c8c7470a737ab32b7ec28237c4d094ea967ec3184168fd12452196fcc424a5d7860b6176117301aeaecba39467bf1a6e8492a8e5c9639d1 - languageName: node - linkType: hard - -"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.2.0": +"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.4, dom-helpers@npm:^5.2.0": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" dependencies: @@ -10036,6 +10027,13 @@ __metadata: languageName: node linkType: hard +"page-lifecycle@npm:^0.1.2": + version: 0.1.2 + resolution: "page-lifecycle@npm:0.1.2" + checksum: 10c0/509dbbc2ad2000dffcf591f66ab13d80fb1dba9337d85c76269173f7a5c3959b5a876e3bfb1e4494f6b932c1dc02a0b5824ebd452ab1a7204d4abdf498cb27c5 + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -11277,21 +11275,6 @@ __metadata: languageName: node linkType: hard -"react-router-scroll-4@npm:^1.0.0-beta.1": - version: 1.0.0-beta.2 - resolution: "react-router-scroll-4@npm:1.0.0-beta.2" - dependencies: - scroll-behavior: "npm:^0.9.1" - warning: "npm:^3.0.0" - peerDependencies: - prop-types: ^15.6.0 - react: ^15.0.0 || ^16.0.0 - react-dom: ^15.0.0 || ^16.0.0 - react-router-dom: ^4.0 - checksum: 10c0/ad195b7359fd3146530cf299ec437f0a619c577b2cacfb2c76a156d3cd9d5d3e97af56e17c300c37ca8c485041e93124fe63f0c86db6aea468caf838281e62cb - languageName: node - linkType: hard - "react-router@npm:5.3.4, react-router@npm:^5.3.4": version: 5.3.4 resolution: "react-router@npm:5.3.4" @@ -12051,13 +12034,14 @@ __metadata: languageName: node linkType: hard -"scroll-behavior@npm:^0.9.1": - version: 0.9.12 - resolution: "scroll-behavior@npm:0.9.12" +"scroll-behavior@npm:^0.11.0": + version: 0.11.0 + resolution: "scroll-behavior@npm:0.11.0" dependencies: - dom-helpers: "npm:^3.4.0" + dom-helpers: "npm:^5.1.4" invariant: "npm:^2.2.4" - checksum: 10c0/4f438c48b93a1dcc2ab51a18670fac6f5ce41885291d8aa13251b4a187be9d0c6dd518ee974eb52ac9bbe227b9811c2615ecca73192a1a190b78dfdadb9c2cf2 + page-lifecycle: "npm:^0.1.2" + checksum: 10c0/c54010c9fdd9fc360fd7887ecf64f16972f9557ac679723709612cd54fc4778c7433ab46a9637933179ef31471f78e2591fb35351dc0e15537fecf1c8c89d32c languageName: node linkType: hard @@ -14013,15 +13997,6 @@ __metadata: languageName: node linkType: hard -"warning@npm:^3.0.0": - version: 3.0.0 - resolution: "warning@npm:3.0.0" - dependencies: - loose-envify: "npm:^1.0.0" - checksum: 10c0/6a2a56ab3139d3927193d926a027e74e1449fa47cc692feea95f8a81a4bb5b7f10c312def94cce03f3b58cb26ba3247858e75d17d596451d2c483a62e8204705 - languageName: node - linkType: hard - "warning@npm:^4.0.1, warning@npm:^4.0.3": version: 4.0.3 resolution: "warning@npm:4.0.3" From 11bd51564898854c652451c9a28d8f73afc6a293 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 25 Sep 2025 18:14:49 +0200 Subject: [PATCH 24/58] Allow accessing ref of ScrollContainer's child (#36265) --- .../mastodon/components/scrollable_list.jsx | 2 +- .../containers/scroll_container/index.tsx | 16 +++++++++++++++- .../mastodon/features/status/index.jsx | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx index 47b6235c9e..38c3cd991b 100644 --- a/app/javascript/mastodon/components/scrollable_list.jsx +++ b/app/javascript/mastodon/components/scrollable_list.jsx @@ -399,7 +399,7 @@ class ScrollableList extends PureComponent { if (trackScroll) { return ( - + {scrollableArea} ); diff --git a/app/javascript/mastodon/containers/scroll_container/index.tsx b/app/javascript/mastodon/containers/scroll_container/index.tsx index e7d2726715..0d0ab364dc 100644 --- a/app/javascript/mastodon/containers/scroll_container/index.tsx +++ b/app/javascript/mastodon/containers/scroll_container/index.tsx @@ -1,4 +1,9 @@ -import React, { useContext, useEffect, useRef } from 'react'; +import React, { + useContext, + useEffect, + useImperativeHandle, + useRef, +} from 'react'; import { defaultShouldUpdateScroll } from './default_should_update_scroll'; import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; @@ -11,6 +16,7 @@ interface ScrollContainerProps { */ scrollKey: string; shouldUpdateScroll?: ShouldUpdateScrollFn; + childRef?: React.ForwardedRef; children: React.ReactElement; } @@ -23,12 +29,20 @@ interface ScrollContainerProps { export const ScrollContainer: React.FC = ({ children, scrollKey, + childRef, shouldUpdateScroll = defaultShouldUpdateScroll, }) => { const scrollBehaviorContext = useContext(ScrollBehaviorContext); const containerRef = useRef(); + /** + * If a childRef is passed, sync it with the containerRef. This + * is necessary because in this component's return statement, + * we're overwriting the immediate child component's ref prop. + */ + useImperativeHandle(childRef, () => containerRef.current, []); + /** * Register/unregister scrollable element with ScrollBehavior */ diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 2ceff2577f..7c38af3277 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -602,7 +602,7 @@ class Status extends ImmutablePureComponent { )} /> - +
{ancestors} From f61d8cb02a22180d0502581c370e6d0e80c5b4bf Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 26 Sep 2025 04:21:03 -0400 Subject: [PATCH 25/58] Hold usable value lists in admin settings form (#36268) --- app/models/form/admin_settings.rb | 8 +++++--- app/views/admin/settings/about/show.html.haml | 4 ++-- app/views/admin/settings/registrations/show.html.haml | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 5f23e683b9..a19a6308fa 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -82,15 +82,17 @@ class Form::AdminSettings }.freeze DESCRIPTION_LIMIT = 200 + DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze + REGISTRATION_MODES = %w(open approved none).freeze attr_accessor(*KEYS) - validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } + validates :registrations_mode, inclusion: { in: REGISTRATION_MODES }, if: -> { defined?(@registrations_mode) } validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) } validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) } validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) } - validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } - validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } + validates :show_domain_blocks, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks) } + validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml index 1eb47a0b54..adc8f1ff04 100644 --- a/app/views/admin/settings/about/show.html.haml +++ b/app/views/admin/settings/about/show.html.haml @@ -24,7 +24,7 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :show_domain_blocks, collection_wrapper_tag: 'ul', - collection: %i(disabled users all), + collection: f.object.class::DOMAIN_BLOCK_AUDIENCES, include_blank: false, item_wrapper_tag: 'li', label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, @@ -32,7 +32,7 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :show_domain_blocks_rationale, collection_wrapper_tag: 'ul', - collection: %i(disabled users all), + collection: f.object.class::DOMAIN_BLOCK_AUDIENCES, include_blank: false, item_wrapper_tag: 'li', label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index cb5a3eb6ba..7303eca662 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -18,7 +18,7 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :registrations_mode, - collection: %w(open approved none), + collection: f.object.class::REGISTRATION_MODES, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.registrations_mode.modes.#{mode}") }, warning_hint: I18n.t('admin.settings.registrations_mode.warning_hint'), From c2d426a565c72d616284a61a0f6f2eca68336fae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:21:06 +0200 Subject: [PATCH 26/58] chore(deps): update dependency rubocop to v1.81.0 (#36269) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b4d58cbb69..db436c01be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,7 +345,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.13.2) + json (2.15.0) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -626,7 +626,7 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) - prism (1.4.0) + prism (1.5.1) prometheus_exporter (2.3.0) webrick propshaft (1.3.1) @@ -722,7 +722,7 @@ GEM redis (4.8.1) redis-client (0.26.0) connection_pool - regexp_parser (2.11.2) + regexp_parser (2.11.3) reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) @@ -766,7 +766,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.80.2) + rubocop (1.81.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -774,10 +774,10 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.46.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.46.0) + rubocop-ast (1.47.1) parser (>= 3.3.7.2) prism (~> 1.4) rubocop-capybara (2.22.1) @@ -901,9 +901,9 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.9.1) - unicode-display_width (3.1.5) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) uri (1.0.3) useragent (0.16.11) validate_url (1.0.15) From 7431c505668d8face7415cc0d09be76e770ec821 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:42:28 +0200 Subject: [PATCH 27/58] New Crowdin Translations (automated) (#36270) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 9 +++++++++ app/javascript/mastodon/locales/cs.json | 7 +++++++ app/javascript/mastodon/locales/de.json | 10 +++++----- app/javascript/mastodon/locales/et.json | 7 +++++++ app/javascript/mastodon/locales/fo.json | 7 +++++++ app/javascript/mastodon/locales/ga.json | 7 +++++++ app/javascript/mastodon/locales/nl.json | 7 +++++++ 7 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index c61938faf5..6086fbf77f 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -864,6 +864,14 @@ "status.cancel_reblog_private": "Прыбраць", "status.cannot_quote": "Вы не маеце дазвол цытаваць гэты допіс", "status.cannot_reblog": "Гэты допіс нельга пашырыць", + "status.contains_quote": "Утрымлівае цытату", + "status.context.loading": "Загружаюцца іншыя адказы", + "status.context.loading_error": "Немагчыма загрузіць новыя адказы", + "status.context.loading_more": "Загружаюцца іншыя адказы", + "status.context.loading_success": "Усе адказы загружаныя", + "status.context.more_replies_found": "Знойдзеныя іншыя адказы", + "status.context.retry": "Паспрабаваць зноў", + "status.context.show": "Паказаць", "status.continued_thread": "Працяг ланцужка", "status.copy": "Скапіраваць спасылку на допіс", "status.delete": "Выдаліць", @@ -901,6 +909,7 @@ "status.quote_error.revoked": "Аўтар выдаліў допіс", "status.quote_followers_only": "Толькі падпісчыкі могуць цытаваць гэты допіс", "status.quote_manual_review": "Аўтар зробіць агляд уручную", + "status.quote_noun": "Цытаваць", "status.quote_policy_change": "Змяніць, хто можа цытаваць", "status.quote_post_author": "Цытаваў допіс @{name}", "status.quote_private": "Прыватныя допісы нельга цытаваць", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 3b4138cbc8..d972d4705b 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", "status.contains_quote": "Obsahuje citaci", + "status.context.loading": "Načítání dalších odpovědí", + "status.context.loading_error": "Nelze načíst nové odpovědi", + "status.context.loading_more": "Načítání dalších odpovědí", + "status.context.loading_success": "Všechny odpovědi načteny", + "status.context.more_replies_found": "Nalezeny další odpovědi", + "status.context.retry": "Zkusit znovu", + "status.context.show": "Zobrazit", "status.continued_thread": "Pokračuje ve vlákně", "status.copy": "Zkopírovat odkaz na příspěvek", "status.delete": "Smazat", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 910ed0c19a..e756b6bab6 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -865,12 +865,12 @@ "status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", "status.contains_quote": "Enthält Zitat", - "status.context.loading": "Weitere Antworten werden geladen", - "status.context.loading_error": "Neue Antworten konnten nicht geladen werden", - "status.context.loading_more": "Weitere Antworten werden geladen", - "status.context.loading_success": "Alle Antworten geladen", + "status.context.loading": "Weitere Antworten laden", + "status.context.loading_error": "Weitere Antworten konnten nicht geladen werden", + "status.context.loading_more": "Weitere Antworten laden", + "status.context.loading_success": "Alle weiteren Antworten geladen", "status.context.more_replies_found": "Weitere Antworten verfügbar", - "status.context.retry": "Wiederholen", + "status.context.retry": "Erneut versuchen", "status.context.show": "Anzeigen", "status.continued_thread": "Fortgeführter Thread", "status.copy": "Link zum Beitrag kopieren", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index b57383da23..b6e02a0ba6 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Sul pole õigust seda postitust tsiteerida", "status.cannot_reblog": "Seda postitust ei saa jagada", "status.contains_quote": "Sisaldab tsitaati", + "status.context.loading": "Laadin veel vastuseid", + "status.context.loading_error": "Uute vastuste laadimine ei õnnestunud", + "status.context.loading_more": "Laadin veel vastuseid", + "status.context.loading_success": "Kõik vastused on laaditud", + "status.context.more_replies_found": "Leidub veel vastuseid", + "status.context.retry": "Proovi uuesti", + "status.context.show": "Näita", "status.continued_thread": "Jätkatud lõim", "status.copy": "Kopeeri postituse link", "status.delete": "Kustuta", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 348de34eb1..938db30d7a 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", "status.contains_quote": "Inniheldur sitat", + "status.context.loading": "Tekur fleiri svar niður", + "status.context.loading_error": "Fekk ikki tikið nýggj svar niður", + "status.context.loading_more": "Tekur fleiri svar niður", + "status.context.loading_success": "Øll svar tikin niður", + "status.context.more_replies_found": "Fleiri svar funnin", + "status.context.retry": "Royn aftur", + "status.context.show": "Vís", "status.continued_thread": "Framhaldandi tráður", "status.copy": "Kopiera leinki til postin", "status.delete": "Strika", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 2277128032..88d03ea43c 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Ní cheadaítear duit an post seo a lua", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", "status.contains_quote": "Tá luachan ann", + "status.context.loading": "Ag lódáil tuilleadh freagraí", + "status.context.loading_error": "Níorbh fhéidir freagraí nua a lódáil", + "status.context.loading_more": "Ag lódáil tuilleadh freagraí", + "status.context.loading_success": "Luchtaithe na freagraí uile", + "status.context.more_replies_found": "Tuilleadh freagraí aimsithe", + "status.context.retry": "Déan iarracht arís", + "status.context.show": "Taispeáin", "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/nl.json b/app/javascript/mastodon/locales/nl.json index 894a93b5fb..4250040040 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren", "status.cannot_reblog": "Dit bericht kan niet geboost worden", "status.contains_quote": "Bevat citaat", + "status.context.loading": "Meer reacties laden", + "status.context.loading_error": "Kon geen nieuwe reacties laden", + "status.context.loading_more": "Meer reacties laden", + "status.context.loading_success": "Alle reacties zijn geladen", + "status.context.more_replies_found": "Meer reacties gevonden", + "status.context.retry": "Opnieuw proberen", + "status.context.show": "Tonen", "status.continued_thread": "Vervolg van gesprek", "status.copy": "Link naar bericht kopiëren", "status.delete": "Verwijderen", From 238d74fe81b960de974c8a7459ec81f832842261 Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Fri, 26 Sep 2025 04:53:08 -0400 Subject: [PATCH 28/58] Refactor `getFocusedItemIndex` to avoid conditionals that `closest` already handles (#36267) --- .../mastodon/features/ui/util/focusUtils.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/app/javascript/mastodon/features/ui/util/focusUtils.ts b/app/javascript/mastodon/features/ui/util/focusUtils.ts index a19852e0d2..9bcd3f8943 100644 --- a/app/javascript/mastodon/features/ui/util/focusUtils.ts +++ b/app/javascript/mastodon/features/ui/util/focusUtils.ts @@ -60,23 +60,13 @@ export function focusColumn({ * Get the index of the currently focused item in one of our item lists */ export function getFocusedItemIndex() { - const focusedElement = document.activeElement; - const itemList = focusedElement?.closest('.item-list'); - - if (!focusedElement || !itemList) { - return -1; - } - - let focusedItem: HTMLElement | null = null; - if (focusedElement.parentElement === itemList) { - focusedItem = focusedElement as HTMLElement; - } else { - focusedItem = focusedElement.closest('.item-list > *'); - } - + const focusedItem = document.activeElement?.closest('.item-list > *'); if (!focusedItem) return -1; - const items = Array.from(itemList.children); + const { parentElement } = focusedItem; + if (!parentElement) return -1; + + const items = Array.from(parentElement.children); return items.indexOf(focusedItem); } From 1571514e49ec02a57c050612b3bca856f54933fb Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 26 Sep 2025 11:23:30 +0200 Subject: [PATCH 29/58] Fix page being vertically scrollable in Advanced UI (#36271) --- app/javascript/styles/mastodon/components.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b390a8a8e5..614b268ec7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2972,8 +2972,12 @@ a.account__display-name { justify-content: flex-start; position: relative; - &.unscrollable { - overflow-x: hidden; + .layout-multiple-columns & { + overflow-x: auto; + + &.unscrollable { + overflow-x: hidden; + } } &__panels { From e07b9dfdc12adb9c8b79d89f80049335053c3324 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 26 Sep 2025 11:50:59 +0200 Subject: [PATCH 30/58] Adds new HTMLBlock component (#36262) --- app/javascript/config/html-tags.json | 61 +++++++++ .../html_block/html_block.stories.tsx | 40 ++++++ .../mastodon/components/html_block/index.tsx | 50 +++++++ .../mastodon/features/emoji/hooks.ts | 29 +---- .../mastodon/features/emoji/normalize.ts | 23 +++- .../__tests__/__snapshots__/html-test.ts.snap | 3 + .../mastodon/utils/__tests__/html-test.ts | 11 +- app/javascript/mastodon/utils/html.ts | 122 +++++++++++------- 8 files changed, 260 insertions(+), 79 deletions(-) create mode 100644 app/javascript/config/html-tags.json create mode 100644 app/javascript/mastodon/components/html_block/html_block.stories.tsx create mode 100644 app/javascript/mastodon/components/html_block/index.tsx diff --git a/app/javascript/config/html-tags.json b/app/javascript/config/html-tags.json new file mode 100644 index 0000000000..c788113487 --- /dev/null +++ b/app/javascript/config/html-tags.json @@ -0,0 +1,61 @@ +{ + "global": { + "class": "className", + "id": true, + "title": true, + "dir": true, + "lang": true + }, + "tags": { + "p": {}, + "br": { + "children": false + }, + "span": { + "attributes": { + "translate": true + } + }, + "a": { + "attributes": { + "href": true, + "rel": true, + "translate": true, + "target": true + } + }, + "del": {}, + "s": {}, + "pre": {}, + "blockquote": {}, + "code": {}, + "b": {}, + "strong": {}, + "u": {}, + "i": {}, + "img": { + "children": false, + "attributes": { + "src": true, + "alt": true, + "title": true + } + }, + "em": {}, + "ul": {}, + "ol": { + "attributes": { + "start": true, + "reversed": true + } + }, + "li": { + "attributes": { + "value": true + } + }, + "ruby": {}, + "rt": {}, + "rp": {} + } +} diff --git a/app/javascript/mastodon/components/html_block/html_block.stories.tsx b/app/javascript/mastodon/components/html_block/html_block.stories.tsx new file mode 100644 index 0000000000..9c104ba45c --- /dev/null +++ b/app/javascript/mastodon/components/html_block/html_block.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect } from 'storybook/test'; + +import { HTMLBlock } from './index'; + +const meta = { + title: 'Components/HTMLBlock', + component: HTMLBlock, + args: { + contents: + '

Hello, world!

\n

A link

\n

This should be filtered out:

', + }, + render(args) { + return ( + // Just for visual clarity in Storybook. +
+ +
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + async play({ canvas }) { + const link = canvas.queryByRole('link'); + await expect(link).toBeInTheDocument(); + const button = canvas.queryByRole('button'); + await expect(button).not.toBeInTheDocument(); + }, +}; diff --git a/app/javascript/mastodon/components/html_block/index.tsx b/app/javascript/mastodon/components/html_block/index.tsx new file mode 100644 index 0000000000..51baea614d --- /dev/null +++ b/app/javascript/mastodon/components/html_block/index.tsx @@ -0,0 +1,50 @@ +import type { FC, ReactNode } from 'react'; +import { useMemo } from 'react'; + +import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize'; +import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types'; +import { createLimitedCache } from '@/mastodon/utils/cache'; + +import { htmlStringToComponents } from '../../utils/html'; + +// Use a module-level cache to avoid re-rendering the same HTML multiple times. +const cache = createLimitedCache({ maxSize: 1000 }); + +interface HTMLBlockProps { + contents: string; + extraEmojis?: CustomEmojiMapArg; +} + +export const HTMLBlock: FC = ({ + contents: raw, + extraEmojis, +}) => { + const customEmojis = useMemo( + () => cleanExtraEmojis(extraEmojis), + [extraEmojis], + ); + const contents = useMemo(() => { + const key = JSON.stringify({ raw, customEmojis }); + if (cache.has(key)) { + return cache.get(key); + } + + const rendered = htmlStringToComponents(raw, { + onText, + extraArgs: { customEmojis }, + }); + + cache.set(key, rendered); + return rendered; + }, [raw, customEmojis]); + + return contents; +}; + +function onText( + text: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Doesn't do anything, just showing how typing would work. + { customEmojis }: { customEmojis: CustomEmojiMapArg | null }, +) { + return text; +} diff --git a/app/javascript/mastodon/features/emoji/hooks.ts b/app/javascript/mastodon/features/emoji/hooks.ts index 7e91486780..b3b27d274a 100644 --- a/app/javascript/mastodon/features/emoji/hooks.ts +++ b/app/javascript/mastodon/features/emoji/hooks.ts @@ -1,19 +1,13 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; -import { isList } from 'immutable'; - -import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { useAppSelector } from '@/mastodon/store'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; +import { cleanExtraEmojis } from './normalize'; import { emojifyElement, emojifyText } from './render'; -import type { - CustomEmojiMapArg, - EmojiAppState, - ExtraCustomEmojiMap, -} from './types'; +import type { CustomEmojiMapArg, EmojiAppState } from './types'; import { stringHasAnyEmoji } from './utils'; interface UseEmojifyOptions { @@ -30,20 +24,7 @@ export function useEmojify({ const [emojifiedText, setEmojifiedText] = useState(null); const appState = useEmojiAppState(); - const extra: ExtraCustomEmojiMap = useMemo(() => { - if (!extraEmojis) { - return {}; - } - if (isList(extraEmojis)) { - return ( - extraEmojis.toJS() as ApiCustomEmojiJSON[] - ).reduce( - (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), - {}, - ); - } - return extraEmojis; - }, [extraEmojis]); + const extra = useMemo(() => cleanExtraEmojis(extraEmojis), [extraEmojis]); const emojify = useCallback( async (input: string) => { @@ -51,11 +32,11 @@ export function useEmojify({ if (deep) { const wrapper = document.createElement('div'); wrapper.innerHTML = input; - if (await emojifyElement(wrapper, appState, extra)) { + if (await emojifyElement(wrapper, appState, extra ?? {})) { result = wrapper.innerHTML; } } else { - result = await emojifyText(text, appState, extra); + result = await emojifyText(text, appState, extra ?? {}); } if (result) { setEmojifiedText(result); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 6a64c3b8bf..959732f985 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -1,3 +1,5 @@ +import { isList } from 'immutable'; + import { VARIATION_SELECTOR_CODE, KEYCAP_CODE, @@ -7,7 +9,11 @@ import { EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_LIGHT_BORDER, } from './constants'; -import type { TwemojiBorderInfo } from './types'; +import type { + CustomEmojiMapArg, + ExtraCustomEmojiMap, + TwemojiBorderInfo, +} from './types'; // Misc codes that have special handling const SKIER_CODE = 0x26f7; @@ -150,6 +156,21 @@ export function twemojiToUnicodeInfo( return hexNumbersToString(mappedCodes); } +export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) { + if (!extraEmojis) { + return null; + } + if (!isList(extraEmojis)) { + return extraEmojis; + } + return extraEmojis + .toJSON() + .reduce( + (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), + {}, + ); +} + function hexStringToNumbers(hexString: string): number[] { return hexString .split('-') diff --git a/app/javascript/mastodon/utils/__tests__/__snapshots__/html-test.ts.snap b/app/javascript/mastodon/utils/__tests__/__snapshots__/html-test.ts.snap index a579efa406..ea4561bc61 100644 --- a/app/javascript/mastodon/utils/__tests__/__snapshots__/html-test.ts.snap +++ b/app/javascript/mastodon/utils/__tests__/__snapshots__/html-test.ts.snap @@ -26,9 +26,11 @@ exports[`html > htmlStringToComponents > handles nested elements 1`] = ` exports[`html > htmlStringToComponents > ignores empty text nodes 1`] = ` [

+ lorem ipsum +

, ] `; @@ -37,6 +39,7 @@ exports[`html > htmlStringToComponents > respects allowedTags option 1`] = ` [

lorem + dolor diff --git a/app/javascript/mastodon/utils/__tests__/html-test.ts b/app/javascript/mastodon/utils/__tests__/html-test.ts index 6c08cc7cbf..6aacc396dc 100644 --- a/app/javascript/mastodon/utils/__tests__/html-test.ts +++ b/app/javascript/mastodon/utils/__tests__/html-test.ts @@ -48,7 +48,7 @@ describe('html', () => { const input = '

lorem ipsum

'; const onText = vi.fn((text: string) => text); html.htmlStringToComponents(input, { onText }); - expect(onText).toHaveBeenCalledExactlyOnceWith('lorem ipsum'); + expect(onText).toHaveBeenCalledExactlyOnceWith('lorem ipsum', {}); }); it('calls onElement callback', () => { @@ -61,6 +61,7 @@ describe('html', () => { expect(onElement).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ tagName: 'P' }), expect.arrayContaining(['lorem ipsum']), + {}, ); }); @@ -71,6 +72,7 @@ describe('html', () => { expect(onElement).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ tagName: 'P' }), expect.arrayContaining(['lorem ipsum']), + {}, ); expect(output).toMatchSnapshot(); }); @@ -88,15 +90,16 @@ describe('html', () => { 'href', 'https://example.com', 'a', + {}, ); - expect(onAttribute).toHaveBeenCalledWith('target', '_blank', 'a'); - expect(onAttribute).toHaveBeenCalledWith('rel', 'nofollow', 'a'); + expect(onAttribute).toHaveBeenCalledWith('target', '_blank', 'a', {}); + expect(onAttribute).toHaveBeenCalledWith('rel', 'nofollow', 'a', {}); }); it('respects allowedTags option', () => { const input = '

lorem ipsum dolor

'; const output = html.htmlStringToComponents(input, { - allowedTags: new Set(['p', 'em']), + allowedTags: { p: {}, em: {} }, }); expect(output).toMatchSnapshot(); }); diff --git a/app/javascript/mastodon/utils/html.ts b/app/javascript/mastodon/utils/html.ts index 1686322300..971aefa6d1 100644 --- a/app/javascript/mastodon/utils/html.ts +++ b/app/javascript/mastodon/utils/html.ts @@ -1,5 +1,7 @@ import React from 'react'; +import htmlConfig from '../../config/html-tags.json'; + // NB: This function can still return unsafe HTML export const unescapeHTML = (html: string) => { const wrapper = document.createElement('div'); @@ -10,64 +12,49 @@ export const unescapeHTML = (html: string) => { return wrapper.textContent; }; +interface AllowedTag { + /* True means allow, false disallows global attributes, string renames the attribute name for React. */ + attributes?: Record; + /* If false, the tag cannot have children. Undefined or true means allowed. */ + children?: boolean; +} + +type AllowedTagsType = { + [Tag in keyof React.ReactHTML]?: AllowedTag; +}; + +const globalAttributes: Record = htmlConfig.global; +const defaultAllowedTags: AllowedTagsType = htmlConfig.tags; + interface QueueItem { node: Node; parent: React.ReactNode[]; depth: number; } -interface Options { +export interface HTMLToStringOptions> { maxDepth?: number; - onText?: (text: string) => React.ReactNode; + onText?: (text: string, extra: Arg) => React.ReactNode; onElement?: ( element: HTMLElement, children: React.ReactNode[], + extra: Arg, ) => React.ReactNode; onAttribute?: ( name: string, value: string, tagName: string, + extra: Arg, ) => [string, unknown] | null; - allowedTags?: Set; + allowedTags?: AllowedTagsType; + extraArgs?: Arg; } -const DEFAULT_ALLOWED_TAGS: ReadonlySet = new Set([ - 'a', - 'abbr', - 'b', - 'blockquote', - 'br', - 'cite', - 'code', - 'del', - 'dfn', - 'dl', - 'dt', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'li', - 'ol', - 'p', - 'pre', - 'small', - 'span', - 'strong', - 'sub', - 'sup', - 'time', - 'u', - 'ul', -]); -export function htmlStringToComponents( +let uniqueIdCounter = 0; + +export function htmlStringToComponents>( htmlString: string, - options: Options = {}, + options: HTMLToStringOptions = {}, ) { const wrapper = document.createElement('template'); wrapper.innerHTML = htmlString; @@ -79,10 +66,11 @@ export function htmlStringToComponents( const { maxDepth = 10, - allowedTags = DEFAULT_ALLOWED_TAGS, + allowedTags = defaultAllowedTags, onAttribute, onElement, onText, + extraArgs = {} as Arg, } = options; while (queue.length > 0) { @@ -109,9 +97,9 @@ export function htmlStringToComponents( // Text can be added directly if it has any non-whitespace content. case Node.TEXT_NODE: { const text = node.textContent; - if (text && text.trim() !== '') { + if (text) { if (onText) { - parent.push(onText(text)); + parent.push(onText(text, extraArgs)); } else { parent.push(text); } @@ -127,7 +115,9 @@ export function htmlStringToComponents( } // If the tag is not allowed, skip it and its children. - if (!allowedTags.has(node.tagName.toLowerCase())) { + const tagName = node.tagName.toLowerCase(); + const tagInfo = allowedTags[tagName as keyof typeof allowedTags]; + if (!tagInfo) { continue; } @@ -137,7 +127,8 @@ export function htmlStringToComponents( // If onElement is provided, use it to create the element. if (onElement) { - const component = onElement(node, children); + const component = onElement(node, children, extraArgs); + // Check for undefined to allow returning null. if (component !== undefined) { element = component; @@ -147,25 +138,56 @@ export function htmlStringToComponents( // If the element wasn't created, use the default conversion. if (element === undefined) { const props: Record = {}; + props.key = `html-${uniqueIdCounter++}`; // Get the current key and then increment it. for (const attr of node.attributes) { + let name = attr.name.toLowerCase(); + + // Custom attribute handler. if (onAttribute) { const result = onAttribute( - attr.name, + name, attr.value, node.tagName.toLowerCase(), + extraArgs, ); if (result) { - const [name, value] = result; - props[name] = value; + const [cbName, value] = result; + props[cbName] = value; } } else { - props[attr.name] = attr.value; + // Check global attributes first, then tag-specific ones. + const globalAttr = globalAttributes[name]; + const tagAttr = tagInfo.attributes?.[name]; + + // Exit if neither global nor tag-specific attribute is allowed. + if (!globalAttr && !tagAttr) { + continue; + } + + // Rename if needed. + if (typeof tagAttr === 'string') { + name = tagAttr; + } else if (typeof globalAttr === 'string') { + name = globalAttr; + } + + let value: string | boolean | number = attr.value; + + // Handle boolean attributes. + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } + + props[name] = value; } } + element = React.createElement( - node.tagName.toLowerCase(), + tagName, props, - children, + tagInfo.children !== false ? children : undefined, ); } From cb5bbbfb051e175afb538bf9b83a7ca2d2b2b867 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 26 Sep 2025 12:00:50 +0200 Subject: [PATCH 31/58] Update "Follow" button labels (#36264) --- .../mastodon/components/follow_button.tsx | 70 +++++++++- .../directory/components/account_card.tsx | 123 +----------------- .../components/inline_follow_suggestions.tsx | 2 - .../features/ui/hooks/useBreakpoint.tsx | 3 +- app/javascript/mastodon/locales/en.json | 7 +- 5 files changed, 78 insertions(+), 127 deletions(-) diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 98ef3ba3f1..15a9046848 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -5,24 +5,61 @@ import { useIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; import { useIdentity } from '@/mastodon/identity_context'; -import { fetchRelationships, followAccount } from 'mastodon/actions/accounts'; +import { + fetchRelationships, + followAccount, + unblockAccount, + unmuteAccount, +} from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import { Button } from 'mastodon/components/button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -const messages = defineMessages({ +import { useBreakpoint } from '../features/ui/hooks/useBreakpoint'; + +const longMessages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' }, + followRequest: { + id: 'account.follow_request', + defaultMessage: 'Request to follow', + }, + followRequestCancel: { + id: 'account.follow_request_cancel', + defaultMessage: 'Cancel request', + }, editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); +const shortMessages = { + ...longMessages, // Align type signature of shortMessages and longMessages + ...defineMessages({ + followBack: { + id: 'account.follow_back_short', + defaultMessage: 'Follow back', + }, + followRequest: { + id: 'account.follow_request_short', + defaultMessage: 'Request', + }, + followRequestCancel: { + id: 'account.follow_request_cancel_short', + defaultMessage: 'Cancel', + }, + editProfile: { id: 'account.edit_profile_short', defaultMessage: 'Edit' }, + }), +}; + export const FollowButton: React.FC<{ accountId?: string; compact?: boolean; -}> = ({ accountId, compact }) => { + labelLength?: 'auto' | 'short' | 'long'; +}> = ({ accountId, compact, labelLength = 'auto' }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const { signedIn } = useIdentity(); @@ -57,29 +94,48 @@ export const FollowButton: React.FC<{ if (accountId === me) { return; + } else if (relationship.muting) { + dispatch(unmuteAccount(accountId)); } else if (account && (relationship.following || relationship.requested)) { dispatch( openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), ); + } else if (relationship.blocking) { + dispatch(unblockAccount(accountId)); } else { dispatch(followAccount(accountId)); } }, [dispatch, accountId, relationship, account, signedIn]); + const isNarrow = useBreakpoint('narrow'); + const useShortLabel = + labelLength === 'short' || (labelLength === 'auto' && isNarrow); + const messages = useShortLabel ? shortMessages : longMessages; + + const followMessage = account?.locked + ? messages.followRequest + : messages.follow; + let label; if (!signedIn) { - label = intl.formatMessage(messages.follow); + label = intl.formatMessage(followMessage); } else if (accountId === me) { label = intl.formatMessage(messages.editProfile); } else if (!relationship) { label = ; - } else if (relationship.following || relationship.requested) { + } else if (relationship.muting) { + label = intl.formatMessage(messages.unmute); + } else if (relationship.following) { label = intl.formatMessage(messages.unfollow); - } else if (relationship.followed_by) { + } else if (relationship.blocking) { + label = intl.formatMessage(messages.unblock); + } else if (relationship.requested) { + label = intl.formatMessage(messages.followRequestCancel); + } else if (relationship.followed_by && !account?.locked) { label = intl.formatMessage(messages.followBack); } else { - label = intl.formatMessage(messages.follow); + label = intl.formatMessage(followMessage); } if (accountId === me) { diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx index 9d317efd43..6dc70532ab 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.tsx +++ b/app/javascript/mastodon/features/directory/components/account_card.tsx @@ -1,134 +1,23 @@ -import { useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; -import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; - -import classNames from 'classnames'; import { Link } from 'react-router-dom'; -import { - followAccount, - unblockAccount, - unmuteAccount, -} from 'mastodon/actions/accounts'; -import { openModal } from 'mastodon/actions/modal'; import { Avatar } from 'mastodon/components/avatar'; -import { Button } from 'mastodon/components/button'; import { DisplayName } from 'mastodon/components/display_name'; +import { FollowButton } from 'mastodon/components/follow_button'; import { ShortNumber } from 'mastodon/components/short_number'; -import { autoPlayGif, me } from 'mastodon/initial_state'; +import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; import { makeGetAccount } from 'mastodon/selectors'; -import { useAppDispatch, useAppSelector } from 'mastodon/store'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { - id: 'account.cancel_follow_request', - defaultMessage: 'Withdraw follow request', - }, - requested: { - id: 'account.requested', - defaultMessage: 'Awaiting approval. Click to cancel follow request', - }, - unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, - unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, -}); +import { useAppSelector } from 'mastodon/store'; const getAccount = makeGetAccount(); export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { - const intl = useIntl(); const account = useAppSelector((s) => getAccount(s, accountId)); - const dispatch = useAppDispatch(); - - const handleFollow = useCallback(() => { - if (!account) return; - - if ( - account.getIn(['relationship', 'following']) || - account.getIn(['relationship', 'requested']) - ) { - dispatch( - openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), - ); - } else { - dispatch(followAccount(account.get('id'))); - } - }, [account, dispatch]); - - const handleBlock = useCallback(() => { - if (account?.relationship?.blocking) { - dispatch(unblockAccount(account.get('id'))); - } - }, [account, dispatch]); - - const handleMute = useCallback(() => { - if (account?.relationship?.muting) { - dispatch(unmuteAccount(account.get('id'))); - } - }, [account, dispatch]); - - const handleEditProfile = useCallback(() => { - window.open('/settings/profile', '_blank'); - }, []); if (!account) return null; - let actionBtn; - - if (me !== account.get('id')) { - if (!account.get('relationship')) { - // Wait until the relationship is loaded - actionBtn = ''; - } else if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( -
-
{actionBtn}
+
+ +
); diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx index 3df6d67ecf..05799ccb82 100644 --- a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx @@ -25,8 +25,6 @@ import { domain } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, next: { id: 'lightbox.next', defaultMessage: 'Next' }, dismiss: { diff --git a/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx b/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx index af96ab3766..cb7b3551f2 100644 --- a/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx +++ b/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx @@ -1,11 +1,12 @@ import { useState, useEffect } from 'react'; const breakpoints = { + narrow: 479, // Device width under which horizontal space is constrained openable: 759, // Device width at which the sidebar becomes an openable hamburger menu full: 1174, // Device width at which all 3 columns can be displayed }; -type Breakpoint = 'openable' | 'full'; +type Breakpoint = keyof typeof breakpoints; export const useBreakpoint = (breakpoint: Breakpoint) => { const [isMatching, setIsMatching] = useState(false); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f949c30339..9dc405ab14 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Stop notifying me when @{name} posts", "account.domain_blocking": "Blocking domain", "account.edit_profile": "Edit profile", + "account.edit_profile_short": "Edit", "account.enable_notifications": "Notify me when @{name} posts", "account.endorse": "Feature on profile", "account.familiar_followers_many": "Followed by {name1}, {name2}, and {othersCount, plural, one {one other you know} other {# others you know}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "No posts", "account.follow": "Follow", "account.follow_back": "Follow back", + "account.follow_back_short": "Follow back", + "account.follow_request": "Request to follow", + "account.follow_request_cancel": "Cancel request", + "account.follow_request_cancel_short": "Cancel", + "account.follow_request_short": "Request", "account.followers": "Followers", "account.followers.empty": "No one follows this user yet.", "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Posts and replies", "account.remove_from_followers": "Remove {name} from followers", "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", "account.requested_follow": "{name} has requested to follow you", "account.requests_to_follow_you": "Requests to follow you", "account.share": "Share @{name}'s profile", From a44a3f6d4047568921469ff9fbd212f553b1e7f4 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 26 Sep 2025 12:00:53 +0200 Subject: [PATCH 32/58] Expand test coverage of `ActivityPub::TagManager` class (#36260) --- spec/lib/activitypub/tag_manager_spec.rb | 255 ++++++++++++++++++++++- 1 file changed, 245 insertions(+), 10 deletions(-) diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 7a4cf3c1b8..e536883a55 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -7,7 +7,7 @@ RSpec.describe ActivityPub::TagManager do subject { described_class.instance } - let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" } + let(:host_prefix) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" } describe '#public_collection?' do it 'returns true for the special public collection and common shorthands' do @@ -22,18 +22,123 @@ RSpec.describe ActivityPub::TagManager do end describe '#url_for' do - it 'returns a string starting with web domain' do - account = Fabricate(:account) - expect(subject.url_for(account)).to be_a(String) - .and start_with(domain) + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.url_for(account)) + .to eq("#{host_prefix}/@#{account.username}") + end + end + + context 'with a remote account' do + let(:account) { Fabricate(:account, domain: 'example.com', url: 'https://example.com/profiles/dskjfsdf') } + + it 'returns the expected URL' do + expect(subject.url_for(account)).to eq account.url + end + end + + context 'with a local status' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.url_for(status)) + .to eq("#{host_prefix}/@#{status.account.username}/#{status.id}") + end + end + + context 'with a remote status' do + let(:account) { Fabricate(:account, domain: 'example.com', url: 'https://example.com/profiles/dskjfsdf') } + let(:status) { Fabricate(:status, account: account, url: 'https://example.com/posts/1234') } + + it 'returns the expected URL' do + expect(subject.url_for(status)).to eq status.url + end end end describe '#uri_for' do - it 'returns a string starting with web domain' do - account = Fabricate(:account) - expect(subject.uri_for(account)).to be_a(String) - .and start_with(domain) + context 'with the instance actor' do + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(Account.representative)) + .to eq("#{host_prefix}/actor") + end + end + + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}") + end + end + + context 'with a remote account' do + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/profiles/dskjfsdf') } + + it 'returns the expected URL' do + expect(subject.uri_for(account)).to eq account.uri + end + end + + context 'with a local status' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status)) + .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}") + end + end + + context 'with a remote status' do + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/profiles/dskjfsdf') } + let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/posts/1234') } + + it 'returns the expected URL' do + expect(subject.uri_for(status)).to eq status.uri + end + end + + context 'with a local conversation' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status.conversation)) + .to eq("#{host_prefix}/contexts/#{status.account.id}-#{status.id}") + end + end + + context 'with a remote conversation' do + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/profiles/dskjfsdf') } + let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/posts/1234') } + + before do + status.conversation.update!(uri: 'https://example.com/conversations/1234') + end + + it 'returns the expected URL' do + expect(subject.uri_for(status.conversation)).to eq status.conversation.uri + end + end + end + + describe '#key_uri_for' do + context 'with the instance actor' do + it 'returns a string starting with web domain and with the expected path' do + expect(subject.key_uri_for(Account.representative)) + .to eq("#{host_prefix}/actor#main-key") + end + end + + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.key_uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}#main-key") + end end end @@ -49,7 +154,137 @@ RSpec.describe ActivityPub::TagManager do it 'returns a string starting with web domain' do status = Fabricate(:status) expect(subject.uri_for(status)).to be_a(String) - .and start_with(domain) + .and start_with(host_prefix) + end + end + end + + describe '#approval_uri_for' do + context 'with a valid local approval' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + it 'returns a string with the web domain and expected path' do + expect(subject.approval_uri_for(quote)) + .to eq("#{host_prefix}/users/#{quote.quoted_account.username}/quote_authorizations/#{quote.id}") + end + end + + context 'with an unapproved local quote' do + let(:quote) { Fabricate(:quote, state: :rejected) } + + it 'returns nil' do + expect(subject.approval_uri_for(quote)) + .to be_nil + end + end + + context 'with a valid remote approval' do + let(:quoted_account) { Fabricate(:account, domain: 'example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:quote) { Fabricate(:quote, state: :accepted, quoted_status: quoted_status, approval_uri: 'https://example.com/approvals/1') } + + it 'returns the expected URI' do + expect(subject.approval_uri_for(quote)).to eq quote.approval_uri + end + end + + context 'with an unapproved local quote but check_approval override' do + let(:quote) { Fabricate(:quote, state: :rejected) } + + it 'returns a string with the web domain and expected path' do + expect(subject.approval_uri_for(quote, check_approval: false)) + .to eq("#{host_prefix}/users/#{quote.quoted_account.username}/quote_authorizations/#{quote.id}") + end + end + end + + describe '#replies_uri_for' do + context 'with a local status' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.replies_uri_for(status)) + .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/replies") + end + end + end + + describe '#likes_uri_for' do + context 'with a local status' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.likes_uri_for(status)) + .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/likes") + end + end + end + + describe '#shares_uri_for' do + context 'with a local status' do + let(:status) { Fabricate(:status) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.shares_uri_for(status)) + .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/shares") + end + end + end + + describe '#following_uri_for' do + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.following_uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}/following") + end + end + end + + describe '#followers_uri_for' do + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.followers_uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}/followers") + end + end + end + + describe '#inbox_uri_for' do + context 'with the instance actor' do + it 'returns a string starting with web domain and with the expected path' do + expect(subject.inbox_uri_for(Account.representative)) + .to eq("#{host_prefix}/actor/inbox") + end + end + + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.inbox_uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}/inbox") + end + end + end + + describe '#outbox_uri_for' do + context 'with the instance actor' do + it 'returns a string starting with web domain and with the expected path' do + expect(subject.outbox_uri_for(Account.representative)) + .to eq("#{host_prefix}/actor/outbox") + end + end + + context 'with a local account' do + let(:account) { Fabricate(:account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.outbox_uri_for(account)) + .to eq("#{host_prefix}/users/#{account.username}/outbox") end end end From aae9a5528a1677177a65891a058870f2e763e176 Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Mon, 29 Sep 2025 05:10:27 -0400 Subject: [PATCH 33/58] Remove shallow prop from Wrapper (#36275) --- app/javascript/mastodon/features/emoji/emoji_html.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index 08d62b2c37..b4c352073c 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -51,7 +51,14 @@ export const EmojiHTML = ( if (isModernEmojiEnabled()) { return ; } - const { as: asElement, htmlString, extraEmojis, className, ...rest } = props; + const { + as: asElement, + htmlString, + extraEmojis, + className, + shallow: _, + ...rest + } = props; const Wrapper = asElement ?? 'div'; return ( Date: Mon, 29 Sep 2025 11:16:16 +0200 Subject: [PATCH 34/58] New Crowdin Translations (automated) (#36276) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/af.json | 1 - app/javascript/mastodon/locales/an.json | 1 - app/javascript/mastodon/locales/ar.json | 1 - app/javascript/mastodon/locales/az.json | 1 - app/javascript/mastodon/locales/be.json | 7 +++- app/javascript/mastodon/locales/bg.json | 1 - app/javascript/mastodon/locales/bn.json | 1 - app/javascript/mastodon/locales/br.json | 1 - app/javascript/mastodon/locales/ca.json | 1 - app/javascript/mastodon/locales/ckb.json | 1 - app/javascript/mastodon/locales/co.json | 1 - app/javascript/mastodon/locales/cs.json | 7 +++- app/javascript/mastodon/locales/cy.json | 14 +++++++- app/javascript/mastodon/locales/da.json | 7 +++- app/javascript/mastodon/locales/de.json | 7 +++- app/javascript/mastodon/locales/el.json | 7 +++- app/javascript/mastodon/locales/en-GB.json | 1 - app/javascript/mastodon/locales/eo.json | 1 - app/javascript/mastodon/locales/es-AR.json | 7 +++- app/javascript/mastodon/locales/es-MX.json | 7 +++- app/javascript/mastodon/locales/es.json | 7 +++- app/javascript/mastodon/locales/et.json | 7 +++- app/javascript/mastodon/locales/eu.json | 1 - app/javascript/mastodon/locales/fa.json | 1 - app/javascript/mastodon/locales/fi.json | 7 +++- app/javascript/mastodon/locales/fil.json | 1 - app/javascript/mastodon/locales/fo.json | 7 +++- app/javascript/mastodon/locales/fr-CA.json | 1 - app/javascript/mastodon/locales/fr.json | 1 - app/javascript/mastodon/locales/fy.json | 1 - app/javascript/mastodon/locales/ga.json | 7 +++- app/javascript/mastodon/locales/gd.json | 1 - app/javascript/mastodon/locales/gl.json | 7 +++- app/javascript/mastodon/locales/he.json | 7 +++- app/javascript/mastodon/locales/hi.json | 1 - app/javascript/mastodon/locales/hr.json | 1 - app/javascript/mastodon/locales/hu.json | 1 - app/javascript/mastodon/locales/hy.json | 1 - app/javascript/mastodon/locales/ia.json | 7 +++- app/javascript/mastodon/locales/id.json | 1 - app/javascript/mastodon/locales/ie.json | 1 - app/javascript/mastodon/locales/io.json | 1 - app/javascript/mastodon/locales/is.json | 20 +++++++++-- app/javascript/mastodon/locales/it.json | 1 - app/javascript/mastodon/locales/ja.json | 1 - app/javascript/mastodon/locales/ka.json | 1 - app/javascript/mastodon/locales/kab.json | 17 ++++++++-- app/javascript/mastodon/locales/kk.json | 1 - app/javascript/mastodon/locales/kn.json | 1 - app/javascript/mastodon/locales/ko.json | 1 - app/javascript/mastodon/locales/ku.json | 1 - app/javascript/mastodon/locales/kw.json | 1 - app/javascript/mastodon/locales/lad.json | 1 - app/javascript/mastodon/locales/lt.json | 1 - app/javascript/mastodon/locales/lv.json | 1 - app/javascript/mastodon/locales/mk.json | 1 - app/javascript/mastodon/locales/ml.json | 1 - app/javascript/mastodon/locales/mr.json | 1 - app/javascript/mastodon/locales/ms.json | 1 - app/javascript/mastodon/locales/my.json | 1 - app/javascript/mastodon/locales/nan.json | 16 ++++++++- app/javascript/mastodon/locales/ne.json | 1 - app/javascript/mastodon/locales/nl.json | 7 +++- app/javascript/mastodon/locales/nn.json | 1 - app/javascript/mastodon/locales/no.json | 1 - app/javascript/mastodon/locales/oc.json | 1 - app/javascript/mastodon/locales/pa.json | 1 - app/javascript/mastodon/locales/pl.json | 1 - app/javascript/mastodon/locales/pt-BR.json | 1 - app/javascript/mastodon/locales/pt-PT.json | 7 +++- app/javascript/mastodon/locales/ro.json | 1 - app/javascript/mastodon/locales/ru.json | 1 - app/javascript/mastodon/locales/ry.json | 1 - app/javascript/mastodon/locales/sa.json | 1 - app/javascript/mastodon/locales/sc.json | 1 - app/javascript/mastodon/locales/sco.json | 1 - app/javascript/mastodon/locales/si.json | 1 - app/javascript/mastodon/locales/sk.json | 1 - app/javascript/mastodon/locales/sl.json | 1 - app/javascript/mastodon/locales/sq.json | 1 - app/javascript/mastodon/locales/sr-Latn.json | 1 - app/javascript/mastodon/locales/sr.json | 1 - app/javascript/mastodon/locales/sv.json | 1 - app/javascript/mastodon/locales/szl.json | 1 - app/javascript/mastodon/locales/ta.json | 1 - app/javascript/mastodon/locales/tai.json | 1 - app/javascript/mastodon/locales/te.json | 1 - app/javascript/mastodon/locales/th.json | 1 - app/javascript/mastodon/locales/tok.json | 1 - app/javascript/mastodon/locales/tr.json | 7 +++- app/javascript/mastodon/locales/tt.json | 1 - app/javascript/mastodon/locales/ug.json | 1 - app/javascript/mastodon/locales/uk.json | 1 - app/javascript/mastodon/locales/ur.json | 1 - app/javascript/mastodon/locales/uz.json | 1 - app/javascript/mastodon/locales/vi.json | 7 +++- app/javascript/mastodon/locales/zgh.json | 1 - app/javascript/mastodon/locales/zh-CN.json | 1 - app/javascript/mastodon/locales/zh-HK.json | 1 - app/javascript/mastodon/locales/zh-TW.json | 7 +++- config/locales/kab.yml | 24 ++++++++++++-- config/locales/nan.yml | 35 ++++++++++++++++++-- 102 files changed, 234 insertions(+), 108 deletions(-) diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json index 6df4608528..7ab1d793ed 100644 --- a/app/javascript/mastodon/locales/af.json +++ b/app/javascript/mastodon/locales/af.json @@ -44,7 +44,6 @@ "account.posts": "Plasings", "account.posts_with_replies": "Plasings en antwoorde", "account.report": "Rapporteer @{name}", - "account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer", "account.requested_follow": "{name} het versoek om jou te volg", "account.share": "Deel @{name} se profiel", "account.show_reblogs": "Wys aangestuurde plasings van @{name}", diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json index 661a1bebdb..bd4c061266 100644 --- a/app/javascript/mastodon/locales/an.json +++ b/app/javascript/mastodon/locales/an.json @@ -44,7 +44,6 @@ "account.posts": "Publicacions", "account.posts_with_replies": "Publicacions y respuestas", "account.report": "Denunciar a @{name}", - "account.requested": "Esperando l'aprebación", "account.requested_follow": "{name} ha demandau seguir-te", "account.share": "Compartir lo perfil de @{name}", "account.show_reblogs": "Amostrar retutz de @{name}", diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index eb4933e188..affadf671f 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "المنشورات والرُدود", "account.remove_from_followers": "إزالة {name} من المتابعين", "account.report": "الإبلاغ عن @{name}", - "account.requested": "في انتظار القبول. اضغط لإلغاء طلب المُتابعة", "account.requested_follow": "لقد طلب {name} متابعتك", "account.requests_to_follow_you": "طلبات المتابعة", "account.share": "شارِك الملف التعريفي لـ @{name}", diff --git a/app/javascript/mastodon/locales/az.json b/app/javascript/mastodon/locales/az.json index 4c7a61746f..a6b11dd910 100644 --- a/app/javascript/mastodon/locales/az.json +++ b/app/javascript/mastodon/locales/az.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Paylaşım və cavablar", "account.remove_from_followers": "{name} - izləyicilərdən çıxart", "account.report": "@{name} istifadəçisini şikayət et", - "account.requested": "Təsdiq edilməsi gözlənilir. İzləmə sorğusunu ləğv etmək üçün kliklə", "account.requested_follow": "{name} sizi izləmək sorğusu göndərib", "account.requests_to_follow_you": "Sizi izləmək istəyir", "account.share": "@{name} profilini paylaş", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 6086fbf77f..56430cee50 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Не паведамляць мне пра публікацыі @{name}", "account.domain_blocking": "Блакіраванне дамена", "account.edit_profile": "Рэдагаваць профіль", + "account.edit_profile_short": "Рэдагаваць", "account.enable_notifications": "Апавяшчаць мяне пра допісы @{name}", "account.endorse": "Паказваць у профілі", "account.familiar_followers_many": "Мае сярод падпісчыкаў {name1}, {name2}, і {othersCount, plural, one {яшчэ # чалавека, знаёмага вам} few {яшчэ # чалавекі, знаёмыя вам} many {яшчэ # чалавек, знаёмых вам} other {яшчэ # чалавекі, знаёмыя вам}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Няма допісаў", "account.follow": "Падпісацца", "account.follow_back": "Падпісацца ў адказ", + "account.follow_back_short": "Падпісацца ў адказ", + "account.follow_request": "Даслаць запыт на падпіску", + "account.follow_request_cancel": "Скасаваць запыт", + "account.follow_request_cancel_short": "Скасаваць", + "account.follow_request_short": "Запыт", "account.followers": "Падпісчыкі", "account.followers.empty": "Ніхто пакуль не падпісаны на гэтага карыстальніка.", "account.followers_counter": "{count, plural, one {{counter} падпісчык} few {{counter} падпісчыкі} many {{counter} падпісчыкаў} other {{counter} падпісчыка}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Допісы і адказы", "account.remove_from_followers": "Выдаліць {name} з падпісчыкаў", "account.report": "Паскардзіцца на @{name}", - "account.requested": "Чакаецца ўхваленне. Націсніце, каб скасаваць запыт на падпіску", "account.requested_follow": "{name} адправіў(-ла) запыт на падпіску", "account.requests_to_follow_you": "Хоча падпісацца на вас", "account.share": "Абагуліць профіль @{name}", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index b148994b55..259d6e71e0 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Публ. и отговори", "account.remove_from_followers": "Премахване на {name} от последователи", "account.report": "Докладване на @{name}", - "account.requested": "Чака се одобрение. Щракнете за отмяна на заявката за последване", "account.requested_follow": "{name} поиска да ви последва", "account.requests_to_follow_you": "Заявки да ви последват", "account.share": "Споделяне на профила на @{name}", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 4caa8bc395..e6c4ae0e7d 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -53,7 +53,6 @@ "account.posts": "পোষ্টসমূহ", "account.posts_with_replies": "টুট এবং মতামত", "account.report": "@{name} কে রিপোর্ট করুন", - "account.requested": "অনুমতির অপেক্ষা। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন", "account.requested_follow": "{name} আপনাকে অনুসরণ করার জন্য অনুরোধ করেছে", "account.share": "@{name} র প্রোফাইল অন্যদের দেখান", "account.show_reblogs": "@{name} র সমর্থনগুলো দেখান", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 5a4828daf5..b3e9d22027 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -68,7 +68,6 @@ "account.posts_with_replies": "Embannadurioù ha respontoù", "account.remove_from_followers": "Dilemel {name} eus an heulierien·ezed", "account.report": "Disklêriañ @{name}", - "account.requested": "O c'hortoz an asant. Klikit evit nullañ ar goulenn heuliañ", "account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ", "account.requests_to_follow_you": "Rekedoù d'ho heuliañ", "account.share": "Skignañ profil @{name}", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index ee6e5050a8..712adc59bb 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Tuts i respostes", "account.remove_from_followers": "Elimina {name} dels seguidors", "account.report": "Informa sobre @{name}", - "account.requested": "S'espera l'aprovació. Clica per a cancel·lar la petició de seguiment", "account.requested_follow": "{name} ha demanat de seguir-te", "account.requests_to_follow_you": "Peticions de seguir-vos", "account.share": "Comparteix el perfil de @{name}", diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json index 0188b67e27..00e9462569 100644 --- a/app/javascript/mastodon/locales/ckb.json +++ b/app/javascript/mastodon/locales/ckb.json @@ -52,7 +52,6 @@ "account.posts": "نووسراوەکان", "account.posts_with_replies": "توتس و وەڵامەکان", "account.report": "گوزارشت @{name}", - "account.requested": "چاوەڕێی ڕەزامەندین. کرتە بکە بۆ هەڵوەشاندنەوەی داواکاری شوێنکەوتن", "account.requested_follow": "{name} داوای کردووە شوێنت بکەوێت", "account.share": "پرۆفایلی @{name} هاوبەش بکە", "account.show_reblogs": "پیشاندانی بەرزکردنەوەکان لە @{name}", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index 7a0306bc31..c8df1d444c 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -24,7 +24,6 @@ "account.posts": "Statuti", "account.posts_with_replies": "Statuti è risposte", "account.report": "Palisà @{name}", - "account.requested": "In attesa d'apprubazione. Cliccate per annullà a dumanda", "account.share": "Sparte u prufile di @{name}", "account.show_reblogs": "Vede spartere da @{name}", "account.unblock": "Sbluccà @{name}", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index d972d4705b..5ec5fa7bb5 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek", "account.domain_blocking": "Blokované domény", "account.edit_profile": "Upravit profil", + "account.edit_profile_short": "Upravit", "account.enable_notifications": "Oznamovat mi příspěvky @{name}", "account.endorse": "Zvýraznit na profilu", "account.familiar_followers_many": "Sleduje je {name1}, {name2} a {othersCount, plural, one {jeden další, které znáte} few {# další, které znáte} many {# dalších, které znáte} other {# dalších, které znáte}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Žádné příspěvky", "account.follow": "Sledovat", "account.follow_back": "Také sledovat", + "account.follow_back_short": "Také sledovat", + "account.follow_request": "Požádat o sledování", + "account.follow_request_cancel": "Zrušit požadavek", + "account.follow_request_cancel_short": "Zrušit", + "account.follow_request_short": "Požádat", "account.followers": "Sledující", "account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.", "account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Příspěvky a odpovědi", "account.remove_from_followers": "Odebrat {name} ze sledujících", "account.report": "Nahlásit @{name}", - "account.requested": "Čeká na schválení. Kliknutím žádost o sledování zrušíte", "account.requested_follow": "{name} tě požádal o sledování", "account.requests_to_follow_you": "Žádosti o sledování", "account.share": "Sdílet profil @{name}", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 851ec1d559..941bc016c9 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Stopiwch fy hysbysu pan fydd @{name} yn postio", "account.domain_blocking": "Parthau'n cael eu rhwystro", "account.edit_profile": "Golygu'r proffil", + "account.edit_profile_short": "Golygu", "account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio", "account.endorse": "Dangos ar fy mhroffil", "account.familiar_followers_many": "Yn cael ei ddilyn gan {name1},{name2}, a {othersCount, plural, one {one other you know} other{# others you know}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Dim postiadau", "account.follow": "Dilyn", "account.follow_back": "Dilyn nôl", + "account.follow_back_short": "Dilyn nôl", + "account.follow_request": "Cais i ddilyn", + "account.follow_request_cancel": "Diddymu cais", + "account.follow_request_cancel_short": "Diddymu", + "account.follow_request_short": "Gofyn", "account.followers": "Dilynwyr", "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.", "account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwr}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Postiadau ac ymatebion", "account.remove_from_followers": "Tynnu {name} o'ch dilynwyr", "account.report": "Adrodd @{name}", - "account.requested": "Aros am gymeradwyaeth. Cliciwch er mwyn canslo cais dilyn", "account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn", "account.requests_to_follow_you": "Ceisiadau i'ch dilyn", "account.share": "Rhannu proffil @{name}", @@ -865,6 +870,13 @@ "status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn", "status.cannot_reblog": "Does dim modd hybu'r postiad hwn", "status.contains_quote": "Yn cynnwys dyfyniad", + "status.context.loading": "Yn llwytho mwy o atebion", + "status.context.loading_error": "Wedi methu llwytho atebion newydd", + "status.context.loading_more": "Yn llwytho mwy o atebion", + "status.context.loading_success": "Wedi llwytho'r holl atebion", + "status.context.more_replies_found": "Mwy o atebion wedi'u canfod", + "status.context.retry": "Ceisio eto", + "status.context.show": "Dangos", "status.continued_thread": "Edefyn parhaus", "status.copy": "Copïo dolen i'r post", "status.delete": "Dileu", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index b22190881f..0180e2bf7e 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Giv mig ikke længere en notifikation, når @{name} laver indlæg", "account.domain_blocking": "Blokerer domæne", "account.edit_profile": "Redigér profil", + "account.edit_profile_short": "Redigér", "account.enable_notifications": "Giv mig besked, når @{name} laver indlæg", "account.endorse": "Fremhæv på profil", "account.familiar_followers_many": "Følges af {name1}, {name2} og {othersCount, plural, one {# mere, man kender} other {# mere, du kender}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Ingen indlæg", "account.follow": "Følg", "account.follow_back": "Følg tilbage", + "account.follow_back_short": "Følg tilbage", + "account.follow_request": "Anmod om at følge", + "account.follow_request_cancel": "Annuller anmodning", + "account.follow_request_cancel_short": "Annullér", + "account.follow_request_short": "Anmod", "account.followers": "Følgere", "account.followers.empty": "Ingen følger denne bruger endnu.", "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Indlæg og svar", "account.remove_from_followers": "Fjern {name} fra følgere", "account.report": "Anmeld @{name}", - "account.requested": "Afventer godkendelse. Tryk for at annullere følgeanmodning", "account.requested_follow": "{name} har anmodet om at følge dig", "account.requests_to_follow_you": "Anmodninger om at følge dig", "account.share": "Del @{name}s profil", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index e756b6bab6..80c3820508 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Höre auf mich zu benachrichtigen wenn @{name} etwas postet", "account.domain_blocking": "Domain blockiert", "account.edit_profile": "Profil bearbeiten", + "account.edit_profile_short": "Bearbeiten", "account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet", "account.endorse": "Im Profil vorstellen", "account.familiar_followers_many": "Gefolgt von {name1}, {name2} und {othersCount, plural, one {einem weiteren Profil, das dir bekannt ist} other {# weiteren Profilen, die dir bekannt sind}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Keine Beiträge", "account.follow": "Folgen", "account.follow_back": "Ebenfalls folgen", + "account.follow_back_short": "Ebenfalls folgen", + "account.follow_request": "Anfrage zum Folgen", + "account.follow_request_cancel": "Anfrage zurückziehen", + "account.follow_request_cancel_short": "Abbrechen", + "account.follow_request_short": "Anfragen", "account.followers": "Follower", "account.followers.empty": "Diesem Profil folgt noch niemand.", "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Beiträge und Antworten", "account.remove_from_followers": "{name} als Follower entfernen", "account.report": "@{name} melden", - "account.requested": "Die Genehmigung steht noch aus. Klicke hier, um die Follower-Anfrage zurückzuziehen", "account.requested_follow": "{name} möchte dir folgen", "account.requests_to_follow_you": "Möchte dir folgen", "account.share": "Profil von @{name} teilen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 9b5479feeb..f52a480c90 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Σταμάτα να με ειδοποιείς όταν δημοσιεύει ο @{name}", "account.domain_blocking": "Αποκλείεται ο τομέας", "account.edit_profile": "Επεξεργασία προφίλ", + "account.edit_profile_short": "Επεξεργασία", "account.enable_notifications": "Ειδοποίησέ με όταν δημοσιεύει ο @{name}", "account.endorse": "Προβολή στο προφίλ", "account.familiar_followers_many": "Ακολουθείται από {name1}, {name2}, και {othersCount, plural, one {ένας ακόμα που ξέρεις} other {# ακόμα που ξέρεις}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Καμία ανάρτηση", "account.follow": "Ακολούθησε", "account.follow_back": "Ακολούθησε και εσύ", + "account.follow_back_short": "Ακολούθησε και εσύ", + "account.follow_request": "Αίτημα για ακολούθηση", + "account.follow_request_cancel": "Ακύρωση αιτήματος", + "account.follow_request_cancel_short": "Ακύρωση", + "account.follow_request_short": "Αίτημα", "account.followers": "Ακόλουθοι", "account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμα.", "account.followers_counter": "{count, plural, one {{counter} ακόλουθος} other {{counter} ακόλουθοι}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Τουτ και απαντήσεις", "account.remove_from_followers": "Κατάργηση {name} από τους ακόλουθους", "account.report": "Κατάγγειλε @{name}", - "account.requested": "Εκκρεμεί έγκριση. Κάνε κλικ για να ακυρώσεις το αίτημα παρακολούθησης", "account.requested_follow": "Ο/Η {name} αιτήθηκε να σε ακολουθήσει", "account.requests_to_follow_you": "Αιτήματα για να σε ακολουθήσουν", "account.share": "Κοινοποίηση του προφίλ @{name}", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 60f4e0d8f9..bea3c988af 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Posts and replies", "account.remove_from_followers": "Remove {name} from followers", "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", "account.requested_follow": "{name} has requested to follow you", "account.requests_to_follow_you": "Requests to follow you", "account.share": "Share @{name}'s profile", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index c1d650e3a1..a1f2185528 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -69,7 +69,6 @@ "account.posts_with_replies": "Afiŝoj kaj respondoj", "account.remove_from_followers": "Forigi {name}-n de sekvantoj", "account.report": "Raporti @{name}", - "account.requested": "Atendo de aprobo. Klaku por nuligi la peton por sekvado", "account.requested_follow": "{name} petis sekvi vin", "account.requests_to_follow_you": "Petoj sekvi vin", "account.share": "Diskonigi la profilon de @{name}", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 3bd5172e13..8f49351a91 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Dejar de notificarme cuando @{name} envíe mensajes", "account.domain_blocking": "Dominio bloqueado", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Notificarme cuando @{name} envíe mensajes", "account.endorse": "Destacar en el perfil", "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural, one {# cuenta más que conocés} other {# cuentas más que conocés}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sin mensajes", "account.follow": "Seguir", "account.follow_back": "Seguir", + "account.follow_back_short": "Seguir", + "account.follow_request": "Solicitud para seguir", + "account.follow_request_cancel": "Cancelar solicitud", + "account.follow_request_cancel_short": "Cancelar", + "account.follow_request_short": "Solicitar", "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Mnsjs y resp. públicas", "account.remove_from_followers": "Quitar a {name} de tus seguidores", "account.report": "Denunciar a @{name}", - "account.requested": "Esperando aprobación. Hacé clic para cancelar la solicitud de seguimiento", "account.requested_follow": "{name} solicitó seguirte", "account.requests_to_follow_you": "Solicita seguirte", "account.share": "Compartir el perfil de @{name}", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 9c37456a3e..d06d67c4d8 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo", "account.domain_blocking": "Bloqueando dominio", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Notificarme cuando @{name} publique algo", "account.endorse": "Destacar en mi perfil", "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural,one {# más que conoces}other {# más que conoces}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sin publicaciones", "account.follow": "Seguir", "account.follow_back": "Seguir también", + "account.follow_back_short": "Seguir también", + "account.follow_request": "Solicitud de seguimiento", + "account.follow_request_cancel": "Cancelar solicitud", + "account.follow_request_cancel_short": "Cancelar", + "account.follow_request_short": "Solicitar", "account.followers": "Seguidores", "account.followers.empty": "Nadie sigue a este usuario todavía.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Publicaciones y respuestas", "account.remove_from_followers": "Eliminar {name} de tus seguidores", "account.report": "Denunciar a @{name}", - "account.requested": "Esperando aprobación. Haz clic para cancelar la solicitud de seguimiento", "account.requested_follow": "{name} ha solicitado seguirte", "account.requests_to_follow_you": "Solicita seguirte", "account.share": "Compartir el perfil de @{name}", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 67eda71cf9..9d1f34c91b 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo", "account.domain_blocking": "Bloqueando dominio", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Notificarme cuando @{name} publique algo", "account.endorse": "Destacar en el perfil", "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural,one {# más que conoces}other {# más que conoces}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sin publicaciones", "account.follow": "Seguir", "account.follow_back": "Seguir también", + "account.follow_back_short": "Seguir también", + "account.follow_request": "Solicitud de seguimiento", + "account.follow_request_cancel": "Cancelar solicitud", + "account.follow_request_cancel_short": "Cancelar", + "account.follow_request_short": "Solicitar", "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Publicaciones y respuestas", "account.remove_from_followers": "Eliminar {name} de tus seguidores", "account.report": "Reportar a @{name}", - "account.requested": "Esperando aprobación. Haz clic para cancelar la solicitud de seguimiento", "account.requested_follow": "{name} ha solicitado seguirte", "account.requests_to_follow_you": "Solicita seguirte", "account.share": "Compartir el perfil de @{name}", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index b6e02a0ba6..1811f7b939 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Peata teavitused @{name} postitustest", "account.domain_blocking": "Blokeeritud domeen", "account.edit_profile": "Muuda profiili", + "account.edit_profile_short": "Muuda", "account.enable_notifications": "Teavita mind @{name} postitustest", "account.endorse": "Too profiilil esile", "account.familiar_followers_many": "Jälgijateks {name1}, {name2} ja veel {othersCount, plural, one {üks kasutaja, keda tead} other {# kasutajat, keda tead}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Postitusi pole", "account.follow": "Jälgi", "account.follow_back": "Jälgi vastu", + "account.follow_back_short": "Jälgi vastu", + "account.follow_request": "Jälgimispäring", + "account.follow_request_cancel": "Tühista päring", + "account.follow_request_cancel_short": "Katkesta", + "account.follow_request_short": "Koosta päring", "account.followers": "Jälgijad", "account.followers.empty": "Keegi ei jälgi veel seda kasutajat.", "account.followers_counter": "{count, plural, one {{counter} jälgija} other {{counter} jälgijat}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Postitused ja vastused", "account.remove_from_followers": "Eemalda {name} jälgijate seast", "account.report": "Raporteeri @{name}", - "account.requested": "Ootab kinnitust. Klõpsa jälgimise soovi tühistamiseks", "account.requested_follow": "{name} on taodelnud sinu jälgimist", "account.requests_to_follow_you": "soovib sind jälgida", "account.share": "Jaga @{name} profiili", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 579701a525..f4101a5ba0 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Bidalketak eta erantzunak", "account.remove_from_followers": "Kendu {name} zure jarraitzaileengandik", "account.report": "Salatu @{name}", - "account.requested": "Onarpenaren zain. Egin klik jarraipen-eskaera ezeztatzeko", "account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du", "account.requests_to_follow_you": "Zu jarraitzeko eskaera egin du", "account.share": "Partekatu @{name} erabiltzailearen profila", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 64e3ec83d1..91afee6e24 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "فرسته‌ها و پاسخ‌ها", "account.remove_from_followers": "برداشتن {name} از پی‌گیران", "account.report": "گزارش ‎@{name}", - "account.requested": "منتظر پذیرش است. برای لغو درخواست پی‌گیری کلیک کنید", "account.requested_follow": "{name} درخواست پی‌گیریتان را داد", "account.requests_to_follow_you": "درخواست پی‌گیریتان را دارد", "account.share": "هم‌رسانی نمایهٔ ‎@{name}", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index aad79c9fe9..553727da95 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Lopeta ilmoittamasta minulle, kun @{name} julkaisee", "account.domain_blocking": "Verkkotunnus estetty", "account.edit_profile": "Muokkaa profiilia", + "account.edit_profile_short": "Muokkaa", "account.enable_notifications": "Ilmoita minulle, kun @{name} julkaisee", "account.endorse": "Suosittele profiilissa", "account.familiar_followers_many": "Seuraajina {name1}, {name2} ja {othersCount, plural, one {1 muu, jonka tunnet} other {# muuta, jotka tunnet}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Ei julkaisuja", "account.follow": "Seuraa", "account.follow_back": "Seuraa takaisin", + "account.follow_back_short": "Seuraa takaisin", + "account.follow_request": "Pyydä lupaa seurata", + "account.follow_request_cancel": "Peruuta pyyntö", + "account.follow_request_cancel_short": "Peruuta", + "account.follow_request_short": "Pyyntö", "account.followers": "Seuraajat", "account.followers.empty": "Kukaan ei seuraa tätä käyttäjää vielä.", "account.followers_counter": "{count, plural, one {{counter} seuraaja} other {{counter} seuraajaa}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Julkaisut ja vastaukset", "account.remove_from_followers": "Poista {name} seuraajista", "account.report": "Raportoi @{name}", - "account.requested": "Odottaa hyväksyntää. Peruuta seurantapyyntö napsauttamalla", "account.requested_follow": "{name} on pyytänyt lupaa seurata sinua", "account.requests_to_follow_you": "Pyynnöt seurata sinua", "account.share": "Jaa käyttäjän @{name} profiili", diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json index f8425dd8ef..37db5a1b15 100644 --- a/app/javascript/mastodon/locales/fil.json +++ b/app/javascript/mastodon/locales/fil.json @@ -60,7 +60,6 @@ "account.open_original_page": "Buksan ang pinagmulang pahina", "account.posts": "Mga post", "account.report": "I-ulat si/ang @{name}", - "account.requested": "Naghihintay ng pag-apruba. I-click upang ikansela ang hiling sa pagsunod", "account.requested_follow": "Hinihiling ni {name} na sundan ka", "account.share": "Ibahagi ang profile ni @{name}", "account.show_reblogs": "Ipakita ang mga pagpapalakas mula sa/kay {name}", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 938db30d7a..7b956dc38a 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Ikki boða mær frá, tá @{name} skrivar", "account.domain_blocking": "Banni økisnavn", "account.edit_profile": "Broyt vanga", + "account.edit_profile_short": "Rætta", "account.enable_notifications": "Boða mær frá, tá @{name} skrivar", "account.endorse": "Víst á vangamyndini", "account.familiar_followers_many": "{name1}, {name2} og {othersCount, plural, one {ein annar/onnur tú kennir} other {# onnur tú kennir}} fylgja", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Einki uppslag", "account.follow": "Fylg", "account.follow_back": "Fylg aftur", + "account.follow_back_short": "Fylg aftur", + "account.follow_request": "Umbønir um at fylgja tær", + "account.follow_request_cancel": "Strika víðaribeining", + "account.follow_request_cancel_short": "Ógilda", + "account.follow_request_short": "Áheitan", "account.followers": "Fylgjarar", "account.followers.empty": "Ongar fylgjarar enn.", "account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Uppsløg og svar", "account.remove_from_followers": "Strika {name} av fylgjaralista", "account.report": "Melda @{name}", - "account.requested": "Bíðar eftir góðkenning. Trýst fyri at angra umbønina", "account.requested_follow": "{name} hevur biðið um at fylgja tær", "account.requests_to_follow_you": "Umbønir um at fylgja tær", "account.share": "Deil vanga @{name}'s", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 742d502fd5..81ea95a974 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Publications et réponses", "account.remove_from_followers": "Retirer {name} des suiveurs", "account.report": "Signaler @{name}", - "account.requested": "En attente d’approbation. Cliquez pour annuler la demande", "account.requested_follow": "{name} a demandé à vous suivre", "account.requests_to_follow_you": "Demande a vous suivre", "account.share": "Partager le profil de @{name}", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 38a1215fd4..4ecfcfab7f 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Messages et réponses", "account.remove_from_followers": "Retirer {name} des suiveurs", "account.report": "Signaler @{name}", - "account.requested": "En attente d’approbation. Cliquez pour annuler la demande", "account.requested_follow": "{name} a demandé à vous suivre", "account.requests_to_follow_you": "Demande a vous suivre", "account.share": "Partager le profil de @{name}", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index d0da2af6bb..cb68231583 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Berjochten en reaksjes", "account.remove_from_followers": "{name} as folger fuortsmite", "account.report": "@{name} rapportearje", - "account.requested": "Wacht op goedkarring. Klik om it folchfersyk te annulearjen", "account.requested_follow": "{name} hat dy in folchfersyk stjoerd", "account.requests_to_follow_you": "Fersiken om jo te folgjen", "account.share": "Profyl fan @{name} diele", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 88d03ea43c..5a25ef9f47 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Éirigh as ag cuir mé in eol nuair bpostálann @{name}", "account.domain_blocking": "Fearann a bhlocáil", "account.edit_profile": "Cuir an phróifíl in eagar", + "account.edit_profile_short": "Cuir in Eagar", "account.enable_notifications": "Cuir mé in eol nuair bpostálann @{name}", "account.endorse": "Cuir ar an phróifíl mar ghné", "account.familiar_followers_many": "Ina dhiaidh sin ag {name1}, {name2}, agus {othersCount, plural, \n one {duine eile atá aithnid duit} \n two {# duine eile atá aithnid duit} \n few {# dhuine eile atá aithnid duit} \n many {# nduine eile atá aithnid duit} \n other {# duine eile atá aithnid duit}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Gan aon phoist", "account.follow": "Lean", "account.follow_back": "Leanúint ar ais", + "account.follow_back_short": "Lean ar ais", + "account.follow_request": "Iarratas chun leanúint", + "account.follow_request_cancel": "Cealaigh an t-iarratas", + "account.follow_request_cancel_short": "Cealaigh", + "account.follow_request_short": "Iarratas", "account.followers": "Leantóirí", "account.followers.empty": "Ní leanann éinne an t-úsáideoir seo fós.", "account.followers_counter": "{count, plural, one {{counter} leantóir} other {{counter} leantóirí}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Postálacha agus freagraí", "account.remove_from_followers": "Bain {name} de na leantóirí", "account.report": "Tuairiscigh @{name}", - "account.requested": "Ag fanacht le ceadú. Cliceáil chun an iarratas leanúnaí a chealú", "account.requested_follow": "D'iarr {name} ort do chuntas a leanúint", "account.requests_to_follow_you": "Iarratais chun tú a leanúint", "account.share": "Roinn próifíl @{name}", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 0b582c6fda..4c3ab7d2a5 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Postaichean ’s freagairtean", "account.remove_from_followers": "Thoir {name} air falbh on luchd-leantainn", "account.report": "Dèan gearan mu @{name}", - "account.requested": "A’ feitheamh air aontachadh. Briog airson sgur dhen iarrtas leantainn", "account.requested_follow": "Dh’iarr {name} ’gad leantainn", "account.requests_to_follow_you": "Iarrtasan leantainn", "account.share": "Co-roinn a’ phròifil aig @{name}", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index c724e3d9cf..3f20dfb82d 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Deixar de notificarme cando @{name} publica", "account.domain_blocking": "Bloqueo do dominio", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Noficarme cando @{name} publique", "account.endorse": "Amosar no perfil", "account.familiar_followers_many": "Seguida por {name1}, {name2}, e {othersCount, plural, one {outra conta que coñeces} other {outras # contas que coñeces}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sen publicacións", "account.follow": "Seguir", "account.follow_back": "Seguir tamén", + "account.follow_back_short": "Seguir tamén", + "account.follow_request": "Solicitar seguir", + "account.follow_request_cancel": "Desbotar a petición", + "account.follow_request_cancel_short": "Desbotar", + "account.follow_request_short": "Solicitar", "account.followers": "Seguidoras", "account.followers.empty": "Aínda ninguén segue esta usuaria.", "account.followers_counter": "{count, plural, one {{counter} seguidora} other {{counter} seguidoras}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Publicacións e respostas", "account.remove_from_followers": "Retirar a {name} das seguidoras", "account.report": "Informar sobre @{name}", - "account.requested": "Agardando aprobación. Preme para desbotar a solicitude", "account.requested_follow": "{name} solicitou seguirte", "account.requests_to_follow_you": "Solicita seguirte", "account.share": "Compartir o perfil de @{name}", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 6adcc4b406..e7839cdea5 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -28,6 +28,7 @@ "account.disable_notifications": "הפסק לשלוח לי התראות כש@{name} מפרסמים", "account.domain_blocking": "רשימת השרתים החסומים", "account.edit_profile": "עריכת פרופיל", + "account.edit_profile_short": "עריכה", "account.enable_notifications": "שלח לי התראות כש@{name} מפרסם", "account.endorse": "קדם את החשבון בפרופיל", "account.familiar_followers_many": "החשבון נעקב על ידי {name1}, {name2} ועוד {othersCount, plural,one {אחד נוסף שמוכר לך}other {# נוספים שמוכרים לך}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "אין חצרוצים", "account.follow": "לעקוב", "account.follow_back": "לעקוב בחזרה", + "account.follow_back_short": "לעקוב בחזרה", + "account.follow_request": "בקשה לעקוב אחרי", + "account.follow_request_cancel": "ביטול בקשה", + "account.follow_request_cancel_short": "ביטול", + "account.follow_request_short": "בקשה", "account.followers": "עוקבים", "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.", "account.followers_counter": "{count, plural,one {עוקב אחד} other {{counter} עוקבים}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "הודעות ותגובות", "account.remove_from_followers": "הסרת {name} מעוקבי", "account.report": "דווח על @{name}", - "account.requested": "בהמתנה לאישור. לחצי כדי לבטל בקשת מעקב", "account.requested_follow": "{name} ביקשו לעקוב אחריך", "account.requests_to_follow_you": "ביקשו לעקוב אחריך", "account.share": "שתף את הפרופיל של @{name}", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index ca56ad484e..bc71c80068 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -52,7 +52,6 @@ "account.posts": "टूट्स", "account.posts_with_replies": "टूट्स एवं जवाब", "account.report": "रिपोर्ट @{name}", - "account.requested": "मंजूरी का इंतजार। फॉलो रिक्वेस्ट को रद्द करने के लिए क्लिक करें", "account.requested_follow": "{name} ने आपको फॉलो करने के लिए अनुरोध किया है", "account.share": "@{name} की प्रोफाइल शेयर करे", "account.show_reblogs": "@{name} के बूस्ट दिखाए", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index cb3f25de06..0cde3da84b 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -51,7 +51,6 @@ "account.posts": "Objave", "account.posts_with_replies": "Objave i odgovori", "account.report": "Prijavi @{name}", - "account.requested": "Čekanje na potvrdu. Kliknite za poništavanje zahtjeva za praćenje", "account.requested_follow": "{name} zatražio/la je praćenje", "account.share": "Podijeli profil @{name}", "account.show_reblogs": "Prikaži boostove od @{name}", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 80693f586e..1d3764e8e8 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Bejegyzések és válaszok", "account.remove_from_followers": "{name} eltávolítása a követők közül", "account.report": "@{name} jelentése", - "account.requested": "Jóváhagyásra vár. Kattints a követési kérés visszavonásához", "account.requested_follow": "{name} kérte, hogy követhessen", "account.requests_to_follow_you": "Kéri, hogy követhessen", "account.share": "@{name} profiljának megosztása", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 62669eda9b..bd9fd71c23 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -43,7 +43,6 @@ "account.posts": "Գրառումներ", "account.posts_with_replies": "Գրառումներ եւ պատասխաններ", "account.report": "Բողոքել @{name}֊ի մասին", - "account.requested": "Հաստատման կարիք ունի։ Սեղմիր՝ հետեւելու հայցը չեղարկելու համար։", "account.requested_follow": "{name}-ը ցանկանում է հետեւել քեզ", "account.share": "Կիսուել @{name}֊ի էջով", "account.show_reblogs": "Ցուցադրել @{name}֊ի տարածածները", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index 9b3c8f8ed4..f9325b623c 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Non plus notificar me quando @{name} publica", "account.domain_blocking": "Dominio blocate", "account.edit_profile": "Modificar profilo", + "account.edit_profile_short": "Modificar", "account.enable_notifications": "Notificar me quando @{name} publica", "account.endorse": "Evidentiar sur le profilo", "account.familiar_followers_many": "Sequite per {name1}, {name2}, e {othersCount, plural, one {un altere que tu cognosce} other {# alteres que tu cognosce}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Necun message", "account.follow": "Sequer", "account.follow_back": "Sequer in retorno", + "account.follow_back_short": "Sequer in retorno", + "account.follow_request": "Requestar de sequer", + "account.follow_request_cancel": "Cancellar requesta", + "account.follow_request_cancel_short": "Cancellar", + "account.follow_request_short": "Requesta", "account.followers": "Sequitores", "account.followers.empty": "Necuno seque ancora iste usator.", "account.followers_counter": "{count, plural, one {{counter} sequitor} other {{counter} sequitores}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Messages e responsas", "account.remove_from_followers": "Remover {name} del sequitores", "account.report": "Reportar @{name}", - "account.requested": "Attendente le approbation. Clicca pro cancellar le requesta de sequer", "account.requested_follow": "{name} ha requestate de sequer te", "account.requests_to_follow_you": "Requestas de sequer te", "account.share": "Compartir profilo de @{name}", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 7c5c816c67..253a9d93c4 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -64,7 +64,6 @@ "account.posts": "Kiriman", "account.posts_with_replies": "Kiriman dan balasan", "account.report": "Laporkan @{name}", - "account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan", "account.requested_follow": "{name} ingin mengikuti Anda", "account.share": "Bagikan profil @{name}", "account.show_reblogs": "Tampilkan boost dari @{name}", diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json index 4a464ef16e..bbb85ee2ad 100644 --- a/app/javascript/mastodon/locales/ie.json +++ b/app/javascript/mastodon/locales/ie.json @@ -52,7 +52,6 @@ "account.posts": "Postas", "account.posts_with_replies": "Postas e replicas", "account.report": "Raportar @{name}", - "account.requested": "Atendent aprobation. Cliccar por anullar li petition de sequer", "account.requested_follow": "{name} ha petit sequer te", "account.share": "Distribuer li profil de @{name}", "account.show_reblogs": "Monstrar boosts de @{name}", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index b51d32d05a..15c10af863 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -55,7 +55,6 @@ "account.posts": "Mesaji", "account.posts_with_replies": "Afishi e respondi", "account.report": "Denuncar @{name}", - "account.requested": "Vartante aprobo", "account.requested_follow": "{name} demandis sequar tu", "account.share": "Partigez profilo di @{name}", "account.show_reblogs": "Montrez repeti de @{name}", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 92bae01531..012b3c313e 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Hætta að láta mig vita þegar @{name} sendir inn", "account.domain_blocking": "Útiloka lén", "account.edit_profile": "Breyta notandasniði", + "account.edit_profile_short": "Breyta", "account.enable_notifications": "Láta mig vita þegar @{name} sendir inn", "account.endorse": "Birta á notandasniði", "account.familiar_followers_many": "Fylgt af {name1}, {name2} og {othersCount, plural, one {einum öðrum sem þú þekkir} other {# öðrum sem þú þekkir}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Engar færslur", "account.follow": "Fylgjast með", "account.follow_back": "Fylgjast með til baka", + "account.follow_back_short": "Fylgjast með til baka", + "account.follow_request": "Beiðni um að fylgjast með", + "account.follow_request_cancel": "Hætta við beiðni", + "account.follow_request_cancel_short": "Hætta við", + "account.follow_request_short": "Beiðni", "account.followers": "Fylgjendur", "account.followers.empty": "Ennþá fylgist enginn með þessum notanda.", "account.followers_counter": "{count, plural, one {Fylgjandi: {counter}} other {Fylgjendur: {counter}}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Færslur og svör", "account.remove_from_followers": "Fjarlægja {name} úr fylgjendum", "account.report": "Kæra @{name}", - "account.requested": "Bíður eftir samþykki. Smelltu til að hætta við beiðni um að fylgjast með", "account.requested_follow": "{name} hefur beðið um að fylgjast með þér", "account.requests_to_follow_you": "Bað um að fylgjast með þér", "account.share": "Deila notandasniði fyrir @{name}", @@ -864,6 +869,14 @@ "status.cancel_reblog_private": "Taka úr endurbirtingu", "status.cannot_quote": "Þú hefur ekki heimild til að vitna í þessa færslu", "status.cannot_reblog": "Þessa færslu er ekki hægt að endurbirta", + "status.contains_quote": "Inniheldur tilvitnun", + "status.context.loading": "Hleð inn fleiri svörum", + "status.context.loading_error": "Gat ekki hlaðið inn nýjum svörum", + "status.context.loading_more": "Hleð inn fleiri svörum", + "status.context.loading_success": "Öllum svörum hlaðið inn", + "status.context.more_replies_found": "Fleiri svör fundust", + "status.context.retry": "Reyna aftur", + "status.context.show": "Sýna", "status.continued_thread": "Hélt samtali áfram", "status.copy": "Afrita tengil í færslu", "status.delete": "Eyða", @@ -893,12 +906,15 @@ "status.quote": "Tilvitnun", "status.quote.cancel": "Hætta við tilvitnun", "status.quote_error.filtered": "Falið vegna einnar síu sem er virk", + "status.quote_error.limited_account_hint.action": "Birta samt", + "status.quote_error.limited_account_hint.title": "Þessi notandaaðgangur hefur verið falinn af stjórnendum á {domain}.", "status.quote_error.not_available": "Færsla ekki tiltæk", "status.quote_error.pending_approval": "Færsla í bið", "status.quote_error.pending_approval_popout.body": "Á Mastodon geturðu stjórnað því hvort aðrir geti vitnað í þig. Þessi færsla bíður eftir samþykki upprunalegs höfundar.", "status.quote_error.revoked": "Færsla fjarlægð af höfundi", "status.quote_followers_only": "Einungis fylgjendur geta vitnað í þessa færslu", "status.quote_manual_review": "Höfundur mun yfirfara handvirkt", + "status.quote_noun": "Tilvitnun", "status.quote_policy_change": "Breyttu því hver getur tilvitnað", "status.quote_post_author": "Vitnaði í færslu frá @{name}", "status.quote_private": "Ekki er hægt að vitna í einkafærslur", @@ -990,7 +1006,7 @@ "visibility_modal.helper.privacy_private_self_quote": "Tilvitnanir í sjálfan sig úr einkaspjallfærslum er ekki hægt að gera opinberar.", "visibility_modal.helper.private_quoting": "Ekki er hægt að vitna í færslur einungis til fylgjenda sem skrifaðar eru á Mastodon.", "visibility_modal.helper.unlisted_quoting": "Þegar fólk vitnar í þig verða færslurnar þeirr einnig faldar á vinsældatímalínum.", - "visibility_modal.instructions": ". Stýrðu því hverjir geta átt við þessa færslu. Þú getur líka ákvarðað stillingar fyrir allar færslur í framtíðinni með því að fara í Kjörstillingar > Sjálfgefin gildi við gerð færslna.", + "visibility_modal.instructions": "Stýrðu því hverjir geta átt við þessa færslu. Þú getur líka ákvarðað stillingar fyrir allar færslur í framtíðinni með því að fara í Kjörstillingar > Sjálfgefin gildi við gerð færslna.", "visibility_modal.privacy_label": "Sýnileiki", "visibility_modal.quote_followers": "Einungis fylgjendur", "visibility_modal.quote_label": "Hverjir geta gert tilvitnanir", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 3dde3f5775..aa5f47a4c1 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Post e risposte", "account.remove_from_followers": "Rimuovi {name} dai seguaci", "account.report": "Segnala @{name}", - "account.requested": "In attesa d'approvazione. Clicca per annullare la richiesta di seguire", "account.requested_follow": "{name} ha richiesto di seguirti", "account.requests_to_follow_you": "Richieste di seguirti", "account.share": "Condividi il profilo di @{name}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 22377dc786..6acfbac133 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "投稿と返信", "account.remove_from_followers": "{name}さんをフォロワーから削除", "account.report": "@{name}さんを通報", - "account.requested": "フォロー承認待ちです。クリックしてキャンセル", "account.requested_follow": "{name}さんがあなたにフォローリクエストしました", "account.requests_to_follow_you": "フォローリクエスト", "account.share": "@{name}さんのプロフィールを共有する", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index ea7375fd00..a6a3758a0b 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -21,7 +21,6 @@ "account.posts": "პოსტები", "account.posts_with_replies": "ტუტები და პასუხები", "account.report": "დაარეპორტე @{name}", - "account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა", "account.share": "გააზიარე @{name}-ის პროფილი", "account.show_reblogs": "აჩვენე ბუსტები @{name}-სგან", "account.unblock": "განბლოკე @{name}", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index f14f3b5c70..d508320d3b 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -63,7 +63,6 @@ "account.posts_with_replies": "Tisuffaɣ d tririyin", "account.remove_from_followers": "Kkes {name} seg ineḍfaren", "account.report": "Cetki ɣef @{name}", - "account.requested": "Di laɛḍil ad yettwaqbel. Ssit i wakken ad yefsex usuter n uḍfar", "account.requested_follow": "{name} yessuter ad k·m-yeḍfer", "account.share": "Bḍu amaɣnu n @{name}", "account.show_reblogs": "Ssken-d inebḍa n @{name}", @@ -267,6 +266,7 @@ "explore.trending_links": "Isallen", "explore.trending_statuses": "Tisuffaɣ", "explore.trending_tags": "Ihacṭagen", + "featured_carousel.header": "{count, plural, one {n tsuffeɣt tunṭiḍt} other {n tsuffaɣ tunṭiḍin}}", "featured_carousel.next": "Uḍfiṛ", "featured_carousel.post": "Tasuffeɣt", "featured_carousel.previous": "Uzwir", @@ -352,6 +352,7 @@ "keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.down": "i kennu ɣer wadda n tebdart", "keyboard_shortcuts.enter": "i tildin n tsuffeɣt", + "keyboard_shortcuts.favourite": "Smenyef tassuɣeft", "keyboard_shortcuts.favourites": "Ldi tabdert n yismenyifen", "keyboard_shortcuts.federated": "i tildin n tsuddemt tamatut n yisallen", "keyboard_shortcuts.heading": "Inegzumen n unasiw", @@ -364,8 +365,9 @@ "keyboard_shortcuts.my_profile": "akken ad d-teldiḍ amaɣnu-ik", "keyboard_shortcuts.notifications": "Ad d-yeldi ajgu n yilɣa", "keyboard_shortcuts.open_media": "i tiɣwalin yeldin", - "keyboard_shortcuts.pinned": "akken ad teldiḍ tabdart n tjewwiqin yettwasentḍen", + "keyboard_shortcuts.pinned": "akken ad teldiḍ tabdart n tsuffaɣ tunṭiḍin", "keyboard_shortcuts.profile": "akken ad d-teldiḍ amaɣnu n umeskar", + "keyboard_shortcuts.quote": "Tanebdurt n tsuffeɣt", "keyboard_shortcuts.reply": "i tririt", "keyboard_shortcuts.requests": "akken ad d-teldiḍ tabdert n yisuturen n teḍfeṛt", "keyboard_shortcuts.search": "to focus search", @@ -444,6 +446,7 @@ "navigation_bar.privacy_and_reach": "Tabḍnit akked wagwaḍ", "navigation_bar.search": "Nadi", "navigation_bar.search_trends": "Anadi / Anezzuɣ", + "navigation_panel.collapse_lists": "Sneḍfes umuɣ n tebdart", "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "notification.admin.report": "Yemla-t-id {name} {target}", "notification.admin.sign_up": "Ijerred {name}", @@ -464,6 +467,7 @@ "notification.moderation_warning.action_suspend": "Yettwaseḥbes umiḍan-ik.", "notification.own_poll": "Tafrant-ik·im tfuk", "notification.reblog": "{name} yebḍa tajewwiqt-ik i tikelt-nniḍen", + "notification.reblog.name_and_others_with_link": "{name} akked {count, plural, one {# nnayeḍ} other {# nniḍen}} zzuzren tasuffeɣt-ik·im", "notification.relationships_severance_event.learn_more": "Issin ugar", "notification.status": "{name} akken i d-yessufeɣ", "notification_requests.accept": "Qbel", @@ -555,6 +559,8 @@ "regeneration_indicator.please_stand_by": "Ttxil rǧu.", "regeneration_indicator.preparing_your_home_feed": "Ha-tt-an tsuddemt-ik·im tagejdant tettwaheggay…", "relative_time.days": "{number}u", + "relative_time.full.days": "{number, plural, one {# n wass} other {# n wussan}} aya", + "relative_time.full.hours": "{number, plural, one {# n usrag} other {# n yesragen}} aya", "relative_time.full.just_now": "tura kan", "relative_time.hours": "{number}isr", "relative_time.just_now": "tura", @@ -562,6 +568,7 @@ "relative_time.seconds": "{number}tas", "relative_time.today": "ass-a", "remove_quote_hint.button_label": "Gziɣ-t", + "remove_quote_hint.title": "D tidet, tebɣiḍ ad tekkseḍ tasuffeɣt-inek·inem i d-yettwabedren?", "reply_indicator.attachments": "{count, plural, one {# n umedday} other {# n imeddayen}}", "reply_indicator.cancel": "Sefsex", "reply_indicator.poll": "Afmiḍi", @@ -630,6 +637,7 @@ "search_results.title": "Igemmaḍ n unadi ɣef \"{q}\"", "server_banner.active_users": "iseqdacen urmiden", "server_banner.administered_by": "Yettwadbel sɣur :", + "server_banner.is_one_of_many": "{domain} d yiwen seg seg waṭṭas n iqeddacen imzurag n Mastodon i tzemreḍ ad tsqesdceḍ i wakken ad tettekkiḍ deg fediverse.", "server_banner.server_stats": "Tidaddanin n uqeddac:", "sign_in_banner.create_account": "Snulfu-d amiḍan", "sign_in_banner.sign_in": "Qqen", @@ -639,6 +647,8 @@ "status.bookmark": "Creḍ", "status.cancel_reblog_private": "Sefsex beṭṭu", "status.cannot_reblog": "Tasuffeɣt-a ur tezmir ara ad tettwabḍu tikelt-nniḍen", + "status.context.retry": "Ɛreḍ tikkelt nniḍen", + "status.context.show": "Sken-d", "status.continued_thread": "Asqerdec yettkemmil", "status.copy": "Nɣel assaɣ ɣer tasuffeɣt", "status.delete": "Kkes", @@ -667,7 +677,10 @@ "status.quote": "Tanebdurt", "status.quote.cancel": "Semmet tanebdurt", "status.quote_error.limited_account_hint.action": "Sken-d akken ibɣu yili", + "status.quote_error.not_available": "Tasuffeɣt-a ulac-itt", "status.quote_error.revoked": "Tasuffeɣt-a yekkes-itt umeskar-is", + "status.quote_noun": "Tanebdurt", + "status.quote_policy_change": "Snifel anwa i izemren ad d-yebder", "status.quote_post_author": "Yebder-d tasuffeɣt sɣur @{name}", "status.read_more": "Issin ugar", "status.reblog": "Bḍu", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index b04790d9a1..c549bddd3f 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -65,7 +65,6 @@ "account.posts_with_replies": "Постар мен жауаптар", "account.remove_from_followers": "{name} жазылушылардан жою", "account.report": "Шағымдану @{name}", - "account.requested": "Растауын күтіңіз. Жазылудан бас тарту үшін басыңыз", "account.requested_follow": "{name} сізге жазылуға сұраныс жіберді", "account.requests_to_follow_you": "Сізге жазылу сұраныстары", "account.share": "@{name} профилін бөлісу\"", diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json index ae4e4dd7b3..1d383c6b27 100644 --- a/app/javascript/mastodon/locales/kn.json +++ b/app/javascript/mastodon/locales/kn.json @@ -13,7 +13,6 @@ "account.followers": "ಹಿಂಬಾಲಕರು", "account.posts": "ಟೂಟ್‌ಗಳು", "account.posts_with_replies": "Toots and replies", - "account.requested": "Awaiting approval", "account.unblock_domain": "Unhide {domain}", "account_note.placeholder": "Click to add a note", "alert.unexpected.title": "ಅಯ್ಯೋ!", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 89137643f3..0a20036907 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "게시물과 답장", "account.remove_from_followers": "팔로워에서 {name} 제거", "account.report": "@{name} 신고", - "account.requested": "승인 대기 중. 클릭해서 취소하기", "account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다", "account.requests_to_follow_you": "팔로우 요청", "account.share": "@{name}의 프로필 공유", diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json index 3518959e2b..a6a9a8f1e0 100644 --- a/app/javascript/mastodon/locales/ku.json +++ b/app/javascript/mastodon/locales/ku.json @@ -58,7 +58,6 @@ "account.posts": "Şandî", "account.posts_with_replies": "Şandî û bersiv", "account.report": "@{name} ragihîne", - "account.requested": "Li benda erêkirinê ye. Ji bo betal kirina daxwazê pêl bikin", "account.requested_follow": "{name} dixwaze te bişopîne", "account.share": "Profîla @{name} parve bike", "account.show_reblogs": "Bilindkirinên ji @{name} nîşan bike", diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json index b1116a7a31..6af6652a54 100644 --- a/app/javascript/mastodon/locales/kw.json +++ b/app/javascript/mastodon/locales/kw.json @@ -25,7 +25,6 @@ "account.posts": "Postow", "account.posts_with_replies": "Postow ha gorthebow", "account.report": "Reportya @{name}", - "account.requested": "Ow kortos komendyans. Klyckyewgh dhe hedhi govyn holya", "account.share": "Kevrenna profil @{name}", "account.show_reblogs": "Diskwedhes kenerthow a @{name}", "account.unblock": "Anlettya @{name}", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 754f6eb3c6..d8d0a06718 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -63,7 +63,6 @@ "account.posts": "Publikasyones", "account.posts_with_replies": "Kon repuestas", "account.report": "Raporta @{name}", - "account.requested": "Asperando achetasion. Klika para anular la solisitud de segimiento", "account.requested_follow": "{name} tiene solisitado segirte", "account.requests_to_follow_you": "Solisita segirte", "account.share": "Partaja el profil de @{name}", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 5f4d357f0d..f02af533fd 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -64,7 +64,6 @@ "account.posts_with_replies": "Įrašai ir atsakymai", "account.remove_from_followers": "Šalinti {name} iš sekėjų", "account.report": "Pranešti apie @{name}", - "account.requested": "Laukiama patvirtinimo. Spustelėk, kad atšauktum sekimo prašymą", "account.requested_follow": "{name} paprašė tave sekti", "account.requests_to_follow_you": "Prašymai sekti jus", "account.share": "Bendrinti @{name} profilį", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index ff532bb295..eb0a6cbe8a 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Ieraksti un atbildes", "account.remove_from_followers": "Dzēst sekotāju {name}", "account.report": "Ziņot par @{name}", - "account.requested": "Gaida apstiprinājumu. Nospied, lai atceltu sekošanas pieparasījumu", "account.requested_follow": "{name} nosūtīja Tev sekošanas pieprasījumu", "account.requests_to_follow_you": "Sekošanas pieprasījumi", "account.share": "Dalīties ar @{name} profilu", diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json index 67e561dcee..83153d4d73 100644 --- a/app/javascript/mastodon/locales/mk.json +++ b/app/javascript/mastodon/locales/mk.json @@ -33,7 +33,6 @@ "account.posts": "Тутови", "account.posts_with_replies": "Тутови и реплики", "account.report": "Пријави @{name}", - "account.requested": "Се чека одобрување. Кликни за да одкажиш барање за следење", "account.share": "Сподели @{name} профил", "account.show_reblogs": "Прикажи бустови од @{name}", "account.unblock": "Одблокирај @{name}", diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json index 63fc97c8ab..16c183b492 100644 --- a/app/javascript/mastodon/locales/ml.json +++ b/app/javascript/mastodon/locales/ml.json @@ -44,7 +44,6 @@ "account.posts": "പോസ്റ്റുകൾ", "account.posts_with_replies": "പോസ്റ്റുകളും മറുപടികളും", "account.report": "റിപ്പോർട്ട് ചെയ്യുക @{name}", - "account.requested": "അനുവാദത്തിനായി കാത്തിരിക്കുന്നു. പിന്തുടരാനുള്ള അപേക്ഷ റദ്ദാക്കുവാൻ ഞെക്കുക", "account.share": "@{name} ന്റെ പ്രൊഫൈൽ പങ്കിടുക", "account.show_reblogs": "@{name} ൽ നിന്നുള്ള ബൂസ്റ്റുകൾ കാണിക്കുക", "account.unblock": "@{name} തടഞ്ഞത് മാറ്റുക", diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json index 28d3ae9ab3..6a762c1527 100644 --- a/app/javascript/mastodon/locales/mr.json +++ b/app/javascript/mastodon/locales/mr.json @@ -49,7 +49,6 @@ "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.report": "@{name} ची तक्रार करा", - "account.requested": "Awaiting approval", "account.requested_follow": "{name} ने आपल्याला फॉलो करण्याची रिक्वेस्ट केली आहे", "account.share": "@{name} चे प्रोफाइल शेअर करा", "account.show_reblogs": "{name}चे सर्व बुस्ट्स दाखवा", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 2ffdf6cfd0..da72a2fc14 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -58,7 +58,6 @@ "account.posts": "Hantaran", "account.posts_with_replies": "Hantaran dan balasan", "account.report": "Laporkan @{name}", - "account.requested": "Menunggu kelulusan. Klik untuk batalkan permintaan ikut", "account.requested_follow": "{name} has requested to follow you", "account.share": "Kongsi profil @{name}", "account.show_reblogs": "Tunjukkan galakan daripada @{name}", diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json index a65eab5aa7..66af87dc62 100644 --- a/app/javascript/mastodon/locales/my.json +++ b/app/javascript/mastodon/locales/my.json @@ -51,7 +51,6 @@ "account.posts": "ပို့စ်များ", "account.posts_with_replies": "ပို့စ်နှင့် ရီပလိုင်းများ", "account.report": "တိုင်ကြားမည်{name}", - "account.requested": "ခွင့်ပြုချက်စောင့်နေသည်။ ဖော်လိုးပယ်ဖျက်ရန်နှိပ်ပါ", "account.requested_follow": "{name} က သင့်ကို စောင့်ကြည့်ရန် တောင်းဆိုထားသည်", "account.share": "{name}၏ပရိုဖိုင်ကိုမျှဝေပါ", "account.show_reblogs": "@{name} မှ မျှ၀ေမှုများကို ပြပါ\n", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index b070566564..0ee1958d5b 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -28,6 +28,7 @@ "account.disable_notifications": "停止佇 {name} PO文ê時通知我", "account.domain_blocking": "Teh封鎖ê網域", "account.edit_profile": "編輯個人資料", + "account.edit_profile_short": "編輯", "account.enable_notifications": "佇 {name} PO文ê時通知我", "account.endorse": "用個人資料推薦對方", "account.familiar_followers_many": "Hōo {name1}、{name2},kap {othersCount, plural, other {其他 lí熟似ê # ê lâng}} 跟tuè", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "無PO文", "account.follow": "跟tuè", "account.follow_back": "Tuè tńg去", + "account.follow_back_short": "Tuè tńg去", + "account.follow_request": "請求跟tuè lí", + "account.follow_request_cancel": "取消跟tuè", + "account.follow_request_cancel_short": "取消", + "account.follow_request_short": "請求", "account.followers": "跟tuè lí ê", "account.followers.empty": "Tsit ê用者iáu bô lâng跟tuè。", "account.followers_counter": "Hōo {count, plural, other {{count} ê lâng}}跟tuè", @@ -70,7 +76,6 @@ "account.posts_with_replies": "PO文kap回應", "account.remove_from_followers": "Kā {name} tuì跟tuè lí ê ê內底suá掉", "account.report": "檢舉 @{name}", - "account.requested": "Teh等待審查。Tshi̍h tsi̍t-ē 通取消跟tuè請求", "account.requested_follow": "{name} 請求跟tuè lí", "account.requests_to_follow_you": "請求跟tuè lí", "account.share": "分享 @{name} ê個人資料", @@ -864,6 +869,14 @@ "status.cancel_reblog_private": "取消轉送", "status.cannot_quote": "Lí bô允准引用tsit篇PO文。", "status.cannot_reblog": "Tsit篇PO文bē當轉送", + "status.contains_quote": "包含引用", + "status.context.loading": "載入其他回應", + "status.context.loading_error": "Bē當載入新回應", + "status.context.loading_more": "載入其他回應", + "status.context.loading_success": "回應lóng載入ah", + "status.context.more_replies_found": "Tshuē-tio̍h其他回應", + "status.context.retry": "Koh試", + "status.context.show": "顯示", "status.continued_thread": "接續ê討論線", "status.copy": "Khóo-pih PO文ê連結", "status.delete": "Thâi掉", @@ -900,6 +913,7 @@ "status.quote_error.revoked": "PO文已經hōo作者thâi掉", "status.quote_followers_only": "Kan-ta tuè我ê ē當引用PO文", "status.quote_manual_review": "作者ē hōo lâng人工審核", + "status.quote_noun": "引用", "status.quote_policy_change": "改通引用ê lâng", "status.quote_post_author": "引用 @{name} ê PO文ah", "status.quote_private": "私人PO文bē當引用", diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json index a0b99c5973..297feb2ce1 100644 --- a/app/javascript/mastodon/locales/ne.json +++ b/app/javascript/mastodon/locales/ne.json @@ -57,7 +57,6 @@ "account.posts_with_replies": "पोस्ट र जवाफहरू", "account.remove_from_followers": "{name}लाई फलोअरहरूबाट हटाउनुहोस्", "account.report": "@{name}लाई रिपोर्ट गर्नुहोस्", - "account.requested": "स्वीकृतिको पर्खाइमा। फलो अनुरोध रद्द गर्न क्लिक गर्नुहोस्", "account.requested_follow": "{name} ले तपाईंलाई फलो गर्न अनुरोध गर्नुभएको छ", "account.share": "@{name} को प्रोफाइल सेयर गर्नुहोस्", "account.show_reblogs": "@{name} को बूस्टहरू देखाउनुहोस्", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4250040040..289b49c39b 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Geen melding meer geven wanneer @{name} een bericht plaatst", "account.domain_blocking": "Server geblokkeerd", "account.edit_profile": "Profiel bewerken", + "account.edit_profile_short": "Bewerken", "account.enable_notifications": "Geef een melding wanneer @{name} een bericht plaatst", "account.endorse": "Op profiel weergeven", "account.familiar_followers_many": "Gevolgd door {name1}, {name2} en {othersCount, plural, one {één ander bekend account} other {# andere bekende accounts}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Geen berichten", "account.follow": "Volgen", "account.follow_back": "Terugvolgen", + "account.follow_back_short": "Terugvolgen", + "account.follow_request": "Verzoeken om te volgen", + "account.follow_request_cancel": "Verzoek annuleren", + "account.follow_request_cancel_short": "Annuleren", + "account.follow_request_short": "Verzoek", "account.followers": "Volgers", "account.followers.empty": "Deze gebruiker heeft nog geen volgers of heeft deze verborgen.", "account.followers_counter": "{count, plural, one {{counter} volger} other {{counter} volgers}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Reacties", "account.remove_from_followers": "{name} als volger verwijderen", "account.report": "@{name} rapporteren", - "account.requested": "Wachten op goedkeuring. Klik om het volgverzoek te annuleren", "account.requested_follow": "{name} wil jou graag volgen", "account.requests_to_follow_you": "Wil jou graag volgen", "account.share": "Account van @{name} delen", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index fe2c5fc925..4a38a01e78 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Tut og svar", "account.remove_from_followers": "Fjern {name} frå fylgjarane dine", "account.report": "Rapporter @{name}", - "account.requested": "Ventar på aksept. Klikk for å avbryta fylgjeførespurnaden", "account.requested_follow": "{name} har bedt om å få fylgja deg", "account.requests_to_follow_you": "Folk som vil fylgja deg", "account.share": "Del @{name} sin profil", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index fa03560194..7c574e7d8c 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Innlegg med svar", "account.remove_from_followers": "Fjern {name} fra følgere", "account.report": "Rapporter @{name}", - "account.requested": "Venter på godkjennelse. Klikk for å avbryte forespørselen", "account.requested_follow": "{name} har bedt om å få følge deg", "account.requests_to_follow_you": "Forespørsler om å følge deg", "account.share": "Del @{name} sin profil", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 0d87228ea1..87793d14fe 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -50,7 +50,6 @@ "account.posts": "Tuts", "account.posts_with_replies": "Tuts e responsas", "account.report": "Senhalar @{name}", - "account.requested": "Invitacion mandada. Clicatz per anullar", "account.requested_follow": "{name} a demandat a vos sègre", "account.share": "Partejar lo perfil a @{name}", "account.show_reblogs": "Mostrar los partatges de @{name}", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index cd20e4bce0..83614973fe 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -56,7 +56,6 @@ "account.posts": "ਪੋਸਟਾਂ", "account.posts_with_replies": "ਪੋਸਟਾਂ ਅਤੇ ਜਵਾਬ", "account.report": "{name} ਬਾਰੇ ਰਿਪੋਰਟ ਕਰੋ", - "account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ", "account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ", "account.share": "{name} ਦਾ ਪਰੋਫ਼ਾਇਲ ਸਾਂਝਾ ਕਰੋ", "account.statuses_counter": "{count, plural, one {{counter} ਪੋਸਟ} other {{counter} ਪੋਸਟਾਂ}}", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 2fb0a6f1bd..ef6769d6f7 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Wpisy i odpowiedzi", "account.remove_from_followers": "Usuń {name} z obserwujących", "account.report": "Zgłoś @{name}", - "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.requested_follow": "{name} chce cię zaobserwować", "account.requests_to_follow_you": "Prośby o obserwowanie", "account.share": "Udostępnij profil @{name}", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index e550b7463a..fb3b403d45 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Com respostas", "account.remove_from_followers": "Remover {name} dos seguidores", "account.report": "Denunciar @{name}", - "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação", "account.requested_follow": "{name} quer te seguir", "account.requests_to_follow_you": "Pediu para seguir você", "account.share": "Compartilhar perfil de @{name}", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 8167b9b49e..7646bd226d 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Parar de me notificar das publicações de @{name}", "account.domain_blocking": "A bloquear domínio", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Notificar-me das publicações de @{name}", "account.endorse": "Destacar no perfil", "account.familiar_followers_many": "Seguido por {name1}, {name2} e {othersCount, plural,one {mais uma pessoa que conhece} other {# outras pessoas que conhece}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sem publicações", "account.follow": "Seguir", "account.follow_back": "Seguir também", + "account.follow_back_short": "Seguir de volta", + "account.follow_request": "Pedir para seguir", + "account.follow_request_cancel": "Cancelar pedido", + "account.follow_request_cancel_short": "Cancelar", + "account.follow_request_short": "Pedido", "account.followers": "Seguidores", "account.followers.empty": "Ainda ninguém segue este utilizador.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Publicações e respostas", "account.remove_from_followers": "Remover {name} dos seguidores", "account.report": "Denunciar @{name}", - "account.requested": "A aguardar aprovação. Clica para cancelar o pedido para seguir", "account.requested_follow": "{name} pediu para seguir-te", "account.requests_to_follow_you": "Pediu para seguir-te", "account.share": "Partilhar o perfil @{name}", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index ce3dde8eea..9259226ee6 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -55,7 +55,6 @@ "account.posts": "Postări", "account.posts_with_replies": "Postări și răspunsuri", "account.report": "Raportează pe @{name}", - "account.requested": "Se așteaptă aprobarea. Apasă pentru a anula cererea de urmărire", "account.requested_follow": "{name} A cerut să vă urmărească", "account.share": "Distribuie profilul lui @{name}", "account.show_reblogs": "Afișează distribuirile de la @{name}", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 1416198a42..e8c96ac25f 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Посты и ответы", "account.remove_from_followers": "Убрать {name} из подписчиков", "account.report": "Пожаловаться на @{name}", - "account.requested": "Ожидает подтверждения. Нажмите для отмены запроса", "account.requested_follow": "{name} отправил(а) вам запрос на подписку", "account.requests_to_follow_you": "Отправил(а) вам запрос на подписку", "account.share": "Поделиться профилем @{name}", diff --git a/app/javascript/mastodon/locales/ry.json b/app/javascript/mastodon/locales/ry.json index 20cc8e8e4e..9426a9c5c0 100644 --- a/app/javascript/mastodon/locales/ry.json +++ b/app/javascript/mastodon/locales/ry.json @@ -51,7 +51,6 @@ "account.posts": "Публикації", "account.posts_with_replies": "Публикації тай удповіді", "account.report": "Скарговати ся на {name}", - "account.requested": "Чекат ся на пудтвердженя. Нажміт убы удмінити запрос на слідованя", "account.requested_follow": "Хосновач {name} просит ся пудписати ся на вас", "account.share": "Пошырити профіл хосновача {name}", "account.show_reblogs": "Указати друленя уд {name}", diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json index b9c13df058..1cd3cdb616 100644 --- a/app/javascript/mastodon/locales/sa.json +++ b/app/javascript/mastodon/locales/sa.json @@ -46,7 +46,6 @@ "account.posts": "पत्राणि", "account.posts_with_replies": "पत्राणि प्रत्युत्तराणि च", "account.report": "आविद्यताम् @{name}", - "account.requested": "स्वीकृतिः प्रतीक्ष्यते । नश्यतामित्यस्मिन्नुद्यतां निराकर्तुम् ।", "account.requested_follow": "{name} त्वामनुसर्तुमयाचीत्", "account.share": "@{name} मित्रस्य विवरणं विभाज्यताम्", "account.show_reblogs": "@{name} मित्रस्य प्रकाशनानि दृश्यन्ताम्", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index d126379384..781ea7c014 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -69,7 +69,6 @@ "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}", diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json index 52445feced..de24b5cdee 100644 --- a/app/javascript/mastodon/locales/sco.json +++ b/app/javascript/mastodon/locales/sco.json @@ -44,7 +44,6 @@ "account.posts": "Posts", "account.posts_with_replies": "Posts an repones", "account.report": "Clype @{name}", - "account.requested": "Haudin fir approval. Chap tae cancel follae request", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Shaw heezes frae @{name}", "account.unblock": "Undingie @{name}", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 107ef3f1cf..92216ed074 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -68,7 +68,6 @@ "account.posts_with_replies": "ලිපි සහ පිළිතුරු", "account.remove_from_followers": "අනුගාමිකයින්ගෙන් {name} ඉවත් කරන්න", "account.report": "@{name} වාර්තා කරන්න", - "account.requested": "අනුමැතිය බලාපොරොත්තුවෙන්. අනුගමනය කිරීමේ ඉල්ලීම අවලංගු කිරීමට ක්ලික් කරන්න.", "account.requested_follow": "{name} ඔබව අනුගමනය කිරීමට ඉල්ලා ඇත.", "account.requests_to_follow_you": "ඔබව අනුගමනය කිරීමට ඉල්ලීම්", "account.share": "@{name} ගේ පැතිකඩ බෙදාගන්න", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 771f52618c..9a408657b1 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -64,7 +64,6 @@ "account.posts": "Príspevky", "account.posts_with_replies": "Príspevky a odpovede", "account.report": "Nahlásiť @{name}", - "account.requested": "Čaká na schválenie. Žiadosť zrušíte kliknutím sem", "account.requested_follow": "{name} vás chce sledovať", "account.share": "Zdieľaj profil @{name}", "account.show_reblogs": "Zobrazovať zdieľania od @{name}", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 9127605dba..a87648bf7a 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -55,7 +55,6 @@ "account.posts": "Objave", "account.posts_with_replies": "Objave in odgovori", "account.report": "Prijavi @{name}", - "account.requested": "Čakanje na odobritev. Kliknite, da prekličete prošnjo za sledenje", "account.requested_follow": "{name} vam želi slediti", "account.share": "Deli profil osebe @{name}", "account.show_reblogs": "Pokaži izpostavitve osebe @{name}", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 56db9a5ac3..8ad097427b 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Mesazhe dhe përgjigje", "account.remove_from_followers": "Hiqe {name} nga ndjekësit", "account.report": "Raportojeni @{name}", - "account.requested": "Në pritje të miratimit. Që të anuloni kërkesën për ndjekje, klikojeni", "account.requested_follow": "{name} ka kërkuar t’ju ndjekë", "account.requests_to_follow_you": "Kërkesa për t’ju ndjekur", "account.share": "Ndajeni profilin e @{name} me të tjerët", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 8cb2e30cb5..0db607b12c 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -54,7 +54,6 @@ "account.posts": "Objave", "account.posts_with_replies": "Objave i odgovori", "account.report": "Prijavi @{name}", - "account.requested": "Čekanje odobrenja. Kliknite za otkazivanje zahteva za praćenje", "account.requested_follow": "{name} je zatražio da vas prati", "account.share": "Podeli profil korisnika @{name}", "account.show_reblogs": "Prikaži podržavanja od korisnika @{name}", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 485109105b..48e6b49968 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -54,7 +54,6 @@ "account.posts": "Објаве", "account.posts_with_replies": "Објаве и одговори", "account.report": "Пријави @{name}", - "account.requested": "Чекање одобрења. Кликните за отказивање захтева за праћење", "account.requested_follow": "{name} је затражио да вас прати", "account.share": "Подели профил корисника @{name}", "account.show_reblogs": "Прикажи подржавања од корисника @{name}", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index e56c9da326..97a4b6fd4f 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "Inlägg och svar", "account.remove_from_followers": "Ta bort {name} från följare", "account.report": "Rapportera @{name}", - "account.requested": "Inväntar godkännande. Klicka för att ta tillbaka din begäran om att få följa", "account.requested_follow": "{name} har begärt att följa dig", "account.requests_to_follow_you": "Fråga om att följa dig", "account.share": "Dela @{name}s profil", diff --git a/app/javascript/mastodon/locales/szl.json b/app/javascript/mastodon/locales/szl.json index 55913be924..709b0f9e89 100644 --- a/app/javascript/mastodon/locales/szl.json +++ b/app/javascript/mastodon/locales/szl.json @@ -19,7 +19,6 @@ "account.mute": "Wycisz @{name}", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", - "account.requested": "Awaiting approval", "account_note.placeholder": "Click to add a note", "column.pins": "Pinned toot", "community.column_settings.media_only": "Media only", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 10d461e9e8..8ebbd17117 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -37,7 +37,6 @@ "account.posts": "டூட்டுகள்", "account.posts_with_replies": "Toots மற்றும் பதில்கள்", "account.report": "@{name} -ஐப் புகாரளி", - "account.requested": "ஒப்புதலுக்காகக் காத்திருக்கிறது. பின்தொடரும் கோரிக்கையை நீக்க அழுத்தவும்", "account.share": "@{name} உடைய விவரத்தை பகிர்", "account.show_reblogs": "காட்டு boosts இருந்து @{name}", "account.unblock": "@{name} மீது தடை நீக்குக", diff --git a/app/javascript/mastodon/locales/tai.json b/app/javascript/mastodon/locales/tai.json index 3799455050..507894ba14 100644 --- a/app/javascript/mastodon/locales/tai.json +++ b/app/javascript/mastodon/locales/tai.json @@ -8,7 +8,6 @@ "account.mention": "Thê-khí @{name}", "account.posts": "Huah-siann", "account.posts_with_replies": "Huah-siann kah huê-ìng", - "account.requested": "Tán-thāi phue-tsún", "account_note.placeholder": "Tiám tsi̍t-ē ka-thiam pī-tsù", "column.pins": "Tah thâu-tsîng ê huah-siann", "community.column_settings.media_only": "Kan-na muî-thé", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index cbdea417b7..015fe3f12a 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -24,7 +24,6 @@ "account.posts": "టూట్లు", "account.posts_with_replies": "టూట్లు మరియు ప్రత్యుత్తరములు", "account.report": "@{name}పై ఫిర్యాదుచేయు", - "account.requested": "ఆమోదం కోసం వేచి ఉంది. అభ్యర్థనను రద్దు చేయడానికి క్లిక్ చేయండి", "account.share": "@{name} యొక్క ప్రొఫైల్ను పంచుకోండి", "account.show_reblogs": "@{name}నుంచి బూస్ట్ లను చూపించు", "account.unblock": "@{name}పై బ్లాక్ ను తొలగించు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index a1fade41ca..ed71791f73 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -64,7 +64,6 @@ "account.posts": "โพสต์", "account.posts_with_replies": "โพสต์และการตอบกลับ", "account.report": "รายงาน @{name}", - "account.requested": "กำลังรอการอนุมัติ คลิกเพื่อยกเลิกคำขอติดตาม", "account.requested_follow": "{name} ได้ขอติดตามคุณ", "account.share": "แชร์โปรไฟล์ของ @{name}", "account.show_reblogs": "แสดงการดันจาก @{name}", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index dbc6b4b871..3eaf22911c 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "toki ale", "account.remove_from_followers": "sijelo kute la o weka e sijelo \"{name}\".", "account.report": "jan @{name} la o toki e ike tawa lawa", - "account.requested": "jan ni o ken e kute sina", "account.requested_follow": "jan {name} li wile kute e sina", "account.requests_to_follow_you": "jan ni li wile kute e sina", "account.share": "o pana e lipu jan @{name}", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index cc94d324d7..335fa07d80 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -28,6 +28,7 @@ "account.disable_notifications": "@{name} kişisinin gönderi bildirimlerini kapat", "account.domain_blocking": "Alan adını engelleme", "account.edit_profile": "Profili düzenle", + "account.edit_profile_short": "Düzenle", "account.enable_notifications": "@{name} kişisinin gönderi bildirimlerini aç", "account.endorse": "Profilimde öne çıkar", "account.familiar_followers_many": "{name1}, {name2}, {othersCount, plural, one {# diğer} other {# diğer}} bildiğiniz kişi tarafından takip ediliyor", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Gönderi yok", "account.follow": "Takip et", "account.follow_back": "Geri takip et", + "account.follow_back_short": "Geri takip et", + "account.follow_request": "Takip isteği gönder", + "account.follow_request_cancel": "İsteği iptal et", + "account.follow_request_cancel_short": "İptal", + "account.follow_request_short": "İstek", "account.followers": "Takipçi", "account.followers.empty": "Henüz kimse bu kullanıcıyı takip etmiyor.", "account.followers_counter": "{count, plural, one {{counter} takipçi} other {{counter} takipçi}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Gönderiler ve yanıtlar", "account.remove_from_followers": "{name} takipçilerinden kaldır", "account.report": "@{name} adlı kişiyi bildir", - "account.requested": "Onay bekleniyor. Takip isteğini iptal etmek için tıklayın", "account.requested_follow": "{name} size takip isteği gönderdi", "account.requests_to_follow_you": "Size takip isteği gönderdi", "account.share": "@{name} adlı kişinin profilini paylaş", diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json index 4c454c37fb..d1ef3cd3b0 100644 --- a/app/javascript/mastodon/locales/tt.json +++ b/app/javascript/mastodon/locales/tt.json @@ -49,7 +49,6 @@ "account.posts": "Язма", "account.posts_with_replies": "Язма һәм җавап", "account.report": "@{name} кулланучыга шикаять итү", - "account.requested": "Awaiting approval", "account.requested_follow": "{name} Сезгә язылу соравын җиберде", "account.share": "@{name} профиле белән уртаклашу", "account.show_reblogs": "Күрсәтергә көчәйтү нче @{name}", diff --git a/app/javascript/mastodon/locales/ug.json b/app/javascript/mastodon/locales/ug.json index 527457ca37..c87d8ee5dd 100644 --- a/app/javascript/mastodon/locales/ug.json +++ b/app/javascript/mastodon/locales/ug.json @@ -13,7 +13,6 @@ "account.posts": "يازما", "account.posts_with_replies": "يازما ۋە ئىنكاس", "account.report": "@{name} نى پاش قىل", - "account.requested": "Awaiting approval", "account_note.placeholder": "چېكىلسە ئىزاھات قوشىدۇ", "column.pins": "چوققىلانغان يازما", "community.column_settings.media_only": "ۋاسىتەلا", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 02370341df..99dd177b2b 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -61,7 +61,6 @@ "account.posts": "Дописи", "account.posts_with_replies": "Дописи й відповіді", "account.report": "Поскаржитися на @{name}", - "account.requested": "Очікує підтвердження. Натисніть, щоб скасувати запит на підписку", "account.requested_follow": "{name} надсилає запит на стеження", "account.share": "Поділитися профілем @{name}", "account.show_reblogs": "Показати поширення від @{name}", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index b1db5d819f..e06ec34e6e 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -46,7 +46,6 @@ "account.posts": "ٹوٹ", "account.posts_with_replies": "ٹوٹ اور جوابات", "account.report": "@{name} اطلاع کریں", - "account.requested": "منظوری کا منتظر۔ درخواستِ پیروی منسوخ کرنے کیلئے کلک کریں", "account.requested_follow": "{name} آپ کو فالو کرنا چھاتا ہے۔", "account.share": "@{name} کے مشخص کو بانٹیں", "account.show_reblogs": "@{name} کی افزائشات کو دکھائیں", diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json index d4b37a32f7..2e4038edd3 100644 --- a/app/javascript/mastodon/locales/uz.json +++ b/app/javascript/mastodon/locales/uz.json @@ -44,7 +44,6 @@ "account.posts": "Postlar", "account.posts_with_replies": "Xabarlar va javoblar", "account.report": "@{name} xabar berish", - "account.requested": "Tasdiqlash kutilmoqda. Kuzatuv soʻrovini bekor qilish uchun bosing", "account.requested_follow": "{name} sizni kuzatishni soʻradi", "account.share": "@{name} profilini ulashing", "account.show_reblogs": "@{name} dan bootlarni ko'rsatish", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index c4e21bbe7e..72a1cd23ae 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Tắt thông báo khi @{name} đăng tút", "account.domain_blocking": "Máy chủ đang chủ", "account.edit_profile": "Sửa hồ sơ", + "account.edit_profile_short": "Sửa", "account.enable_notifications": "Nhận thông báo khi @{name} đăng tút", "account.endorse": "Nêu bật người này", "account.familiar_followers_many": "Theo dõi bởi {name1}, {name2} và {othersCount, plural, other {# người khác mà bạn biết}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Chưa có tút", "account.follow": "Theo dõi", "account.follow_back": "Theo dõi lại", + "account.follow_back_short": "Theo dõi lại", + "account.follow_request": "Yêu cầu theo dõi", + "account.follow_request_cancel": "Hủy yêu cầu", + "account.follow_request_cancel_short": "Hủy bỏ", + "account.follow_request_short": "Yêu cầu", "account.followers": "Người theo dõi", "account.followers.empty": "Chưa có người theo dõi nào.", "account.followers_counter": "{count, plural, other {{counter} Người theo dõi}}", @@ -70,7 +76,6 @@ "account.posts_with_replies": "Trả lời", "account.remove_from_followers": "Xóa người theo dõi {name}", "account.report": "Báo cáo @{name}", - "account.requested": "Đang chờ chấp thuận. Nhấp vào đây để hủy yêu cầu theo dõi", "account.requested_follow": "{name} yêu cầu theo dõi bạn", "account.requests_to_follow_you": "Yêu cầu theo dõi bạn", "account.share": "Chia sẻ @{name}", diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json index ff37d75a07..0f0be9da42 100644 --- a/app/javascript/mastodon/locales/zgh.json +++ b/app/javascript/mastodon/locales/zgh.json @@ -14,7 +14,6 @@ "account.muted": "ⵉⵜⵜⵓⵥⵉⵥⵏ", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", - "account.requested": "Awaiting approval", "account.share": "ⴱⴹⵓ ⵉⴼⵔⵙ ⵏ @{name}", "account.unfollow": "ⴽⴽⵙ ⴰⴹⴼⴼⵓⵕ", "account_note.placeholder": "Click to add a note", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 1c52a6334f..a73a218fa5 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -70,7 +70,6 @@ "account.posts_with_replies": "嘟文和回复", "account.remove_from_followers": "从关注者中移除 {name}", "account.report": "举报 @{name}", - "account.requested": "正在等待对方同意。点击取消发送关注请求", "account.requested_follow": "{name} 向你发送了关注请求", "account.requests_to_follow_you": "请求关注你", "account.share": "分享 @{name} 的个人资料", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 1637e1cf58..5a113ae8ae 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -65,7 +65,6 @@ "account.posts": "帖文", "account.posts_with_replies": "帖文與回覆", "account.report": "檢舉 @{name}", - "account.requested": "正在等待核准。按一下以取消追蹤請求", "account.requested_follow": "{name} 要求追蹤你", "account.share": "分享 @{name} 的個人檔案", "account.show_reblogs": "顯示 @{name} 的轉推", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 02edd5f452..d0da9aa718 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -28,6 +28,7 @@ "account.disable_notifications": "取消來自 @{name} 嘟文的通知", "account.domain_blocking": "封鎖中網域", "account.edit_profile": "編輯個人檔案", + "account.edit_profile_short": "編輯", "account.enable_notifications": "當 @{name} 嘟文時通知我", "account.endorse": "於個人檔案推薦對方", "account.familiar_followers_many": "被 {name1}、{name2}、及 {othersCount, plural, other {其他您認識的 # 人}} 跟隨", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "沒有嘟文", "account.follow": "跟隨", "account.follow_back": "跟隨回去", + "account.follow_back_short": "跟隨回去", + "account.follow_request": "要求跟隨您", + "account.follow_request_cancel": "取消跟隨請求", + "account.follow_request_cancel_short": "取消", + "account.follow_request_short": "跟隨請求", "account.followers": "跟隨者", "account.followers.empty": "尚未有人跟隨這位使用者。", "account.followers_counter": "被 {count, plural, other {{count} 人}}跟隨", @@ -70,7 +76,6 @@ "account.posts_with_replies": "嘟文與回覆", "account.remove_from_followers": "自跟隨者中移除 {name}", "account.report": "檢舉 @{name}", - "account.requested": "正在等候審核。按一下以取消跟隨請求", "account.requested_follow": "{name} 要求跟隨您", "account.requests_to_follow_you": "要求跟隨您", "account.share": "分享 @{name} 的個人檔案", diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 9ec00b897a..61fb800301 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -33,6 +33,11 @@ kab: new_email: Imayl amaynut submit: Beddel imayl title: Beddel imayl-ik s %{username} + change_role: + edit_roles: Sefrek timlilin n usqdac + label: Snifel tamlilt + no_role: War tamlilt + title: Snifel tamlilt n %{username} confirm: Sentem confirmed: Yettwasentem confirming: Asentem @@ -95,6 +100,7 @@ kab: reset: Wennez reset_password: Beddel awal uffir resubscribe: Ales ajerred + role: Tamlilt search: Nadi search_same_ip: Imseqdacen-nniḍen s tansa IP am tinn-ik security: Taɣellist @@ -121,6 +127,7 @@ kab: whitelisted: Deg tebdert tamellalt action_logs: action_types: + change_role_user: Snifel tamlilt n useqdac confirm_user: Sentem aseqdac create_announcement: Rnu-d ulɣu create_custom_emoji: Rnu imujit udmawan @@ -128,6 +135,7 @@ kab: create_domain_block: Rnu-d asewḥel n taɣult create_ip_block: Rnu alugen n IP create_unavailable_domain: Rnu-d taɣult ur nelli ara + create_user_role: Snulfu-d tamlilt destroy_announcement: Kkes ulɣu destroy_custom_emoji: Kkes imujit udmawan destroy_domain_allow: Kkes taɣult yettusirgen @@ -135,6 +143,7 @@ kab: destroy_ip_block: Kkes alugen n IP destroy_status: Kkes tasufeɣt destroy_unavailable_domain: Kkes taɣult ur nelli ara + destroy_user_role: Senger tamlilt disable_2fa_user: Gdel 2FA disable_custom_emoji: Sens imujit udmawan disable_user: Sens aseqdac @@ -150,6 +159,7 @@ kab: update_custom_emoji: Leqqem imuji udmawan update_domain_block: Leqqem iḥder n taɣult update_status: Leqqem tasufeɣt + update_user_role: Leqqem tamlilt actions: assigned_to_self_report_html: "%{name} imudd aneqqis %{target} i yiman-nsen" create_account_warning_html: "%{name} yuzen alɣu i %{target}" @@ -400,7 +410,10 @@ kab: privileges: administrator: Anedbal manage_federation: Sefrek Tafidiralit + manage_roles: Sefrek ilugan + manage_rules: Sefrek ilugan manage_settings: Asefrek n iɣewwaṛen + manage_users: Sefrek iqeddacen view_dashboard: Timẓriwt n tfelwit rules: add_new: Rnu alugen @@ -566,10 +579,10 @@ kab: migrate_account: Gujj ɣer umiḍan nniḍen or_log_in_with: Neɣ eqqen s progress: - confirm: Sentem imayl - details: Isalli-inek + confirm: Asentem n imayl + details: Isalli-inek·inem review: Tamuɣli-nneɣ - rules: Qbel ilugan + rules: Abal n ilugan providers: cas: CAS saml: SAML @@ -599,6 +612,8 @@ kab: account_status: Addad n umiḍan functional: Amiḍan-inek·inem yetteddu s lekmal-is. use_security_key: Seqdec tasarut n teɣlist + user_agreement_html: Ɣriɣ yerna qebleɣ " target="_blank">tiwtilin ne useqdec akked tsertit n tbaḍnit + user_privacy_agreement_html: Ɣriɣ yerna qebleɣ tasertit n tbaḍnit author_attribution: example_title: Amedya n weḍris more_from_html: Ugar s ɣur %{name} @@ -887,6 +902,8 @@ kab: one: "%{count} n tbidyutt" other: "%{count} n tbidyutin" edited_at_html: Tettwaẓreg ass n %{date} + pin_errors: + reblog: Azuzer ur yezmir ara ad yili d unṭiḍ quote_policies: followers: Imeḍfaṛen kan nobody: Nekki kan @@ -900,6 +917,7 @@ kab: unlisted: Azayez asusam statuses_cleanup: enabled: Tukksa n tsuffaɣ tiqburin s wudem awurman + keep_pinned: Eǧǧ tisuffaɣ tunṭiḍin min_age: '1209600': 2 n yimalasen '15778476': 6 n wayyuren diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 21474c6d70..20e53b4a86 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -213,7 +213,7 @@ nan: enable_user: 啟用口座 memorialize_account: 設做故人ê口座 promote_user: Kā用者升級 - publish_terms_of_service: 公佈服務ê使用規則 + publish_terms_of_service: 公佈服務規定 reject_appeal: 拒絕申訴 reject_user: 拒絕用者 remove_avatar_user: Thâi掉標頭 @@ -281,7 +281,7 @@ nan: enable_user_html: "%{name} kā 用者 %{target} 設做允准登入" memorialize_account_html: "%{name} kā %{target} 設做故人口座" promote_user_html: "%{name} kā 用者 %{target} 升級" - publish_terms_of_service_html: "%{name} 公佈服務規則ê更新" + publish_terms_of_service_html: "%{name} 公佈服務規定ê更新" reject_appeal_html: "%{name} 拒絕 %{target} 所寫ê tuì管理決定ê投訴" reject_user_html: "%{name} 拒絕 %{target} ê 註冊" remove_avatar_user_html: "%{name} thâi掉 %{target} ê標頭" @@ -787,7 +787,7 @@ nan: add_new: 加添規則 add_translation: 加添翻譯 delete: Thâi掉 - description_html: 雖bóng大部份ê lóng講有讀kap同意使用規則,m̄-koh攏無讀了,直到發生問題ê時。提供in列單ē當hōo tsi̍t kái看服侍器ê規則khah快。請試kā個別ê規則寫kah短koh簡單,m̄-kú m̄通kā in拆做tsē-tsē分開ê項目。 + description_html: 雖bóng大部份ê lóng講有讀kap同意服務規定,m̄-koh攏無讀了,直到發生問題ê時。提供in列單ē當hōo tsi̍t kái看服侍器ê規則khah快。請試kā個別ê規則寫kah短koh簡單,m̄-kú m̄通kā in拆做tsē-tsē分開ê項目。 edit: 編輯規則 empty: Iáu bē定義服侍器ê規則。 move_down: Suá khah落來 @@ -972,7 +972,36 @@ nan: search: Tshiau-tshuē title: Hashtag updated_msg: Hashtag設定更新成功ah + terms_of_service: + back: 轉去服務規定 + changelog: Siánn物有改 + create: 用lí家tī ê + current: 目前ê + draft: 草稿 + generate: 用枋模 + generates: + action: 生成 + chance_to_review_html: "所生成ê服務規定bē自動發布。Lí ē有機會來看結果。請添必要ê詳細來繼續。" + explanation_html: 提供ê服務規定kan-ta做參考用,bē當成做任何法律建議。請照lí ê情形kap有ê特別ê法律問題諮詢lí ê法律顧問。 + title: 設定服務規定 + going_live_on_html: 目前規定,tuì %{date} 施行 + history: 歷史 + live: 目前ê + no_history: Iáu無半項服務規定ê改變記錄。 + no_terms_of_service_html: Lí目前iáu無設定任何服務規定。服務規定是beh提供明確性,兼保護lí佇kap用者ê爭議中毋免承受可能ê責任。 + notified_on_html: 佇 %{date} 通知ê用者 + notify_users: 通知用者 trends: + preview_card_providers: + title: 發布者 + rejected: 拒絕ê + statuses: + allow: 允准PO文 + allow_account: 允准作者 + confirm_allow: Lí kám確定beh允准所揀ê狀態? + confirm_allow_account: Lí kám確定beh允准所揀ê口座? + confirm_disallow: Lí kám確定無愛允准所揀ê狀態? + confirm_disallow_account: Lí kám確定無愛允准所揀ê口座? tags: dashboard: tag_languages_dimension: Tsia̍p用ê語言 From bbb698937a55ad89bc24a4ae58cda2b5ab20200a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:29:15 +0200 Subject: [PATCH 35/58] chore(deps): update dependency pino to v9.12.0 (#36287) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index beca808c93..17fbda9670 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7436,13 +7436,6 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.1.1": - version: 3.3.0 - resolution: "fast-redact@npm:3.3.0" - checksum: 10c0/d81562510681e9ba6404ee5d3838ff5257a44d2f80937f5024c099049ff805437d0fae0124458a7e87535cc9dcf4de305bb075cab8f08d6c720bbc3447861b4e - languageName: node - linkType: hard - "fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -10346,11 +10339,10 @@ __metadata: linkType: hard "pino@npm:^9.0.0": - version: 9.11.0 - resolution: "pino@npm:9.11.0" + version: 9.12.0 + resolution: "pino@npm:9.12.0" dependencies: atomic-sleep: "npm:^1.0.0" - fast-redact: "npm:^3.1.1" on-exit-leak-free: "npm:^2.1.0" pino-abstract-transport: "npm:^2.0.0" pino-std-serializers: "npm:^7.0.0" @@ -10358,11 +10350,12 @@ __metadata: quick-format-unescaped: "npm:^4.0.3" real-require: "npm:^0.2.0" safe-stable-stringify: "npm:^2.3.1" + slow-redact: "npm:^0.3.0" sonic-boom: "npm:^4.0.1" thread-stream: "npm:^3.0.0" bin: pino: bin.js - checksum: 10c0/ba908f95b61fa2c2d6c432e1f39a4394cc0dbf356c4f8837bd9c07538d749699b78204a5557e6050870f2988c25c3f0b6a88693d4bd185ebeef57d75a3b25e38 + checksum: 10c0/5cfe093e972a8471a90f7f380c01379eed3fd937038acb97d1de9180f097c044855ca89a2e70baa699aec3e8dcaec037d03e2c90dde235102a3e17b40f54cc1f languageName: node linkType: hard @@ -12287,6 +12280,13 @@ __metadata: languageName: node linkType: hard +"slow-redact@npm:^0.3.0": + version: 0.3.0 + resolution: "slow-redact@npm:0.3.0" + checksum: 10c0/bb2f77830f64fb01079849e0c6433c15e782b88cccb82d4b0d62ce216307cf514ea3f92e9b2c6ae1b1d613ac7743305d5f0324e94c9dc8e41908939456248f9a + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" From 8779bbc4c1dd824b31b649fdafec9f76ea62742a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:29:37 +0200 Subject: [PATCH 36/58] chore(deps): update rubocop (non-major) to v1.81.1 (#36277) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index db436c01be..1190cab586 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -766,7 +766,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.81.0) + rubocop (1.81.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -790,7 +790,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rails (2.33.3) + rubocop-rails (2.33.4) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) From adbd57e5a9de13ec8a335f4344116b8df6db0ac2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:29:52 +0200 Subject: [PATCH 37/58] chore(deps): update dependency rubyzip to v3.1.1 (#36278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1190cab586..582d55840e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -812,7 +812,7 @@ GEM ruby-vips (2.2.5) ffi (~> 1.12) logger - rubyzip (3.1.0) + rubyzip (3.1.1) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) safety_net_attestation (0.5.0) From 9f1a12b7492cad88aad8f7e1f49a5ee2c14ce3ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:30:01 +0200 Subject: [PATCH 38/58] chore(deps): update dependency @vitejs/plugin-react to v5.0.4 (#36282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 17fbda9670..b6090455e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3329,10 +3329,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.35": - version: 1.0.0-beta.35 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.35" - checksum: 10c0/feb6ab8f77ef2bde675099409c3ccd6a168f35a3c3e88482df3ca42494260fd42befe36e8e90ce358847a12aaab94cd8fe7069cf1e905edf91eb411d933906d9 +"@rolldown/pluginutils@npm:1.0.0-beta.38": + version: 1.0.0-beta.38 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.38" + checksum: 10c0/8353ec2528349f79e27d1a3193806725b85830da334e935cbb606d88c1177c58ea6519c578e4e93e5f677f5b22aecb8738894dbed14603e14b6bffe3facf1002 languageName: node linkType: hard @@ -4817,18 +4817,18 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^5.0.0": - version: 5.0.3 - resolution: "@vitejs/plugin-react@npm:5.0.3" + version: 5.0.4 + resolution: "@vitejs/plugin-react@npm:5.0.4" dependencies: "@babel/core": "npm:^7.28.4" "@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.35" + "@rolldown/pluginutils": "npm:1.0.0-beta.38" "@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 - checksum: 10c0/3fc071455630a0584c170c544d20fc3edaccfb60a1e03ea14ca76f049f2657eb645aba9c216db016b8d70e4f894285a78fcd92ef63a2fcfa7864da378ac52761 + checksum: 10c0/bb9360a4b4c0abf064d22211756b999faf23889ac150de490590ca7bd029b0ef7f4cd8ba3a32b86682a62d46fb7bebd75b3fa9835c57c78123f4a646de2e0136 languageName: node linkType: hard From dc72719f4cf1b34f2293733e1d669f2bc844d3aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:30:10 +0000 Subject: [PATCH 39/58] chore(deps): update dependency hiredis-client to v0.26.1 (#36286) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 582d55840e..fa785d6876 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -300,8 +300,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.26.0) - redis-client (= 0.26.0) + hiredis-client (0.26.1) + redis-client (= 0.26.1) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -720,7 +720,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.26.0) + redis-client (0.26.1) connection_pool regexp_parser (2.11.3) reline (0.6.2) From 4d7c208da365ea51c458754946a3206351a21bdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:45:31 +0000 Subject: [PATCH 40/58] chore(deps): update node.js to 22.20 (#36252) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 6e77d0a749..403f75d038 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.19 +22.20 From 150f0fcba5585782e2cac49d971904f02d5e6fbd Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 29 Sep 2025 14:05:48 +0200 Subject: [PATCH 41/58] Add support for numeric-based URIs for local accounts (#32724) --- app/controllers/accounts_controller.rb | 4 + .../activitypub/likes_controller.rb | 2 +- .../activitypub/outboxes_controller.rb | 4 +- .../activitypub/replies_controller.rb | 10 +- .../activitypub/shares_controller.rb | 2 +- .../concerns/account_owned_concern.rb | 6 +- .../follower_accounts_controller.rb | 6 +- .../following_accounts_controller.rb | 8 +- app/lib/activitypub/tag_manager.rb | 63 ++++-- app/models/account.rb | 2 + app/models/concerns/account/interactions.rb | 5 +- app/workers/activitypub/delivery_worker.rb | 2 +- config/routes.rb | 34 +++- ...0250924170259_add_id_scheme_to_accounts.rb | 7 + db/schema.rb | 3 +- spec/lib/activitypub/tag_manager_spec.rb | 179 +++++++++++++++++- .../concerns/account/interactions_spec.rb | 16 ++ spec/requests/accounts_spec.rb | 8 + .../synchronize_followers_service_spec.rb | 2 +- .../activitypub/delivery_worker_spec.rb | 13 +- 20 files changed, 324 insertions(+), 52 deletions(-) create mode 100644 db/migrate/20250924170259_add_id_scheme_to_accounts.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c3131edce9..efd0c92cef 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -71,6 +71,10 @@ class AccountsController < ApplicationController params[:username] end + def account_id_param + params[:id] + end + def skip_temporary_suspension_response? request.format == :json end diff --git a/app/controllers/activitypub/likes_controller.rb b/app/controllers/activitypub/likes_controller.rb index 4aa6a4a771..e875517b02 100644 --- a/app/controllers/activitypub/likes_controller.rb +++ b/app/controllers/activitypub/likes_controller.rb @@ -28,7 +28,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController def likes_collection_presenter ActivityPub::CollectionPresenter.new( - id: account_status_likes_url(@account, @status), + id: ActivityPub::TagManager.instance.likes_uri_for(@status), type: :unordered, size: @status.favourites_count ) diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index a9476b806f..928977768b 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -73,6 +73,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController end def set_account - @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative + return super if params[:account_username].present? || params[:account_id].present? + + @account = Account.representative end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 0a19275d38..1959f50d67 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -37,7 +37,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def replies_collection_presenter page = ActivityPub::CollectionPresenter.new( - id: account_status_replies_url(@account, @status, page_params), + id: ActivityPub::TagManager.instance.replies_uri_for(@status, page_params), type: :unordered, part_of: account_status_replies_url(@account, @status), next: next_page, @@ -47,7 +47,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController return page if page_requested? ActivityPub::CollectionPresenter.new( - id: account_status_replies_url(@account, @status), + id: ActivityPub::TagManager.instance.replies_uri_for(@status), type: :unordered, first: page ) @@ -66,8 +66,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController # Only consider remote accounts return nil if @replies.size < DESCENDANTS_LIMIT - account_status_replies_url( - @account, + ActivityPub::TagManager.instance.replies_uri_for( @status, page: true, min_id: @replies&.last&.id, @@ -77,8 +76,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController # For now, we're serving only self-replies, but next page might be other accounts next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT - account_status_replies_url( - @account, + ActivityPub::TagManager.instance.replies_uri_for( @status, page: true, min_id: next_only_other_accounts ? nil : @replies&.last&.id, diff --git a/app/controllers/activitypub/shares_controller.rb b/app/controllers/activitypub/shares_controller.rb index 65b4a5b383..2d1e389885 100644 --- a/app/controllers/activitypub/shares_controller.rb +++ b/app/controllers/activitypub/shares_controller.rb @@ -28,7 +28,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController def shares_collection_presenter ActivityPub::CollectionPresenter.new( - id: account_status_shares_url(@account, @status), + id: ActivityPub::TagManager.instance.shares_uri_for(@status), type: :unordered, size: @status.reblogs_count ) diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 2b132417f7..7b3cd4d3ea 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -18,7 +18,11 @@ module AccountOwnedConcern end def set_account - @account = Account.find_local!(username_param) + @account = username_param.present? ? Account.find_local!(username_param) : Account.local.find(account_id_param) + end + + def account_id_param + params[:account_id] end def username_param diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f4c7b37088..e9727b756a 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -60,17 +60,17 @@ class FollowerAccountsController < ApplicationController def collection_presenter if page_requested? ActivityPub::CollectionPresenter.new( - id: account_followers_url(@account, page: params.fetch(:page, 1)), + id: page_url(params.fetch(:page, 1)), type: :ordered, size: @account.followers_count, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) }, - part_of: account_followers_url(@account), + part_of: ActivityPub::TagManager.instance.followers_uri_for(@account), next: next_page_url, prev: prev_page_url ) else ActivityPub::CollectionPresenter.new( - id: account_followers_url(@account), + id: ActivityPub::TagManager.instance.followers_uri_for(@account), type: :ordered, size: @account.followers_count, first: page_url(1) diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 268fad96d0..803d6e342a 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -49,7 +49,7 @@ class FollowingAccountsController < ApplicationController end def page_url(page) - account_following_index_url(@account, page: page) unless page.nil? + ActivityPub::TagManager.instance.following_uri_for(@account, page: page) unless page.nil? end def next_page_url @@ -63,17 +63,17 @@ class FollowingAccountsController < ApplicationController def collection_presenter if page_requested? ActivityPub::CollectionPresenter.new( - id: account_following_index_url(@account, page: params.fetch(:page, 1)), + id: page_url(params.fetch(:page, 1)), type: :ordered, size: @account.following_count, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) }, - part_of: account_following_index_url(@account), + part_of: ActivityPub::TagManager.instance.following_uri_for(@account), next: next_page_url, prev: prev_page_url ) else ActivityPub::CollectionPresenter.new( - id: account_following_index_url(@account), + id: ActivityPub::TagManager.instance.following_uri_for(@account), type: :ordered, size: @account.following_count, first: page_url(1) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 870cbea7e4..43574d3657 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -39,13 +39,25 @@ class ActivityPub::TagManager case target.object_type when :person - target.instance_actor? ? instance_actor_url : account_url(target) + if target.instance_actor? + instance_actor_url + elsif target.numeric_ap_id? + ap_account_url(target.id) + else + account_url(target) + end when :conversation context_url(target) unless target.parent_account_id.nil? || target.parent_status_id.nil? when :note, :comment, :activity - return activity_account_status_url(target.account, target) if target.reblog? + if target.account.numeric_ap_id? + return activity_ap_account_status_url(target.account, target) if target.reblog? - account_status_url(target.account, target) + ap_account_status_url(target.account.id, target) + else + return activity_account_status_url(target.account, target) if target.reblog? + + account_status_url(target.account, target) + end when :emoji emoji_url(target) when :flag @@ -57,7 +69,7 @@ class ActivityPub::TagManager return quote.approval_uri unless quote.quoted_account&.local? return if check_approval && !quote.accepted? - account_quote_authorization_url(quote.quoted_account, quote) + quote.quoted_account.numeric_ap_id? ? ap_account_quote_authorization_url(quote.quoted_account_id, quote) : account_quote_authorization_url(quote.quoted_account, quote) end def key_uri_for(target) @@ -68,6 +80,10 @@ class ActivityPub::TagManager account_url(username: username) end + def uri_for_account_id(id) + ap_account_url(id: id) + end + def generate_uri_for(_target) URI.join(root_url, 'payloads', SecureRandom.uuid) end @@ -75,7 +91,7 @@ class ActivityPub::TagManager def activity_uri_for(target) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - activity_account_status_url(target.account, target) + target.account.numeric_ap_id? ? activity_ap_account_status_url(target.account.id, target) : activity_account_status_url(target.account, target) end def context_uri_for(target, page_params = nil) @@ -87,49 +103,61 @@ class ActivityPub::TagManager def replies_uri_for(target, page_params = nil) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - account_status_replies_url(target.account, target, page_params) + target.account.numeric_ap_id? ? ap_account_status_replies_url(target.account.id, target, page_params) : account_status_replies_url(target.account, target, page_params) end def likes_uri_for(target) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - account_status_likes_url(target.account, target) + target.account.numeric_ap_id? ? ap_account_status_likes_url(target.account.id, target) : account_status_likes_url(target.account, target) end def shares_uri_for(target) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - account_status_shares_url(target.account, target) + target.account.numeric_ap_id? ? ap_account_status_shares_url(target.account.id, target) : account_status_shares_url(target.account, target) end def following_uri_for(target, ...) raise ArgumentError, 'target must be a local account' unless target.local? - account_following_index_url(target, ...) + target.numeric_ap_id? ? ap_account_following_index_url(target.id, ...) : account_following_index_url(target, ...) end def followers_uri_for(target, ...) return target.followers_url.presence unless target.local? - account_followers_url(target, ...) + target.numeric_ap_id? ? ap_account_followers_url(target.id, ...) : account_followers_url(target, ...) end def collection_uri_for(target, ...) - raise NotImplementedError unless target.local? + raise ArgumentError, 'target must be a local account' unless target.local? - account_collection_url(target, ...) + target.numeric_ap_id? ? ap_account_collection_url(target.id, ...) : account_collection_url(target, ...) end def inbox_uri_for(target) - raise NotImplementedError unless target.local? + raise ArgumentError, 'target must be a local account' unless target.local? - target.instance_actor? ? instance_actor_inbox_url : account_inbox_url(target) + if target.instance_actor? + instance_actor_inbox_url + elsif target.numeric_ap_id? + ap_account_inbox_url(target.id) + else + account_inbox_url(target) + end end def outbox_uri_for(target, ...) - raise NotImplementedError unless target.local? + raise ArgumentError, 'target must be a local account' unless target.local? - target.instance_actor? ? instance_actor_outbox_url(...) : account_outbox_url(target, ...) + if target.instance_actor? + instance_actor_outbox_url(...) + elsif target.numeric_ap_id? + ap_account_outbox_url(target.id, ...) + else + account_outbox_url(target, ...) + end end # Primary audience of a status @@ -262,10 +290,9 @@ class ActivityPub::TagManager path_params = Rails.application.routes.recognize_path(uri) - # TODO: handle numeric IDs case path_params[:controller] when 'accounts' - [:username, path_params[:username]] + path_params.key?(:username) ? [:username, path_params[:username]] : [:id, path_params[:id]] when 'instance_actors' [:id, -99] end diff --git a/app/models/account.rb b/app/models/account.rb index 01644fdc92..79fba3472d 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -52,6 +52,7 @@ # requested_review_at :datetime # indexable :boolean default(FALSE), not null # attribution_domains :string default([]), is an Array +# id_scheme :integer default("username_ap_id") # class Account < ApplicationRecord @@ -105,6 +106,7 @@ class Account < ApplicationRecord enum :protocol, { ostatus: 0, activitypub: 1 } enum :suspension_origin, { local: 0, remote: 1 }, prefix: true + enum :id_scheme, { username_ap_id: 0, numeric_ap_id: 1 } validates :username, presence: true validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? } diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index 4eab55ca3e..7f1d91a160 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -215,8 +215,9 @@ module Account::Interactions def local_followers_hash Rails.cache.fetch("followers_hash:#{id}:local") do digest = "\x00" * 32 - followers.where(domain: nil).pluck_each(:username) do |username| - Xorcist.xor!(digest, Digest::SHA256.digest(ActivityPub::TagManager.instance.uri_for_username(username))) + followers.where(domain: nil).pluck_each(:id_scheme, :id, :username) do |id_scheme, id, username| + uri = id_scheme == 'numeric_ap_id' ? ActivityPub::TagManager.instance.uri_for_account_id(id) : ActivityPub::TagManager.instance.uri_for_username(username) + Xorcist.xor!(digest, Digest::SHA256.digest(uri)) end digest.unpack1('H*') end diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 40b5c42404..ade7175c9d 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -55,7 +55,7 @@ class ActivityPub::DeliveryWorker end def synchronization_header - "collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\"" + "collectionId=\"#{ActivityPub::TagManager.instance.followers_uri_for(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\"" end def perform_request diff --git a/config/routes.rb b/config/routes.rb index 227b229a5b..0a6178399e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -95,7 +95,20 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } - resources :accounts, path: 'users', only: [:show], param: :username do + concern :account_resources do + resources :followers, only: [:index], controller: :follower_accounts + resources :following, only: [:index], controller: :following_accounts + + scope module: :activitypub do + resource :outbox, only: [:show] + resource :inbox, only: [:create] + resources :collections, only: [:show] + resource :followers_synchronization, only: [:show] + resources :quote_authorizations, only: [:show] + end + end + + resources :accounts, path: 'users', only: [:show], param: :username, concerns: :account_resources do resources :statuses, only: [:show] do member do get :activity @@ -106,16 +119,19 @@ Rails.application.routes.draw do resources :likes, only: [:index], module: :activitypub resources :shares, only: [:index], module: :activitypub end + end - resources :followers, only: [:index], controller: :follower_accounts - resources :following, only: [:index], controller: :following_accounts + scope path: 'ap', as: 'ap' do + resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do + resources :statuses, module: :activitypub, only: [:show] do + member do + get :activity + end - scope module: :activitypub do - resource :outbox, only: [:show] - resource :inbox, only: [:create] - resources :collections, only: [:show] - resource :followers_synchronization, only: [:show] - resources :quote_authorizations, only: [:show] + resources :replies, only: [:index] + resources :likes, only: [:index] + resources :shares, only: [:index] + end end end diff --git a/db/migrate/20250924170259_add_id_scheme_to_accounts.rb b/db/migrate/20250924170259_add_id_scheme_to_accounts.rb new file mode 100644 index 0000000000..7dd987dcc0 --- /dev/null +++ b/db/migrate/20250924170259_add_id_scheme_to_accounts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIdSchemeToAccounts < ActiveRecord::Migration[8.0] + def change + add_column :accounts, :id_scheme, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 3b3c1bdfe5..78e75f787a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_12_082651) do +ActiveRecord::Schema[8.0].define(version: 2025_09_24_170259) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -199,6 +199,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_12_082651) do t.boolean "indexable", default: false, null: false t.string "attribution_domains", default: [], array: true t.string "following_url", default: "", null: false + t.integer "id_scheme", default: 0 t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["domain", "id"], name: "index_accounts_on_domain_and_id" diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index e536883a55..70e084a9c9 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -29,6 +29,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.url_for(account)) .to eq("#{host_prefix}/@#{account.username}") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.url_for(account)) + .to eq("#{host_prefix}/@#{account.username}") + end + end end context 'with a remote account' do @@ -46,6 +55,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.url_for(status)) .to eq("#{host_prefix}/@#{status.account.username}/#{status.id}") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.url_for(status)) + .to eq("#{host_prefix}/@#{status.account.username}/#{status.id}") + end + end end context 'with a remote status' do @@ -73,6 +92,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}") + end + end end context 'with a remote account' do @@ -90,6 +118,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_for(status)) .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status)) + .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}") + end + end end context 'with a remote status' do @@ -108,6 +146,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_for(status.conversation)) .to eq("#{host_prefix}/contexts/#{status.account.id}-#{status.id}") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status.conversation)) + .to eq("#{host_prefix}/contexts/#{status.account.id}-#{status.id}") + end + end end context 'with a remote conversation' do @@ -139,6 +187,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.key_uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}#main-key") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.key_uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}#main-key") + end + end end end @@ -167,6 +224,17 @@ RSpec.describe ActivityPub::TagManager do expect(subject.approval_uri_for(quote)) .to eq("#{host_prefix}/users/#{quote.quoted_account.username}/quote_authorizations/#{quote.id}") end + + context 'when using a numeric ID based scheme' do + let(:quoted_account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:quote) { Fabricate(:quote, state: :accepted, quoted_status: quoted_status) } + + it 'returns a string with the web domain and expected path' do + expect(subject.approval_uri_for(quote)) + .to eq("#{host_prefix}/ap/users/#{quote.quoted_account_id}/quote_authorizations/#{quote.id}") + end + end end context 'with an unapproved local quote' do @@ -176,6 +244,17 @@ RSpec.describe ActivityPub::TagManager do expect(subject.approval_uri_for(quote)) .to be_nil end + + context 'when using a numeric ID based scheme' do + let(:quoted_account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:quote) { Fabricate(:quote, state: :rejected, quoted_status: quoted_status) } + + it 'returns nil' do + expect(subject.approval_uri_for(quote)) + .to be_nil + end + end end context 'with a valid remote approval' do @@ -195,6 +274,17 @@ RSpec.describe ActivityPub::TagManager do expect(subject.approval_uri_for(quote, check_approval: false)) .to eq("#{host_prefix}/users/#{quote.quoted_account.username}/quote_authorizations/#{quote.id}") end + + context 'when using a numeric ID based scheme' do + let(:quoted_account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:quote) { Fabricate(:quote, state: :rejected, quoted_status: quoted_status) } + + it 'returns a string with the web domain and expected path' do + expect(subject.approval_uri_for(quote, check_approval: false)) + .to eq("#{host_prefix}/ap/users/#{quote.quoted_account_id}/quote_authorizations/#{quote.id}") + end + end end end @@ -206,6 +296,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.replies_uri_for(status)) .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/replies") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.replies_uri_for(status)) + .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}/replies") + end + end end end @@ -217,6 +317,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.likes_uri_for(status)) .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/likes") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.likes_uri_for(status)) + .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}/likes") + end + end end end @@ -228,6 +338,16 @@ RSpec.describe ActivityPub::TagManager do expect(subject.shares_uri_for(status)) .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/shares") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + let(:status) { Fabricate(:status, account: account) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.shares_uri_for(status)) + .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}/shares") + end + end end end @@ -239,6 +359,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.following_uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}/following") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.following_uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}/following") + end + end end end @@ -250,6 +379,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.followers_uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}/followers") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.followers_uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}/followers") + end + end end end @@ -268,6 +406,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.inbox_uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}/inbox") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.inbox_uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}/inbox") + end + end end end @@ -286,6 +433,15 @@ RSpec.describe ActivityPub::TagManager do expect(subject.outbox_uri_for(account)) .to eq("#{host_prefix}/users/#{account.username}/outbox") end + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.outbox_uri_for(account)) + .to eq("#{host_prefix}/ap/users/#{account.id}/outbox") + end + end end end @@ -300,16 +456,28 @@ RSpec.describe ActivityPub::TagManager do expect(subject.to(status)).to eq [account_followers_url(status.account)] end + it 'returns followers collection for unlisted status when using a numeric ID based scheme' do + status = Fabricate(:status, visibility: :unlisted, account: Fabricate(:account, id_scheme: :numeric_ap_id)) + expect(subject.to(status)).to eq [ap_account_followers_url(status.account_id)] + end + it 'returns followers collection for private status' do status = Fabricate(:status, visibility: :private) expect(subject.to(status)).to eq [account_followers_url(status.account)] end + it 'returns followers collection for private status when using a numeric ID based scheme' do + status = Fabricate(:status, visibility: :private, account: Fabricate(:account, id_scheme: :numeric_ap_id)) + expect(subject.to(status)).to eq [ap_account_followers_url(status.account_id)] + end + it 'returns URIs of mentions for direct status' do status = Fabricate(:status, visibility: :direct) mentioned = Fabricate(:account) + mentioned_numeric = Fabricate(:account, id_scheme: :numeric_ap_id) status.mentions.create(account: mentioned) - expect(subject.to(status)).to eq [subject.uri_for(mentioned)] + status.mentions.create(account: mentioned_numeric) + expect(subject.to(status)).to eq [subject.uri_for(mentioned), subject.uri_for(mentioned_numeric)] end it "returns URIs of mentioned group's followers for direct statuses to groups" do @@ -350,6 +518,11 @@ RSpec.describe ActivityPub::TagManager do expect(subject.cc(status)).to eq [account_followers_url(status.account)] end + it 'returns followers collection for public status when using a numeric ID based scheme' do + status = Fabricate(:status, visibility: :public, account: Fabricate(:account, id_scheme: :numeric_ap_id)) + expect(subject.cc(status)).to eq [ap_account_followers_url(status.account_id)] + end + it 'returns public collection for unlisted status' do status = Fabricate(:status, visibility: :unlisted) expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] @@ -368,8 +541,10 @@ RSpec.describe ActivityPub::TagManager do it 'returns URIs of mentions for non-direct status' do status = Fabricate(:status, visibility: :public) mentioned = Fabricate(:account) + mentioned_numeric = Fabricate(:account, id_scheme: :numeric_ap_id) status.mentions.create(account: mentioned) - expect(subject.cc(status)).to include(subject.uri_for(mentioned)) + status.mentions.create(account: mentioned_numeric) + expect(subject.cc(status)).to include(subject.uri_for(mentioned), subject.uri_for(mentioned_numeric)) end context 'with followers and requested followers' do diff --git a/spec/models/concerns/account/interactions_spec.rb b/spec/models/concerns/account/interactions_spec.rb index e6e9076edb..b683259c8c 100644 --- a/spec/models/concerns/account/interactions_spec.rb +++ b/spec/models/concerns/account/interactions_spec.rb @@ -563,6 +563,22 @@ RSpec.describe Account::Interactions do me.follow!(remote_alice) expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) end + + context 'when using numeric ID based scheme' do + let(:me) { Fabricate(:account, username: 'Me', id_scheme: :numeric_ap_id) } + + it 'returns correct hash for local users' do + expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) + end + + it 'invalidates cache as needed when removing or adding followers' do + expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) + me.unfollow!(remote_alice) + expect(remote_alice.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000' + me.follow!(remote_alice) + expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) + end + end end describe 'muting an account' do diff --git a/spec/requests/accounts_spec.rb b/spec/requests/accounts_spec.rb index 72913ebf22..cc2a5be7c5 100644 --- a/spec/requests/accounts_spec.rb +++ b/spec/requests/accounts_spec.rb @@ -5,6 +5,14 @@ require 'rails_helper' RSpec.describe 'Accounts show response' do let(:account) { Fabricate(:account) } + context 'with numeric-based identifiers' do + it 'returns http success' do + get "/ap/users/#{account.id}" + + expect(response).to have_http_status(200) + end + end + context 'with an unapproved account' do before { account.user.update(approved: false) } diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index b0bd02dac8..813658d149 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do subject { described_class.new } let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account', inbox_url: 'http://example.com/inbox') } - let(:alice) { Fabricate(:account, username: 'alice') } + let(:alice) { Fabricate(:account, username: 'alice', id_scheme: :numeric_ap_id) } let(:bob) { Fabricate(:account, username: 'bob') } let(:eve) { Fabricate(:account, username: 'eve') } let(:mallory) { Fabricate(:account, username: 'mallory') } diff --git a/spec/workers/activitypub/delivery_worker_spec.rb b/spec/workers/activitypub/delivery_worker_spec.rb index 9e6805c68b..a1eb7ebfa9 100644 --- a/spec/workers/activitypub/delivery_worker_spec.rb +++ b/spec/workers/activitypub/delivery_worker_spec.rb @@ -25,12 +25,23 @@ RSpec.describe ActivityPub::DeliveryWorker do .to have_been_made.once end + context 'when using a numeric ID based scheme' do + let(:sender) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'performs a request to synchronize collection' do + subject.perform(payload, sender.id, url, { synchronize_followers: true }) + + expect(request_to_url) + .to have_been_made.once + end + end + def request_to_url a_request(:post, url) .with( headers: { 'Collection-Synchronization' => <<~VALUES.squish, - collectionId="#{account_followers_url(sender)}", digest="somehash", url="#{account_followers_synchronization_url(sender)}" + collectionId="#{ActivityPub::TagManager.instance.followers_uri_for(sender)}", digest="somehash", url="#{account_followers_synchronization_url(sender)}" VALUES } ) From 5b97f25a1577c48f2f3b3b20f371f2488a2d1c91 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Tue, 30 Sep 2025 09:27:09 +0200 Subject: [PATCH 42/58] Add integration tests for mastodon-streaming (#36025) Co-authored-by: Claire Co-authored-by: David Roetzel --- Gemfile | 3 + Gemfile.lock | 1 + spec/rails_helper.rb | 3 +- spec/support/streaming_client.rb | 205 ++++++++++++++++++ spec/support/streaming_server_manager.rb | 11 +- .../streaming/channel_subscriptions_spec.rb | 62 ++++++ spec/system/streaming/streaming_spec.rb | 77 +++++++ 7 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 spec/support/streaming_client.rb create mode 100644 spec/system/streaming/channel_subscriptions_spec.rb create mode 100644 spec/system/streaming/streaming_spec.rb diff --git a/Gemfile b/Gemfile index 126d73f9ca..16be707bfb 100644 --- a/Gemfile +++ b/Gemfile @@ -160,6 +160,9 @@ group :test do # Stub web requests for specs gem 'webmock', '~> 3.18' + + # Websocket driver for testing integration between rails/sidekiq and streaming + gem 'websocket-driver', '~> 0.8', require: false end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index fa785d6876..5886fd6085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1102,6 +1102,7 @@ DEPENDENCIES webauthn (~> 3.0) webmock (~> 3.18) webpush! + websocket-driver (~> 0.8) xorcist (~> 1.1) RUBY VERSION diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 3d3e556f35..6be93ecb70 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -30,7 +30,8 @@ end # This needs to be defined before Rails is initialized STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020') -ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}" +STREAMING_HOST = ENV.fetch('TEST_STREAMING_HOST', 'localhost') +ENV['STREAMING_API_BASE_URL'] = "http://#{STREAMING_HOST}:#{STREAMING_PORT}" require_relative '../config/environment' diff --git a/spec/support/streaming_client.rb b/spec/support/streaming_client.rb new file mode 100644 index 0000000000..02186e781c --- /dev/null +++ b/spec/support/streaming_client.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'websocket/driver' + +class StreamingClient + module AUTHENTICATION + SUBPROTOCOL = 1 + AUTHORIZATION_HEADER = 2 + QUERY_PARAMETER = 3 + end + + class Connection + attr_reader :url, :messages, :last_error + attr_accessor :logger, :protocols + + def initialize(url) + @uri = URI.parse(url) + @query_params = @uri.query.present? ? URI.decode_www_form(@uri.query).to_h : {} + @protocols = nil + @headers = {} + + @dead = false + + @events_queue = Thread::Queue.new + @messages = [] + @last_error = nil + end + + def set_header(key, value) + @headers[key] = value + end + + def set_query_param(key, value) + @query_params[key] = value + end + + def driver + return @driver if defined?(@driver) + + @uri.query = URI.encode_www_form(@query_params) + @url = @uri.to_s + @tcp = TCPSocket.new(@uri.host, @uri.port) + + @driver = WebSocket::Driver.client(self, { + protocols: @protocols, + }) + + @headers.each_pair do |key, value| + @driver.set_header(key, value) + end + + at_exit do + @driver.close + end + + @driver.on(:open) do + @events_queue.enq({ event: :opened }) + end + + @driver.on(:message) do |event| + @events_queue.enq({ event: :message, payload: event.data }) + @messages << event.data + end + + @driver.on(:error) do |event| + logger&.debug(event.message) + @events_queue.enq({ event: :error, payload: event }) + @last_error = event + end + + @driver.on(:close) do |event| + @events_queue.enq({ event: :closing, payload: event }) + finalize(event) + end + + @thread = Thread.new do + @driver.parse(@tcp.read(1)) until @dead || @tcp.closed? + rescue Errno::ECONNRESET + # Create a synthetic close event: + close_event = WebSocket::Driver::CloseEvent.new( + WebSocket::Driver::Hybi::ERRORS[:unexpected_condition], + 'Connection reset' + ) + + finalize(close_event) + end + + @driver + end + + def wait_for_event(expected_event, timeout: 10) + Timeout.timeout(timeout) do + loop do + event = dequeue_event + + return nil if event.nil? && @events_queue.closed? + return event[:payload] unless event.nil? || event[:event] != expected_event + end + end + end + + def write(data) + @tcp.write(data) + rescue Errno::EPIPE => e + logger&.debug("EPIPE: #{e}") + end + + def finalize(event) + @dead = true + @events_queue.enq({ event: :closed, payload: event }) + @events_queue.close + @thread.kill + end + + def dequeue_event + event = @events_queue.pop + logger&.debug(event) unless event.nil? + event + end + end + + def initialize + @logger = Logger.new($stdout) + @logger.level = 'info' + + @connection = Connection.new("ws://#{STREAMING_HOST}:#{STREAMING_PORT}/api/v1/streaming") + @connection.logger = @logger + end + + def debug! + @logger.debug! + end + + def authenticate(access_token, authentication_method = StreamingClient::AUTHENTICATION::SUBPROTOCOL) + raise 'Invalid access_token passed to StreamingClient, expected a string' unless access_token.is_a?(String) + + case authentication_method + when AUTHENTICATION::QUERY_PARAMETER + @connection.set_query_param('access_token', access_token) + when AUTHENTICATION::SUBPROTOCOL + @connection.protocols = access_token + when AUTHENTICATION::AUTHORIZATION_HEADER + @connection.set_header('Authorization', "Bearer #{access_token}") + else + raise 'Invalid authentication method' + end + end + + def connect + @connection.driver.start + @connection.wait_for_event(:opened) + end + + def subscribe(channel, **params) + send(Oj.dump({ type: 'subscribe', stream: channel }.merge(params))) + end + + def wait_for(event = nil) + @connection.wait_for_event(event) + end + + def wait_for_message + message = @connection.wait_for_event(:message) + event = Oj.load(message) + event['payload'] = Oj.load(event['payload']) if event['payload'] + + event.deep_symbolize_keys + end + + delegate :status, :state, to: :'@connection.driver' + delegate :messages, to: :@connection + + def open? + state == :open + end + + def closing? + state == :closing + end + + def closed? + state == :closed + end + + def send(message) + @connection.driver.text(message) if open? + end + + def close + return if closed? + + @connection.driver.close unless closing? + @connection.wait_for_event(:closed) + end +end + +module StreamingClientHelper + def streaming_client + @streaming_client ||= StreamingClient.new + end +end + +RSpec.configure do |config| + config.include StreamingClientHelper, :streaming +end diff --git a/spec/support/streaming_server_manager.rb b/spec/support/streaming_server_manager.rb index d98f7dd960..b565ed79a8 100644 --- a/spec/support/streaming_server_manager.rb +++ b/spec/support/streaming_server_manager.rb @@ -12,6 +12,11 @@ class StreamingServerManager queue = Queue.new + if ENV['DEBUG_STREAMING_SERVER'].present? + logger = Logger.new($stdout) + logger.level = 'debug' + end + @queue = queue @running_thread = Thread.new do @@ -31,7 +36,7 @@ class StreamingServerManager # Spawn a thread to listen on streaming server output output_thread = Thread.new do stdout_err.each_line do |line| - Rails.logger.info "Streaming server: #{line}" + logger&.info "Streaming server: #{line}" if status == :starting && line.match('Streaming API now listening on') status = :started @@ -115,12 +120,12 @@ RSpec.configure do |config| self.use_transactional_tests = true end - private - def streaming_server_manager @streaming_server_manager ||= StreamingServerManager.new end + private + def streaming_examples_present? RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:streaming] == true } end diff --git a/spec/system/streaming/channel_subscriptions_spec.rb b/spec/system/streaming/channel_subscriptions_spec.rb new file mode 100644 index 0000000000..54e125c293 --- /dev/null +++ b/spec/system/streaming/channel_subscriptions_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'debug' + +RSpec.describe 'Channel Subscriptions', :inline_jobs, :streaming do + let(:application) { Fabricate(:application, confidential: false) } + let(:scopes) { nil } + let(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user_account.user.id, application: application, scopes: scopes) } + + let(:user_account) { Fabricate(:account, username: 'alice', domain: nil) } + let(:bob_account) { Fabricate(:account, username: 'bob') } + + after do + streaming_client.close + end + + context 'when the access token has read scope' do + let(:scopes) { 'read' } + + it 'can subscribing to the public:local channel' do + streaming_client.authenticate(access_token.token) + + streaming_client.connect + streaming_client.subscribe('public:local') + + # We need to publish a status as there is no positive acknowledgement of + # subscriptions: + status = PostStatusService.new.call(bob_account, text: 'Hello @alice') + + # And then we want to receive that status: + message = streaming_client.wait_for_message + + expect(message).to include( + stream: be_an(Array).and(contain_exactly('public:local')), + event: 'update', + payload: include( + id: status.id.to_s + ) + ) + end + end + + context 'when the access token cannot read notifications' do + let(:scopes) { 'read:statuses' } + + it 'cannot subscribing to the user:notifications channel' do + streaming_client.authenticate(access_token.token) + + streaming_client.connect + streaming_client.subscribe('user:notification') + + # We should receive an error back immediately: + message = streaming_client.wait_for_message + + expect(message).to include( + error: 'Access token does not have the required scopes', + status: 401 + ) + end + end +end diff --git a/spec/system/streaming/streaming_spec.rb b/spec/system/streaming/streaming_spec.rb new file mode 100644 index 0000000000..c12bd1b18f --- /dev/null +++ b/spec/system/streaming/streaming_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' +RSpec.describe 'Streaming', :inline_jobs, :streaming do + let(:authentication_method) { StreamingClient::AUTHENTICATION::SUBPROTOCOL } + let(:user) { Fabricate(:user) } + let(:scopes) { '' } + let(:application) { Fabricate(:application, confidential: false) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application, scopes: scopes) } + let(:access_token) { token.token } + + before do + streaming_client.authenticate(access_token, authentication_method) + end + + after do + streaming_client.close + end + + context 'when authenticating via subprotocol' do + it 'is able to connect' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + end + end + + context 'when authenticating via authorization header' do + let(:authentication_method) { StreamingClient::AUTHENTICATION::AUTHORIZATION_HEADER } + + it 'is able to connect successfully' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + end + end + + context 'when authenticating via query parameter' do + let(:authentication_method) { StreamingClient::AUTHENTICATION::QUERY_PARAMETER } + + it 'is able to connect successfully' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + end + end + + context 'with a revoked access token' do + before do + token.revoke + end + + it 'receives an 401 unauthorized error' do + streaming_client.connect + + expect(streaming_client.status).to eq(401) + expect(streaming_client.open?).to be(false) + end + end + + context 'when revoking an access token after connection' do + it 'disconnects the client' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + + token.revoke + + expect(streaming_client.wait_for(:closed).code).to be(1000) + expect(streaming_client.open?).to be(false) + end + end +end From a6236148d8f3e42a960eb4509d4d83bcfa683578 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:44:15 +0200 Subject: [PATCH 43/58] chore(deps): update dependency haml-rails to v3 (#36288) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 16be707bfb..0d9ab34271 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'rails', '~> 8.0' gem 'thor', '~> 1.2' gem 'dotenv' -gem 'haml-rails', '~>2.0' +gem 'haml-rails', '~>3.0' gem 'pg', '~> 1.5' gem 'pghero' diff --git a/Gemfile.lock b/Gemfile.lock index 5886fd6085..cd96d60e1d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,7 +282,7 @@ GEM temple (>= 0.8.2) thor tilt - haml-rails (2.1.0) + haml-rails (3.0.0) actionpack (>= 5.1) activesupport (>= 5.1) haml (>= 4.0.6) @@ -990,7 +990,7 @@ DEPENDENCIES flatware-rspec fog-core (<= 2.6.0) fog-openstack (~> 1.0) - haml-rails (~> 2.0) + haml-rails (~> 3.0) haml_lint hcaptcha (~> 7.1) hiredis (~> 0.6) From 45219dbf64805746a472e50bb7c9bcb52972ab2a Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Sep 2025 11:40:58 +0200 Subject: [PATCH 44/58] Fix spurious notification of local boosters and quoters when updating quote policy (#36299) --- .../api/v1/statuses/interaction_policies_controller.rb | 2 +- spec/requests/api/v1/statuses/interaction_policies_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/statuses/interaction_policies_controller.rb b/app/controllers/api/v1/statuses/interaction_policies_controller.rb index 6e2745806d..5cfb2d0e8f 100644 --- a/app/controllers/api/v1/statuses/interaction_policies_controller.rb +++ b/app/controllers/api/v1/statuses/interaction_policies_controller.rb @@ -22,7 +22,7 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base end def broadcast_updates! - DistributionWorker.perform_async(@status.id, { 'update' => true }) + DistributionWorker.perform_async(@status.id, { 'update' => true, 'skip_notifications' => true }) ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 }) end end diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index aa447de17f..321a68cd25 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -60,7 +60,7 @@ RSpec.describe 'Interaction policies' do ) expect(DistributionWorker) - .to have_enqueued_sidekiq_job(status.id, { 'update' => true }) + .to have_enqueued_sidekiq_job(status.id, { 'update' => true, 'skip_notifications' => true }) expect(ActivityPub::StatusUpdateDistributionWorker) .to have_enqueued_sidekiq_job(status.id, { 'updated_at' => anything }) end From 589af7a1cca643199907fbaaaf795314f9a09d3e Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Sep 2025 11:56:03 +0200 Subject: [PATCH 45/58] Change `GET /api/v1/statuses/:id/quotes` to allow listing quotes to other people's posts (#36291) --- .../api/v1/statuses/quotes_controller.rb | 34 ++++++++++--------- app/policies/status_policy.rb | 4 --- spec/requests/api/v1/statuses/quotes_spec.rb | 27 ++++++++++++--- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/app/controllers/api/v1/statuses/quotes_controller.rb b/app/controllers/api/v1/statuses/quotes_controller.rb index 962855884e..be3a4edc83 100644 --- a/app/controllers/api/v1/statuses/quotes_controller.rb +++ b/app/controllers/api/v1/statuses/quotes_controller.rb @@ -4,13 +4,13 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke - before_action :check_owner! + before_action :set_statuses, only: :index + before_action :set_quote, only: :revoke after_action :insert_pagination_headers, only: :index def index cache_if_unauthenticated! - @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer end @@ -24,18 +24,26 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController private - def check_owner! - authorize @status, :list_quotes? - end - def set_quote @quote = @status.quotes.find_by!(status_id: params[:id]) end - def load_statuses + def set_statuses scope = default_statuses scope = scope.not_excluded_by_account(current_account) unless current_account.nil? - scope.merge(paginated_quotes).to_a + @statuses = scope.merge(paginated_quotes).to_a + + # Store next page info before filtering + @records_continue = @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) + @pagination_since_id = @statuses.first.quote.id unless @statuses.empty? + @pagination_max_id = @statuses.last.quote.id if @records_continue + + if current_account&.id != @status.account_id + domains = @statuses.filter_map(&:account_domain).uniq + account_ids = @statuses.map(&:account_id).uniq + relations = current_account&.relations_map(account_ids, domains) || {} + @statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? } + end end def default_statuses @@ -58,15 +66,9 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? end - def pagination_max_id - @statuses.last.quote.id - end - - def pagination_since_id - @statuses.first.quote.id - end + attr_reader :pagination_max_id, :pagination_since_id def records_continue? - @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) + @records_continue end end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index b746386995..03ceae718e 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -36,10 +36,6 @@ class StatusPolicy < ApplicationPolicy owned? end - def list_quotes? - owned? - end - alias unreblog? destroy? def update? diff --git a/spec/requests/api/v1/statuses/quotes_spec.rb b/spec/requests/api/v1/statuses/quotes_spec.rb index 9456556ce9..01e9e17b07 100644 --- a/spec/requests/api/v1/statuses/quotes_spec.rb +++ b/spec/requests/api/v1/statuses/quotes_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'API V1 Statuses Quotes' do let!(:accepted_quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } let!(:rejected_quote) { Fabricate(:quote, quoted_status: status, state: :rejected) } let!(:pending_quote) { Fabricate(:quote, quoted_status: status, state: :pending) } - let!(:another_accepted_quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + let!(:accepted_private_quote) { Fabricate(:quote, status: Fabricate(:status, visibility: :private), quoted_status: status, state: :accepted) } context 'with an OAuth token' do let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } @@ -30,7 +30,7 @@ RSpec.describe 'API V1 Statuses Quotes' do expect(response) .to have_http_status(200) .and include_pagination_headers( - prev: api_v1_status_quotes_url(limit: 2, since_id: another_accepted_quote.id), + prev: api_v1_status_quotes_url(limit: 2, since_id: accepted_private_quote.id), next: api_v1_status_quotes_url(limit: 2, max_id: accepted_quote.id) ) expect(response.content_type) @@ -39,7 +39,7 @@ RSpec.describe 'API V1 Statuses Quotes' do expect(response.parsed_body) .to contain_exactly( include(id: accepted_quote.status.id.to_s), - include(id: another_accepted_quote.status.id.to_s) + include(id: accepted_private_quote.status.id.to_s) ) expect(response.parsed_body) @@ -52,12 +52,29 @@ RSpec.describe 'API V1 Statuses Quotes' do context 'with a different user than the post owner' do let(:status) { Fabricate(:status) } - it 'returns http forbidden' do + it 'returns http success and statuses but not private ones' do subject - expect(response).to have_http_status(403) + expect(response) + .to have_http_status(200) + .and include_pagination_headers( + prev: api_v1_status_quotes_url(limit: 2, since_id: accepted_private_quote.id), + next: api_v1_status_quotes_url(limit: 2, max_id: accepted_quote.id) + ) expect(response.content_type) .to start_with('application/json') + + expect(response.parsed_body) + .to contain_exactly( + include(id: accepted_quote.status.id.to_s) + ) + + expect(response.parsed_body) + .to_not include( + include(id: rejected_quote.status.id.to_s), + include(id: pending_quote.status.id.to_s), + include(id: accepted_private_quote.id.to_s) + ) end end end From ac50e5eebc4300dacb6bef6b76e63e6569f12adc Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Tue, 30 Sep 2025 07:14:58 -0400 Subject: [PATCH 46/58] Convert mastodon/initial_state to TypeScript (#36274) --- .../mastodon/containers/compose_container.jsx | 2 +- .../mastodon/containers/mastodon.jsx | 2 +- .../mastodon/features/emoji/index.ts | 2 +- .../features/standalone/status/index.tsx | 2 +- app/javascript/mastodon/features/ui/index.jsx | 2 +- .../mastodon/features/ui/util/focusUtils.ts | 2 +- app/javascript/mastodon/initial_state.js | 145 ------------------ app/javascript/mastodon/initial_state.ts | 141 +++++++++++++++++ app/javascript/mastodon/utils/environment.ts | 2 +- 9 files changed, 148 insertions(+), 152 deletions(-) delete mode 100644 app/javascript/mastodon/initial_state.js create mode 100644 app/javascript/mastodon/initial_state.ts diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index a2513cc552..3e6d20c74c 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -5,7 +5,7 @@ import { fetchServer } from 'mastodon/actions/server'; import { hydrateStore } from 'mastodon/actions/store'; import { Router } from 'mastodon/components/router'; import Compose from 'mastodon/features/standalone/compose'; -import initialState from 'mastodon/initial_state'; +import { initialState } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 086a7681c4..ee861366a5 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -13,7 +13,7 @@ import ErrorBoundary from 'mastodon/components/error_boundary'; import { Router } from 'mastodon/components/router'; import UI from 'mastodon/features/ui'; import { IdentityContext, createIdentityContext } from 'mastodon/identity_context'; -import initialState, { title as siteTitle } from 'mastodon/initial_state'; +import { initialState, title as siteTitle } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; import { isProduction } from 'mastodon/utils/environment'; diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 99c16fe361..d128da6b53 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,4 +1,4 @@ -import initialState from '@/mastodon/initial_state'; +import { initialState } from '@/mastodon/initial_state'; import { loadWorker } from '@/mastodon/utils/workers'; import { toSupportedLocale } from './locale'; diff --git a/app/javascript/mastodon/features/standalone/status/index.tsx b/app/javascript/mastodon/features/standalone/status/index.tsx index a7850eae1c..a53d6e6b23 100644 --- a/app/javascript/mastodon/features/standalone/status/index.tsx +++ b/app/javascript/mastodon/features/standalone/status/index.tsx @@ -11,7 +11,7 @@ import { hydrateStore } from 'mastodon/actions/store'; import { Router } from 'mastodon/components/router'; import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; import { useRenderSignal } from 'mastodon/hooks/useRenderSignal'; -import initialState from 'mastodon/initial_state'; +import { initialState } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; import { store, useAppSelector, useAppDispatch } from 'mastodon/store'; diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index efec38caf4..04c7f33dfd 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -27,7 +27,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; +import { initialState, me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; diff --git a/app/javascript/mastodon/features/ui/util/focusUtils.ts b/app/javascript/mastodon/features/ui/util/focusUtils.ts index 9bcd3f8943..e46ede3553 100644 --- a/app/javascript/mastodon/features/ui/util/focusUtils.ts +++ b/app/javascript/mastodon/features/ui/util/focusUtils.ts @@ -1,4 +1,4 @@ -import initialState from '@/mastodon/initial_state'; +import { initialState } from '@/mastodon/initial_state'; interface FocusColumnOptions { index?: number; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js deleted file mode 100644 index 4a078b86f5..0000000000 --- a/app/javascript/mastodon/initial_state.js +++ /dev/null @@ -1,145 +0,0 @@ -// @ts-check - -/** - * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage - */ - -/** - * @typedef InitialStateMeta - * @property {string} access_token - * @property {boolean=} advanced_layout - * @property {boolean} auto_play_gif - * @property {boolean} activity_api_enabled - * @property {string} admin - * @property {boolean=} boost_modal - * @property {boolean=} delete_modal - * @property {boolean=} missing_alt_text_modal - * @property {boolean=} disable_swiping - * @property {boolean=} disable_hover_cards - * @property {string=} disabled_account_id - * @property {string} display_media - * @property {string} domain - * @property {boolean=} expand_spoilers - * @property {boolean} limited_federation_mode - * @property {string} locale - * @property {string | null} mascot - * @property {string=} me - * @property {string=} moved_to_account_id - * @property {string=} owner - * @property {boolean} profile_directory - * @property {boolean} registrations_open - * @property {boolean} reduce_motion - * @property {string} repository - * @property {boolean} search_enabled - * @property {boolean} trends_enabled - * @property {boolean} single_user_mode - * @property {string} source_url - * @property {string} streaming_api_base_url - * @property {boolean} timeline_preview - * @property {string} title - * @property {boolean} show_trends - * @property {boolean} trends_as_landing_page - * @property {boolean} use_blurhash - * @property {boolean=} use_pending_items - * @property {string} version - * @property {string} sso_redirect - * @property {string} status_page_url - * @property {boolean} terms_of_service_enabled - * @property {string?} emoji_style - */ - -/** - * @typedef Role - * @property {string} id - * @property {string} name - * @property {string} permissions - * @property {string} color - * @property {boolean} highlighted - */ - -/** - * @typedef InitialState - * @property {Record} accounts - * @property {InitialStateLanguage[]} languages - * @property {boolean=} critical_updates_pending - * @property {InitialStateMeta} meta - * @property {Role?} role - * @property {string[]} features - */ - -const element = document.getElementById('initial-state'); -/** @type {InitialState | undefined} */ -const initialState = element?.textContent && JSON.parse(element.textContent); - -/** @type {string} */ -const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? ''; -/** @type {boolean} */ -export const hasMultiColumnPath = initialPath === '/' - || initialPath === '/getting-started' - || initialPath === '/home' - || initialPath.startsWith('/deck'); - -/** - * @template {keyof InitialStateMeta} K - * @param {K} prop - * @returns {InitialStateMeta[K] | undefined} - */ -const getMeta = (prop) => initialState?.meta && initialState.meta[prop]; - -export const activityApiEnabled = getMeta('activity_api_enabled'); -export const autoPlayGif = getMeta('auto_play_gif'); -export const boostModal = getMeta('boost_modal'); -export const deleteModal = getMeta('delete_modal'); -export const missingAltTextModal = getMeta('missing_alt_text_modal'); -export const disableSwiping = getMeta('disable_swiping'); -export const disableHoverCards = getMeta('disable_hover_cards'); -export const disabledAccountId = getMeta('disabled_account_id'); -export const displayMedia = getMeta('display_media'); -export const domain = getMeta('domain'); -export const emojiStyle = getMeta('emoji_style') || 'auto'; -export const expandSpoilers = getMeta('expand_spoilers'); -export const forceSingleColumn = !getMeta('advanced_layout'); -export const limitedFederationMode = getMeta('limited_federation_mode'); -export const mascot = getMeta('mascot'); -export const me = getMeta('me'); -export const movedToAccountId = getMeta('moved_to_account_id'); -export const owner = getMeta('owner'); -export const profile_directory = getMeta('profile_directory'); -export const reduceMotion = getMeta('reduce_motion'); -export const registrationsOpen = getMeta('registrations_open'); -export const repository = getMeta('repository'); -export const searchEnabled = getMeta('search_enabled'); -export const trendsEnabled = getMeta('trends_enabled'); -export const showTrends = getMeta('show_trends'); -export const singleUserMode = getMeta('single_user_mode'); -export const source_url = getMeta('source_url'); -export const timelinePreview = getMeta('timeline_preview'); -export const title = getMeta('title'); -export const trendsAsLanding = getMeta('trends_as_landing_page'); -export const useBlurhash = getMeta('use_blurhash'); -export const usePendingItems = getMeta('use_pending_items'); -export const version = getMeta('version'); -export const criticalUpdatesPending = initialState?.critical_updates_pending; -export const statusPageUrl = getMeta('status_page_url'); -export const sso_redirect = getMeta('sso_redirect'); -export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); - -const displayNames = Intl.DisplayNames && new Intl.DisplayNames(getMeta('locale'), { - type: 'language', - fallback: 'none', - languageDisplay: 'standard', -}); - -export const languages = initialState?.languages?.map(lang => { - // zh-YUE is not a valid CLDR unicode_language_id - return [lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) || lang[1], lang[2]]; -}); - -/** - * @returns {string | undefined} - */ -export function getAccessToken() { - return getMeta('access_token'); -} - -export default initialState; diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts new file mode 100644 index 0000000000..b6d7d55483 --- /dev/null +++ b/app/javascript/mastodon/initial_state.ts @@ -0,0 +1,141 @@ +import type { ApiAccountJSON } from './api_types/accounts'; + +type InitialStateLanguage = [code: string, name: string, localName: string]; + +interface InitialStateMeta { + access_token: string; + advanced_layout?: boolean; + auto_play_gif: boolean; + activity_api_enabled: boolean; + admin: string; + boost_modal?: boolean; + delete_modal?: boolean; + missing_alt_text_modal?: boolean; + disable_swiping?: boolean; + disable_hover_cards?: boolean; + disabled_account_id?: string; + display_media: string; + domain: string; + expand_spoilers?: boolean; + limited_federation_mode: boolean; + locale: string; + mascot: string | null; + me?: string; + moved_to_account_id?: string; + owner?: string; + profile_directory: boolean; + registrations_open: boolean; + reduce_motion: boolean; + repository: string; + search_enabled: boolean; + trends_enabled: boolean; + single_user_mode: boolean; + source_url: string; + streaming_api_base_url: string; + timeline_preview: boolean; + title: string; + show_trends: boolean; + trends_as_landing_page: boolean; + use_blurhash: boolean; + use_pending_items?: boolean; + version: string; + sso_redirect: string; + status_page_url: string; + terms_of_service_enabled: boolean; + emoji_style?: string; +} + +interface Role { + id: string; + name: string; + permissions: string; + color: string; + highlighted: boolean; +} + +export interface InitialState { + accounts: Record; + languages: InitialStateLanguage[]; + critical_updates_pending?: boolean; + meta: InitialStateMeta; + role?: Role; + features: string[]; +} + +const element = document.getElementById('initial-state'); +export const initialState: InitialState | undefined = element?.textContent + ? (JSON.parse(element.textContent) as InitialState) + : undefined; + +const initialPath: string = + document + .querySelector('head meta[name=initialPath]') + ?.getAttribute('content') ?? ''; +export const hasMultiColumnPath: boolean = + initialPath === '/' || + initialPath === '/getting-started' || + initialPath === '/home' || + initialPath.startsWith('/deck'); + +function getMeta( + prop: K, +): InitialStateMeta[K] | undefined { + return initialState?.meta[prop]; +} + +export const activityApiEnabled = getMeta('activity_api_enabled'); +export const autoPlayGif = getMeta('auto_play_gif'); +export const boostModal = getMeta('boost_modal'); +export const deleteModal = getMeta('delete_modal'); +export const missingAltTextModal = getMeta('missing_alt_text_modal'); +export const disableSwiping = getMeta('disable_swiping'); +export const disableHoverCards = getMeta('disable_hover_cards'); +export const disabledAccountId = getMeta('disabled_account_id'); +export const displayMedia = getMeta('display_media'); +export const domain = getMeta('domain'); +export const emojiStyle = getMeta('emoji_style') ?? 'auto'; +export const expandSpoilers = getMeta('expand_spoilers'); +export const forceSingleColumn = !getMeta('advanced_layout'); +export const limitedFederationMode = getMeta('limited_federation_mode'); +export const mascot = getMeta('mascot'); +export const me = getMeta('me'); +export const movedToAccountId = getMeta('moved_to_account_id'); +export const owner = getMeta('owner'); +export const profile_directory = getMeta('profile_directory'); +export const reduceMotion = getMeta('reduce_motion'); +export const registrationsOpen = getMeta('registrations_open'); +export const repository = getMeta('repository'); +export const searchEnabled = getMeta('search_enabled'); +export const trendsEnabled = getMeta('trends_enabled'); +export const showTrends = getMeta('show_trends'); +export const singleUserMode = getMeta('single_user_mode'); +export const source_url = getMeta('source_url'); +export const timelinePreview = getMeta('timeline_preview'); +export const title = getMeta('title'); +export const trendsAsLanding = getMeta('trends_as_landing_page'); +export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); +export const version = getMeta('version'); +export const criticalUpdatesPending = initialState?.critical_updates_pending; +export const statusPageUrl = getMeta('status_page_url'); +export const sso_redirect = getMeta('sso_redirect'); +export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); + +const displayNames = new Intl.DisplayNames(getMeta('locale'), { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', +}); + +export const languages = initialState?.languages.map((lang) => { + // zh-YUE is not a valid CLDR unicode_language_id + return [ + lang[0], + displayNames.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1], + lang[2], + ]; +}); + +export function getAccessToken(): string | undefined { + return getMeta('access_token'); +} diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 2d544417e3..c666e2c94d 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -1,4 +1,4 @@ -import initialState from '../initial_state'; +import { initialState } from '../initial_state'; export function isDevelopment() { if (typeof process !== 'undefined') From c12b8f51c132baeb81652b61f178b7de7b41e424 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 30 Sep 2025 15:06:02 +0200 Subject: [PATCH 47/58] Emoji Component (#36293) --- .../mastodon/components/account_bio.tsx | 12 +- .../components/display_name/no-domain.tsx | 12 +- .../components/display_name/simple.tsx | 18 +- .../mastodon/components/emoji/context.tsx | 108 ++++++++++++ .../mastodon/components/emoji/html.tsx | 61 +++++++ .../mastodon/components/emoji/index.tsx | 99 +++++++++++ .../mastodon/components/status_content.jsx | 6 +- .../components/account_header.tsx | 7 +- .../components/conversation.jsx | 5 +- .../mastodon/features/emoji/constants.ts | 2 - .../mastodon/features/emoji/database.ts | 9 +- .../mastodon/features/emoji/emoji_html.tsx | 70 -------- .../mastodon/features/emoji/loader.ts | 6 +- .../mastodon/features/emoji/normalize.test.ts | 23 --- .../mastodon/features/emoji/normalize.ts | 28 ++- .../mastodon/features/emoji/render.test.ts | 101 +---------- .../mastodon/features/emoji/render.ts | 166 +++++++++++++----- .../mastodon/features/emoji/types.ts | 40 ++--- .../mastodon/features/emoji/utils.ts | 12 ++ .../components/embedded_status.tsx | 7 +- app/javascript/types/polymorphic.ts | 75 ++++++++ 21 files changed, 566 insertions(+), 301 deletions(-) create mode 100644 app/javascript/mastodon/components/emoji/context.tsx create mode 100644 app/javascript/mastodon/components/emoji/html.tsx create mode 100644 app/javascript/mastodon/components/emoji/index.tsx delete mode 100644 app/javascript/mastodon/features/emoji/emoji_html.tsx create mode 100644 app/javascript/types/polymorphic.ts diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index b720b4746d..b5ff686f86 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -1,11 +1,15 @@ import { useCallback } from 'react'; +import classNames from 'classnames'; + import { useLinks } from 'mastodon/hooks/useLinks'; -import { EmojiHTML } from '../features/emoji/emoji_html'; import { useAppSelector } from '../store'; import { isModernEmojiEnabled } from '../utils/environment'; +import { AnimateEmojiProvider } from './emoji/context'; +import { EmojiHTML } from './emoji/html'; + interface AccountBioProps { className: string; accountId: string; @@ -44,13 +48,13 @@ export const AccountBio: React.FC = ({ } return ( -
-
+ ); }; diff --git a/app/javascript/mastodon/components/display_name/no-domain.tsx b/app/javascript/mastodon/components/display_name/no-domain.tsx index 3a66fe5042..bb5a093659 100644 --- a/app/javascript/mastodon/components/display_name/no-domain.tsx +++ b/app/javascript/mastodon/components/display_name/no-domain.tsx @@ -2,9 +2,10 @@ import type { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; -import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; +import { AnimateEmojiProvider } from '../emoji/context'; +import { EmojiHTML } from '../emoji/html'; import { Skeleton } from '../skeleton'; import type { DisplayNameProps } from './index'; @@ -14,9 +15,10 @@ export const DisplayNameWithoutDomain: FC< ComponentPropsWithoutRef<'span'> > = ({ account, className, children, ...props }) => { return ( - {account ? ( @@ -27,8 +29,8 @@ export const DisplayNameWithoutDomain: FC< ? account.get('display_name') : account.get('display_name_html') } - shallow as='strong' + extraEmojis={account.get('emojis')} /> ) : ( @@ -37,6 +39,6 @@ export const DisplayNameWithoutDomain: FC< )} {children} - + ); }; diff --git a/app/javascript/mastodon/components/display_name/simple.tsx b/app/javascript/mastodon/components/display_name/simple.tsx index 3190c4384b..375f4932b2 100644 --- a/app/javascript/mastodon/components/display_name/simple.tsx +++ b/app/javascript/mastodon/components/display_name/simple.tsx @@ -1,8 +1,9 @@ import type { ComponentPropsWithoutRef, FC } from 'react'; -import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; +import { EmojiHTML } from '../emoji/html'; + import type { DisplayNameProps } from './index'; export const DisplayNameSimple: FC< @@ -12,12 +13,19 @@ export const DisplayNameSimple: FC< if (!account) { return null; } - const accountName = isModernEmojiEnabled() - ? account.get('display_name') - : account.get('display_name_html'); + return ( - + ); }; diff --git a/app/javascript/mastodon/components/emoji/context.tsx b/app/javascript/mastodon/components/emoji/context.tsx new file mode 100644 index 0000000000..9fda5714d9 --- /dev/null +++ b/app/javascript/mastodon/components/emoji/context.tsx @@ -0,0 +1,108 @@ +import type { MouseEventHandler, PropsWithChildren } from 'react'; +import { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import classNames from 'classnames'; + +import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize'; +import { autoPlayGif } from '@/mastodon/initial_state'; +import { polymorphicForwardRef } from '@/types/polymorphic'; +import type { + CustomEmojiMapArg, + ExtraCustomEmojiMap, +} from 'mastodon/features/emoji/types'; + +// Animation context +export const AnimateEmojiContext = createContext(null); + +// Polymorphic provider component +type AnimateEmojiProviderProps = Required & { + className?: string; +}; + +export const AnimateEmojiProvider = polymorphicForwardRef< + 'div', + AnimateEmojiProviderProps +>( + ( + { + children, + as: Wrapper = 'div', + className, + onMouseEnter, + onMouseLeave, + ...props + }, + ref, + ) => { + const [animate, setAnimate] = useState(autoPlayGif ?? false); + + const handleEnter: MouseEventHandler = useCallback( + (event) => { + onMouseEnter?.(event); + if (!autoPlayGif) { + setAnimate(true); + } + }, + [onMouseEnter], + ); + const handleLeave: MouseEventHandler = useCallback( + (event) => { + onMouseLeave?.(event); + if (!autoPlayGif) { + setAnimate(false); + } + }, + [onMouseLeave], + ); + + // If there's a parent context or GIFs autoplay, we don't need handlers. + const parentContext = useContext(AnimateEmojiContext); + if (parentContext !== null || autoPlayGif === true) { + return ( + + {children} + + ); + } + + return ( + + + {children} + + + ); + }, +); +AnimateEmojiProvider.displayName = 'AnimateEmojiProvider'; + +// Handle custom emoji +export const CustomEmojiContext = createContext({}); + +export const CustomEmojiProvider = ({ + children, + emojis: rawEmojis, +}: PropsWithChildren<{ emojis?: CustomEmojiMapArg }>) => { + const emojis = useMemo(() => cleanExtraEmojis(rawEmojis) ?? {}, [rawEmojis]); + return ( + + {children} + + ); +}; diff --git a/app/javascript/mastodon/components/emoji/html.tsx b/app/javascript/mastodon/components/emoji/html.tsx new file mode 100644 index 0000000000..a6ecc869c1 --- /dev/null +++ b/app/javascript/mastodon/components/emoji/html.tsx @@ -0,0 +1,61 @@ +import { useMemo } from 'react'; +import type { ComponentPropsWithoutRef, ElementType } from 'react'; + +import classNames from 'classnames'; + +import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; +import { htmlStringToComponents } from '@/mastodon/utils/html'; + +import { AnimateEmojiProvider, CustomEmojiProvider } from './context'; +import { textToEmojis } from './index'; + +type EmojiHTMLProps = Omit< + ComponentPropsWithoutRef, + 'dangerouslySetInnerHTML' | 'className' +> & { + htmlString: string; + extraEmojis?: CustomEmojiMapArg; + as?: Element; + className?: string; +}; + +export const ModernEmojiHTML = ({ + extraEmojis, + htmlString, + as: asProp = 'div', // Rename for syntax highlighting + shallow, + className = '', + ...props +}: EmojiHTMLProps) => { + const contents = useMemo( + () => htmlStringToComponents(htmlString, { onText: textToEmojis }), + [htmlString], + ); + + return ( + + + {contents} + + + ); +}; + +export const LegacyEmojiHTML = ( + props: EmojiHTMLProps, +) => { + const { as: asElement, htmlString, extraEmojis, className, ...rest } = props; + const Wrapper = asElement ?? 'div'; + return ( + + ); +}; + +export const EmojiHTML = isModernEmojiEnabled() + ? ModernEmojiHTML + : LegacyEmojiHTML; diff --git a/app/javascript/mastodon/components/emoji/index.tsx b/app/javascript/mastodon/components/emoji/index.tsx new file mode 100644 index 0000000000..e070eb30dd --- /dev/null +++ b/app/javascript/mastodon/components/emoji/index.tsx @@ -0,0 +1,99 @@ +import type { FC } from 'react'; +import { useContext, useEffect, useState } from 'react'; + +import { EMOJI_TYPE_CUSTOM } from '@/mastodon/features/emoji/constants'; +import { useEmojiAppState } from '@/mastodon/features/emoji/hooks'; +import { unicodeHexToUrl } from '@/mastodon/features/emoji/normalize'; +import { + isStateLoaded, + loadEmojiDataToState, + shouldRenderImage, + stringToEmojiState, + tokenizeText, +} from '@/mastodon/features/emoji/render'; + +import { AnimateEmojiContext, CustomEmojiContext } from './context'; + +interface EmojiProps { + code: string; + showFallback?: boolean; + showLoading?: boolean; +} + +export const Emoji: FC = ({ + code, + showFallback = true, + showLoading = true, +}) => { + const customEmoji = useContext(CustomEmojiContext); + + // First, set the emoji state based on the input code. + const [state, setState] = useState(() => + stringToEmojiState(code, customEmoji), + ); + + // If we don't have data, then load emoji data asynchronously. + const appState = useEmojiAppState(); + useEffect(() => { + if (state !== null) { + void loadEmojiDataToState(state, appState.currentLocale).then(setState); + } + }, [appState.currentLocale, state]); + + const animate = useContext(AnimateEmojiContext); + const fallback = showFallback ? code : null; + + // If the code is invalid or we otherwise know it's not valid, show the fallback. + if (!state) { + return fallback; + } + + if (!shouldRenderImage(state, appState.mode)) { + return code; + } + + if (!isStateLoaded(state)) { + if (showLoading) { + return ; + } + return fallback; + } + + if (state.type === EMOJI_TYPE_CUSTOM) { + const shortcode = `:${state.code}:`; + return ( + {shortcode} + ); + } + + const src = unicodeHexToUrl(state.code, appState.darkTheme); + + return ( + {state.data.unicode} + ); +}; + +/** + * Takes a text string and converts it to an array of React nodes. + * @param text The text to be tokenized and converted. + */ +export function textToEmojis(text: string) { + return tokenizeText(text).map((token, index) => { + if (typeof token === 'string') { + return token; + } + return ; + }); +} diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index af0059c7d6..d766793d87 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -13,10 +13,12 @@ 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, languages as preloadedLanguages } from 'mastodon/initial_state'; -import { EmojiHTML } from '../features/emoji/emoji_html'; +import { languages as preloadedLanguages } from 'mastodon/initial_state'; + import { isModernEmojiEnabled } from '../utils/environment'; +import { EmojiHTML } from './emoji/html'; + const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) /** diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index f58f1f4a8c..2be026c8f9 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -8,6 +8,7 @@ import { NavLink } from 'react-router-dom'; import { AccountBio } from '@/mastodon/components/account_bio'; import { DisplayName } from '@/mastodon/components/display_name'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; @@ -777,8 +778,8 @@ export const AccountHeader: React.FC<{ )} -
@@ -967,7 +968,7 @@ export const AccountHeader: React.FC<{
)} - + {!(hideTabs || hidden) && (
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index bb0815087b..fbe37f58a2 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -25,6 +25,7 @@ import StatusContent from 'mastodon/components/status_content'; import { Dropdown } from 'mastodon/components/dropdown_menu'; import { makeGetStatus } from 'mastodon/selectors'; import { LinkedDisplayName } from '@/mastodon/components/display_name'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -136,9 +137,9 @@ export const Conversation = ({ conversation, scrollKey }) => { {unread && }
-
+ {names} }} /> -
+ { if (loadedLocales.has(locale)) { return true; diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx deleted file mode 100644 index b4c352073c..0000000000 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { ComponentPropsWithoutRef, ElementType } from 'react'; - -import classNames from 'classnames'; - -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; - -import { useEmojify } from './hooks'; -import type { CustomEmojiMapArg } from './types'; - -type EmojiHTMLProps = Omit< - ComponentPropsWithoutRef, - 'dangerouslySetInnerHTML' | 'className' -> & { - htmlString: string; - extraEmojis?: CustomEmojiMapArg; - as?: Element; - shallow?: boolean; - className?: string; -}; - -export const ModernEmojiHTML = ({ - extraEmojis, - htmlString, - as: Wrapper = 'div', // Rename for syntax highlighting - shallow, - className = '', - ...props -}: EmojiHTMLProps) => { - const emojifiedHtml = useEmojify({ - text: htmlString, - extraEmojis, - deep: !shallow, - }); - - if (emojifiedHtml === null) { - return null; - } - - return ( - - ); -}; - -export const EmojiHTML = ( - props: EmojiHTMLProps, -) => { - if (isModernEmojiEnabled()) { - return ; - } - const { - as: asElement, - htmlString, - extraEmojis, - className, - shallow: _, - ...rest - } = props; - const Wrapper = asElement ?? 'div'; - return ( - - ); -}; diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index 72f57b6f6c..3196b28b9c 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -1,8 +1,6 @@ import { flattenEmojiData } from 'emojibase'; import type { CompactEmoji, FlatCompactEmoji } from 'emojibase'; -import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; - import { putEmojiData, putCustomEmojiData, @@ -10,7 +8,7 @@ import { putLatestEtag, } from './database'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; -import type { LocaleOrCustom } from './types'; +import type { CustomEmojiData, LocaleOrCustom } from './types'; import { emojiLogger } from './utils'; const log = emojiLogger('loader'); @@ -27,7 +25,7 @@ export async function importEmojiData(localeString: string) { } export async function importCustomEmojiData() { - const emojis = await fetchAndCheckEtag('custom'); + const emojis = await fetchAndCheckEtag('custom'); if (!emojis) { return; } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index f0ea140590..b4c7669961 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -5,11 +5,8 @@ import { flattenEmojiData } from 'emojibase'; import unicodeRawEmojis from 'emojibase-data/en/data.json'; import { - twemojiHasBorder, twemojiToUnicodeInfo, unicodeToTwemojiHex, - CODES_WITH_DARK_BORDER, - CODES_WITH_LIGHT_BORDER, emojiToUnicodeHex, } from './normalize'; @@ -57,26 +54,6 @@ describe('unicodeToTwemojiHex', () => { }); }); -describe('twemojiHasBorder', () => { - test.concurrent.for( - svgFileNames - .filter((file) => file.endsWith('_border')) - .map((file) => { - const hexCode = file.replace('_border', ''); - return [ - hexCode, - CODES_WITH_LIGHT_BORDER.includes(hexCode.toUpperCase()), - CODES_WITH_DARK_BORDER.includes(hexCode.toUpperCase()), - ] as const; - }), - )('twemojiHasBorder for %s', ([hexCode, isLight, isDark], { expect }) => { - const result = twemojiHasBorder(hexCode); - expect(result).toHaveProperty('hexCode', hexCode); - expect(result).toHaveProperty('hasLightBorder', isLight); - expect(result).toHaveProperty('hasDarkBorder', isDark); - }); -}); - describe('twemojiToUnicodeInfo', () => { const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode)); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 959732f985..65667dfe6d 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -1,5 +1,7 @@ import { isList } from 'immutable'; +import { assetHost } from '@/mastodon/utils/config'; + import { VARIATION_SELECTOR_CODE, KEYCAP_CODE, @@ -9,11 +11,7 @@ import { EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_LIGHT_BORDER, } from './constants'; -import type { - CustomEmojiMapArg, - ExtraCustomEmojiMap, - TwemojiBorderInfo, -} from './types'; +import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types'; // Misc codes that have special handling const SKIER_CODE = 0x26f7; @@ -67,21 +65,17 @@ export const CODES_WITH_DARK_BORDER = export const CODES_WITH_LIGHT_BORDER = EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); -export function twemojiHasBorder(twemojiHex: string): TwemojiBorderInfo { - const normalizedHex = twemojiHex.toUpperCase(); - let hasLightBorder = false; - let hasDarkBorder = false; - if (CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { - hasLightBorder = true; +export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string { + const normalizedHex = unicodeToTwemojiHex(unicodeHex); + let url = `${assetHost}/emoji/${normalizedHex}`; + if (darkMode && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { + url += '_border'; } if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) { - hasDarkBorder = true; + url += '_border'; } - return { - hexCode: twemojiHex, - hasLightBorder, - hasDarkBorder, - }; + url += '.svg'; + return url; } interface TwemojiSpecificEmoji { diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index e9609e15dc..108cf74750 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -1,10 +1,6 @@ import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories'; -import { - EMOJI_MODE_NATIVE, - EMOJI_MODE_NATIVE_WITH_FLAGS, - EMOJI_MODE_TWEMOJI, -} from './constants'; +import { EMOJI_MODE_TWEMOJI } from './constants'; import * as db from './database'; import { emojifyElement, @@ -12,7 +8,7 @@ import { testCacheClear, tokenizeText, } from './render'; -import type { EmojiAppState, ExtraCustomEmojiMap } from './types'; +import type { EmojiAppState } from './types'; function mockDatabase() { return { @@ -40,18 +36,6 @@ const expectedSmileImage = '😊'; const expectedFlagImage = '🇪🇺'; -const expectedCustomEmojiImage = - ':custom:'; -const expectedRemoteCustomEmojiImage = - ':remote:'; - -const mockExtraCustom: ExtraCustomEmojiMap = { - remote: { - shortcode: 'remote', - static_url: 'remote.social/static', - url: 'remote.social/custom', - }, -}; function testAppState(state: Partial = {}) { return { @@ -86,64 +70,10 @@ describe('emojifyElement', () => { 'en', ); expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([ - 'custom', + ':custom:', ]); }); - test('emojifies custom emoji in native mode', async () => { - const { searchEmojisByHexcodes } = mockDatabase(); - const actual = await emojifyElement( - testElement(), - testAppState({ mode: EMOJI_MODE_NATIVE }), - ); - assert(actual); - expect(actual.innerHTML).toBe( - `

Hello 😊🇪🇺!

${expectedCustomEmojiImage}

`, - ); - expect(searchEmojisByHexcodes).not.toHaveBeenCalled(); - }); - - test('emojifies flag emoji in native-with-flags mode', async () => { - const { searchEmojisByHexcodes } = mockDatabase(); - const actual = await emojifyElement( - testElement(), - testAppState({ mode: EMOJI_MODE_NATIVE_WITH_FLAGS }), - ); - assert(actual); - expect(actual.innerHTML).toBe( - `

Hello 😊${expectedFlagImage}!

${expectedCustomEmojiImage}

`, - ); - expect(searchEmojisByHexcodes).toHaveBeenCalledOnce(); - }); - - test('emojifies everything in twemoji mode', async () => { - const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } = - mockDatabase(); - const actual = await emojifyElement(testElement(), testAppState()); - assert(actual); - expect(actual.innerHTML).toBe( - `

Hello ${expectedSmileImage}${expectedFlagImage}!

${expectedCustomEmojiImage}

`, - ); - expect(searchEmojisByHexcodes).toHaveBeenCalledOnce(); - expect(searchCustomEmojisByShortcodes).toHaveBeenCalledOnce(); - }); - - test('emojifies with provided custom emoji', async () => { - const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } = - mockDatabase(); - const actual = await emojifyElement( - testElement('

hi :remote:

'), - testAppState(), - mockExtraCustom, - ); - assert(actual); - expect(actual.innerHTML).toBe( - `

hi ${expectedRemoteCustomEmojiImage}

`, - ); - expect(searchEmojisByHexcodes).not.toHaveBeenCalled(); - expect(searchCustomEmojisByShortcodes).not.toHaveBeenCalled(); - }); - test('returns null when no emoji are found', async () => { mockDatabase(); const actual = await emojifyElement( @@ -165,28 +95,9 @@ describe('emojifyText', () => { const actual = await emojifyText('Hello 😊🇪🇺!', testAppState()); expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`); }); - - test('renders custom emojis', async () => { - mockDatabase(); - const actual = await emojifyText('Hello :custom:!', testAppState()); - expect(actual).toBe(`Hello ${expectedCustomEmojiImage}!`); - }); - - test('renders provided extra emojis', async () => { - const actual = await emojifyText( - 'remote emoji :remote:', - testAppState(), - mockExtraCustom, - ); - expect(actual).toBe(`remote emoji ${expectedRemoteCustomEmojiImage}`); - }); }); describe('tokenizeText', () => { - test('returns empty array for string with only whitespace', () => { - expect(tokenizeText(' \n')).toEqual([]); - }); - test('returns an array of text to be a single token', () => { expect(tokenizeText('Hello')).toEqual(['Hello']); }); @@ -212,7 +123,7 @@ describe('tokenizeText', () => { 'Hello ', { type: 'custom', - code: 'smile', + code: ':smile:', }, '!!', ]); @@ -223,7 +134,7 @@ describe('tokenizeText', () => { 'Hello ', { type: 'custom', - code: 'smile_123', + code: ':smile_123:', }, '!!', ]); @@ -239,7 +150,7 @@ describe('tokenizeText', () => { ' ', { type: 'custom', - code: 'smile', + code: ':smile:', }, '!!', ]); diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 8d2299fd89..e0c8fd8dce 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -1,6 +1,5 @@ import { autoPlayGif } from '@/mastodon/initial_state'; import { createLimitedCache } from '@/mastodon/utils/cache'; -import { assetHost } from '@/mastodon/utils/config'; import * as perf from '@/mastodon/utils/performance'; import { @@ -8,38 +7,130 @@ import { EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, - EMOJI_STATE_MISSING, } from './constants'; import { + loadCustomEmojiByShortcode, + loadEmojiByHexcode, + LocaleNotLoadedError, searchCustomEmojisByShortcodes, searchEmojisByHexcodes, } from './database'; -import { - emojiToUnicodeHex, - twemojiHasBorder, - unicodeToTwemojiHex, -} from './normalize'; +import { importEmojiData } from './loader'; +import { emojiToUnicodeHex, unicodeHexToUrl } from './normalize'; import type { - CustomEmojiToken, EmojiAppState, EmojiLoadedState, EmojiMode, EmojiState, + EmojiStateCustom, EmojiStateMap, - EmojiToken, + EmojiStateUnicode, ExtraCustomEmojiMap, LocaleOrCustom, - UnicodeEmojiToken, } from './types'; import { anyEmojiRegex, emojiLogger, + isCustomEmoji, + isUnicodeEmoji, stringHasAnyEmoji, stringHasUnicodeFlags, } from './utils'; const log = emojiLogger('render'); +/** + * Parses emoji string to extract emoji state. + * @param code Hex code or custom shortcode. + * @param customEmoji Extra custom emojis. + */ +export function stringToEmojiState( + code: string, + customEmoji: ExtraCustomEmojiMap = {}, +): EmojiState | null { + if (isUnicodeEmoji(code)) { + return { + type: EMOJI_TYPE_UNICODE, + code: emojiToUnicodeHex(code), + }; + } + + if (isCustomEmoji(code)) { + const shortCode = code.slice(1, -1); + return { + type: EMOJI_TYPE_CUSTOM, + code: shortCode, + data: customEmoji[shortCode], + }; + } + + return null; +} + +/** + * Loads emoji data into the given state if not already loaded. + * @param state Emoji state to load data for. + * @param locale Locale to load data for. Only for Unicode emoji. + * @param retry Internal. Whether this is a retry after loading the locale. + */ +export async function loadEmojiDataToState( + state: EmojiState, + locale: string, + retry = false, +): Promise { + if (isStateLoaded(state)) { + return state; + } + + // First, try to load the data from IndexedDB. + try { + // This is duplicative, but that's because TS can't distinguish the state type easily. + if (state.type === EMOJI_TYPE_UNICODE) { + const data = await loadEmojiByHexcode(state.code, locale); + if (data) { + return { + ...state, + data, + }; + } + } else { + const data = await loadCustomEmojiByShortcode(state.code); + if (data) { + return { + ...state, + data, + }; + } + } + // If not found, assume it's not an emoji and return null. + log( + 'Could not find emoji %s of type %s for locale %s', + state.code, + state.type, + locale, + ); + return null; + } catch (err: unknown) { + // If the locale is not loaded, load it and retry once. + if (!retry && err instanceof LocaleNotLoadedError) { + log( + 'Error loading emoji %s for locale %s, loading locale and retrying.', + state.code, + locale, + ); + await importEmojiData(locale); // Use this from the loader file as it can be awaited. + return loadEmojiDataToState(state, locale, true); + } + + console.warn('Error loading emoji data, not retrying:', state, locale, err); + return null; + } +} + +export function isStateLoaded(state: EmojiState): state is EmojiLoadedState { + return !!state.data; +} + /** * Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions. */ @@ -177,7 +268,11 @@ async function textToElementArray( if (token.type === EMOJI_TYPE_CUSTOM) { const extraEmojiData = extraEmojis[token.code]; if (extraEmojiData) { - state = { type: EMOJI_TYPE_CUSTOM, data: extraEmojiData }; + state = { + type: EMOJI_TYPE_CUSTOM, + data: extraEmojiData, + code: token.code, + }; } else { state = emojiForLocale(token.code, EMOJI_TYPE_CUSTOM); } @@ -189,7 +284,7 @@ async function textToElementArray( } // If the state is valid, create an image element. Otherwise, just append as text. - if (state && typeof state !== 'string') { + if (state && typeof state !== 'string' && isStateLoaded(state)) { const image = stateToImage(state, appState); renderedFragments.push(image); continue; @@ -202,11 +297,11 @@ async function textToElementArray( return renderedFragments; } -type TokenizedText = (string | EmojiToken)[]; +type TokenizedText = (string | EmojiState)[]; export function tokenizeText(text: string): TokenizedText { if (!text.trim()) { - return []; + return [text]; } const tokens = []; @@ -222,14 +317,14 @@ export function tokenizeText(text: string): TokenizedText { // Custom emoji tokens.push({ type: EMOJI_TYPE_CUSTOM, - code: code.slice(1, -1), // Remove the colons - } satisfies CustomEmojiToken); + code, + } satisfies EmojiStateCustom); } else { // Unicode emoji tokens.push({ type: EMOJI_TYPE_UNICODE, code: code, - } satisfies UnicodeEmojiToken); + } satisfies EmojiStateUnicode); } lastIndex = match.index + code.length; } @@ -304,13 +399,11 @@ async function loadMissingEmojiIntoCache( const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale); const cache = cacheForLocale(currentLocale); for (const emoji of emojis) { - cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji }); - } - const notFoundEmojis = missingEmojis.filter((code) => - emojis.every((emoji) => emoji.hexcode !== code), - ); - for (const code of notFoundEmojis) { - cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji. + cache.set(emoji.hexcode, { + type: EMOJI_TYPE_UNICODE, + data: emoji, + code: emoji.hexcode, + }); } localeCacheMap.set(currentLocale, cache); } @@ -320,19 +413,17 @@ async function loadMissingEmojiIntoCache( const emojis = await searchCustomEmojisByShortcodes(missingEmojis); const cache = cacheForLocale(EMOJI_TYPE_CUSTOM); for (const emoji of emojis) { - cache.set(emoji.shortcode, { type: EMOJI_TYPE_CUSTOM, data: emoji }); - } - const notFoundEmojis = missingEmojis.filter((code) => - emojis.every((emoji) => emoji.shortcode !== code), - ); - for (const code of notFoundEmojis) { - cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji. + cache.set(emoji.shortcode, { + type: EMOJI_TYPE_CUSTOM, + data: emoji, + code: emoji.shortcode, + }); } localeCacheMap.set(EMOJI_TYPE_CUSTOM, cache); } } -function shouldRenderImage(token: EmojiToken, mode: EmojiMode): boolean { +export function shouldRenderImage(token: EmojiState, mode: EmojiMode): boolean { if (token.type === EMOJI_TYPE_UNICODE) { // If the mode is native or native with flags for non-flag emoji // we can just append the text node directly. @@ -354,18 +445,9 @@ function stateToImage(state: EmojiLoadedState, appState: EmojiAppState) { image.classList.add('emojione'); if (state.type === EMOJI_TYPE_UNICODE) { - const emojiInfo = twemojiHasBorder(unicodeToTwemojiHex(state.data.hexcode)); - let fileName = emojiInfo.hexCode; - if ( - (appState.darkTheme && emojiInfo.hasDarkBorder) || - (!appState.darkTheme && emojiInfo.hasLightBorder) - ) { - fileName = `${emojiInfo.hexCode}_border`; - } - image.alt = state.data.unicode; image.title = state.data.label; - image.src = `${assetHost}/emoji/${fileName}.svg`; + image.src = unicodeHexToUrl(state.data.hexcode, appState.darkTheme); } else { // Custom emoji const shortCode = `:${state.data.shortcode}:`; diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index 85bbe6d1a5..043b21361b 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -10,7 +10,6 @@ import type { EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_TWEMOJI, - EMOJI_STATE_MISSING, EMOJI_TYPE_CUSTOM, EMOJI_TYPE_UNICODE, } from './constants'; @@ -29,45 +28,40 @@ export interface EmojiAppState { darkTheme: boolean; } -export interface UnicodeEmojiToken { - type: typeof EMOJI_TYPE_UNICODE; - code: string; -} -export interface CustomEmojiToken { - type: typeof EMOJI_TYPE_CUSTOM; - code: string; -} -export type EmojiToken = UnicodeEmojiToken | CustomEmojiToken; - export type CustomEmojiData = ApiCustomEmojiJSON; export type UnicodeEmojiData = FlatCompactEmoji; export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData; -export type EmojiStateMissing = typeof EMOJI_STATE_MISSING; +type CustomEmojiRenderFields = Pick< + CustomEmojiData, + 'shortcode' | 'static_url' | 'url' +>; + export interface EmojiStateUnicode { type: typeof EMOJI_TYPE_UNICODE; - data: UnicodeEmojiData; + code: string; + data?: UnicodeEmojiData; } export interface EmojiStateCustom { type: typeof EMOJI_TYPE_CUSTOM; - data: CustomEmojiRenderFields; + code: string; + data?: CustomEmojiRenderFields; } -export type EmojiState = - | EmojiStateMissing - | EmojiStateUnicode - | EmojiStateCustom; -export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom; +export type EmojiState = EmojiStateUnicode | EmojiStateCustom; +export type EmojiLoadedState = + | Required + | Required; export type EmojiStateMap = LimitedCache; export type CustomEmojiMapArg = | ExtraCustomEmojiMap | ImmutableList; -export type CustomEmojiRenderFields = Pick< - CustomEmojiData, - 'shortcode' | 'static_url' | 'url' + +export type ExtraCustomEmojiMap = Record< + string, + Pick >; -export type ExtraCustomEmojiMap = Record; export interface TwemojiBorderInfo { hexCode: string; diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index ce35919929..e811565c27 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -10,6 +10,13 @@ export function stringHasUnicodeEmoji(input: string): boolean { return new RegExp(EMOJI_REGEX, supportedFlags()).test(input); } +export function isUnicodeEmoji(input: string): boolean { + return ( + input.length > 0 && + new RegExp(`^(${EMOJI_REGEX})+$`, supportedFlags()).test(input) + ); +} + export function stringHasUnicodeFlags(input: string): boolean { if (supportsRegExpSets()) { return new RegExp( @@ -27,6 +34,11 @@ export function stringHasUnicodeFlags(input: string): boolean { // Constant as this is supported by all browsers. const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; + +export function isCustomEmoji(input: string): boolean { + return new RegExp(`^${CUSTOM_EMOJI_REGEX.source}$`, 'i').test(input); +} + export function stringHasCustomEmoji(input: string) { return CUSTOM_EMOJI_REGEX.test(input); } diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx index a17425169b..8e5e72b6aa 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom'; import type { List as ImmutableList, RecordOf } from 'immutable'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; import { toggleStatusSpoilers } from 'mastodon/actions/statuses'; @@ -96,8 +97,8 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ ).size; return ( -
= ({ )}
)} - + ); }; diff --git a/app/javascript/types/polymorphic.ts b/app/javascript/types/polymorphic.ts new file mode 100644 index 0000000000..e58aa7b75e --- /dev/null +++ b/app/javascript/types/polymorphic.ts @@ -0,0 +1,75 @@ +import { forwardRef } from 'react'; +import type { + ElementType, + ComponentPropsWithRef, + ForwardRefRenderFunction, + ReactElement, + Ref, + ForwardRefExoticComponent, +} from 'react'; + +// This complicated type file is based on the following posts: +// - https://www.tsteele.dev/posts/react-polymorphic-forwardref +// - https://www.kripod.dev/blog/behind-the-as-prop-polymorphism-done-well/ +// - https://github.com/radix-ui/primitives/blob/7101e7d6efb2bff13cc6761023ab85aeec73539e/packages/react/polymorphic/src/forwardRefWithAs.ts +// Whenever we upgrade to React 19 or later, we can remove all this because ref is a prop there. + +// Utils +interface AsProp { + as?: As; +} +type PropsOf = ComponentPropsWithRef; + +/** + * Extract the element instance type (e.g. HTMLButtonElement) from ComponentPropsWithRef: + * - For intrinsic elements, look up in JSX.IntrinsicElements + * - For components, infer from `ComponentPropsWithRef` + */ +type ElementRef = + As extends keyof React.JSX.IntrinsicElements + ? React.JSX.IntrinsicElements[As] extends { ref?: Ref } + ? Inst + : never + : ComponentPropsWithRef extends { ref?: Ref } + ? Inst + : never; + +/** + * Merge additional props with intrinsic/element props for `as`. + * Additional props win on conflicts. + */ +type PolymorphicProps< + As extends ElementType, + AdditionalProps extends object = object, +> = AdditionalProps & + AsProp & + Omit, keyof AdditionalProps | 'ref'>; + +/** + * Signature of a component created with `polymorphicForwardRef`. + */ +type PolymorphicWithRef< + DefaultAs extends ElementType, + AdditionalProps extends object = object, +> = ( + props: PolymorphicProps & { ref?: Ref> }, +) => ReactElement | null; + +/** + * The type of `polymorphicForwardRef`. + */ +type PolyRefFunction = < + DefaultAs extends ElementType, + AdditionalProps extends object = object, +>( + render: ForwardRefRenderFunction< + ElementRef, + PolymorphicProps + >, +) => PolymorphicWithRef & + ForwardRefExoticComponent>; + +/** + * Polymorphic `forwardRef` function. + */ +export const polymorphicForwardRef = forwardRef as PolyRefFunction; From 473bd84c24ac2197ed16c7e4a7f8271653511f8f Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 30 Sep 2025 16:55:25 +0200 Subject: [PATCH 48/58] Update confirmation dialogs for follow button actions "unfollow", "unblock", and "withdraw request" (#36289) --- .../mastodon/components/follow_button.tsx | 25 ++-- .../components/account_header.tsx | 50 ++++---- .../confirmation_modal.tsx | 4 +- .../components/confirmation_modals/index.ts | 2 + .../confirmation_modals/unblock.tsx | 45 ++++++++ .../confirmation_modals/unfollow.tsx | 13 +-- .../withdraw_follow_request.tsx | 45 ++++++++ .../features/ui/components/modal_root.jsx | 4 + app/javascript/mastodon/locales/en.json | 7 +- .../styles/mastodon/components.scss | 109 +++++++++++------- 10 files changed, 218 insertions(+), 86 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/unblock.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/withdraw_follow_request.tsx diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 15a9046848..97aaecd1aa 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -8,7 +8,6 @@ import { useIdentity } from '@/mastodon/identity_context'; import { fetchRelationships, followAccount, - unblockAccount, unmuteAccount, } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; @@ -59,7 +58,8 @@ export const FollowButton: React.FC<{ accountId?: string; compact?: boolean; labelLength?: 'auto' | 'short' | 'long'; -}> = ({ accountId, compact, labelLength = 'auto' }) => { + className?: string; +}> = ({ accountId, compact, labelLength = 'auto', className }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const { signedIn } = useIdentity(); @@ -96,12 +96,24 @@ export const FollowButton: React.FC<{ return; } else if (relationship.muting) { dispatch(unmuteAccount(accountId)); - } else if (account && (relationship.following || relationship.requested)) { + } else if (account && relationship.following) { dispatch( openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), ); + } else if (account && relationship.requested) { + dispatch( + openModal({ + modalType: 'CONFIRM_WITHDRAW_REQUEST', + modalProps: { account }, + }), + ); } else if (relationship.blocking) { - dispatch(unblockAccount(accountId)); + dispatch( + openModal({ + modalType: 'CONFIRM_UNBLOCK', + modalProps: { account }, + }), + ); } else { dispatch(followAccount(accountId)); } @@ -144,7 +156,7 @@ export const FollowButton: React.FC<{ href='/settings/profile' target='_blank' rel='noopener' - className={classNames('button button-secondary', { + className={classNames(className, 'button button-secondary', { 'button--compact': compact, })} > @@ -158,13 +170,12 @@ export const FollowButton: React.FC<{ onClick={handleClick} disabled={ relationship?.blocked_by || - relationship?.blocking || (!(relationship?.following || relationship?.requested) && (account?.suspended || !!account?.moved)) } secondary={following} compact={compact} - className={following ? 'button--destructive' : undefined} + className={classNames(className, { 'button--destructive': following })} > {label} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 2be026c8f9..776157ccf5 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -34,7 +34,6 @@ import { initMuteModal } from 'mastodon/actions/mutes'; import { initReport } from 'mastodon/actions/reports'; import { Avatar } from 'mastodon/components/avatar'; import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; -import { Button } from 'mastodon/components/button'; import { CopyIconButton } from 'mastodon/components/copy_icon_button'; import { FollowersCounter, @@ -384,7 +383,7 @@ export const AccountHeader: React.FC<{ const isRemote = account?.acct !== account?.username; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; - const menu = useMemo(() => { + const menuItems = useMemo(() => { const arr: MenuItem[] = []; if (!account) { @@ -606,6 +605,15 @@ export const AccountHeader: React.FC<{ handleUnblockDomain, ]); + const menu = accountId !== me && ( + + ); + if (!account) { return null; } @@ -719,21 +727,16 @@ export const AccountHeader: React.FC<{ ); } - if (relationship?.blocking) { + const isMovedAndUnfollowedAccount = account.moved && !relationship?.following; + + if (!isMovedAndUnfollowedAccount) { actionBtn = ( -