diff --git a/.nvmrc b/.nvmrc index b0195acf781..12fd1fc2777 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.12 +24.13 diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html index 7a92b6f95ff..7c078c0b3b7 100644 --- a/.storybook/preview-body.html +++ b/.storybook/preview-body.html @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index abbd193c681..10d45acfe65 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -50,9 +50,19 @@ const preview: Preview = { dynamicTitle: true, }, }, + theme: { + description: 'Theme for the story', + toolbar: { + title: 'Theme', + icon: 'circlehollow', + items: [{ value: 'light' }, { value: 'dark' }], + dynamicTitle: true, + }, + }, }, initialGlobals: { locale: 'en', + theme: 'light', }, decorators: [ (Story, { parameters, globals, args, argTypes }) => { @@ -135,6 +145,13 @@ const preview: Preview = { ); }, + (Story, { globals }) => { + const theme = (globals.theme as string) || 'light'; + useEffect(() => { + document.body.setAttribute('data-color-scheme', theme); + }, [theme]); + return ; + }, (Story) => ( diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5ec67d854..39e975479e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. +## [4.5.5] - 2026-01-20 + +### Security + +- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g) +- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp) +- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3) +- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4) + +### Changed + +- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire) + +### Fixed + +- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire) +- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire) +- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire) +- Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436 and #37474 by @mjankowski and @shleeable) +- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec) +- Fix missing URI scheme test in `QuoteRequest` handling (#37425 by @MegaManSec) +- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec) +- Fix URI generation for reblogs by accounts with numerical ActivityPub identifiers (#37415 by @oneiros) +- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable) +- Fix emoji with variant selector not being rendered properly (#37320 by @ChaosExAnima) +- Fix mobile admin sidebar displaying under batch table toolbar (#37307 by @diondiondion) + ## [4.5.4] - 2026-01-07 ### Security diff --git a/Dockerfile b/Dockerfile index 865d14402cd..b9dcbe59fd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -183,7 +183,7 @@ FROM build AS libvips # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.17.3 +ARG VIPS_VERSION=8.18.0 # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] ARG VIPS_URL=https://github.com/libvips/libvips/releases/download diff --git a/FEDERATION.md b/FEDERATION.md index 03ea5449de3..eb91d9545fe 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques ### Additional documentation - [Mastodon documentation](https://docs.joinmastodon.org/) + +## Size limits + +Mastodon imposes a few hard limits on federated content. +These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. +The following table attempts to summary those limits. + +| Limited property | Size limit | Consequence of exceeding the limit | +| ------------------------------------------------------------- | ---------- | ---------------------------------- | +| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** | +| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated | +| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated | +| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated | +| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** | +| Account display name (actor `name`) length | 2048 | Display name will be truncated | +| Account note (actor `summary`) length | 20kB | Account note will be truncated | +| Account `attributionDomains` | 256 | List will be truncated | +| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated | +| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected | diff --git a/Gemfile b/Gemfile index 3cc9580fea2..f5da754b1a7 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem 'hiredis-client' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.3.0' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.7.0', require: false +gem 'httplog', '~> 1.8.0', require: false gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' @@ -109,12 +109,12 @@ group :opentelemetry do gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.34.0', require: false + gem 'opentelemetry-instrumentation-excon', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.31.0', require: false + gem 'opentelemetry-instrumentation-http', '~> 0.28.0', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-net_http', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-pg', '~> 0.35.0', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d73316b4e98..3ee1f77aa0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,8 +96,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1200.0) - aws-sdk-core (3.240.0) + aws-partitions (1.1201.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -105,11 +105,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.209.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -282,7 +282,7 @@ GEM rake (>= 13) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) - haml (7.1.0) + haml (7.2.0) temple (>= 0.8.2) thor tilt @@ -291,7 +291,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.68.0) + haml_lint (0.69.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -305,8 +305,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.26.2) - redis-client (= 0.26.2) + hiredis-client (0.26.3) + redis-client (= 0.26.3) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -320,7 +320,8 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.3) + httplog (1.8.0) + benchmark rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.8) @@ -520,7 +521,8 @@ GEM opentelemetry-semantic_conventions opentelemetry-helpers-sql (0.3.0) opentelemetry-api (~> 1.7) - opentelemetry-helpers-sql-processor (0.3.1) + opentelemetry-helpers-sql-processor (0.4.0) + opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) opentelemetry-instrumentation-action_mailer (0.6.1) opentelemetry-instrumentation-active_support (~> 0.10) @@ -544,17 +546,17 @@ GEM opentelemetry-registry (~> 0.1) opentelemetry-instrumentation-concurrent_ruby (0.24.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.26.1) + opentelemetry-instrumentation-excon (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-faraday (0.30.1) + opentelemetry-instrumentation-faraday (0.31.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http (0.27.1) + opentelemetry-instrumentation-http (0.28.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.26.1) + opentelemetry-instrumentation-http_client (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-net_http (0.26.1) + opentelemetry-instrumentation-net_http (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-pg (0.34.1) + opentelemetry-instrumentation-pg (0.35.0) opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) @@ -587,7 +589,7 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.10.0) + parser (3.3.10.1) ast (~> 2.4.1) racc parslet (2.0.0) @@ -611,7 +613,7 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) - prism (1.7.0) + prism (1.8.0) prometheus_exporter (2.3.1) webrick propshaft (1.3.1) @@ -706,7 +708,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.26.2) + redis-client (0.26.3) connection_pool regexp_parser (2.11.3) reline (0.6.3) @@ -720,10 +722,10 @@ GEM rotp (6.3.0) rouge (4.7.0) rpam2 (4.0.2) - rqrcode (3.1.1) + rqrcode (3.2.0) chunky_png (~> 1.0) rqrcode_core (~> 2.0) - rqrcode_core (2.0.1) + rqrcode_core (2.1.0) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -752,7 +754,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.6) - rubocop (1.81.7) + rubocop (1.82.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -760,7 +762,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -782,7 +784,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.8.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) rubocop (~> 1.81) rubocop-rspec_rails (2.32.0) @@ -823,7 +825,7 @@ GEM sidekiq-scheduler (6.0.1) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) - sidekiq-unique-jobs (8.0.11) + sidekiq-unique-jobs (8.0.13) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) @@ -860,8 +862,8 @@ GEM terrapin (1.1.1) climate_control test-prof (1.5.0) - thor (1.4.0) - tilt (2.6.1) + thor (1.5.0) + tilt (2.7.0) timeout (0.6.0) tpm-key_attestation (0.14.1) bindata (~> 2.4) @@ -985,7 +987,7 @@ DEPENDENCIES htmlentities (~> 4.3) http (~> 5.3.0) http_accept_language (~> 2.1) - httplog (~> 1.7.0) + httplog (~> 1.8.0) i18n i18n-tasks (~> 1.0) idn-ruby @@ -1021,12 +1023,12 @@ DEPENDENCIES opentelemetry-instrumentation-active_job (~> 0.10.0) opentelemetry-instrumentation-active_model_serializers (~> 0.24.0) opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0) - opentelemetry-instrumentation-excon (~> 0.26.0) - opentelemetry-instrumentation-faraday (~> 0.30.0) - opentelemetry-instrumentation-http (~> 0.27.0) - opentelemetry-instrumentation-http_client (~> 0.26.0) - opentelemetry-instrumentation-net_http (~> 0.26.0) - opentelemetry-instrumentation-pg (~> 0.34.0) + opentelemetry-instrumentation-excon (~> 0.27.0) + opentelemetry-instrumentation-faraday (~> 0.31.0) + opentelemetry-instrumentation-http (~> 0.28.0) + opentelemetry-instrumentation-http_client (~> 0.27.0) + opentelemetry-instrumentation-net_http (~> 0.27.0) + opentelemetry-instrumentation-pg (~> 0.35.0) opentelemetry-instrumentation-rack (~> 0.29.0) opentelemetry-instrumentation-rails (~> 0.39.0) opentelemetry-instrumentation-redis (~> 0.28.0) diff --git a/SECURITY.md b/SECURITY.md index 12052652e6c..e5790a66fa2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,5 +18,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | 4.5.x | Yes | | 4.4.x | Yes | | 4.3.x | Until 2026-05-06 | -| 4.2.x | Until 2026-01-08 | -| < 4.2 | No | +| < 4.3 | No | diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index c80db3500de..a03f424e0f1 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::CollectionsController < ActivityPub::BaseController + SUPPORTED_COLLECTIONS = %w(featured tags).freeze + vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature!, if: :authorized_fetch_mode? diff --git a/app/controllers/activitypub/featured_collections_controller.rb b/app/controllers/activitypub/featured_collections_controller.rb new file mode 100644 index 00000000000..872d03423d2 --- /dev/null +++ b/app/controllers/activitypub/featured_collections_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class ActivityPub::FeaturedCollectionsController < ApplicationController + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + PER_PAGE = 5 + + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + + before_action :check_feature_enabled + before_action :require_account_signature!, if: -> { authorized_fetch_mode? } + before_action :set_collections + + skip_around_action :set_locale + skip_before_action :require_functional!, unless: :limited_federation_mode? + + def index + respond_to do |format| + format.json do + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) + + render json: collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json' + end + end + end + + private + + def set_collections + authorize @account, :index_collections? + @collections = @account.collections.page(params[:page]).per(PER_PAGE) + rescue Mastodon::NotPermittedError + not_found + end + + def page_requested? + params[:page].present? + end + + def next_page_url + ap_account_featured_collections_url(@account, page: @collections.next_page) if @collections.respond_to?(:next_page) + end + + def prev_page_url + ap_account_featured_collections_url(@account, page: @collections.prev_page) if @collections.respond_to?(:prev_page) + end + + def collection_presenter + if page_requested? + ActivityPub::CollectionPresenter.new( + id: ap_account_featured_collections_url(@account, page: params.fetch(:page, 1)), + type: :unordered, + size: @account.collections.count, + items: @collections, + part_of: ap_account_featured_collections_url(@account), + next: next_page_url, + prev: prev_page_url + ) + else + ActivityPub::CollectionPresenter.new( + id: ap_account_featured_collections_url(@account), + type: :unordered, + size: @account.collections.count, + first: ap_account_featured_collections_url(@account, page: 1) + ) + end + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end +end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 1f7abb97fa5..cf46bf21b5e 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -3,6 +3,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController include JsonLdHelper + before_action :skip_large_payload before_action :skip_unknown_actor_activity before_action :require_actor_signature! skip_before_action :authenticate_user! @@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController private + def skip_large_payload + head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE + end + def skip_unknown_actor_activity head 202 if unknown_affected_account? end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index ec6b93e4085..f07dda1247f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -106,9 +106,7 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.where(account: current_account).find(params[:id]) authorize @status, :update? - UpdateStatusService.new.call( - @status, - current_account.id, + update_options = { text: status_params[:status], media_ids: status_params[:media_ids], media_attributes: status_params[:media_attributes], @@ -116,8 +114,11 @@ class Api::V1::StatusesController < Api::BaseController language: status_params[:language], spoiler_text: status_params[:spoiler_text], poll: status_params[:poll], - quote_approval_policy: quote_approval_policy - ) + } + + update_options[:quote_approval_policy] = quote_approval_policy if status_params[:quote_approval_policy].present? + + UpdateStatusService.new.call(@status, current_account.id, update_options) render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index d0c4e0f3f04..4b07b5012a2 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -74,6 +74,7 @@ class Api::V1Alpha::CollectionsController < Api::BaseController .order(created_at: :desc) .offset(offset_param) .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections = @collections.discoverable unless @account == current_account end def set_collection @@ -81,11 +82,11 @@ class Api::V1Alpha::CollectionsController < Api::BaseController end def collection_creation_params - params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: []) + params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name, account_ids: []) end def collection_update_params - params.permit(:name, :description, :sensitive, :discoverable, :tag_name) + params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name) end def check_feature_enabled diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index ced68d39fc7..2edd92dbc7b 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def set_push_subscription - @push_subscription = ::Web::PushSubscription.find(params[:id]) + @push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id]) end def subscription_params diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb new file mode 100644 index 00000000000..3e2ba714702 --- /dev/null +++ b/app/controllers/collections_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class CollectionsController < ApplicationController + include WebAppControllerConcern + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + + before_action :check_feature_enabled + before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :set_collection + + skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode? + + def show + respond_to do |format| + # TODO: format.html + + format.json do + expires_in expiration_duration, public: true if public_fetch_mode? + render_with_cache json: @collection, content_type: 'application/activity+json', serializer: ActivityPub::FeaturedCollectionSerializer, adapter: ActivityPub::Adapter + end + end + end + + private + + def set_collection + @collection = @account.collections.find(params[:id]) + authorize @collection, :show? + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + not_found + end + + def expiration_duration + recently_updated = @collection.updated_at > 15.minutes.ago + recently_updated ? 30.seconds : 5.minutes + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index b1b09f2aab0..3527cdaca03 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -19,7 +19,7 @@ module CacheConcern # from being used as cache keys, while allowing to `Vary` on them (to not serve # anonymous cached data to authenticated requests when authentication matters) def enforce_cache_control! - vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?) return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } response.cache_control.replace(private: true, no_store: true) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1076d9ced84..b23968e3731 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -89,6 +89,12 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end + def page_color_scheme + return content_for(:force_color_scheme) if content_for(:force_color_scheme) + + color_scheme + end + def label_for_scope(scope) safe_join [ tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }), @@ -153,6 +159,19 @@ module ApplicationHelper tag.meta(content: content, property: property) end + def html_attributes + base = { + lang: I18n.locale, + class: html_classes, + 'data-contrast': contrast.parameterize, + 'data-color-scheme': page_color_scheme.parameterize, + } + + base[:'data-system-theme'] = 'true' if page_color_scheme == 'auto' + + base + end + def html_classes output = [] output << content_for(:html_classes) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 00b4a6d2b3f..f651a495ffe 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -1,25 +1,40 @@ # frozen_string_literal: true module ThemeHelper - def theme_style_tags(theme) - if theme == 'system' - ''.html_safe.tap do |tags| - tags << vite_stylesheet_tag('themes/mastodon-light', type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') - tags << vite_stylesheet_tag('themes/default', type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') + def javascript_inline_tag(path) + entry = InlineScriptManager.instance.file(path) + + # Only add hash if we don't allow arbitrary includes already, otherwise it's going + # to break the React Tools browser extension or other inline scripts + unless Rails.env.development? && request.content_security_policy.dup.script_src.include?("'unsafe-inline'") + request.content_security_policy = request.content_security_policy.clone.tap do |policy| + values = policy.script_src + values << "'sha256-#{entry[:digest]}'" + policy.script_src(*values) end - else - vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous' end + + content_tag(:script, entry[:contents], type: 'text/javascript') end - def theme_color_tags(theme) - if theme == 'system' + def theme_style_tags(theme) + # TODO: get rid of that when we retire the themes and perform the settings migration + theme = 'default' if %w(mastodon-light contrast system).include?(theme) + + vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous' + end + + def theme_color_tags(color_scheme) + case color_scheme + when 'auto' ''.html_safe.tap do |tags| tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)') end - else - tag.meta name: 'theme-color', content: theme_color_for(theme) + when 'light' + tag.meta name: 'theme-color', content: Themes::THEME_COLORS[:light] + when 'dark' + tag.meta name: 'theme-color', content: Themes::THEME_COLORS[:dark] end end @@ -49,8 +64,4 @@ module ThemeHelper Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) } end end - - def theme_color_for(theme) - theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] - end end diff --git a/app/javascript/entrypoints/theme-selection.ts b/app/javascript/entrypoints/theme-selection.ts new file mode 100644 index 00000000000..76e46e15f19 --- /dev/null +++ b/app/javascript/entrypoints/theme-selection.ts @@ -0,0 +1 @@ +import '../inline/theme-selection'; diff --git a/app/javascript/images/icons/icon_admin.svg b/app/javascript/images/icons/icon_admin.svg new file mode 100644 index 00000000000..7e40dc46437 --- /dev/null +++ b/app/javascript/images/icons/icon_admin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/images/icons/icon_verified.svg b/app/javascript/images/icons/icon_verified.svg new file mode 100644 index 00000000000..65873b9dc43 --- /dev/null +++ b/app/javascript/images/icons/icon_verified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/inline/theme-selection.js b/app/javascript/inline/theme-selection.js new file mode 100644 index 00000000000..680fbb23ec2 --- /dev/null +++ b/app/javascript/inline/theme-selection.js @@ -0,0 +1,24 @@ +(function (element) { + const {colorScheme, contrast} = element.dataset; + + const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)'); + const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)'); + + const updateColorScheme = () => { + const useDarkMode = colorScheme === 'auto' ? colorSchemeMediaWatcher.matches : colorScheme === 'dark'; + + element.dataset.colorScheme = useDarkMode ? 'dark' : 'light'; + }; + + const updateContrast = () => { + const useHighContrast = contrast === 'high' || contrastMediaWatcher.matches; + + element.dataset.contrast = useHighContrast ? 'high' : 'default'; + } + + colorSchemeMediaWatcher.addEventListener('change', updateColorScheme); + contrastMediaWatcher.addEventListener('change', updateContrast); + + updateColorScheme(); + updateContrast(); +})(document.documentElement); diff --git a/app/javascript/mastodon/actions/directory.ts b/app/javascript/mastodon/actions/directory.ts index 34ac309c66c..a50a377ffcf 100644 --- a/app/javascript/mastodon/actions/directory.ts +++ b/app/javascript/mastodon/actions/directory.ts @@ -6,15 +6,17 @@ import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +const DIRECTORY_FETCH_LIMIT = 20; + export const fetchDirectory = createDataLoadingThunk( 'directory/fetch', async (params: Parameters[0]) => - apiGetDirectory(params), + apiGetDirectory(params, DIRECTORY_FETCH_LIMIT), (data, { dispatch }) => { dispatch(importFetchedAccounts(data)); dispatch(fetchRelationships(data.map((x) => x.id))); - return { accounts: data }; + return { accounts: data, isLast: data.length < DIRECTORY_FETCH_LIMIT }; }, ); @@ -26,12 +28,15 @@ export const expandDirectory = createDataLoadingThunk( 'items', ]) as ImmutableList; - return apiGetDirectory({ ...params, offset: loadedItems.size }, 20); + return apiGetDirectory( + { ...params, offset: loadedItems.size }, + DIRECTORY_FETCH_LIMIT, + ); }, (data, { dispatch }) => { dispatch(importFetchedAccounts(data)); dispatch(fetchRelationships(data.map((x) => x.id))); - return { accounts: data }; + return { accounts: data, isLast: data.length < DIRECTORY_FETCH_LIMIT }; }, ); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index 1820e00a537..2af29c783e0 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -128,15 +128,18 @@ export default function api(withAuthorization = true) { } type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`; -type RequestParamsOrData = Record; +type RequestParamsOrData = T | Record; -export async function apiRequest( +export async function apiRequest< + ApiResponse = unknown, + ApiParamsOrData = unknown, +>( method: Method, url: string, args: { signal?: AbortSignal; - params?: RequestParamsOrData; - data?: RequestParamsOrData; + params?: RequestParamsOrData; + data?: RequestParamsOrData; timeout?: number; } = {}, ) { @@ -149,30 +152,30 @@ export async function apiRequest( return data; } -export async function apiRequestGet( +export async function apiRequestGet( url: ApiUrl, - params?: RequestParamsOrData, + params?: RequestParamsOrData, ) { return apiRequest('GET', url, { params }); } -export async function apiRequestPost( +export async function apiRequestPost( url: ApiUrl, - data?: RequestParamsOrData, + data?: RequestParamsOrData, ) { return apiRequest('POST', url, { data }); } -export async function apiRequestPut( +export async function apiRequestPut( url: ApiUrl, - data?: RequestParamsOrData, + data?: RequestParamsOrData, ) { return apiRequest('PUT', url, { data }); } -export async function apiRequestDelete( - url: ApiUrl, - params?: RequestParamsOrData, -) { +export async function apiRequestDelete< + ApiResponse = unknown, + ApiParams = unknown, +>(url: ApiUrl, params?: RequestParamsOrData) { return apiRequest('DELETE', url, { params }); } diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index 1451ea82a07..d61d8ceed06 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -41,11 +41,10 @@ export interface ApiPreviewCardJSON { url: string; title: string; description: string; - language: string; - type: string; + language: string | null; + type: 'video' | 'link'; author_name: string; author_url: string; - author_account?: ApiAccountJSON; provider_name: string; provider_url: string; html: string; @@ -55,7 +54,7 @@ export interface ApiPreviewCardJSON { image_description: string; embed_url: string; blurhash: string; - published_at: string; + published_at: string | null; authors: ApiPreviewCardAuthorJSON[]; } diff --git a/app/javascript/mastodon/components/badge.jsx b/app/javascript/mastodon/components/badge.jsx deleted file mode 100644 index 2a335d7f506..00000000000 --- a/app/javascript/mastodon/components/badge.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; -import PersonIcon from '@/material-icons/400-24px/person.svg?react'; -import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; - - -export const Badge = ({ icon = , label, domain, roleId }) => ( -
- {icon} - {label} - {domain && {domain}} -
-); - -Badge.propTypes = { - icon: PropTypes.node, - label: PropTypes.node, - domain: PropTypes.node, - roleId: PropTypes.string -}; - -export const GroupBadge = () => ( - } label={} /> -); - -export const AutomatedBadge = () => ( - } label={} /> -); diff --git a/app/javascript/mastodon/components/badge.tsx b/app/javascript/mastodon/components/badge.tsx new file mode 100644 index 00000000000..b7dc169edbc --- /dev/null +++ b/app/javascript/mastodon/components/badge.tsx @@ -0,0 +1,46 @@ +import type { FC, ReactNode } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; +import PersonIcon from '@/material-icons/400-24px/person.svg?react'; +import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; + +export const Badge: FC<{ + label: ReactNode; + icon?: ReactNode; + className?: string; + domain?: ReactNode; + roleId?: string; +}> = ({ icon = , label, className, domain, roleId }) => ( +
+ {icon} + {label} + {domain && {domain}} +
+); + +export const GroupBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); + +export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); diff --git a/app/javascript/mastodon/components/emoji/index.tsx b/app/javascript/mastodon/components/emoji/index.tsx index 0b1ba7fef38..9b917df6ea0 100644 --- a/app/javascript/mastodon/components/emoji/index.tsx +++ b/app/javascript/mastodon/components/emoji/index.tsx @@ -88,7 +88,10 @@ export const Emoji: FC = ({ ); } - const src = unicodeHexToUrl(state.code, appState.darkTheme); + const src = unicodeHexToUrl({ + unicodeHex: state.code, + ...appState, + }); return ( = ({ + label, + value, + className, + hidden, +}) => { + if (!label) { + return null; + } + + return ( +
+
{label}
+
{value}
+
+ ); +}; diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx new file mode 100644 index 00000000000..318c5849538 --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -0,0 +1,221 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { FC, Key, MouseEventHandler } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import { MiniCard } from '.'; +import type { MiniCardProps } from '.'; +import classes from './styles.module.css'; + +interface MiniCardListProps { + cards?: (Pick & { + key?: Key; + })[]; + className?: string; + onOverflowClick?: MouseEventHandler; +} + +export const MiniCardList: FC = ({ + cards = [], + className, + onOverflowClick, +}) => { + const { + wrapperRef, + listRef, + hiddenCount, + hasOverflow, + hiddenIndex, + maxWidth, + } = useOverflow(); + + if (!cards.length) { + return null; + } + + return ( +
+
+ {cards.map((card, index) => ( +
+ {cards.length > 1 && ( +
+ +
+ )} +
+ ); +}; + +function useOverflow() { + const [hiddenIndex, setHiddenIndex] = useState(-1); + const [hiddenCount, setHiddenCount] = useState(0); + const [maxWidth, setMaxWidth] = useState('none'); + + // This is the item container element. + const listRef = useRef(null); + + // The main recalculation function. + const handleRecalculate = useCallback(() => { + const listEle = listRef.current; + if (!listEle) return; + + const reset = () => { + setHiddenIndex(-1); + setHiddenCount(0); + setMaxWidth('none'); + }; + + // Calculate the width via the parent element, minus the more button, minus the padding. + const maxWidth = + (listEle.parentElement?.offsetWidth ?? 0) - + (listEle.nextElementSibling?.scrollWidth ?? 0) - + 4; + if (maxWidth <= 0) { + reset(); + return; + } + + // Iterate through children until we exceed max width. + let visible = 0; + let index = 0; + let totalWidth = 0; + for (const child of listEle.children) { + if (child instanceof HTMLElement) { + const rightOffset = child.offsetLeft + child.offsetWidth; + if (rightOffset <= maxWidth) { + visible += 1; + totalWidth = rightOffset; + } else { + break; + } + } + index++; + } + + // All are visible, so remove max-width restriction. + if (visible === listEle.children.length) { + reset(); + return; + } + + // Set the width to avoid wrapping, and set hidden count. + setHiddenIndex(index); + setHiddenCount(listEle.children.length - visible); + setMaxWidth(totalWidth); + }, []); + + // Set up observers to watch for size and content changes. + const resizeObserverRef = useRef(null); + const mutationObserverRef = useRef(null); + + // Helper to get or create the resize observer. + const resizeObserver = useCallback(() => { + const observer = (resizeObserverRef.current ??= new ResizeObserver( + handleRecalculate, + )); + return observer; + }, [handleRecalculate]); + + // Iterate through children and observe them for size changes. + const handleChildrenChange = useCallback(() => { + const listEle = listRef.current; + const observer = resizeObserver(); + + if (listEle) { + for (const child of listEle.children) { + if (child instanceof HTMLElement) { + observer.observe(child); + } + } + } + handleRecalculate(); + }, [handleRecalculate, resizeObserver]); + + // Helper to get or create the mutation observer. + const mutationObserver = useCallback(() => { + const observer = (mutationObserverRef.current ??= new MutationObserver( + handleChildrenChange, + )); + return observer; + }, [handleChildrenChange]); + + // Set up observers. + const handleObserve = useCallback(() => { + if (wrapperRef.current) { + resizeObserver().observe(wrapperRef.current); + } + if (listRef.current) { + mutationObserver().observe(listRef.current, { childList: true }); + handleChildrenChange(); + } + }, [handleChildrenChange, mutationObserver, resizeObserver]); + + // Watch the wrapper for size changes, and recalculate when it resizes. + const wrapperRef = useRef(null); + const wrapperRefCallback = useCallback( + (node: HTMLElement | null) => { + if (node) { + wrapperRef.current = node; + handleObserve(); + } + }, + [handleObserve], + ); + + // If there are changes to the children, recalculate which are visible. + const listRefCallback = useCallback( + (node: HTMLElement | null) => { + if (node) { + listRef.current = node; + handleObserve(); + } + }, + [handleObserve], + ); + + useEffect(() => { + handleObserve(); + + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect(); + resizeObserverRef.current = null; + } + if (mutationObserverRef.current) { + mutationObserverRef.current.disconnect(); + mutationObserverRef.current = null; + } + }; + }, [handleObserve]); + + return { + hiddenCount, + hasOverflow: hiddenCount > 0, + wrapperRef: wrapperRefCallback, + hiddenIndex, + maxWidth, + listRef: listRefCallback, + recalculate: handleRecalculate, + }; +} diff --git a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx new file mode 100644 index 00000000000..60534f05f6b --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { action } from 'storybook/actions'; + +import { MiniCardList } from './list'; + +const meta = { + title: 'Components/MiniCard', + component: MiniCardList, + args: { + onOverflowClick: action('Overflow clicked'), + }, + render(args) { + return ( +
+ +
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + cards: [ + { label: 'Pronouns', value: 'they/them' }, + { + label: 'Website', + value: bowie-the-db.meow, + }, + { + label: 'Free playlists', + value: soundcloud.com, + }, + { label: 'Location', value: 'Purris, France' }, + ], + }, +}; + +export const LongValue: Story = { + args: { + cards: [ + { + label: 'Username', + value: 'bowie-the-dj', + }, + { + label: 'Bio', + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + ], + }, +}; + +export const OneCard: Story = { + args: { + cards: [{ label: 'Pronouns', value: 'they/them' }], + }, +}; diff --git a/app/javascript/mastodon/components/mini_card/styles.module.css b/app/javascript/mastodon/components/mini_card/styles.module.css new file mode 100644 index 00000000000..642c08c5fac --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/styles.module.css @@ -0,0 +1,57 @@ +.wrapper { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + gap: 4px; +} + +.list { + display: flex; + gap: 4px; + overflow: hidden; + position: relative; +} + +.card, +.more { + border: 1px solid var(--color-border-primary); + padding: 8px; + border-radius: 8px; + flex-shrink: 0; +} + +.more { + color: var(--color-text-secondary); + font-weight: 600; + appearance: none; + background: none; + aspect-ratio: 1; + height: 100%; + transition: all 300ms linear; +} + +.more:hover { + background-color: var(--color-bg-brand-softer); + color: var(--color-text-primary); +} + +.hidden { + display: none; +} + +.label { + color: var(--color-text-secondary); + margin-bottom: 2px; +} + +.value { + color: var(--color-text-primary); + font-weight: 600; +} + +.label, +.value { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/app/javascript/mastodon/features/account/components/domain_pill.tsx b/app/javascript/mastodon/features/account/components/domain_pill.tsx index 13f5ebacf1e..1f334bc004a 100644 --- a/app/javascript/mastodon/features/account/components/domain_pill.tsx +++ b/app/javascript/mastodon/features/account/components/domain_pill.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { useState, useRef, useCallback, useId } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -15,7 +16,9 @@ export const DomainPill: React.FC<{ domain: string; username: string; isSelf: boolean; -}> = ({ domain, username, isSelf }) => { + children?: ReactNode; + className?: string; +}> = ({ domain, username, isSelf, children, className }) => { const accessibilityId = useId(); const [open, setOpen] = useState(false); const [expanded, setExpanded] = useState(false); @@ -32,7 +35,9 @@ export const DomainPill: React.FC<{ return ( <> { const displayName = account.display_name; @@ -190,150 +45,12 @@ export const AccountHeader: React.FC<{ hideTabs?: boolean; }> = ({ accountId, hideTabs }) => { const dispatch = useAppDispatch(); - const intl = useIntl(); - const { signedIn, permissions } = useIdentity(); const account = useAppSelector((state) => state.accounts.get(accountId)); const relationship = useAppSelector((state) => state.relationships.get(accountId), ); const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); - const handleBlock = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.blocking) { - dispatch(unblockAccount(account.id)); - } else { - dispatch(initBlockModal(account)); - } - }, [dispatch, account, relationship]); - - const handleMention = useCallback(() => { - if (!account) { - return; - } - - dispatch(mentionCompose(account)); - }, [dispatch, account]); - - const handleDirect = useCallback(() => { - if (!account) { - return; - } - - dispatch(directCompose(account)); - }, [dispatch, account]); - - const handleReport = useCallback(() => { - if (!account) { - return; - } - - dispatch(initReport(account)); - }, [dispatch, account]); - - const handleReblogToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.showing_reblogs) { - dispatch(followAccount(account.id, { reblogs: false })); - } else { - dispatch(followAccount(account.id, { reblogs: true })); - } - }, [dispatch, account, relationship]); - - const handleNotifyToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.notifying) { - dispatch(followAccount(account.id, { notify: false })); - } else { - dispatch(followAccount(account.id, { notify: true })); - } - }, [dispatch, account, relationship]); - - const handleMute = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.muting) { - dispatch(unmuteAccount(account.id)); - } else { - dispatch(initMuteModal(account)); - } - }, [dispatch, account, relationship]); - - const handleBlockDomain = useCallback(() => { - if (!account) { - return; - } - - dispatch(initDomainBlockModal(account)); - }, [dispatch, account]); - - const handleUnblockDomain = useCallback(() => { - if (!account) { - return; - } - - const domain = account.acct.split('@')[1]; - - if (!domain) { - return; - } - - dispatch(unblockDomain(domain)); - }, [dispatch, account]); - - const handleEndorseToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.endorsed) { - dispatch(unpinAccount(account.id)); - } else { - dispatch(pinAccount(account.id)); - } - }, [dispatch, account, relationship]); - - const handleAddToList = useCallback(() => { - if (!account) { - return; - } - - dispatch( - openModal({ - modalType: 'LIST_ADDER', - modalProps: { - accountId: account.id, - }, - }), - ); - }, [dispatch, account]); - - const handleChangeLanguages = useCallback(() => { - if (!account) { - return; - } - - dispatch( - openModal({ - modalType: 'SUBSCRIBED_LANGUAGES', - modalProps: { - accountId: account.id, - }, - }), - ); - }, [dispatch, account]); - const handleOpenAvatar = useCallback( (e: React.MouseEvent) => { if (e.button !== 0 || e.ctrlKey || e.metaKey) { @@ -359,410 +76,12 @@ export const AccountHeader: React.FC<{ [dispatch, account], ); - const handleShare = useCallback(() => { - if (!account) { - return; - } - - void navigator.share({ - url: account.url, - }); - }, [account]); - - const suspended = account?.suspended; - const isRemote = account?.acct !== account?.username; - const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; - - const menuItems = useMemo(() => { - const arr: MenuItem[] = []; - - if (!account) { - return arr; - } - - if (signedIn && !account.suspended) { - arr.push({ - text: intl.formatMessage(messages.mention, { - name: account.username, - }), - action: handleMention, - }); - arr.push({ - text: intl.formatMessage(messages.direct, { - name: account.username, - }), - action: handleDirect, - }); - arr.push(null); - } - - if (isRemote) { - arr.push({ - text: intl.formatMessage(messages.openOriginalPage), - href: account.url, - }); - arr.push(null); - } - - if (signedIn) { - if (relationship?.following) { - if (!relationship.muting) { - if (relationship.showing_reblogs) { - arr.push({ - text: intl.formatMessage(messages.hideReblogs, { - name: account.username, - }), - action: handleReblogToggle, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.showReblogs, { - name: account.username, - }), - action: handleReblogToggle, - }); - } - - arr.push({ - text: intl.formatMessage(messages.languages), - action: handleChangeLanguages, - }); - arr.push(null); - } - - arr.push({ - text: intl.formatMessage( - relationship.endorsed ? messages.unendorse : messages.endorse, - ), - action: handleEndorseToggle, - }); - arr.push({ - text: intl.formatMessage(messages.add_or_remove_from_list), - action: handleAddToList, - }); - arr.push(null); - } - - if (relationship?.followed_by) { - const handleRemoveFromFollowers = () => { - dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - title: intl.formatMessage( - messages.confirmRemoveFromFollowersTitle, - ), - message: intl.formatMessage( - messages.confirmRemoveFromFollowersMessage, - { name: {account.acct} }, - ), - confirm: intl.formatMessage( - messages.confirmRemoveFromFollowersButton, - ), - onConfirm: () => { - void dispatch(removeAccountFromFollowers({ accountId })); - }, - }, - }), - ); - }; - - arr.push({ - text: intl.formatMessage(messages.removeFromFollowers, { - name: account.username, - }), - action: handleRemoveFromFollowers, - dangerous: true, - }); - } - - if (relationship?.muting) { - arr.push({ - text: intl.formatMessage(messages.unmute, { - name: account.username, - }), - action: handleMute, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.mute, { - name: account.username, - }), - action: handleMute, - dangerous: true, - }); - } - - if (relationship?.blocking) { - arr.push({ - text: intl.formatMessage(messages.unblock, { - name: account.username, - }), - action: handleBlock, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.block, { - name: account.username, - }), - action: handleBlock, - dangerous: true, - }); - } - - if (!account.suspended) { - arr.push({ - text: intl.formatMessage(messages.report, { - name: account.username, - }), - action: handleReport, - dangerous: true, - }); - } - } - - if (signedIn && isRemote) { - arr.push(null); - - if (relationship?.domain_blocking) { - arr.push({ - text: intl.formatMessage(messages.unblockDomain, { - domain: remoteDomain, - }), - action: handleUnblockDomain, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.blockDomain, { - domain: remoteDomain, - }), - action: handleBlockDomain, - dangerous: true, - }); - } - } - - if ( - (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || - (isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION) - ) { - arr.push(null); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { - arr.push({ - text: intl.formatMessage(messages.admin_account, { - name: account.username, - }), - href: `/admin/accounts/${account.id}`, - }); - } - if ( - isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION - ) { - arr.push({ - text: intl.formatMessage(messages.admin_domain, { - domain: remoteDomain, - }), - href: `/admin/instances/${remoteDomain}`, - }); - } - } - - return arr; - }, [ - dispatch, - accountId, - account, - relationship, - permissions, - isRemote, - remoteDomain, - intl, - signedIn, - handleAddToList, - handleBlock, - handleBlockDomain, - handleChangeLanguages, - handleDirect, - handleEndorseToggle, - handleMention, - handleMute, - handleReblogToggle, - handleReport, - handleUnblockDomain, - ]); - - const menu = accountId !== me && ( - - ); - if (!account) { return null; } - let actionBtn: React.ReactNode, - bellBtn: React.ReactNode, - lockedIcon: React.ReactNode, - shareBtn: React.ReactNode; - - const info: React.ReactNode[] = []; - - if (me !== account.id && relationship) { - if ( - relationship.followed_by && - (relationship.following || relationship.requested) - ) { - info.push( - - - , - ); - } else if (relationship.followed_by) { - info.push( - - - , - ); - } else if (relationship.requested_by) { - info.push( - - - , - ); - } - - if (relationship.blocking) { - info.push( - - - , - ); - } - - if (relationship.muting) { - info.push( - - - , - ); - } - - if (relationship.domain_blocking) { - info.push( - - - , - ); - } - } - - if (relationship?.requested || relationship?.following) { - bellBtn = ( - - ); - } - - if ('share' in navigator) { - shareBtn = ( - - ); - } else { - shareBtn = ( - - ); - } - - const isMovedAndUnfollowedAccount = account.moved && !relationship?.following; - - if (!isMovedAndUnfollowedAccount) { - actionBtn = ( - - ); - } - - if (account.locked) { - lockedIcon = ( - - ); - } - - const fields = account.fields; + const suspendedOrHidden = hidden || account.suspended; const isLocal = !account.acct.includes('@'); - const username = account.acct.split('@')[0]; - const domain = isLocal ? localDomain : account.acct.split('@')[1]; - const isIndexable = !account.noindex; - - const badges = []; - - if (account.bot) { - badges.push(); - } else if (account.group) { - badges.push(); - } - - account.roles.forEach((role) => { - badges.push( - {role.get('name')}} - domain={domain} - roleId={role.get('id')} - />, - ); - }); return (
@@ -776,15 +95,16 @@ export const AccountHeader: React.FC<{ inactive: !!account.moved, })} > - {!(suspended || hidden || account.moved) && - relationship?.requested_by && ( - - )} + {!suspendedOrHidden && !account.moved && relationship?.requested_by && ( + + )}
-
{info}
+ {me !== account.id && relationship && ( + + )} - {!(suspended || hidden) && ( + {!suspendedOrHidden && (
-
-

- - - - @{username} - @{domain} - - - {lockedIcon} - -

+
+ + {isRedesignEnabled() && }
- {badges.length > 0 && ( -
{badges}
- )} + - {account.id !== me && signedIn && !(suspended || hidden) && ( + {me && account.id !== me && !suspendedOrHidden && ( )} -
- {!hidden && actionBtn} - {!hidden && bellBtn} - {menu} -
+ - {!(suspended || hidden) && ( + {!suspendedOrHidden && (
- {account.id !== me && signedIn && ( + {me && account.id !== me && ( )} @@ -860,91 +175,22 @@ export const AccountHeader: React.FC<{ className='account__header__content' /> -
-
-
- -
-
- -
-
- - -
+
-
- - - - - - - - - - - -
+
)}
- {!(hideTabs || hidden) && ( -
- - - - - - - - - - - - -
- )} + {!hideTabs && !hidden && } {titleFromAccount(account)} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx new file mode 100644 index 00000000000..90ccf7486d3 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx @@ -0,0 +1,68 @@ +import type { FC } from 'react'; + +import { useIntl } from 'react-intl'; + +import { DisplayName } from '@/mastodon/components/display_name'; +import { Icon } from '@/mastodon/components/icon'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useAppSelector } from '@/mastodon/store'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import LockIcon from '@/material-icons/400-24px/lock.svg?react'; + +import { DomainPill } from '../../account/components/domain_pill'; +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountName: FC<{ accountId: string; className?: string }> = ({ + accountId, + className, +}) => { + const intl = useIntl(); + const account = useAccount(accountId); + const me = useAppSelector((state) => state.meta.get('me') as string); + const localDomain = useAppSelector( + (state) => state.meta.get('domain') as string, + ); + + if (!account) { + return null; + } + + const [username = '', domain = localDomain] = account.acct.split('@'); + + return ( +

+ + + + @{username} + {isRedesignEnabled() && '@'} + + {!isRedesignEnabled() && '@'} + {domain} + + + + {isRedesignEnabled() && } + + {!isRedesignEnabled() && account.locked && ( + + )} + +

+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx new file mode 100644 index 00000000000..1c5942d90d3 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx @@ -0,0 +1,65 @@ +import type { FC, ReactNode } from 'react'; + +import IconAdmin from '@/images/icons/icon_admin.svg?react'; +import { AutomatedBadge, Badge, GroupBadge } from '@/mastodon/components/badge'; +import { Icon } from '@/mastodon/components/icon'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { AccountRole } from '@/mastodon/models/account'; +import { useAppSelector } from '@/mastodon/store'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { + const account = useAccount(accountId); + const localDomain = useAppSelector( + (state) => state.meta.get('domain') as string, + ); + const badges = []; + + if (!account) { + return null; + } + + const className = isRedesignEnabled() ? classes.badge : ''; + + if (account.bot) { + badges.push(); + } else if (account.group) { + badges.push(); + } + + const domain = account.acct.includes('@') + ? account.acct.split('@')[1] + : localDomain; + account.roles.forEach((role) => { + let icon: ReactNode = undefined; + if (isAdminBadge(role)) { + icon = ( + + ); + } + badges.push( + , + ); + }); + + if (!badges.length) { + return null; + } + + return
{badges}
; +}; + +function isAdminBadge(role: AccountRole) { + const name = role.name.toLowerCase(); + return isRedesignEnabled() && (name === 'admin' || name === 'owner'); +} diff --git a/app/javascript/mastodon/features/account_timeline/components/buttons.tsx b/app/javascript/mastodon/features/account_timeline/components/buttons.tsx new file mode 100644 index 00000000000..c998d1472ce --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/buttons.tsx @@ -0,0 +1,134 @@ +import { useCallback } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { followAccount } from '@/mastodon/actions/accounts'; +import { CopyIconButton } from '@/mastodon/components/copy_icon_button'; +import { FollowButton } from '@/mastodon/components/follow_button'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { getAccountHidden } from '@/mastodon/selectors/accounts'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; +import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; +import ShareIcon from '@/material-icons/400-24px/share.svg?react'; + +import { AccountMenu } from './menu'; + +const messages = defineMessages({ + enableNotifications: { + id: 'account.enable_notifications', + defaultMessage: 'Notify me when @{name} posts', + }, + disableNotifications: { + id: 'account.disable_notifications', + defaultMessage: 'Stop notifying me when @{name} posts', + }, + share: { id: 'account.share', defaultMessage: "Share @{name}'s profile" }, + copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' }, +}); + +interface AccountButtonsProps { + accountId: string; + className?: string; + noShare?: boolean; +} + +export const AccountButtons: FC = ({ + accountId, + className, + noShare, +}) => { + const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); + const me = useAppSelector((state) => state.meta.get('me') as string); + + return ( +
+ {!hidden && ( + + )} + {accountId !== me && } +
+ ); +}; + +const AccountButtonsOther: FC< + Pick +> = ({ accountId, noShare }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const relationship = useAppSelector((state) => + state.relationships.get(accountId), + ); + + const dispatch = useAppDispatch(); + const handleNotifyToggle = useCallback(() => { + if (account) { + dispatch(followAccount(account.id, { notify: !relationship?.notifying })); + } + }, [dispatch, account, relationship]); + const accountUrl = account?.url; + const handleShare = useCallback(() => { + if (accountUrl) { + void navigator.share({ + url: accountUrl, + }); + } + }, [accountUrl]); + + if (!account) { + return null; + } + + const isMovedAndUnfollowedAccount = account.moved && !relationship?.following; + const isFollowing = relationship?.requested || relationship?.following; + + return ( + <> + {!isMovedAndUnfollowedAccount && ( + + )} + {isFollowing && ( + + )} + {!noShare && + ('share' in navigator ? ( + + ) : ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx new file mode 100644 index 00000000000..ab29a8299e4 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -0,0 +1,116 @@ +import { useCallback, useMemo } from 'react'; +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import IconVerified from '@/images/icons/icon_verified.svg?react'; +import { openModal } from '@/mastodon/actions/modal'; +import { AccountFields } from '@/mastodon/components/account_fields'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { Icon } from '@/mastodon/components/icon'; +import { MiniCardList } from '@/mastodon/components/mini_card/list'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { Account } from '@/mastodon/models/account'; +import { useAppDispatch } from '@/mastodon/store'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountHeaderFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const account = useAccount(accountId); + + if (!account) { + return null; + } + + if (isRedesignEnabled()) { + return ; + } + + return ( +
+
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { + const htmlHandlers = useElementHandledLink(); + const cards = useMemo( + () => + account.fields + .toArray() + .map(({ value_emojified, name_emojified, verified_at }) => ({ + label: ( + <> + + {!!verified_at && ( + + )} + + ), + value: ( + + ), + className: classNames( + classes.fieldCard, + !!verified_at && classes.fieldCardVerified, + ), + })), + [account.emojis, account.fields, htmlHandlers], + ); + + const dispatch = useAppDispatch(); + const handleOverflowClick = useCallback(() => { + dispatch( + openModal({ + modalType: 'ACCOUNT_FIELDS', + modalProps: { accountId: account.id }, + }), + ); + }, [account.id, dispatch]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx new file mode 100644 index 00000000000..103fffca505 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import IconVerified from '@/images/icons/icon_verified.svg?react'; +import { DisplayName } from '@/mastodon/components/display_name'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { Icon } from '@/mastodon/components/icon'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +import classes from './redesign.module.scss'; + +export const AccountFieldsModal: FC<{ + accountId: string; + onClose: () => void; +}> = ({ accountId, onClose }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const htmlHandlers = useElementHandledLink(); + + if (!account) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + , + }} + /> + +
+
+ +
+ {account.fields.map((field, index) => ( +
+ +
+ + {!!field.verified_at && ( + + )} +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/info.tsx b/app/javascript/mastodon/features/account_timeline/components/info.tsx new file mode 100644 index 00000000000..bb99999c41e --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/info.tsx @@ -0,0 +1,68 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import type { Relationship } from '@/mastodon/models/relationship'; + +export const AccountInfo: FC<{ relationship?: Relationship }> = ({ + relationship, +}) => { + if (!relationship) { + return null; + } + return ( +
+ {(relationship.followed_by || relationship.requested_by) && ( + + + + )} + {relationship.blocking && ( + + + + )} + {relationship.muting && ( + + + + )} + {relationship.domain_blocking && ( + + + + )} +
+ ); +}; + +const AccountInfoFollower: FC<{ relationship: Relationship }> = ({ + relationship, +}) => { + if ( + relationship.followed_by && + (relationship.following || relationship.requested) + ) { + return ( + + ); + } else if (relationship.followed_by) { + return ( + + ); + } else if (relationship.requested_by) { + return ( + + ); + } + return null; +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/menu.tsx b/app/javascript/mastodon/features/account_timeline/components/menu.tsx new file mode 100644 index 00000000000..ce98c61f76a --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/menu.tsx @@ -0,0 +1,373 @@ +import { useMemo } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { + blockAccount, + followAccount, + pinAccount, + unblockAccount, + unmuteAccount, + unpinAccount, +} from '@/mastodon/actions/accounts'; +import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed'; +import { directCompose, mentionCompose } from '@/mastodon/actions/compose'; +import { + initDomainBlockModal, + unblockDomain, +} from '@/mastodon/actions/domain_blocks'; +import { openModal } from '@/mastodon/actions/modal'; +import { initMuteModal } from '@/mastodon/actions/mutes'; +import { initReport } from '@/mastodon/actions/reports'; +import { Dropdown } from '@/mastodon/components/dropdown_menu'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useIdentity } from '@/mastodon/identity_context'; +import type { MenuItem } from '@/mastodon/models/dropdown_menu'; +import { + PERMISSION_MANAGE_FEDERATION, + PERMISSION_MANAGE_USERS, +} from '@/mastodon/permissions'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; + +const messages = defineMessages({ + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + blockDomain: { + id: 'account.block_domain', + defaultMessage: 'Block domain {domain}', + }, + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, + hideReblogs: { + id: 'account.hide_reblogs', + defaultMessage: 'Hide boosts from @{name}', + }, + showReblogs: { + id: 'account.show_reblogs', + defaultMessage: 'Show boosts from @{name}', + }, + endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, + unendorse: { + id: 'account.unendorse', + defaultMessage: "Don't feature on profile", + }, + add_or_remove_from_list: { + id: 'account.add_or_remove_from_list', + defaultMessage: 'Add or Remove from lists', + }, + admin_account: { + id: 'status.admin_account', + defaultMessage: 'Open moderation interface for @{name}', + }, + admin_domain: { + id: 'status.admin_domain', + defaultMessage: 'Open moderation interface for {domain}', + }, + languages: { + id: 'account.languages', + defaultMessage: 'Change subscribed languages', + }, + openOriginalPage: { + id: 'account.open_original_page', + defaultMessage: 'Open original page', + }, + removeFromFollowers: { + id: 'account.remove_from_followers', + defaultMessage: 'Remove {name} from followers', + }, + confirmRemoveFromFollowersTitle: { + id: 'confirmations.remove_from_followers.title', + defaultMessage: 'Remove follower?', + }, + confirmRemoveFromFollowersMessage: { + id: 'confirmations.remove_from_followers.message', + defaultMessage: + '{name} will stop following you. Are you sure you want to proceed?', + }, + confirmRemoveFromFollowersButton: { + id: 'confirmations.remove_from_followers.confirm', + defaultMessage: 'Remove follower', + }, +}); + +export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { + const intl = useIntl(); + const { signedIn, permissions } = useIdentity(); + + const account = useAccount(accountId); + const relationship = useAppSelector((state) => + state.relationships.get(accountId), + ); + + const dispatch = useAppDispatch(); + const menuItems = useMemo(() => { + const arr: MenuItem[] = []; + + if (!account) { + return arr; + } + + const isRemote = account.acct !== account.username; + + if (signedIn && !account.suspended) { + arr.push({ + text: intl.formatMessage(messages.mention, { + name: account.username, + }), + action: () => { + dispatch(mentionCompose(account)); + }, + }); + arr.push({ + text: intl.formatMessage(messages.direct, { + name: account.username, + }), + action: () => { + dispatch(directCompose(account)); + }, + }); + arr.push(null); + } + + if (isRemote) { + arr.push({ + text: intl.formatMessage(messages.openOriginalPage), + href: account.url, + }); + arr.push(null); + } + + if (!signedIn) { + return arr; + } + + if (relationship?.following) { + if (!relationship.muting) { + if (relationship.showing_reblogs) { + arr.push({ + text: intl.formatMessage(messages.hideReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: false })); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.showReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: true })); + }, + }); + } + + arr.push({ + text: intl.formatMessage(messages.languages), + action: () => { + dispatch( + openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }); + arr.push(null); + } + + arr.push({ + text: intl.formatMessage( + relationship.endorsed ? messages.unendorse : messages.endorse, + ), + action: () => { + if (relationship.endorsed) { + dispatch(unpinAccount(account.id)); + } else { + dispatch(pinAccount(account.id)); + } + }, + }); + arr.push({ + text: intl.formatMessage(messages.add_or_remove_from_list), + action: () => { + dispatch( + openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }); + arr.push(null); + } + + if (relationship?.followed_by) { + const handleRemoveFromFollowers = () => { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + title: intl.formatMessage( + messages.confirmRemoveFromFollowersTitle, + ), + message: intl.formatMessage( + messages.confirmRemoveFromFollowersMessage, + { name: {account.acct} }, + ), + confirm: intl.formatMessage( + messages.confirmRemoveFromFollowersButton, + ), + onConfirm: () => { + void dispatch( + removeAccountFromFollowers({ accountId: account.id }), + ); + }, + }, + }), + ); + }; + + arr.push({ + text: intl.formatMessage(messages.removeFromFollowers, { + name: account.username, + }), + action: handleRemoveFromFollowers, + dangerous: true, + }); + } + + if (relationship?.muting) { + arr.push({ + text: intl.formatMessage(messages.unmute, { + name: account.username, + }), + action: () => { + dispatch(unmuteAccount(account.id)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.mute, { + name: account.username, + }), + action: () => { + dispatch(initMuteModal(account)); + }, + dangerous: true, + }); + } + + if (relationship?.blocking) { + arr.push({ + text: intl.formatMessage(messages.unblock, { + name: account.username, + }), + action: () => { + dispatch(unblockAccount(account.id)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.block, { + name: account.username, + }), + action: () => { + dispatch(blockAccount(account.id)); + }, + dangerous: true, + }); + } + + if (!account.suspended) { + arr.push({ + text: intl.formatMessage(messages.report, { + name: account.username, + }), + action: () => { + dispatch(initReport(account)); + }, + dangerous: true, + }); + } + + const remoteDomain = isRemote ? account.acct.split('@')[1] : null; + if (remoteDomain) { + arr.push(null); + + if (relationship?.domain_blocking) { + arr.push({ + text: intl.formatMessage(messages.unblockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(unblockDomain(remoteDomain)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.blockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(initDomainBlockModal(account)); + }, + dangerous: true, + }); + } + } + + if ( + (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || + (isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION) + ) { + arr.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + arr.push({ + text: intl.formatMessage(messages.admin_account, { + name: account.username, + }), + href: `/admin/accounts/${account.id}`, + }); + } + if ( + isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION + ) { + arr.push({ + text: intl.formatMessage(messages.admin_domain, { + domain: remoteDomain, + }), + href: `/admin/instances/${remoteDomain}`, + }); + } + } + + return arr; + }, [account, signedIn, permissions, intl, relationship, dispatch]); + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx new file mode 100644 index 00000000000..20df6a0125f --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; +import { NavLink } from 'react-router-dom'; + +import { + FollowersCounter, + FollowingCounter, + StatusesCounter, +} from '@/mastodon/components/counters'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { ShortNumber } from '@/mastodon/components/short_number'; +import { useAccount } from '@/mastodon/hooks/useAccount'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountNumberFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const intl = useIntl(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + return ( +
+ {!isRedesignEnabled() && ( + + + + )} + + + + + + + + + + {isRedesignEnabled() && ( + + + + + ), + }} + /> + + )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss new file mode 100644 index 00000000000..4bc64d05a98 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -0,0 +1,147 @@ +.nameWrapper { + display: flex; + gap: 16px; +} + +.name { + flex-grow: 1; + font-size: 22px; + white-space: initial; + text-overflow: initial; + line-height: normal; + + :global(.icon-info) { + margin-left: 2px; + width: 1em; + height: 1em; + align-self: center; + } +} + +// Overrides .account__header__tabs__name h1 small +h1.name > small { + gap: 0; +} + +.domainPill { + appearance: none; + border: none; + background: none; + padding: 0; + text-decoration: underline; + color: inherit; + font-size: 1em; + font-weight: initial; + + &:global(.active) { + background: none; + color: inherit; + } +} + +.badge { + background-color: var(--color-bg-secondary); + border: none; + color: var(--color-text-secondary); + font-weight: 600; + + > span { + font-weight: unset; + opacity: 1; + } +} + +svg.badgeIcon { + opacity: 1; + fill: revert-layer; + + path { + fill: revert-layer; + } +} + +.fieldList { + margin-top: 16px; +} + +.fieldCard { + position: relative; + + a { + color: var(--color-text-brand); + text-decoration: none; + } +} + +.fieldCardVerified { + background-color: var(--color-bg-brand-softer); + + dt { + padding-right: 1rem; + } + + .fieldIconVerified { + position: absolute; + top: 4px; + right: 4px; + } +} + +.fieldIconVerified { + width: 1rem; + height: 1rem; + + // Need to override .icon path. + path { + fill: revert-layer; + } +} + +.fieldNumbersWrapper { + a { + font-weight: unset; + } +} + +.modalCloseButton { + padding: 8px; + border-radius: 50%; + border: 1px solid var(--color-border-primary); +} + +.modalTitle { + flex-grow: 1; + text-align: center; +} + +.modalFieldsList { + padding: 24px; +} + +.modalFieldItem { + &:not(:first-child) { + padding-top: 12px; + } + + &:not(:last-child)::after { + content: ''; + display: block; + border-bottom: 1px solid var(--color-border-primary); + margin-top: 12px; + } + + dt { + color: var(--color-text-secondary); + font-size: 13px; + } + + dd { + font-weight: 600; + font-size: 15px; + } + + .fieldIconVerified { + vertical-align: middle; + margin-left: 4px; + } +} diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx new file mode 100644 index 00000000000..c08de1390ec --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -0,0 +1,27 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; + +export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { + return ( +
+ + + + + + + + + + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/announcement/index.tsx b/app/javascript/mastodon/features/annual_report/announcement/index.tsx index 283e95f5940..d96b1092715 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/index.tsx +++ b/app/javascript/mastodon/features/annual_report/announcement/index.tsx @@ -1,7 +1,5 @@ import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; - import type { ApiAnnualReportState } from '@/mastodon/api/annual_report'; import { Button } from '@/mastodon/components/button'; @@ -19,7 +17,7 @@ export const AnnualReportAnnouncement: React.FC< AnnualReportAnnouncementProps > = ({ year, state, onRequestBuild, onOpen, onDismiss }) => { return ( -
+
= ({ const topHashtag = report.data.top_hashtags[0]; return ( -
+

Wrapstodon {report.year}

{account &&

@{account.acct}

} diff --git a/app/javascript/mastodon/features/annual_report/modal.tsx b/app/javascript/mastodon/features/annual_report/modal.tsx index 01d7c4bbdbf..d591954bbf3 100644 --- a/app/javascript/mastodon/features/annual_report/modal.tsx +++ b/app/javascript/mastodon/features/annual_report/modal.tsx @@ -60,11 +60,8 @@ const AnnualReportModal: React.FC<{ // default modal backdrop, preventing clicks to pass through. // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
{!showAnnouncement ? ( diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx index 0fe140b4eb4..54317a6c76d 100644 --- a/app/javascript/mastodon/features/directory/index.tsx +++ b/app/javascript/mastodon/features/directory/index.tsx @@ -83,6 +83,9 @@ export const Directory: React.FC<{ (state) => state.user_lists.getIn(['directory', 'isLoading'], true) as boolean, ); + const hasMore = useAppSelector( + (state) => !!state.user_lists.getIn(['directory', 'next']), + ); useEffect(() => { void dispatch(fetchDirectory({ order, local })); @@ -182,7 +185,7 @@ export const Directory: React.FC<{
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 35804de82ae..5d0683dace8 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -86,7 +86,7 @@ describe('emoji', () => { it('does an emoji containing ZWJ properly', () => { expect(emojify('💂‍♀️💂‍♂️')) - .toEqual('💂\u200D♀️💂\u200D♂️'); + .toEqual('💂‍♀️💂‍♂️'); }); it('keeps ordering as expected (issue fixed by PR 20677)', () => { diff --git a/app/javascript/mastodon/features/emoji/constants.ts b/app/javascript/mastodon/features/emoji/constants.ts index e02663c9d84..9969a398c17 100644 --- a/app/javascript/mastodon/features/emoji/constants.ts +++ b/app/javascript/mastodon/features/emoji/constants.ts @@ -15,6 +15,8 @@ export const SKIN_TONE_CODES = [ 0x1f3ff, // Dark skin tone ] as const; +export const EMOJI_MIN_TOKEN_LENGTH = 2; + // Emoji rendering modes. A mode is what we are using to render emojis, a style is what the user has selected. export const EMOJI_MODE_NATIVE = 'native'; export const EMOJI_MODE_NATIVE_WITH_FLAGS = 'native-flags'; diff --git a/app/javascript/mastodon/features/emoji/database.test.ts b/app/javascript/mastodon/features/emoji/database.test.ts index 6b6ea952b74..b7f667e8ab1 100644 --- a/app/javascript/mastodon/features/emoji/database.test.ts +++ b/app/javascript/mastodon/features/emoji/database.test.ts @@ -1,3 +1,4 @@ +import type { CompactEmoji } from 'emojibase'; import { IDBFactory } from 'fake-indexeddb'; import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories'; @@ -6,8 +7,6 @@ import { EMOJI_DB_SHORTCODE_TEST } from './constants'; import { putEmojiData, loadEmojiByHexcode, - searchEmojisByHexcodes, - searchEmojisByTag, testClear, testGet, putCustomEmojiData, @@ -17,6 +16,14 @@ import { putLatestEtag, } from './database'; +function rawEmojiFactory(data: Partial = {}): CompactEmoji { + return { + ...unicodeEmojiFactory(), + tags: ['test', 'emoji'], + ...data, + }; +} + describe('emoji database', () => { afterEach(() => { testClear(); @@ -32,7 +39,7 @@ describe('emoji database', () => { }); test('loads emoji into indexedDB', async () => { - await putEmojiData([unicodeEmojiFactory()], 'en'); + await putEmojiData([rawEmojiFactory()], 'en'); const { db } = await testGet(); await expect(db.get('en', 'test')).resolves.toEqual( unicodeEmojiFactory(), @@ -60,7 +67,7 @@ describe('emoji database', () => { }); await expect(db.get('custom', 'emoji1')).resolves.toBeUndefined(); await expect(db.get('custom', 'emoji2')).resolves.toEqual( - customEmojiFactory({ shortcode: 'emoji2' }), + customEmojiFactory({ shortcode: 'emoji2', tokens: ['emoji2'] }), ); }); }); @@ -79,12 +86,6 @@ describe('emoji database', () => { }); describe('loadEmojiByHexcode', () => { - test('throws if the locale is not loaded', async () => { - await expect(loadEmojiByHexcode('en', 'test')).rejects.toThrowError( - 'Locale en', - ); - }); - test('retrieves the emoji', async () => { await putEmojiData([unicodeEmojiFactory()], 'en'); await expect(loadEmojiByHexcode('test', 'en')).resolves.toEqual( @@ -98,90 +99,6 @@ describe('emoji database', () => { }); }); - describe('searchEmojisByHexcodes', () => { - const data = [ - unicodeEmojiFactory({ hexcode: 'not a number' }), - unicodeEmojiFactory({ hexcode: '1' }), - unicodeEmojiFactory({ hexcode: '2' }), - unicodeEmojiFactory({ hexcode: '3' }), - unicodeEmojiFactory({ hexcode: 'another not a number' }), - ]; - beforeEach(async () => { - await putEmojiData(data, 'en'); - }); - test('finds emoji in consecutive range', async () => { - const actual = await searchEmojisByHexcodes(['1', '2', '3'], 'en'); - expect(actual).toHaveLength(3); - }); - - test('finds emoji in split range', async () => { - const actual = await searchEmojisByHexcodes(['1', '3'], 'en'); - expect(actual).toHaveLength(2); - expect(actual).toContainEqual(data.at(1)); - expect(actual).toContainEqual(data.at(3)); - }); - - test('finds emoji with non-numeric range', async () => { - const actual = await searchEmojisByHexcodes( - ['3', 'not a number', '1'], - 'en', - ); - expect(actual).toHaveLength(3); - expect(actual).toContainEqual(data.at(0)); - expect(actual).toContainEqual(data.at(1)); - expect(actual).toContainEqual(data.at(3)); - }); - - test('not found emoji are not returned', async () => { - const actual = await searchEmojisByHexcodes(['not found'], 'en'); - expect(actual).toHaveLength(0); - }); - - test('only found emojis are returned', async () => { - const actual = await searchEmojisByHexcodes( - ['another not a number', 'not found'], - 'en', - ); - expect(actual).toHaveLength(1); - expect(actual).toContainEqual(data.at(4)); - }); - }); - - describe('searchEmojisByTag', () => { - const data = [ - unicodeEmojiFactory({ hexcode: 'test1', tags: ['test 1'] }), - unicodeEmojiFactory({ - hexcode: 'test2', - tags: ['test 2', 'something else'], - }), - unicodeEmojiFactory({ hexcode: 'test3', tags: ['completely different'] }), - ]; - beforeEach(async () => { - await putEmojiData(data, 'en'); - }); - test('finds emojis with tag', async () => { - const actual = await searchEmojisByTag('test 1', 'en'); - expect(actual).toHaveLength(1); - expect(actual).toContainEqual(data.at(0)); - }); - - test('finds emojis starting with tag', async () => { - const actual = await searchEmojisByTag('test', 'en'); - expect(actual).toHaveLength(2); - expect(actual).not.toContainEqual(data.at(2)); - }); - - test('does not find emojis ending with tag', async () => { - const actual = await searchEmojisByTag('else', 'en'); - expect(actual).toHaveLength(0); - }); - - test('finds nothing with invalid tag', async () => { - const actual = await searchEmojisByTag('not found', 'en'); - expect(actual).toHaveLength(0); - }); - }); - describe('loadLegacyShortcodesByShortcode', () => { const data = { hexcode: 'test_hexcode', diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index fe4010a861d..f64f3fb80d6 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -1,55 +1,25 @@ import { SUPPORTED_LOCALES } from 'emojibase'; -import type { Locale, ShortcodesDataset } from 'emojibase'; -import type { DBSchema, IDBPDatabase } from 'idb'; -import { openDB } from 'idb'; +import type { CompactEmoji, Locale, ShortcodesDataset } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { EMOJI_DB_SHORTCODE_TEST } from './constants'; -import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; -import type { CustomEmojiData, UnicodeEmojiData, EtagTypes } from './types'; +import { openEmojiDB } from './db-schema'; +import type { Database } from './db-schema'; +import { + localeToSegmenter, + toSupportedLocale, + toSupportedLocaleOrCustom, +} from './locale'; +import { + extractTokens, + skinHexcodeToEmoji, + transformCustomEmojiData, + transformEmojiData, +} from './normalize'; +import type { AnyEmojiData, EtagTypes } from './types'; import { emojiLogger } from './utils'; -interface EmojiDB extends LocaleTables, DBSchema { - custom: { - key: string; - value: CustomEmojiData; - indexes: { - category: string; - }; - }; - shortcodes: { - key: string; - value: { - hexcode: string; - shortcodes: string[]; - }; - indexes: { - hexcode: string; - shortcodes: string[]; - }; - }; - etags: { - key: EtagTypes; - value: string; - }; -} - -interface LocaleTable { - key: string; - value: UnicodeEmojiData; - indexes: { - group: number; - label: string; - order: number; - tags: string[]; - shortcodes: string[]; - }; -} -type LocaleTables = Record; - -type Database = IDBPDatabase; - -const SCHEMA_VERSION = 2; - const loadedLocales = new Set(); const log = emojiLogger('database'); @@ -60,75 +30,7 @@ const loadDB = (() => { // Actually load the DB. async function initDB() { - const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { - upgrade(database, oldVersion, newVersion, trx) { - if (!database.objectStoreNames.contains('custom')) { - const customTable = database.createObjectStore('custom', { - keyPath: 'shortcode', - autoIncrement: false, - }); - customTable.createIndex('category', 'category'); - } - - if (!database.objectStoreNames.contains('etags')) { - database.createObjectStore('etags'); - } - - for (const locale of SUPPORTED_LOCALES) { - if (!database.objectStoreNames.contains(locale)) { - const localeTable = database.createObjectStore(locale, { - keyPath: 'hexcode', - autoIncrement: false, - }); - localeTable.createIndex('group', 'group'); - localeTable.createIndex('label', 'label'); - localeTable.createIndex('order', 'order'); - localeTable.createIndex('tags', 'tags', { multiEntry: true }); - localeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - // Added in version 2. - const localeTable = trx.objectStore(locale); - if (!localeTable.indexNames.contains('shortcodes')) { - localeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - } - - if (!database.objectStoreNames.contains('shortcodes')) { - const shortcodeTable = database.createObjectStore('shortcodes', { - keyPath: 'hexcode', - autoIncrement: false, - }); - shortcodeTable.createIndex('hexcode', 'hexcode'); - shortcodeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - - log( - 'Upgraded emoji database from version %d to %d', - oldVersion, - newVersion, - ); - }, - blocked(currentVersion, blockedVersion) { - log( - 'Emoji database upgrade from version %d to %d is blocked', - currentVersion, - blockedVersion, - ); - }, - blocking(currentVersion, blockedVersion) { - log( - 'Emoji database upgrade from version %d is blocking upgrade to %d', - currentVersion, - blockedVersion, - ); - }, - }); + const db = await openEmojiDB(); await syncLocales(db); log('Loaded database version %d', db.version); return db; @@ -149,11 +51,128 @@ const loadDB = (() => { return loadPromise; })(); -export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) { +export async function search({ + query, + locale: localeString, + limit = 0, +}: { + query: string; + locale: string; + limit?: number; +}) { + performance.mark('emoji-search-start'); + + // Get the locale, and extract tokens from the query. + const locale = await toLoadedLocale(localeString); + const segmenter = localeToSegmenter(locale); + const queryTokens = extractTokens(query, segmenter); + + if (queryTokens.length === 0) { + log('no tokens extracted from query "%s"', query); + return []; + } + const lastToken = queryTokens.at(-1); + if (!lastToken) { + throw new Error('Missing tokens from query'); + } + + log('searching for tokens %o in locale %s', queryTokens, locale); + + // Create an array of emoji results + const db = await loadDB(); + const resultArrays: Map[] = []; + for (let i = 0; i < queryTokens.length; i++) { + const token = queryTokens[i]; + if (!token) continue; + + // Only query the range for the last token to allow partial matches. + const range = + i === queryTokens.length - 1 + ? IDBKeyRange.bound(token, token + '\uffff') + : IDBKeyRange.only(token); + + const [unicodeResults, customResults] = await Promise.all([ + db.getAllFromIndex(locale, 'tokens', range), + db.getAllFromIndex('custom', 'tokens', range), + ]); + const resultMap = new Map([ + ...unicodeResults.map( + (emoji) => [emoji.hexcode, emoji] as [string, AnyEmojiData], + ), + ...customResults.map( + (emoji) => [emoji.shortcode, emoji] as [string, AnyEmojiData], + ), + ]); + log('found %d results for token "%s"', resultMap.size, token); + resultArrays.push(resultMap); + } + + // Utilize maps to find the intersection of all result sets. + const results = Array.from( + resultArrays + .reduce((prev, curr) => { + const intersection = new Map(); + for (const [code, emoji] of prev) { + if (curr.has(code)) { + intersection.set(code, emoji); + } + } + return intersection; + }) + .values(), + ); + + results.sort((a, b) => { + // Checks if a or b has the last token exactly, or only a prefix. + const aHasToken = a.tokens.includes(lastToken); + const bHasToken = b.tokens.includes(lastToken); + if (aHasToken && !bHasToken) { + return -1; + } else if (!aHasToken && bHasToken) { + return 1; + } + + // If one is a custom emoji, prioritize it over Unicode emojis. + if ('category' in a) { + return -1; + } else if ('category' in b) { + return 1; + } + + // If both are Unicode emojis, prioritize by order. + if ('order' in a && 'order' in b) { + return (a.order ?? 0) - (b.order ?? 0); // If these are both Unicode emojis, sort by order. + } + + // ¯\_(ツ)_/¯ + return 0; + }); + + const time = performance.measure('emoji-search-end', 'emoji-search-start'); + log( + 'search for "%s" in locale %s returned %d results and took %dms', + query, + locale, + results.length, + time.duration, + ); + if (limit > 0) { + return results.slice(0, limit); + } + return results; +} + +export async function putEmojiData(emojis: CompactEmoji[], locale: Locale) { loadedLocales.add(locale); const db = await loadDB(); const trx = db.transaction(locale, 'readwrite'); - await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await trx.store.clear(); + const segmenter = localeToSegmenter(locale); + await Promise.all( + emojis + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + .map((emoji) => trx.store.put(transformEmojiData(emoji, segmenter))), + ); await trx.done; } @@ -161,7 +180,7 @@ export async function putCustomEmojiData({ emojis, clear = false, }: { - emojis: CustomEmojiData[]; + emojis: ApiCustomEmojiJSON[]; clear?: boolean; }) { const db = await loadDB(); @@ -173,7 +192,9 @@ export async function putCustomEmojiData({ log('Cleared existing custom emojis in database'); } - await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await Promise.all( + emojis.map((emoji) => trx.store.put(transformCustomEmojiData(emoji))), + ); await trx.done; log('Imported %d custom emojis into database', emojis.length); @@ -210,32 +231,25 @@ export async function loadEmojiByHexcode( localeString: string, ) { const db = await loadDB(); - const locale = toLoadedLocale(localeString); - return db.get(locale, hexcode); -} + const locale = await toLoadedLocale(localeString); + const result = await db.get(locale, hexcode); + if (result) { + return result; + } -export async function searchEmojisByHexcodes( - hexcodes: string[], - localeString: string, -) { - const db = await loadDB(); - const locale = toLoadedLocale(localeString); - const sortedCodes = hexcodes.toSorted(); - const results = await db.getAll( + // If the emoji wasn't found, check if it's a skin tone variant. + const skinResult = await db.getFromIndex( locale, - IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)), + 'skinHexcodes', + IDBKeyRange.only(hexcode), ); - return results.filter((emoji) => hexcodes.includes(emoji.hexcode)); -} -export async function searchEmojisByTag(tag: string, localeString: string) { - const db = await loadDB(); - const locale = toLoadedLocale(localeString); - const range = IDBKeyRange.bound( - tag.toLowerCase(), - `${tag.toLowerCase()}\uffff`, - ); - return db.getAllFromIndex(locale, 'tags', range); + if (!skinResult) { + return skinResult; + } + + // Reconstruct the full unicode string from the skin tone hexcode. + return skinHexcodeToEmoji(hexcode, skinResult); } export async function loadCustomEmojiByShortcode(shortcode: string) { @@ -301,13 +315,16 @@ async function syncLocales(db: Database) { log('Loaded %d locales: %o', loadedLocales.size, loadedLocales); } -function toLoadedLocale(localeString: string) { +async function toLoadedLocale(localeString: string) { const locale = toSupportedLocale(localeString); if (localeString !== locale) { log(`Locale ${locale} is different from provided ${localeString}`); } if (!loadedLocales.has(locale)) { - throw new LocaleNotLoadedError(locale); + log('Locale %s not loaded, importing...', locale); + const { importEmojiData } = await import('./loader'); + await importEmojiData(locale); + return locale; } return locale; } diff --git a/app/javascript/mastodon/features/emoji/db-schema.ts b/app/javascript/mastodon/features/emoji/db-schema.ts new file mode 100644 index 00000000000..f5582cf0c33 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/db-schema.ts @@ -0,0 +1,198 @@ +import { SUPPORTED_LOCALES } from 'emojibase'; +import type { Locale } from 'emojibase'; +import { openDB } from 'idb'; +import type { + DBSchema, + IDBPDatabase, + IDBPObjectStore, + IDBPTransaction, + IndexNames, + StoreNames, +} from 'idb'; + +import type { CustomEmojiData, EtagTypes, UnicodeEmojiData } from './types'; +import { emojiLogger } from './utils'; + +const log = emojiLogger('database'); + +interface EmojiDB extends LocaleTables, DBSchema { + custom: { + key: string; + value: CustomEmojiData; + indexes: { + tokens: string[]; + category: string; + }; + }; + shortcodes: { + key: string; + value: { + hexcode: string; + shortcodes: string[]; + }; + indexes: { + shortcodes: string[]; + }; + }; + etags: { + key: EtagTypes; + value: string; + }; +} + +interface LocaleTable { + key: string; + value: UnicodeEmojiData; + indexes: { + shortcodes: string[]; + groupOrder: [number, number]; + tokens: string[]; + skinHexcodes: string[]; + }; +} +type LocaleTables = Record; + +type Transaction = + IDBPTransaction[], Mode>; + +export type Database = IDBPDatabase; + +const SCHEMA_VERSION = 3; + +export async function openEmojiDB() { + const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { + upgrade(database, oldVersion, newVersion, trx) { + if (!database.objectStoreNames.contains('custom')) { + database.createObjectStore('custom', { + keyPath: 'shortcode', + autoIncrement: false, + }); + } + maybeAddIndex({ trx, storeName: 'custom', indexName: 'category' }); + maybeAddIndex({ + trx, + storeName: 'custom', + indexName: 'tokens', + options: { multiEntry: true }, + }); + + if (!database.objectStoreNames.contains('etags')) { + database.createObjectStore('etags'); + } + + SUPPORTED_LOCALES.forEach((locale) => { + createLocaleTable(locale, database, trx); + }); + + const shortcodeTable = database.objectStoreNames.contains('shortcodes') + ? trx.objectStore('shortcodes') + : database.createObjectStore('shortcodes', { + keyPath: 'hexcode', + autoIncrement: false, + }); + maybeAddIndex({ + trx, + storeName: 'shortcodes', + indexName: 'shortcodes', + options: { multiEntry: true }, + }); + deleteOldIndexes(shortcodeTable, ['hexcode']); + + log( + 'Upgraded emoji database from version %d to %d', + oldVersion, + newVersion, + ); + }, + blocked(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d to %d is blocked', + currentVersion, + blockedVersion, + ); + }, + blocking(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d is blocking upgrade to %d', + currentVersion, + blockedVersion, + ); + }, + }); + + return db; +} + +function maybeAddIndex>({ + trx, + storeName, + indexName, + keys, + options, +}: { + trx: Transaction; + storeName: StoreName; + indexName: IndexNames; + keys?: string | string[]; + options?: IDBIndexParameters; +}) { + const store = trx.objectStore(storeName); + if (!store.indexNames.contains(indexName)) { + store.createIndex(indexName, keys ?? indexName, options); + } +} + +function createLocaleTable( + locale: Locale, + database: Database, + trx: Transaction, +) { + if (!database.objectStoreNames.contains(locale)) { + database.createObjectStore(locale, { + keyPath: 'hexcode', + autoIncrement: false, + }); + } + + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'shortcodes', + options: { multiEntry: true }, + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'groupOrder', + keys: ['group', 'order'], + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'tokens', + keys: 'tokens', + options: { multiEntry: true }, + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'skinHexcodes', + keys: 'skinHexcodes', + options: { multiEntry: true }, + }); + + const oldIndexes = ['group', 'order', 'tag', 'label'] as const; + deleteOldIndexes(trx.objectStore(locale), oldIndexes); +} + +function deleteOldIndexes( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type is too complex, so only any works here. + table: IDBPObjectStore, + indexes: readonly string[], +) { + for (const index of indexes) { + if (table.indexNames.contains(index)) { + table.deleteIndex(index); + } + } +} diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index f8fa0ae1923..859665a5312 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,6 +1,6 @@ import Trie from 'substring-trie'; -import { getUserTheme, isDarkMode } from '@/mastodon/utils/theme'; +import { getIsSystemTheme, isDarkMode } from '@/mastodon/utils/theme'; import { assetHost } from 'mastodon/utils/config'; import { autoPlayGif } from '../../initial_state'; @@ -98,7 +98,7 @@ const emojifyTextNode = (node, customEmojis) => { const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; - const isSystemTheme = getUserTheme() === 'system'; + const isSystemTheme = getIsSystemTheme(); const theme = (isSystemTheme || !isDarkMode()) ? 'light' : 'dark'; diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index c6b64fe29c0..7a94d604a98 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -1,10 +1,5 @@ -import { flattenEmojiData } from 'emojibase'; -import type { - CompactEmoji, - FlatCompactEmoji, - Locale, - ShortcodesDataset, -} from 'emojibase'; +import { joinShortcodes } from 'emojibase'; +import type { CompactEmoji, Locale, ShortcodesDataset } from 'emojibase'; import { putEmojiData, @@ -28,7 +23,7 @@ export async function importEmojiData(localeString: string, shortcodes = true) { shortcodes ? ' and shortcodes' : '', ); - const emojis = await fetchAndCheckEtag({ + let emojis = await fetchAndCheckEtag({ etagString: locale, path: localeToEmojiPath(locale), }); @@ -49,12 +44,10 @@ export async function importEmojiData(localeString: string, shortcodes = true) { } } - const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData( - emojis, - shortcodesData, - ); - await putEmojiData(flattenedEmojis, locale); - return flattenedEmojis; + emojis = joinShortcodes(emojis, shortcodesData); + + await putEmojiData(emojis, locale); + return emojis; } export async function importCustomEmojiData() { @@ -135,11 +128,11 @@ async function fetchAndCheckEtag({ checkEtag?: boolean; }): Promise { const etagName = toValidEtagName(etagString); + const oldEtag = checkEtag ? await loadLatestEtag(etagName) : null; // Use location.origin as this script may be loaded from a CDN domain. const url = new URL(path, location.origin); - const oldEtag = checkEtag ? await loadLatestEtag(etagName) : null; const response = await fetch(url, { headers: { 'Content-Type': 'application/json', @@ -148,6 +141,7 @@ async function fetchAndCheckEtag({ }); // If not modified, return null if (response.status === 304) { + log('etag not modified for %s', etagName); return null; } if (!response.ok) { @@ -163,6 +157,8 @@ async function fetchAndCheckEtag({ if (etag && checkEtag) { log(`storing new etag for ${etagName}: ${etag}`); await putLatestEtag(etag, etagName); + } else if (!etag) { + log(`no etag found in response for ${etagName}`); } return data; diff --git a/app/javascript/mastodon/features/emoji/locale.ts b/app/javascript/mastodon/features/emoji/locale.ts index f39b56d47c2..e8f2df340f1 100644 --- a/app/javascript/mastodon/features/emoji/locale.ts +++ b/app/javascript/mastodon/features/emoji/locale.ts @@ -32,6 +32,13 @@ export function toValidEtagName(input: string): EtagTypes { return toSupportedLocale(lower); } +export function localeToSegmenter(locale: Locale): Intl.Segmenter | null { + if (typeof Intl.Segmenter === 'function') { + return new Intl.Segmenter(locale, { granularity: 'word' }); + } + return null; +} + function isSupportedLocale(locale: string): locale is Locale { return SUPPORTED_LOCALES.includes(locale as Locale); } diff --git a/app/javascript/mastodon/features/emoji/mode.ts b/app/javascript/mastodon/features/emoji/mode.ts index 6950626375e..6763fc4b50b 100644 --- a/app/javascript/mastodon/features/emoji/mode.ts +++ b/app/javascript/mastodon/features/emoji/mode.ts @@ -2,6 +2,7 @@ // See: https://github.com/nolanlawson/emoji-picker-element/blob/master/src/picker/utils/testColorEmojiSupported.js import { createAppSelector, useAppSelector } from '@/mastodon/store'; +import { assetHost } from '@/mastodon/utils/config'; import { isDevelopment } from '@/mastodon/utils/environment'; import { isDarkMode } from '@/mastodon/utils/theme'; @@ -29,6 +30,7 @@ export function useEmojiAppState(): EmojiAppState { locales: [locale], mode, darkTheme: isDarkMode(), + assetHost, }; } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index 8222ab81e58..8c6346709b8 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -4,11 +4,7 @@ import { basename, resolve } from 'path'; import { flattenEmojiData } from 'emojibase'; import unicodeRawEmojis from 'emojibase-data/en/data.json'; -import { - twemojiToUnicodeInfo, - unicodeToTwemojiHex, - emojiToUnicodeHex, -} from './normalize'; +import { unicodeToTwemojiHex } from './normalize'; const emojiSVGFiles = await readdir( // This assumes tests are run from project root @@ -26,23 +22,6 @@ const svgFileNamesWithoutBorder = svgFileNames.filter( const unicodeEmojis = flattenEmojiData(unicodeRawEmojis); -describe('emojiToUnicodeHex', () => { - test.concurrent.for([ - ['🎱', '1F3B1'], - ['🐜', '1F41C'], - ['⚫', '26AB'], - ['🖤', '1F5A4'], - ['💀', '1F480'], - ['❤️', '2764'], // Checks for trailing variation selector removal. - ['💂‍♂️', '1F482-200D-2642-FE0F'], - ] as const)( - 'emojiToUnicodeHex converts %s to %s', - ([emoji, hexcode], { expect }) => { - expect(emojiToUnicodeHex(emoji)).toBe(hexcode); - }, - ); -}); - describe('unicodeToTwemojiHex', () => { test.concurrent.for( unicodeEmojis @@ -54,26 +33,3 @@ describe('unicodeToTwemojiHex', () => { expect(svgFileNamesWithoutBorder).toContain(result); }); }); - -describe('twemojiToUnicodeInfo', () => { - const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode)); - - test.concurrent.for(svgFileNamesWithoutBorder)( - 'verifying SVG file %s maps to Unicode emoji', - (svgFileName, { expect }) => { - assert(!!svgFileName); - const result = twemojiToUnicodeInfo(svgFileName); - const hexcode = typeof result === 'string' ? result : result.unqualified; - if (!hexcode) { - // No hexcode means this is a special case like the Shibuya 109 emoji - expect(result).toHaveProperty('label'); - return; - } - assert(!!hexcode); - expect( - unicodeCodeSet.has(hexcode), - `${hexcode} (${svgFileName}) not found`, - ).toBeTruthy(); - }, - ); -}); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 24df808ae1f..bf06e058ef6 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -1,48 +1,124 @@ import { isList } from 'immutable'; -import { assetHost } from '@/mastodon/utils/config'; +import type { CompactEmoji, SkinTone } from 'emojibase'; +import { fromHexcodeToCodepoint } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { VARIATION_SELECTOR_CODE, KEYCAP_CODE, - GENDER_FEMALE_CODE, - GENDER_MALE_CODE, - SKIN_TONE_CODES, EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_LIGHT_BORDER, EMOJIS_REQUIRING_INVERSION_IN_LIGHT_MODE, EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE, + EMOJI_MIN_TOKEN_LENGTH, } from './constants'; -import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types'; +import type { + CustomEmojiData, + CustomEmojiMapArg, + ExtraCustomEmojiMap, + UnicodeEmojiData, +} from './types'; +import { emojiToUnicodeHex } from './utils'; -// Misc codes that have special handling -const SKIER_CODE = 0x26f7; -const CHRISTMAS_TREE_CODE = 0x1f384; -const MR_CLAUS_CODE = 0x1f385; -const EYE_CODE = 0x1f441; -const LEVITATING_PERSON_CODE = 0x1f574; -const SPEECH_BUBBLE_CODE = 0x1f5e8; -const MS_CLAUS_CODE = 0x1f936; +const SKIN_TONE_MAP: Record = { + 0x1f3fb: 1, // Light skin tone + 0x1f3fc: 2, // Medium-light skin tone + 0x1f3fd: 3, // Medium skin tone + 0x1f3fe: 4, // Medium-dark skin tone + 0x1f3ff: 5, // Dark skin tone +}; -export function emojiToUnicodeHex(emoji: string): string { - const codes: number[] = []; - for (const char of emoji) { - const code = char.codePointAt(0); - if (code !== undefined) { - codes.push(code); +export function transformEmojiData( + emoji: CompactEmoji, + segmenter: Intl.Segmenter | null, +): UnicodeEmojiData { + const { + shortcodes = [], + tags = [], + label, + emoticon, + hexcode, + unicode, + group, + order, + skins = [], + } = emoji; + const extract = (str: string) => extractTokens(str, segmenter); + + let normalizedEmoticons: string[] | undefined = undefined; + if (emoticon) { + normalizedEmoticons = Array.isArray(emoticon) ? emoticon : [emoticon]; + } + + const tokens = [ + ...new Set([ + ...shortcodes.map(extract).flat(), + ...tags.map(extract).flat(), + ...extract(label), + ...(normalizedEmoticons ?? []), + ]), + ].sort((a, b) => a.localeCompare(b)); + + const res: UnicodeEmojiData = { + tokens, + shortcodes, + label, + emoticons: normalizedEmoticons, + hexcode, + unicode, + group, + order, + }; + + for (const skin of skins) { + res.skinHexcodes ??= []; + res.skinHexcodes.push(skin.hexcode); + + res.skinTones ??= []; + for (const codePoint of skin.unicode) { + const tone = SKIN_TONE_MAP[codePoint.codePointAt(0) ?? 0]; + if (tone) { + res.skinTones.push(tone); + break; + } } } - // Handles how Emojibase removes the variation selector for single code emojis. - // See: https://emojibase.dev/docs/spec/#merged-variation-selectors - if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { - codes.pop(); - } - return hexNumbersToString(codes); + return res; } +export function transformCustomEmojiData( + emoji: ApiCustomEmojiJSON, +): CustomEmojiData { + const tokens = emoji.shortcode + .split('_') + .filter((word) => word.length >= EMOJI_MIN_TOKEN_LENGTH) + .map((word) => word.toLowerCase()); + return { + ...emoji, + tokens, + }; +} + +export function skinHexcodeToEmoji( + skinHexcode: string, + emoji: UnicodeEmojiData, +): UnicodeEmojiData { + return { + ...emoji, + unicode: String.fromCodePoint(...fromHexcodeToCodepoint(skinHexcode)), + hexcode: skinHexcode, + }; +} + +// Misc codes that have special handling +const EYE_CODE = 0x1f441; +const SPEECH_BUBBLE_CODE = 0x1f5e8; + export function unicodeToTwemojiHex(unicodeHex: string): string { - const codes = hexStringToNumbers(unicodeHex); + const codes = fromHexcodeToCodepoint(unicodeHex); const normalizedCodes: number[] = []; for (let i = 0; i < codes.length; i++) { const code = codes[i]; @@ -64,19 +140,28 @@ export function unicodeToTwemojiHex(unicodeHex: string): string { normalizedCodes.push(code); } - return hexNumbersToString(normalizedCodes, 0).toLowerCase(); + return normalizedCodes + .map((code) => code.toString(16)) + .join('-') + .toLowerCase(); } -export const CODES_WITH_DARK_BORDER = - EMOJIS_WITH_DARK_BORDER.map(emojiToUnicodeHex); +const CODES_WITH_DARK_BORDER = EMOJIS_WITH_DARK_BORDER.map(emojiToUnicodeHex); -export const CODES_WITH_LIGHT_BORDER = - EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); +const CODES_WITH_LIGHT_BORDER = EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); -export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string { +export function unicodeHexToUrl({ + unicodeHex, + darkTheme, + assetHost, +}: { + unicodeHex: string; + darkTheme: boolean; + assetHost: string; +}): string { const normalizedHex = unicodeToTwemojiHex(unicodeHex); let url = `${assetHost}/emoji/${normalizedHex}`; - if (darkMode && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { + if (darkTheme && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { url += '_border'; } if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) { @@ -86,78 +171,6 @@ export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string { return url; } -interface TwemojiSpecificEmoji { - unqualified?: string; - gender?: number; - skin?: number; - label?: string; -} - -// Normalize man/woman to male/female -const GENDER_CODES_MAP: Record = { - [GENDER_FEMALE_CODE]: GENDER_FEMALE_CODE, - [GENDER_MALE_CODE]: GENDER_MALE_CODE, - // These are man/woman markers, but are used for gender sometimes. - [0x1f468]: GENDER_MALE_CODE, - [0x1f469]: GENDER_FEMALE_CODE, -}; - -const TWEMOJI_SPECIAL_CASES: Record = { - '1F441-200D-1F5E8': '1F441-FE0F-200D-1F5E8-FE0F', // Eye in speech bubble - // An emoji that was never ported to the Unicode standard. - // See: https://emojipedia.org/shibuya - E50A: { label: 'Shibuya 109' }, -}; - -export function twemojiToUnicodeInfo( - twemojiHex: string, -): TwemojiSpecificEmoji | string { - const specialCase = TWEMOJI_SPECIAL_CASES[twemojiHex.toUpperCase()]; - if (specialCase) { - return specialCase; - } - const codes = hexStringToNumbers(twemojiHex); - let gender: undefined | number; - let skin: undefined | number; - for (const code of codes) { - if (!gender && code in GENDER_CODES_MAP) { - gender = GENDER_CODES_MAP[code]; - } else if (!skin && code in SKIN_TONE_CODES) { - skin = code; - } - - // Exit if we have both skin and gender - if (skin && gender) { - break; - } - } - - let mappedCodes: unknown[] = codes; - - if (codes.at(-1) === CHRISTMAS_TREE_CODE && codes.length >= 3 && gender) { - // Twemoji uses the christmas tree with a ZWJ for Mr. and Mrs. Claus, - // but in Unicode that only works for Mx. Claus. - const START_CODE = - gender === GENDER_FEMALE_CODE ? MS_CLAUS_CODE : MR_CLAUS_CODE; - mappedCodes = [START_CODE, skin]; - } else if (codes.at(-1) === KEYCAP_CODE && codes.length === 2) { - // For key emoji, insert the variation selector - mappedCodes = [codes[0], VARIATION_SELECTOR_CODE, KEYCAP_CODE]; - } else if ( - (codes.at(0) === SKIER_CODE || codes.at(0) === LEVITATING_PERSON_CODE) && - codes.length > 1 - ) { - // Twemoji offers more gender and skin options for the skier and levitating person emoji. - return { - unqualified: hexNumbersToString([codes.at(0)]), - skin, - gender, - }; - } - - return hexNumbersToString(mappedCodes); -} - export function emojiToInversionClassName(emoji: string): string | null { if (EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE.includes(emoji)) { return 'invert-on-dark'; @@ -189,19 +202,37 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) { return extraEmojis; } -function hexStringToNumbers(hexString: string): number[] { - return hexString - .split('-') - .map((code) => Number.parseInt(code, 16)) - .filter((code) => !Number.isNaN(code)); -} +/** + * Tokenizes an input string into words, using Intl.Segmenter if available. + * @param input Any input string. + * @param segmenter Segmenter, if available. + * @returns Array of tokens in lowercase. + */ +export function extractTokens( + input: string, + segmenter: Intl.Segmenter | null, +): string[] { + if (!input.trim()) { + return []; + } + const tokens: string[] = []; -function hexNumbersToString(codes: unknown[], padding = 4): string { - return codes - .filter( - (code): code is number => - typeof code === 'number' && code > 0 && !Number.isNaN(code), - ) - .map((code) => code.toString(16).padStart(padding, '0').toUpperCase()) - .join('-'); + // Prefer to use Intl.Segmenter if available for better locale support. + if (segmenter) { + for (const { isWordLike, segment } of segmenter.segment( + input.replaceAll('_', ' '), // Handle underscores from shortcodes. + )) { + if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) { + tokens.push(segment.toLowerCase()); + } + } + } else { + // Fallback to simple splitting. + input.split(/[\s_-]+/).forEach((word) => { + if (/\w/.test(word) && word.length >= EMOJI_MIN_TOKEN_LENGTH) { + tokens.push(word.toLowerCase()); + } + }); + } + return tokens; } diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 8fe311014a9..4b65f3abde5 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -4,7 +4,6 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, } from './constants'; -import { emojiToUnicodeHex } from './normalize'; import type { EmojiLoadedState, EmojiMode, @@ -16,6 +15,7 @@ import type { import { anyEmojiRegex, emojiLogger, + emojiToUnicodeHex, isCustomEmoji, isUnicodeEmoji, stringHasUnicodeFlags, @@ -140,12 +140,7 @@ export async function loadEmojiDataToState( } // 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, - ); + log('Could not find emoji %s for locale %s', state.code, locale); return null; } catch (err: unknown) { // If the locale is not loaded, load it and retry once. diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index 03002dda645..8ab756972a8 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -1,6 +1,6 @@ import type { List as ImmutableList } from 'immutable'; -import type { FlatCompactEmoji, Locale } from 'emojibase'; +import type { CompactEmoji, Locale, SkinTone } from 'emojibase'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; @@ -32,10 +32,20 @@ export interface EmojiAppState { currentLocale: Locale; mode: EmojiMode; darkTheme: boolean; + assetHost: string; } -export type CustomEmojiData = ApiCustomEmojiJSON; -export type UnicodeEmojiData = FlatCompactEmoji; +export type CustomEmojiData = ApiCustomEmojiJSON & { tokens: string[] }; +export interface UnicodeEmojiData extends Omit< + CompactEmoji, + 'emoticon' | 'skins' | 'tags' +> { + shortcodes: string[]; + tokens: string[]; + emoticons?: string[]; + skinHexcodes?: string[]; + skinTones?: (SkinTone | SkinTone[])[]; +} export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData; type CustomEmojiRenderFields = Pick< diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index c567afc2ccc..d15aa961500 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -2,6 +2,8 @@ import debug from 'debug'; import { emojiRegexPolyfill } from '@/mastodon/polyfills'; +import { VARIATION_SELECTOR_CODE } from './constants'; + export function emojiLogger(segment: string) { return debug(`emojis:${segment}`); } @@ -44,6 +46,27 @@ export function anyEmojiRegex() { ); } +export function emojiToUnicodeHex(emoji: string): string { + const codes: string[] = []; + for (const char of emoji) { + const code = char.codePointAt(0); + if (code !== undefined) { + codes.push(code.toString(16).toUpperCase().padStart(4, '0')); + } + } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if ( + codes.at(1) === VARIATION_SELECTOR_CODE.toString(16).toUpperCase() && + codes.length === 2 + ) { + codes.pop(); + } + + return codes.join('-'); +} + function supportsRegExpSets() { return 'unicodeSets' in RegExp.prototype; } diff --git a/app/javascript/mastodon/features/status/components/card.tsx b/app/javascript/mastodon/features/status/components/card.tsx index d060d35c2cd..68eea5ea361 100644 --- a/app/javascript/mastodon/features/status/components/card.tsx +++ b/app/javascript/mastodon/features/status/components/card.tsx @@ -118,7 +118,7 @@ const Card: React.FC = ({ card, sensitive }) => { ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); const interactive = card.get('type') === 'video'; - const language = card.get('language') || ''; + const language = card.get('language') ?? ''; const hasImage = (card.get('image')?.length ?? 0) > 0; const largeImage = (hasImage && card.get('width') > card.get('height')) || interactive; @@ -131,7 +131,11 @@ const Card: React.FC = ({ card, sensitive }) => { {card.get('published_at') && ( <> {' '} - · + ·{' '} + )} diff --git a/app/javascript/mastodon/features/ui/components/media_modal.tsx b/app/javascript/mastodon/features/ui/components/media_modal.tsx index ac762aa18d8..25101376820 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.tsx @@ -85,7 +85,7 @@ export const MediaModal: FC = forwardRef< setIndex(newIndex); setZoomedIn(false); if (animate) { - void api.start({ x: `-${newIndex * 100}%` }); + void api.start({ x: `calc(-${newIndex * 100}% + 0px)` }); } }, [api, media.size], diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 6f9b23042d7..0e718747d9c 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -85,6 +85,7 @@ export const MODAL_COMPONENTS = { 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, 'ANNUAL_REPORT': AnnualReportModal, 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), + 'ACCOUNT_FIELDS': () => import('mastodon/features/account_timeline/components/fields_modal.tsx').then(module => ({ default: module.AccountFieldsModal })), }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js index fe873804319..2bf771e2a3d 100644 --- a/app/javascript/mastodon/features/ui/containers/modal_container.js +++ b/app/javascript/mastodon/features/ui/containers/modal_container.js @@ -23,7 +23,7 @@ const mapDispatchToProps = dispatch => ({ confirm: confirmationMessage.confirm, onConfirm: () => dispatch(closeModal({ modalType: undefined, - ignoreFocus: { ignoreFocus }, + ignoreFocus, })), }, }), @@ -31,7 +31,7 @@ const mapDispatchToProps = dispatch => ({ } else { dispatch(closeModal({ modalType: undefined, - ignoreFocus: { ignoreFocus }, + ignoreFocus, })); } }, diff --git a/app/javascript/mastodon/hooks/useAccount.ts b/app/javascript/mastodon/hooks/useAccount.ts new file mode 100644 index 00000000000..277fa16931d --- /dev/null +++ b/app/javascript/mastodon/hooks/useAccount.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +import { fetchAccount } from '../actions/accounts'; +import { createAppSelector, useAppDispatch, useAppSelector } from '../store'; + +export const accountSelector = createAppSelector( + [ + (state) => state.accounts, + (_, accountId: string | null | undefined) => accountId, + ], + (accounts, accountId) => (accountId ? accounts.get(accountId) : undefined), +); + +export function useAccount(accountId: string | null | undefined) { + const account = useAppSelector((state) => accountSelector(state, accountId)); + + const dispatch = useAppDispatch(); + const accountInStore = !!account; + useEffect(() => { + if (accountId && !accountInStore) { + dispatch(fetchAccount(accountId)); + } + }, [accountId, accountInStore, dispatch]); + + return account; +} diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index da4aa544093..02f88f1513a 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Перайсці да профілю", "account.hide_reblogs": "Схаваць пашырэнні ад @{name}", "account.in_memoriam": "У памяць.", + "account.joined_long": "Далучыў(-ла)ся {date}", "account.joined_short": "Далучыўся", "account.languages": "Змяніць выбраныя мовы", "account.link_verified_on": "Права ўласнасці на гэтую спасылку праверана {date}", @@ -90,6 +91,8 @@ "account.unmute": "Не ігнараваць @{name}", "account.unmute_notifications_short": "Апавяшчаць", "account.unmute_short": "Не ігнараваць", + "account_fields_modal.close": "Закрыць", + "account_fields_modal.title": "Інфармацыя пра {name}", "account_note.placeholder": "Націсніце, каб дадаць нататку", "admin.dashboard.daily_retention": "Штодзённы паказчык утрымання карыстальнікаў пасля рэгістрацыі", "admin.dashboard.monthly_retention": "Штомесячны паказчык утрымання карыстальнікаў пасля рэгістрацыі", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}", "loading_indicator.label": "Ідзе загрузка…", "media_gallery.hide": "Схаваць", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны, таму што Вы перайшлі на {movedToAccount}.", "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў", "mute_modal.hide_options": "Схаваць опцыі", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 447bbf56ea0..b29ca9c3521 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Přejít na profil", "account.hide_reblogs": "Skrýt boosty od @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Přidali se {date}", "account.joined_short": "Připojen/a", "account.languages": "Změnit odebírané jazyky", "account.link_verified_on": "Vlastnictví tohoto odkazu bylo zkontrolováno {date}", @@ -90,6 +91,8 @@ "account.unmute": "Zrušit skrytí @{name}", "account.unmute_notifications_short": "Zrušit ztlumení oznámení", "account.unmute_short": "Zrušit skrytí", + "account_fields_modal.close": "Zavřít", + "account_fields_modal.title": "info o {name}", "account_note.placeholder": "Klikněte pro přidání poznámky", "admin.dashboard.daily_retention": "Míra udržení uživatelů podle dne po registraci", "admin.dashboard.monthly_retention": "Míra udržení uživatelů podle měsíce po registraci", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}", "loading_indicator.label": "Načítání…", "media_gallery.hide": "Skrýt", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.", "mute_modal.hide_from_notifications": "Skrýt z oznámení", "mute_modal.hide_options": "Skrýt možnosti", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 035037fccbb..bc738a0ee5f 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul fremhævelser fra @{name}", "account.in_memoriam": "Til minde om.", + "account.joined_long": "Tilmeldt {date}", "account.joined_short": "Oprettet", "account.languages": "Skift abonnementssprog", "account.link_verified_on": "Ejerskab af dette link blev tjekket {date}", @@ -90,6 +91,8 @@ "account.unmute": "Vis @{name} igen", "account.unmute_notifications_short": "Vis notifikationer igen", "account.unmute_short": "Vis igen", + "account_fields_modal.close": "Luk", + "account_fields_modal.title": "Information om {name}", "account_note.placeholder": "Klik for at tilføje notat", "admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding", "admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nyt element} other {# nye elementer}}", "loading_indicator.label": "Indlæser…", "media_gallery.hide": "Skjul", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Din konto {disabledAccount} er i øjeblikket deaktiveret, fordi du er flyttet til {movedToAccount}.", "mute_modal.hide_from_notifications": "Skjul fra notifikationer", "mute_modal.hide_options": "Skjul valgmuligheder", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 9e63460159b..52fa21bead1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Profil aufrufen", "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden", "account.in_memoriam": "Zum Andenken.", + "account.joined_long": "Registriert am {date}", "account.joined_short": "Registriert am", "account.languages": "Sprachen verwalten", "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt", @@ -90,6 +91,8 @@ "account.unmute": "Stummschaltung von @{name} aufheben", "account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben", "account.unmute_short": "Stummschaltung aufheben", + "account_fields_modal.close": "Schließen", + "account_fields_modal.title": "Informationen über {name}", "account_note.placeholder": "Klicken, um private Anmerkung hinzuzufügen", "admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag seit der Registrierung", "admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat seit der Registrierung", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}", "loading_indicator.label": "Lädt …", "media_gallery.hide": "Ausblenden", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.", "mute_modal.hide_from_notifications": "Auch aus den Benachrichtigungen entfernen", "mute_modal.hide_options": "Optionen ausblenden", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 384a152268a..c67074ea2da 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -24,7 +24,7 @@ "account.blocking": "Αποκλείεται", "account.cancel_follow_request": "Απόσυρση αιτήματος παρακολούθησης", "account.copy": "Αντιγραφή συνδέσμου προφίλ", - "account.direct": "Ιδιωτική αναφορά @{name}", + "account.direct": "Ιδιωτική επισήμανση @{name}", "account.disable_notifications": "Σταμάτα να με ειδοποιείς όταν δημοσιεύει ο @{name}", "account.domain_blocking": "Αποκλείεται ο τομέας", "account.edit_profile": "Επεξεργασία προφίλ", @@ -57,25 +57,26 @@ "account.go_to_profile": "Μετάβαση στο προφίλ", "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}", "account.in_memoriam": "Εις μνήμην.", + "account.joined_long": "Έγινε μέλος {date}", "account.joined_short": "Έγινε μέλος", "account.languages": "Αλλαγή εγγεγραμμένων γλωσσών", "account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου ελέχθηκε στις {date}", "account.locked_info": "Η κατάσταση απορρήτου αυτού του λογαριασμού έχει ρυθμιστεί σε κλειδωμένη. Ο ιδιοκτήτης αξιολογεί χειροκίνητα ποιος μπορεί να τον ακολουθήσει.", "account.media": "Πολυμέσα", - "account.mention": "Ανάφερε @{name}", + "account.mention": "Επισήμανε @{name}", "account.moved_to": "Ο/Η {name} έχει υποδείξει ότι ο νέος λογαριασμός του/της είναι τώρα:", - "account.mute": "Σώπασε τον @{name}", + "account.mute": "Σίγαση @{name}", "account.mute_notifications_short": "Σίγαση ειδοποιήσεων", "account.mute_short": "Σίγαση", - "account.muted": "Αποσιωπημένος/η", + "account.muted": "Σε σίγαση", "account.muting": "Σίγαση", "account.mutual": "Ακολουθείτε ο ένας τον άλλο", "account.no_bio": "Δεν υπάρχει περιγραφή.", "account.open_original_page": "Άνοιγμα αυθεντικής σελίδας", - "account.posts": "Τουτ", - "account.posts_with_replies": "Τουτ και απαντήσεις", + "account.posts": "Αναρτήσεις", + "account.posts_with_replies": "Αναρτήσεις και απαντήσεις", "account.remove_from_followers": "Κατάργηση {name} από τους ακόλουθους", - "account.report": "Κατάγγειλε @{name}", + "account.report": "Αναφορά @{name}", "account.requested_follow": "Ο/Η {name} αιτήθηκε να σε ακολουθήσει", "account.requests_to_follow_you": "Αιτήματα για να σε ακολουθήσουν", "account.share": "Κοινοποίηση του προφίλ @{name}", @@ -87,9 +88,11 @@ "account.unblock_short": "Άρση αποκλεισμού", "account.unendorse": "Να μην παρέχεται στο προφίλ", "account.unfollow": "Άρση ακολούθησης", - "account.unmute": "Διακοπή σίγασης @{name}", + "account.unmute": "Άρση σίγασης @{name}", "account.unmute_notifications_short": "Σίγαση ειδοποιήσεων", "account.unmute_short": "Κατάργηση σίγασης", + "account_fields_modal.close": "Κλείσιμο", + "account_fields_modal.title": "Πληροφορίες {name}", "account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση", "admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα", "admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα", @@ -168,7 +171,7 @@ "block_modal.they_cant_see_posts": "Δεν μπορεί να δει τις αναρτήσεις σου και δε θα δεις τις δικές του.", "block_modal.they_will_know": "Μπορούν να δει ότι έχει αποκλειστεί.", "block_modal.title": "Αποκλεισμός χρήστη;", - "block_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.", + "block_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον επισημαίνουν.", "boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις την επόμενη φορά", "boost_modal.reblog": "Ενίσχυση ανάρτησης;", "boost_modal.undo_reblog": "Αναίρεση ενίσχυσης;", @@ -196,7 +199,7 @@ "column.bookmarks": "Σελιδοδείκτες", "column.community": "Τοπική ροή", "column.create_list": "Δημιουργία λίστας", - "column.direct": "Ιδιωτικές αναφορές", + "column.direct": "Ιδιωτικές επισημάνσεις", "column.directory": "Περιήγηση στα προφίλ", "column.domain_blocks": "Αποκλεισμένοι τομείς", "column.edit_list": "Επεξεργασία λίστας", @@ -275,7 +278,7 @@ "confirmations.missing_alt_text.message": "Η ανάρτησή σου περιέχει πολυμέσα χωρίς εναλλακτικό κείμενο. Η προσθήκη περιγραφών βοηθά να γίνει το περιεχόμενό σου προσβάσιμο σε περισσότερους ανθρώπους.", "confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει", "confirmations.missing_alt_text.title": "Προσθήκη εναλλακτικού κειμένου;", - "confirmations.mute.confirm": "Αποσιώπηση", + "confirmations.mute.confirm": "Σίγαση", "confirmations.private_quote_notify.cancel": "Πίσω στην επεξεργασία", "confirmations.private_quote_notify.confirm": "Δημοσίευση ανάρτησης", "confirmations.private_quote_notify.do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα", @@ -297,14 +300,14 @@ "confirmations.unblock.confirm": "Άρση αποκλεισμού", "confirmations.unblock.title": "Άρση αποκλεισμού {name};", "confirmations.unfollow.confirm": "Άρση ακολούθησης", - "confirmations.unfollow.title": "Κατάργηση ακολούθησης του/της {name};", + "confirmations.unfollow.title": "Άρση ακολούθησης του/της {name};", "confirmations.withdraw_request.confirm": "Απόσυρση αιτήματος", "confirmations.withdraw_request.title": "Απόσυρση αιτήματος για να ακολουθήσετε τον/την {name};", "content_warning.hide": "Απόκρυψη ανάρτησης", "content_warning.show": "Εμφάνιση ούτως ή άλλως", "content_warning.show_more": "Εμφάνιση περισσότερων", - "conversation.delete": "Διαγραφή συζήτησης", - "conversation.mark_as_read": "Σήμανση ως αναγνωσμένο", + "conversation.delete": "Διαγραφή συνομιλίας", + "conversation.mark_as_read": "Σήμανση ως αναγνωσμένη", "conversation.open": "Προβολή συνομιλίας", "conversation.with": "Με {names}", "copy_icon_button.copied": "Αντιγράφηκε στο πρόχειρο", @@ -403,7 +406,7 @@ "filter_modal.added.context_mismatch_title": "Ασυμφωνία περιεχομένου!", "filter_modal.added.expired_explanation": "Αυτή η κατηγορία φίλτρων έχει λήξει, πρέπει να αλλάξετε την ημερομηνία λήξης για να ισχύσει.", "filter_modal.added.expired_title": "Ληγμένο φίλτρο!", - "filter_modal.added.review_and_configure": "Για να επιθεωρήσετε και να εξειδικεύσετε περαιτέρω αυτή την κατηγορία φίλτρων, πηγαίνετε στο {settings_link}.", + "filter_modal.added.review_and_configure": "Για να ελέγξετε και να ρυθμίσετε περαιτέρω αυτή την κατηγορία φίλτρων, πηγαίνετε στο {settings_link}.", "filter_modal.added.review_and_configure_title": "Ρυθμίσεις φίλτρου", "filter_modal.added.settings_link": "σελίδα ρυθμίσεων", "filter_modal.added.short_explanation": "Αυτή η ανάρτηση έχει προστεθεί στην ακόλουθη κατηγορία φίλτρου: {title}.", @@ -423,7 +426,7 @@ "firehose.remote": "Άλλοι διακομιστές", "follow_request.authorize": "Εξουσιοδότησε", "follow_request.reject": "Απέρριψε", - "follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησαν πως ίσως να θέλεις να ελέγξεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.", + "follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησε πως ίσως να θέλεις να ελέγχεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.", "follow_suggestions.curated_suggestion": "Επιλογή προσωπικού", "follow_suggestions.dismiss": "Να μην εμφανιστεί ξανά", "follow_suggestions.featured_longer": "Προσεκτικά επιλεγμένα απ' την ομάδα του {domain}", @@ -498,7 +501,7 @@ "ignore_notifications_modal.new_accounts_title": "Αγνόηση ειδοποιήσεων από νέους λογαριασμούς;", "ignore_notifications_modal.not_followers_title": "Αγνόηση ειδοποιήσεων από άτομα που δε σας ακολουθούν;", "ignore_notifications_modal.not_following_title": "Αγνόηση ειδοποιήσεων από άτομα που δεν ακολουθείς;", - "ignore_notifications_modal.private_mentions_title": "Αγνόηση ειδοποιήσεων από μη ζητηθείσες ιδιωτικές αναφορές;", + "ignore_notifications_modal.private_mentions_title": "Αγνόηση ειδοποιήσεων από μη ζητηθείσες ιδιωτικές επισημάνσεις;", "info_button.label": "Βοήθεια", "info_button.what_is_alt_text": "Το εναλλακτικό κείμενο παρέχει περιγραφές εικόνας για άτομα με προβλήματα όρασης, διαδικτυακές συνδέσεις χαμηλής ταχύτητας ή για άτομα που αναζητούν επιπλέον περιεχόμενο.\\n\\nΜπορείς να βελτιώσεις την προσβασιμότητα και την κατανόηση για όλους, γράφοντας σαφές, συνοπτικό και αντικειμενικό εναλλακτικό κείμενο.\\n\\n
  • Κατέγραψε σημαντικά στοιχεία
  • \\n
  • Συνόψισε το κείμενο στις εικόνες
  • \\n
  • Χρησιμοποίησε δομή κανονικής πρότασης
  • \\n
  • Απέφυγε περιττές πληροφορίες
  • \\n
  • Εστίασε στις τάσεις και τα βασικά ευρήματα σε σύνθετα οπτικά στοιχεία (όπως διαγράμματα ή χάρτες)
", "interaction_modal.action": "Για να αλληλεπιδράσετε με την ανάρτηση του/της {name}, πρέπει να συνδεθείτε στον λογαριασμό σας σε οποιονδήποτε διακομιστή Mastodon χρησιμοποιείτε.", @@ -589,16 +592,17 @@ "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}", "loading_indicator.label": "Φόρτωση…", "media_gallery.hide": "Απόκρυψη", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.", "mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις", "mute_modal.hide_options": "Απόκρυψη επιλογών", "mute_modal.indefinite": "Μέχρι να κάνω άρση σίγασης", "mute_modal.show_options": "Εμφάνιση επιλογών", - "mute_modal.they_can_mention_and_follow": "Μπορεί να σε αναφέρει και να σε ακολουθήσει, αλλά δε θα τον βλέπεις.", + "mute_modal.they_can_mention_and_follow": "Μπορεί να σε επισημάνει και να σε ακολουθήσει, αλλά δε θα τον βλέπεις.", "mute_modal.they_wont_know": "Δε θα ξέρει ότι είναι σε σίγαση.", "mute_modal.title": "Σίγαση χρήστη;", - "mute_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.", - "mute_modal.you_wont_see_posts": "Μπορεί ακόμα να δει τις αναρτήσεις σου, αλλά δε θα βλέπεις τις δικές του.", + "mute_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον επισημαίνουν.", + "mute_modal.you_wont_see_posts": "Μπορεί ακόμη να βλέπει τις αναρτήσεις σου, αλλά δε θα βλέπεις τις δικές του.", "navigation_bar.about": "Σχετικά με", "navigation_bar.account_settings": "Κωδικός πρόσβασης και ασφάλεια", "navigation_bar.administration": "Διαχείριση", @@ -631,7 +635,7 @@ "navigation_panel.expand_followed_tags": "Επέκταση μενού ετικετών που ακολουθείτε", "navigation_panel.expand_lists": "Επέκταση μενού λίστας", "not_signed_in_indicator.not_signed_in": "Πρέπει να συνδεθείς για να αποκτήσεις πρόσβαση σε αυτόν τον πόρο.", - "notification.admin.report": "Ο/Η {name} ανέφερε τον {target}", + "notification.admin.report": "Ο/Η {name} ανέφερε τον/την {target}", "notification.admin.report_account": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target} για {category}", "notification.admin.report_account_other": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target}", "notification.admin.report_statuses": "Ο χρήστης {name} ανέφερε τον χρήστη {target} για {category}", @@ -745,8 +749,8 @@ "notifications.policy.filter_not_followers_title": "Άτομα που δε σε ακολουθούν", "notifications.policy.filter_not_following_hint": "Μέχρι να τους εγκρίνεις χειροκίνητα", "notifications.policy.filter_not_following_title": "Άτομα που δεν ακολουθείς", - "notifications.policy.filter_private_mentions_hint": "Φιλτραρισμένο εκτός αν είναι απάντηση σε δική σου αναφορά ή αν ακολουθείς τον αποστολέα", - "notifications.policy.filter_private_mentions_title": "Μη συναινετικές ιδιωτικές αναφορές", + "notifications.policy.filter_private_mentions_hint": "Φιλτράρισμα εκτός αν είναι απάντηση σε δική σου επισήμανση ή αν ακολουθείς τον αποστολέα", + "notifications.policy.filter_private_mentions_title": "Μη συναινετικές ιδιωτικές επισημάνσεις", "notifications.policy.title": "Διαχείριση ειδοποιήσεων από…", "notifications_permission_banner.enable": "Ενεργοποίηση ειδοποιήσεων επιφάνειας εργασίας", "notifications_permission_banner.how_to_control": "Για να λαμβάνεις ειδοποιήσεις όταν το Mastodon δεν είναι ανοιχτό, ενεργοποίησε τις ειδοποιήσεις επιφάνειας εργασίας. Μπορείς να ελέγξεις με ακρίβεια ποιοι τύποι αλληλεπιδράσεων δημιουργούν ειδοποιήσεις επιφάνειας εργασίας μέσω του κουμπιού {icon} μόλις ενεργοποιηθούν.", @@ -854,12 +858,12 @@ "report.statuses.subtitle": "Επίλεξε όλα όσα ισχύουν", "report.statuses.title": "Υπάρχουν αναρτήσεις που τεκμηριώνουν αυτή την αναφορά;", "report.submit": "Υποβολή", - "report.target": "Καταγγελία {target}", + "report.target": "Αναφορά {target}", "report.thanks.take_action": "Αυτές είναι οι επιλογές σας για να ελέγχετε τι βλέπετε στο Mastodon:", "report.thanks.take_action_actionable": "Ενώ το εξετάζουμε, μπορείς να δράσεις εναντίον του @{name}:", "report.thanks.title": "Δε θες να το βλέπεις;", "report.thanks.title_actionable": "Σε ευχαριστούμε για την αναφορά, θα το διερευνήσουμε.", - "report.unfollow": "Κατάργηση ακολούθησης του @{name}", + "report.unfollow": "Άρση ακολούθησης του @{name}", "report.unfollow_explanation": "Ακολουθείς αυτό τον λογαριασμό. Για να μη βλέπεις τις αναρτήσεις τους στη δική σου ροή, πάψε να τον ακολουθείς.", "report_notification.attached_statuses": "{count, plural, one {{count} ανάρτηση} other {{count} αναρτήσεις}} επισυνάπτονται", "report_notification.categories.legal": "Νομικά", @@ -926,7 +930,7 @@ "status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.delete": "Διαγραφή", "status.delete.success": "Η ανάρτηση διαγράφηκε", - "status.detailed_status": "Προβολή λεπτομερούς συζήτησης", + "status.detailed_status": "Προβολή λεπτομερούς συνομιλίας", "status.direct": "Ιδιωτική επισήμανση @{name}", "status.direct_indicator": "Ιδιωτική επισήμανση", "status.edit": "Επεξεργασία", @@ -944,7 +948,7 @@ "status.media_hidden": "Κρυμμένο πολυμέσο", "status.mention": "Επισήμανε @{name}", "status.more": "Περισσότερα", - "status.mute": "Σίγαση σε @{name}", + "status.mute": "Σίγαση @{name}", "status.mute_conversation": "Σίγαση συνομιλίας", "status.open": "Επέκταση ανάρτησης", "status.pin": "Καρφίτσωσε στο προφίλ", @@ -997,7 +1001,7 @@ "status.translate": "Μετάφραση", "status.translated_from_with": "Μεταφράστηκε από {lang} χρησιμοποιώντας {provider}", "status.uncached_media_warning": "Μη διαθέσιμη προεπισκόπηση", - "status.unmute_conversation": "Αναίρεση σίγασης συνομιλίας", + "status.unmute_conversation": "Άρση σίγασης συνομιλίας", "status.unpin": "Ξεκαρφίτσωσε από το προφίλ", "subscribed_languages.lead": "Μόνο αναρτήσεις σε επιλεγμένες γλώσσες θα εμφανίζονται στην αρχική σου και θα παραθέτονται ροές μετά την αλλαγή. Επέλεξε καμία για να λαμβάνεις αναρτήσεις σε όλες τις γλώσσες.", "subscribed_languages.save": "Αποθήκευση αλλαγών", @@ -1053,7 +1057,7 @@ "visibility_modal.direct_quote_warning.text": "Εάν αποθηκεύσετε τις τρέχουσες ρυθμίσεις, η ενσωματωμένη παράθεση θα μετατραπεί σε σύνδεσμο.", "visibility_modal.direct_quote_warning.title": "Οι παραθέσεις δεν μπορούν να ενσωματωθούν σε ιδιωτικές επισημάνσεις", "visibility_modal.header": "Ορατότητα και αλληλεπίδραση", - "visibility_modal.helper.direct_quoting": "Ιδιωτικές αναφορές που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.", + "visibility_modal.helper.direct_quoting": "Ιδιωτικές επισημάνσεις που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.", "visibility_modal.helper.privacy_editing": "Η ορατότητα δεν μπορεί να αλλάξει μετά τη δημοσίευση μιας ανάρτησης.", "visibility_modal.helper.privacy_private_self_quote": "Αυτο-παραθέσεις ιδιωτικών αναρτήσεων δεν μπορούν να γίνουν δημόσιες.", "visibility_modal.helper.private_quoting": "Αναρτήσεις για ακολούθους μόνο που έχουν συνταχθεί στο Mastodon, δεν μπορούν να γίνουν παράθεση από άλλους.", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 0863df66995..91937f69233 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -90,6 +91,8 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_fields_modal.close": "Close", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f1ab23570af..afccb038570 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -90,6 +91,8 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_fields_modal.close": "Close", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 670cc70055e..fd4e291f171 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar adhesiones de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", + "account.joined_long": "En este servidor desde el {date}", "account.joined_short": "En este servidor desde el", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Hacé clic par agregar una nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día, después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes, después del registro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar en las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index db87e024631..9aafae6f236 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "En memoria.", + "account.joined_long": "Se unión el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "El proprietario de este enlace fue comprobado el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Haz clic para agregar una nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 2b7d1905350..8a3672ad4c9 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", + "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Haz clic para añadir nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 4a5ca61710c..e4c303153d8 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -2,15 +2,15 @@ "about.blocks": "Modereeritavad serverid", "about.contact": "Kontakt:", "about.default_locale": "Vaikimisi", - "about.disclaimer": "Mastodon on tasuta ja vaba tarkvara ning Mastodon gGmbH kaubamärk.", + "about.disclaimer": "Mastodon on vaba, tasuta ja avatud lähtekoodiga tarkvara ning Mastodon gGmbH kaubamärk.", "about.domain_blocks.no_reason_available": "Põhjus on teadmata", - "about.domain_blocks.preamble": "Mastodon lubab tavaliselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest fediversumi serverist. Need on erandid, mis on paika pandud sellel kindlal serveril.", - "about.domain_blocks.silenced.explanation": "Sa ei näe üldiselt profiile ja sisu sellelt serverilt, kui sa just tahtlikult seda ei otsi või jälgimise moel nõusolekut ei anna.", + "about.domain_blocks.preamble": "Mastodon lubab üldiselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest födiversumi serverist. Need on erandid, mis kehtivad selles kindlas serveris.", + "about.domain_blocks.silenced.explanation": "Sa üldjuhul ei näe profiile ja sisu sellest serverist, kui sa just tahtlikult neid ei otsi või jälgimise moel nõusolekut ei anna.", "about.domain_blocks.silenced.title": "Piiratud", - "about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse interaktsiooni või kirjavahetuse selle serveri kasutajatega võimatuks.", + "about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse suhestumise või infovahetuse selle serveri kasutajatega võimatuks.", "about.domain_blocks.suspended.title": "Peatatud", "about.language_label": "Keel", - "about.not_available": "See info ei ole sellel serveril saadavaks tehtud.", + "about.not_available": "See info ei ole selles serveris saadavaks tehtud.", "about.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}", "about.rules": "Serveri reeglid", "account.account_note_header": "Isiklik märge", @@ -18,19 +18,19 @@ "account.badges.bot": "Robot", "account.badges.group": "Grupp", "account.block": "Blokeeri @{name}", - "account.block_domain": "Peida kõik domeenist {domain}", + "account.block_domain": "Blokeeri kõik domeenist {domain}", "account.block_short": "Blokeerimine", "account.blocked": "Blokeeritud", "account.blocking": "Blokeeritud kasutaja", "account.cancel_follow_request": "Võta jälgimistaotlus tagasi", "account.copy": "Kopeeri profiili link", "account.direct": "Maini privaatselt @{name}", - "account.disable_notifications": "Peata teavitused @{name} postitustest", + "account.disable_notifications": "Ära teavita, kui @{name} postitab", "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.enable_notifications": "Teavita mind, kui {name} postitab", + "account.endorse": "Too profiilis esile", "account.familiar_followers_many": "Jälgijateks {name1}, {name2} ja veel {othersCount, plural, one {üks kasutaja, keda tead} other {# kasutajat, keda tead}}", "account.familiar_followers_one": "Jälgijaks {name1}", "account.familiar_followers_two": "Jälgijateks {name1} ja {name2}", @@ -57,11 +57,12 @@ "account.go_to_profile": "Mine profiilile", "account.hide_reblogs": "Peida @{name} jagamised", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Liitus {date}", "account.joined_short": "Liitus", "account.languages": "Muuda tellitud keeli", "account.link_verified_on": "Selle lingi autorsust kontrolliti {date}", "account.locked_info": "Selle konto privaatsussätteks on lukustatud. Omanik vaatab käsitsi üle, kes teda jälgida saab.", - "account.media": "Meedia", + "account.media": "Meedium", "account.mention": "Maini @{name}", "account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:", "account.mute": "Summuta @{name}", @@ -81,20 +82,22 @@ "account.share": "Jaga @{name} profiili", "account.show_reblogs": "Näita @{name} jagamisi", "account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}", - "account.unblock": "Eemalda blokeering @{name}", - "account.unblock_domain": "Tee {domain} nähtavaks", + "account.unblock": "Lõpeta {name} kasutaja blokeerimine", + "account.unblock_domain": "Lõpeta {domain} domeeni blokeerimine", "account.unblock_domain_short": "Lõpeta blokeerimine", - "account.unblock_short": "Eemalda blokeering", + "account.unblock_short": "Lõpeta blokeerimine", "account.unendorse": "Ära kuva profiilil", - "account.unfollow": "Jälgid", + "account.unfollow": "Ära jälgi", "account.unmute": "Lõpeta {name} kasutaja summutamine", "account.unmute_notifications_short": "Lõpeta teavituste summutamine", "account.unmute_short": "Lõpeta summutamine", + "account_fields_modal.close": "Sulge", + "account_fields_modal.title": "Kasutaja teave: {name}", "account_note.placeholder": "Klõpsa märke lisamiseks", "admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist", "admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist", "admin.dashboard.retention.average": "Keskmine", - "admin.dashboard.retention.cohort": "Registreerumiskuu", + "admin.dashboard.retention.cohort": "Liitumiskuu", "admin.dashboard.retention.cohort_size": "Uued kasutajad", "admin.impact_report.instance_accounts": "Kontode profiilid, mille see kustutaks", "admin.impact_report.instance_followers": "Jälgijad, kelle meie kasutajad kaotaks", @@ -103,11 +106,11 @@ "alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.", "alert.rate_limited.title": "Kiiruspiirang", "alert.unexpected.message": "Tekkis ootamatu viga.", - "alert.unexpected.title": "Oih!", - "alt_text_badge.title": "Alternatiivtekst", - "alt_text_modal.add_alt_text": "Lisa alt-tekst", + "alert.unexpected.title": "Vaat kus lops!", + "alt_text_badge.title": "Selgitustekst", + "alt_text_modal.add_alt_text": "Lisa selgitustekst", "alt_text_modal.add_text_from_image": "Lisa tekst pildilt", - "alt_text_modal.cancel": "Tühista", + "alt_text_modal.cancel": "Katkesta", "alt_text_modal.change_thumbnail": "Muuda pisipilti", "alt_text_modal.describe_for_people_with_hearing_impairments": "Kirjelda seda kuulmispuudega inimeste jaoks…", "alt_text_modal.describe_for_people_with_visual_impairments": "Kirjelda seda nägemispuudega inimeste jaoks…", @@ -119,7 +122,7 @@ "annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.", "annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis", "annual_report.nav_item.badge": "Uus", - "annual_report.shared_page.donate": "Anneta", + "annual_report.shared_page.donate": "Toeta rahaliselt", "annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt", "annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.", "annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", @@ -517,6 +520,7 @@ "keyboard_shortcuts.column": "Fookus veerule", "keyboard_shortcuts.compose": "Fookus teksti koostamise alale", "keyboard_shortcuts.description": "Kirjeldus", + "keyboard_shortcuts.direct": "Ava privaatsete mainimiste veerg", "keyboard_shortcuts.down": "Liigu loetelus alla", "keyboard_shortcuts.enter": "Ava postitus", "keyboard_shortcuts.favourite": "Lemmikpostitus", @@ -588,6 +592,7 @@ "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}", "loading_indicator.label": "Laadimine…", "media_gallery.hide": "Peida", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.", "mute_modal.hide_from_notifications": "Peida teavituste hulgast", "mute_modal.hide_options": "Peida valikud", @@ -766,7 +771,7 @@ "onboarding.profile.upload_avatar": "Laadi üles profiilipilt", "onboarding.profile.upload_header": "Laadi üles profiili päis", "password_confirmation.exceeds_maxlength": "Salasõnakinnitus on pikem kui salasõna maksimumpikkus", - "password_confirmation.mismatching": "Salasõnakinnitus ei sobi kokku", + "password_confirmation.mismatching": "Salasõnad ei klapi", "picture_in_picture.restore": "Pane tagasi", "poll.closed": "Suletud", "poll.refresh": "Värskenda", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index c0cd9a77cd5..db3da7bf2e7 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Siirry profiiliin", "account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset", "account.in_memoriam": "Muistoissamme.", + "account.joined_long": "Liittynyt {date}", "account.joined_short": "Liittynyt", "account.languages": "Vaihda tilattuja kieliä", "account.link_verified_on": "Linkin omistus tarkistettiin {date}", @@ -90,6 +91,8 @@ "account.unmute": "Poista käyttäjän @{name} mykistys", "account.unmute_notifications_short": "Poista ilmoitusten mykistys", "account.unmute_short": "Poista mykistys", + "account_fields_modal.close": "Sulje", + "account_fields_modal.title": "Käyttäjän {name} tiedot", "account_note.placeholder": "Lisää muistiinpano napsauttamalla", "admin.dashboard.daily_retention": "Käyttäjien pysyvyys päivittäin rekisteröitymisen jälkeen", "admin.dashboard.monthly_retention": "Käyttäjien pysyvyys kuukausittain rekisteröitymisen jälkeen", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# uusi kohde} other {# uutta kohdetta}}", "loading_indicator.label": "Ladataan…", "media_gallery.hide": "Piilota", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tilisi {disabledAccount} on tällä hetkellä poissa käytöstä, koska teit siirron tiliin {movedToAccount}.", "mute_modal.hide_from_notifications": "Piilota ilmoituksista", "mute_modal.hide_options": "Piilota vaihtoehdot", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 52ef3946c51..aac6797b046 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Far til vanga", "account.hide_reblogs": "Fjal stimbran frá @{name}", "account.in_memoriam": "In memoriam.", + "account.joined_long": "Meldaði til {date}", "account.joined_short": "Gjørdist limur", "account.languages": "Broyt fylgd mál", "account.link_verified_on": "Ognarskapur av hesum leinki var eftirkannaður {date}", @@ -90,6 +91,8 @@ "account.unmute": "Doyv ikki @{name}", "account.unmute_notifications_short": "Tendra fráboðanir", "account.unmute_short": "Doyv ikki", + "account_fields_modal.close": "Lat aftur", + "account_fields_modal.title": "Upplýsingarnar hjá {name}", "account_note.placeholder": "Klikka fyri at leggja viðmerking afturat", "admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum", "admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nýtt evni} other {# nýggj evni}}", "loading_indicator.label": "Innlesur…", "media_gallery.hide": "Fjal", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Konta tín {disabledAccount} er í løtuni óvirkin, tí tú flutti til {movedToAccount}.", "mute_modal.hide_from_notifications": "Fjal boð", "mute_modal.hide_options": "Fjal valmøguleikar", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 7de713fcff4..7c8fc6358b7 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -21,7 +21,7 @@ "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", "account.blocked": "Bloqué·e", - "account.blocking": "Bloqué", + "account.blocking": "Bloqué·e", "account.cancel_follow_request": "Retirer cette demande d'abonnement", "account.copy": "Copier le lien du profil", "account.direct": "Mention privée @{name}", @@ -32,8 +32,8 @@ "account.enable_notifications": "Me notifier quand @{name} publie", "account.endorse": "Inclure sur profil", "account.familiar_followers_many": "Suivi par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", - "account.familiar_followers_one": "Suivi par {name1}", - "account.familiar_followers_two": "Suivi par {name1} et {name2}", + "account.familiar_followers_one": "Suivi·e par {name1}", + "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", "account.featured.accounts": "Profils", "account.featured.hashtags": "Hashtags", @@ -49,7 +49,7 @@ "account.followers": "abonné·e·s", "account.followers.empty": "Personne ne suit ce compte pour l'instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", - "account.followers_you_know_counter": "{count, plural, one {{counter} suivi}, other {{counter} suivis}}", + "account.followers_you_know_counter": "{counter} que vous suivez", "account.following": "Abonné·e", "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}", "account.follows.empty": "Ce compte ne suit personne présentement.", @@ -57,6 +57,7 @@ "account.go_to_profile": "Voir ce profil", "account.hide_reblogs": "Masquer les boosts de @{name}", "account.in_memoriam": "En souvenir de", + "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Inscrit·e", "account.languages": "Changer les langues abonnées", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ne plus masquer @{name}", "account.unmute_notifications_short": "Ne plus masquer les notifications", "account.unmute_short": "Ne plus masquer", + "account_fields_modal.close": "Fermer", + "account_fields_modal.title": "Infos de {name}", "account_note.placeholder": "Cliquez pour ajouter une note", "admin.dashboard.daily_retention": "Taux de rétention des comptes par jour après inscription", "admin.dashboard.monthly_retention": "Taux de rétention des comptes par mois après inscription", @@ -164,10 +167,10 @@ "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", - "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", + "block_modal.they_cant_mention": "Elle ne pourra pas vous mentionner ou vous suivre.", "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", - "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", - "block_modal.title": "Bloquer le compte ?", + "block_modal.they_will_know": "Elle pourra voir qu'elle est bloquée.", + "block_modal.title": "Bloquer cette personne ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour sauter ceci la prochaine fois", "boost_modal.reblog": "Booster le message ?", @@ -589,16 +592,17 @@ "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}", "loading_indicator.label": "Chargement…", "media_gallery.hide": "Masquer", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", - "mute_modal.indefinite": "Jusqu'à ce que je les réactive", + "mute_modal.indefinite": "Jusqu'à ce que je l'affiche à nouveau", "mute_modal.show_options": "Afficher les options", - "mute_modal.they_can_mention_and_follow": "Ils peuvent vous mentionner et vous suivre, mais vous ne les verrez pas.", - "mute_modal.they_wont_know": "Ils ne sauront pas qu'ils ont été rendus silencieux.", - "mute_modal.title": "Rendre cet utilisateur silencieux ?", - "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", - "mute_modal.you_wont_see_posts": "Il peut toujours voir vos publications, mais vous ne verrez pas les siennes.", + "mute_modal.they_can_mention_and_follow": "Elle pourra vous mentionner et vous suivre, mais vous ne la verrez pas.", + "mute_modal.they_wont_know": "Elle ne saura pas qu'elle est masquée.", + "mute_modal.title": "Masquer cette personne ?", + "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui la mentionnent.", + "mute_modal.you_wont_see_posts": "Elle pourra toujours voir vos publications, mais vous ne verrez pas les siennes.", "navigation_bar.about": "À propos", "navigation_bar.account_settings": "Mot de passe et sécurité", "navigation_bar.administration": "Administration", @@ -786,7 +790,7 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.anyone": "{visibility}, citations autorisées", "privacy.quote.disabled": "{visibility}, citations désactivées", "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index d14d24e57be..456258ca2ea 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -21,19 +21,19 @@ "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", "account.blocked": "Bloqué·e", - "account.blocking": "Bloqué", + "account.blocking": "Bloqué·e", "account.cancel_follow_request": "Annuler l'abonnement", "account.copy": "Copier le lien du profil", - "account.direct": "Mention privée @{name}", - "account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose", + "account.direct": "Mentionner @{name} en privé", + "account.disable_notifications": "Ne plus me notifier les publications de @{name}", "account.domain_blocking": "Domaine bloqué", "account.edit_profile": "Modifier le profil", "account.edit_profile_short": "Modifier", - "account.enable_notifications": "Me notifier quand @{name} publie quelque chose", + "account.enable_notifications": "Me notifier les publications de @{name}", "account.endorse": "Recommander sur votre profil", "account.familiar_followers_many": "Suivi par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", - "account.familiar_followers_one": "Suivi par {name1}", - "account.familiar_followers_two": "Suivi par {name1} et {name2}", + "account.familiar_followers_one": "Suivi·e par {name1}", + "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", "account.featured.accounts": "Profils", "account.featured.hashtags": "Hashtags", @@ -49,7 +49,7 @@ "account.followers": "Abonné·e·s", "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", - "account.followers_you_know_counter": "{count, plural, one {{counter} suivi}, other {{counter} suivis}}", + "account.followers_you_know_counter": "{counter} que vous suivez", "account.following": "Abonnements", "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}", "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.", @@ -57,6 +57,7 @@ "account.go_to_profile": "Voir le profil", "account.hide_reblogs": "Masquer les partages de @{name}", "account.in_memoriam": "En mémoire de.", + "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Ici depuis", "account.languages": "Modifier les langues d'abonnements", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ne plus masquer @{name}", "account.unmute_notifications_short": "Réactiver les notifications", "account.unmute_short": "Ne plus masquer", + "account_fields_modal.close": "Fermer", + "account_fields_modal.title": "Infos de {name}", "account_note.placeholder": "Cliquez pour ajouter une note", "admin.dashboard.daily_retention": "Taux de rétention des utilisateur·rice·s par jour après inscription", "admin.dashboard.monthly_retention": "Taux de rétention des utilisateur·rice·s par mois après inscription", @@ -164,10 +167,10 @@ "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", - "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", + "block_modal.they_cant_mention": "Elle ne pourra pas vous mentionner ou vous suivre.", "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", - "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", - "block_modal.title": "Bloquer le compte ?", + "block_modal.they_will_know": "Elle pourra voir qu'elle est bloquée.", + "block_modal.title": "Bloquer cette personne ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", "boost_modal.reblog": "Booster le message ?", @@ -589,16 +592,17 @@ "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}", "loading_indicator.label": "Chargement…", "media_gallery.hide": "Masquer", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", - "mute_modal.indefinite": "Jusqu'à ce que je les réactive", + "mute_modal.indefinite": "Jusqu'à ce que je l'affiche à nouveau", "mute_modal.show_options": "Afficher les options", - "mute_modal.they_can_mention_and_follow": "Ils peuvent vous mentionner et vous suivre, mais vous ne les verrez pas.", - "mute_modal.they_wont_know": "Ils ne sauront pas qu'ils ont été rendus silencieux.", - "mute_modal.title": "Rendre cet utilisateur silencieux ?", - "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", - "mute_modal.you_wont_see_posts": "Il peut toujours voir vos publications, mais vous ne verrez pas les siennes.", + "mute_modal.they_can_mention_and_follow": "Elle pourra vous mentionner et vous suivre, mais vous ne la verrez pas.", + "mute_modal.they_wont_know": "Elle ne saura pas qu'elle est masquée.", + "mute_modal.title": "Masquer cette personne ?", + "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui la mentionnent.", + "mute_modal.you_wont_see_posts": "Elle pourra toujours voir vos publications, mais vous ne verrez pas les siennes.", "navigation_bar.about": "À propos", "navigation_bar.account_settings": "Mot de passe et sécurité", "navigation_bar.administration": "Administration", @@ -786,7 +790,7 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.anyone": "{visibility}, citations autorisées", "privacy.quote.disabled": "{visibility}, citations désactivées", "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 4f65848449d..111a0450fff 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Téigh go dtí próifíl", "account.hide_reblogs": "Folaigh moltaí ó @{name}", "account.in_memoriam": "Ón tseanaimsir.", + "account.joined_long": "Chuaigh isteach ar {date}", "account.joined_short": "Cláraithe", "account.languages": "Athraigh teangacha foscríofa", "account.link_verified_on": "Seiceáladh úinéireacht an naisc seo ar {date}", @@ -90,6 +91,8 @@ "account.unmute": "Díbhalbhaigh @{name}", "account.unmute_notifications_short": "Díbhalbhaigh fógraí", "account.unmute_short": "Díbhalbhaigh", + "account_fields_modal.close": "Dún", + "account_fields_modal.title": "Eolas {name}", "account_note.placeholder": "Cliceáil chun nóta a chuir leis", "admin.dashboard.daily_retention": "Ráta coinneála an úsáideora de réir an lae tar éis clárú", "admin.dashboard.monthly_retention": "Ráta coinneála na n-úsáideoirí de réir na míosa tar éis dóibh clárú", @@ -101,9 +104,9 @@ "admin.impact_report.instance_follows": "Leanúna a bheadh ​​​​a n-úsáideoirí chailleadh", "admin.impact_report.title": "Achoimre ar an tionchar", "alert.rate_limited.message": "Atriail aris tar éis {retry_time, time, medium}.", - "alert.rate_limited.title": "Rátatheoranta", + "alert.rate_limited.title": "Ráta teoranta", "alert.unexpected.message": "Tharla earráid gan choinne.", - "alert.unexpected.title": "Hiúps!", + "alert.unexpected.title": "Úps!", "alt_text_badge.title": "Téacs alt", "alt_text_modal.add_alt_text": "Cuir téacs alt leis", "alt_text_modal.add_text_from_image": "Cuir téacs ón íomhá leis", @@ -150,7 +153,7 @@ "annual_report.summary.highlighted_post.reply_count": "Fuair ​​an post seo {count, plural, one {freagra amháin} two {# freagraí} few {# freagraí} many {# freagraí} other {# freagraí}}.", "annual_report.summary.highlighted_post.title": "An post is mó tóir", "annual_report.summary.most_used_app.most_used_app": "aip is mó a úsáidtear", - "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag is mó a úsáidtear", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "haischlib is mó a úsáidtear", "annual_report.summary.most_used_hashtag.used_count": "Chuir tú an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", "annual_report.summary.most_used_hashtag.used_count_public": "Chuir {name} an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", "annual_report.summary.new_posts.new_posts": "postanna nua", @@ -167,14 +170,14 @@ "block_modal.they_cant_mention": "Ní féidir leo tú a lua ná a leanúint.", "block_modal.they_cant_see_posts": "Ní féidir leo do chuid postálacha a fheiceáil agus ní fheicfidh tú a gcuid postanna.", "block_modal.they_will_know": "Is féidir leo a fheiceáil go bhfuil bac orthu.", - "block_modal.title": "An bhfuil fonn ort an t-úsáideoir a bhlocáil?", + "block_modal.title": "Úsáideoir a bhlocáil?", "block_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.", "boost_modal.combo": "Is féidir leat {combo} a bhrú chun é seo a scipeáil an chéad uair eile", "boost_modal.reblog": "An post a threisiú?", "boost_modal.undo_reblog": "An deireadh a chur le postáil?", "bundle_column_error.copy_stacktrace": "Cóipeáil tuairisc earráide", "bundle_column_error.error.body": "Ní féidir an leathanach a iarradh a sholáthar. Seans gurb amhlaidh mar gheall ar fhabht sa chód, nó mar gheall ar mhíréireacht leis an mbrabhsálaí.", - "bundle_column_error.error.title": "Ó, níl sé sin go maith!", + "bundle_column_error.error.title": "Ó, ní hea!", "bundle_column_error.network.body": "Tharla earráid agus an leathanach á lódáil. Seans gur mar gheall ar fhadhb shealadach le do nasc idirlín nó i ndáil leis an bhfreastalaí seo atá sé.", "bundle_column_error.network.title": "Earráid líonra", "bundle_column_error.retry": "Bain triail as arís", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# mír nua} two {# mír nua} few {# mír nua} many {# mír nua} other {# mír nua}}", "loading_indicator.label": "Á lódáil…", "media_gallery.hide": "Folaigh", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tá do chuntas {disabledAccount} díchumasaithe faoi láthair toisc gur bhog tú go {movedToAccount}.", "mute_modal.hide_from_notifications": "Folaigh ó fhógraí", "mute_modal.hide_options": "Folaigh roghanna", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 2f4cb44de80..50db4dfb4bf 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -113,11 +113,52 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Mìnich seo dhan fheadhainn air a bheil cion-lèirsinne…", "alt_text_modal.done": "Deiseil", "announcement.announcement": "Brath-fios", + "annual_report.announcement.action_build": "Cruthaich Wrapstodon dhomh", + "annual_report.announcement.action_dismiss": "Na cruthaich", + "annual_report.announcement.action_view": "Seall an Wrapstodon agam", + "annual_report.announcement.description": "Fidir mar a chaidh leat air Mastodon am bliadhna.", + "annual_report.announcement.title": "Tha Wrapstodon {year} air a thighinn", + "annual_report.nav_item.badge": "Ùr", + "annual_report.shared_page.donate": "Thoir tabhartas", + "annual_report.shared_page.footer": "Chaidh a ghintinn le {heart} le sgioba Mhastodon", + "annual_report.shared_page.footer_server_info": "Tha {username} a’ cleachdadh {domain}, sin fear de dh’iomadh coimhearsnachd le cumhachd Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "Bhiodh {name} an toir air postaichean rim brosnachadh, a’ toirt taic do chruthadairean gu sgiobalta.", + "annual_report.summary.archetype.booster.desc_self": "Bhiodh tu an toir air postaichean rim brosnachadh, a’ toirt taic do chruthadairean gu sgiobalta.", + "annual_report.summary.archetype.booster.name": "Am Boghadair", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Tha fios againn gun robh {name} ann am badeigin, a’ gabhail tlachd à Mastodon air an dòigh fhèin.", + "annual_report.summary.archetype.lurker.desc_self": "Tha fios againn gun robh thu ann am badeigin, a’ gabhail tlachd à Mastodon air do dhòigh fhèin.", + "annual_report.summary.archetype.lurker.name": "An Socaireach", + "annual_report.summary.archetype.oracle.desc_public": "Chruthaich {name} barrachd phostaichean ùra na freagairtean, a’ cumail Mastodon ùr ’s beòthail.", + "annual_report.summary.archetype.oracle.desc_self": "Chruthaich thu barrachd phostaichean ùra na freagairtean, a’ cumail Mastodon ùr ’s beòthail.", + "annual_report.summary.archetype.oracle.name": "Coinneach Odhar", + "annual_report.summary.archetype.pollster.desc_public": "Chruthaich {name} barrachd chunntasan-beachd na seòrsaichean eile de phost, a’ cur ris an iongantas air Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Chruthaich thu barrachd chunntasan-beachd na seòrsaichean eile de phost, a’ cur ris an iongantas air Mastodon.", + "annual_report.summary.archetype.pollster.name": "An Culaidh-iongantais", + "annual_report.summary.archetype.replier.desc_public": "Fhreagradh {name} gu tric air postaichean càich, a’ cur deasbadan ùra gu dol air Mastodon.", + "annual_report.summary.archetype.replier.desc_self": "Fhreagradh tu gu tric air postaichean càich, a’ cur deasbadan ùra gu dol air Mastodon.", + "annual_report.summary.archetype.replier.name": "An Dealan-dè", + "annual_report.summary.archetype.reveal": "Nochd am prìomh-choltas a th’ orm", + "annual_report.summary.archetype.reveal_description": "Mòran taing airson conaltradh air Mastodon! Thàinig an t-àm a gheibheadh tu a-mach dè am prìomh-choltas a bh’ ort {year}.", + "annual_report.summary.archetype.title_public": "Am prìomh-choltas air {name}", + "annual_report.summary.archetype.title_self": "Am prìomh-choltas a th’ ort", + "annual_report.summary.close": "Dùin", + "annual_report.summary.copy_link": "Dèan lethbhreac dhen cheangal", + "annual_report.summary.followers.new_followers": "{count, plural, one {neach-leantainn ùr} two {neach-leantainn ùra} few {luchd-leantainn ùra} other {luchd-leantainn ùra}}", + "annual_report.summary.highlighted_post.boost_count": "Chaidh am post seo a bhrosnachadh {count, plural, one {# turas} two {# thuras} few {# tursan} other {# turas}}.", + "annual_report.summary.highlighted_post.favourite_count": "Chaidh am post seo a chur ris na h-annsachdan {count, plural, one {# turas} two {# thuras} few {# tursan} other {# turas}}.", + "annual_report.summary.highlighted_post.reply_count": "Fhuair am post seo {count, plural, one {# fhreagairt} two {# fhreagairt} few {# freagairtean} other {# freagairt}}.", + "annual_report.summary.highlighted_post.title": "Am post air an robh fèill as motha", "annual_report.summary.most_used_app.most_used_app": "an aplacaid a chaidh a cleachdadh as trice", "annual_report.summary.most_used_hashtag.most_used_hashtag": "an taga hais a chaidh a cleachdadh as trice", + "annual_report.summary.most_used_hashtag.used_count": "Ghabh thu a-staigh an taga hais seo ann an {count, plural, one {# phost} two {# phost} few {# postaichean} other {# post}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "Ghabh {name} a-staigh an taga hais seo ann an {count, plural, one {# phost} two {# phost} few {# postaichean} other {# post}}.", "annual_report.summary.new_posts.new_posts": "postaichean ùra", "annual_report.summary.percentile.text": "Tha thu am measgdhen luchd-cleachdaidh as cliùitiche air {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ainmeil ’nad latha ’s ’nad linn.", + "annual_report.summary.share_elsewhere": "Co-roinn am badeigin eile", + "annual_report.summary.share_message": "’S e {archetype} am prìomh-choltas a th’ orm!", + "annual_report.summary.share_on_mastodon": "Co-roinn air Mastodon", "attachments_list.unprocessed": "(gun phròiseasadh)", "audio.hide": "Falaich an fhuaim", "block_modal.remote_users_caveat": "Iarraidh sinn air an fhrithealaiche {domain} gun gèill iad ri do cho-dhùnadh. Gidheadh, chan eil barantas gun gèill iad on a làimhsicheas cuid a fhrithealaichean bacaidhean air dòigh eadar-dhealaichte. Dh’fhaoidte gum faic daoine gun chlàradh a-steach na postaichean poblach agad fhathast.", @@ -162,7 +203,7 @@ "column.favourites": "Annsachdan", "column.firehose": "An saoghal beò", "column.firehose_local": "Loidhne-ama bheò an fhrithealaiche seo", - "column.firehose_singular": "Loidhne-ama bheò beò", + "column.firehose_singular": "Loidhne-ama bheò", "column.follow_requests": "Iarrtasan leantainn", "column.home": "Dachaigh", "column.list_members": "Stiùir buill na liosta", @@ -242,7 +283,7 @@ "confirmations.private_quote_notify.title": "A bheil thu airson a cho-roinneadh leis an luchd-leantainn ’s na cleachdaichean le iomradh orra?", "confirmations.quiet_post_quote_info.dismiss": "Na cuiribh seo ’nam chuimhne a-rithist", "confirmations.quiet_post_quote_info.got_it": "Tha mi agaibh", - "confirmations.quiet_post_quote_info.message": "Nuair a luaidheas tu post a tha poblach ach sàmhach, thèid am post agad fhalach o loidhnichean-ama nan treandaichean.", + "confirmations.quiet_post_quote_info.message": "Nuair a luaidheas tu post sàmhach, thèid am post agad fhalach o loidhnichean-ama nan treandaichean.", "confirmations.quiet_post_quote_info.title": "Luaidh air postaichean sàmhach", "confirmations.redraft.confirm": "Sguab às ⁊ dèan dreachd ùr", "confirmations.redraft.message": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às agus dreachd ùr a thòiseachadh? Caillidh tu gach annsachd is brosnachadh air agus thèid freagairtean dhan phost thùsail ’nan dìlleachdanan.", @@ -343,6 +384,7 @@ "empty_column.notification_requests": "Glan! Chan eil dad an-seo. Nuair a gheibh thu brathan ùra, nochdaidh iad an-seo a-rèir nan roghainnean agad.", "empty_column.notifications": "Cha d’ fhuair thu brath sam bith fhathast. Nuair a nì càch conaltradh leat, chì thu an-seo e.", "empty_column.public": "Chan eil dad an-seo! Sgrìobh rudeigin gu poblach no lean càch o fhrithealaichean eile a làimh airson seo a lìonadh", + "error.no_hashtag_feed_access": "Cruthaich cunntas no clàraich a-steach airson an taga hais seo a shealltainn is leantainn.", "error.unexpected_crash.explanation": "Air sàilleibh buga sa chòd againn no duilgheadas co-chòrdalachd leis a’ bhrabhsair, chan urrainn dhuinn an duilleag seo a shealltainn mar bu chòir.", "error.unexpected_crash.explanation_addons": "Cha b’ urrainn dhuinn an duilleag seo a shealltainn mar bu chòir. Tha sinn an dùil gu do dh’adhbharaich tuilleadan a’ bhrabhsair no inneal eadar-theangachaidh fèin-obrachail a’ mhearachd.", "error.unexpected_crash.next_steps": "Feuch an ath-nuadhaich thu an duilleag seo. Mura cuidich sin, dh’fhaoidte gur urrainn dhut Mastodon a chleachdadh fhathast le brabhsair eile no le aplacaid thùsail.", @@ -399,6 +441,8 @@ "follow_suggestions.who_to_follow": "Molaidhean leantainn", "followed_tags": "Tagaichean hais ’gan leantainn", "footer.about": "Mu dhèidhinn", + "footer.about_mastodon": "Mu Mhastodon", + "footer.about_server": "Mu {domain}", "footer.about_this_server": "Mu dhèidhinn", "footer.directory": "Eòlaire nam pròifil", "footer.get_app": "Faigh an aplacaid", @@ -473,6 +517,7 @@ "keyboard_shortcuts.column": "Cuir am fòcas air colbh", "keyboard_shortcuts.compose": "Cuir am fòcas air raon teacsa an sgrìobhaidh", "keyboard_shortcuts.description": "Tuairisgeul", + "keyboard_shortcuts.direct": "Fosgail colbh nan iomraidhean prìobhaideach", "keyboard_shortcuts.down": "Gluais sìos air an liosta", "keyboard_shortcuts.enter": "Fosgail post", "keyboard_shortcuts.favourite": "Cuir am post ris na h-annsachdan", @@ -500,6 +545,7 @@ "keyboard_shortcuts.toggle_hidden": "Seall/Falaich an teacsa fo rabhadh susbainte", "keyboard_shortcuts.toggle_sensitivity": "Seall/Falaich na meadhanan", "keyboard_shortcuts.toot": "Tòisich air post ùr", + "keyboard_shortcuts.top": "Gluais gu bàrr na liosta", "keyboard_shortcuts.translate": "airson post eadar-theangachadh", "keyboard_shortcuts.unfocus": "Thoir am fòcas far raon teacsa an sgrìobhaidh/an luirg", "keyboard_shortcuts.up": "Gluais suas air an liosta", @@ -744,7 +790,7 @@ "privacy.quote.disabled": "{visibility}, luaidh à comas", "privacy.quote.limited": "{visibility}, luaidh cuingichte", "privacy.unlisted.additional": "Tha seo coltach ris an fhaicsinneachd phoblach ach cha nochd am post air loidhnichean-ama an t-saoghail phoblaich, nan tagaichean hais no an rùrachaidh no ann an toraidhean luirg Mhastodon fiù ’s ma thug thu ro-aonta airson sin seachad.", - "privacy.unlisted.long": "Poblach ach falaichte o na toraidhean-luirg, na treandaichean ’s na loichnichean-ama poblach", + "privacy.unlisted.long": "Falaichte o na toraidhean-luirg, na treandaichean ’s na loidhnichean-ama poblach", "privacy.unlisted.short": "Sàmhach", "privacy_policy.last_updated": "An t-ùrachadh mu dheireadh {date}", "privacy_policy.title": "Poileasaidh prìobhaideachd", @@ -888,6 +934,7 @@ "status.edited_x_times": "Chaidh a dheasachadh {count, plural, one {{count} turas} two {{count} thuras} few {{count} tursan} other {{count} turas}}", "status.embed": "Faigh còd leabachaidh", "status.favourite": "Cuir ris na h-annsachdan", + "status.favourites_count": "{count, plural, one {{counter} annsachd} two {{counter} annsachd} few {{counter} annsachdan} other {{counter} annsachd}}", "status.filter": "Criathraich am post seo", "status.history.created": "Chruthaich {name} {date} e", "status.history.edited": "Dheasaich {name} {date} e", @@ -922,12 +969,14 @@ "status.quotes.empty": "Chan deach am post seo a luaidh le duine sam bith fhathast. Nuair a luaidheas cuideigin e, nochdaidh iad an-seo.", "status.quotes.local_other_disclaimer": "Cha tèid luaidhean a dhiùilt an ùghdar a shealltainn.", "status.quotes.remote_other_disclaimer": "Cha dèid ach luaidhean o {domain} a shealltainn an-seo le cinnt. Cha dèid luaidhean a dhiùilt an ùghdar a shealltainn.", + "status.quotes_count": "{count, plural, one {{counter} luaidh} two {{counter} luaidh} few {{counter} luaidhean} other {{counter} luaidh}}", "status.read_more": "Leugh an còrr", "status.reblog": "Brosnaich", "status.reblog_or_quote": "Brosnaich no luaidh", "status.reblog_private": "Co-roinn leis an luchd-leantainn agad a-rithist", "status.reblogged_by": "’Ga bhrosnachadh le {name}", "status.reblogs.empty": "Chan deach am post seo a bhrosnachadh le duine sam bith fhathast. Nuair a bhrosnaicheas cuideigin e, nochdaidh iad an-seo.", + "status.reblogs_count": "{count, plural, one {{counter} bhrosnachadh} two {{counter} bhrosnachadh} few {{counter} brosnachaidhean} other {{counter} brosnachadh}}", "status.redraft": "Sguab às ⁊ dèan dreachd ùr", "status.remove_bookmark": "Thoir an comharra-lìn air falbh", "status.remove_favourite": "Thoir air falbh o na h-annsachdan", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 03a4e4acf94..430687916df 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -18,7 +18,7 @@ "account.badges.bot": "Automatizada", "account.badges.group": "Grupo", "account.block": "Bloquear @{name}", - "account.block_domain": "Agochar todo de {domain}", + "account.block_domain": "Bloquear o dominio {domain}", "account.block_short": "Bloquear", "account.blocked": "Bloqueada", "account.blocking": "Bloqueos", @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Agochar promocións de @{name}", "account.in_memoriam": "Lembranzas.", + "account.joined_long": "Uníuse o {date}", "account.joined_short": "Uniuse", "account.languages": "Modificar os idiomas subscritos", "account.link_verified_on": "A propiedade desta ligazón foi verificada o {date}", @@ -90,6 +91,8 @@ "account.unmute": "Deixar de silenciar a @{name}", "account.unmute_notifications_short": "Reactivar notificacións", "account.unmute_short": "Non silenciar", + "account_fields_modal.close": "Fechar", + "account_fields_modal.title": "Info sobre {name}", "account_note.placeholder": "Preme para engadir nota", "admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse", "admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}", "loading_indicator.label": "Estase a cargar…", "media_gallery.hide": "Agochar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A túa conta {disabledAccount} está actualmente desactivada porque movéchela a {movedToAccount}.", "mute_modal.hide_from_notifications": "Agochar nas notificacións", "mute_modal.hide_options": "Opcións ao ocultar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 3cee7adc5e1..f01e43968ce 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -57,6 +57,7 @@ "account.go_to_profile": "מעבר לפרופיל", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", "account.in_memoriam": "פרופיל זכרון.", + "account.joined_long": "הצטרפו ב־{date}", "account.joined_short": "תאריך הצטרפות", "account.languages": "שנה רישום לשפות", "account.link_verified_on": "בעלות על הקישור הזה נבדקה לאחרונה ב{date}", @@ -90,6 +91,8 @@ "account.unmute": "הפסקת השתקת @{name}", "account.unmute_notifications_short": "הפעלת הודעות", "account.unmute_short": "ביטול השתקה", + "account_fields_modal.close": "סגירה", + "account_fields_modal.title": "הפרטים של {name}", "account_note.placeholder": "יש ללחוץ כדי להוסיף הערות", "admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה", "admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# פריט חדש} other {# פריטים חדשים}}", "loading_indicator.label": "בטעינה…", "media_gallery.hide": "להסתיר", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "חשבונך {disabledAccount} אינו פעיל כרגע עקב מעבר ל{movedToAccount}.", "mute_modal.hide_from_notifications": "להסתיר מהתראות", "mute_modal.hide_options": "הסתרת אפשרויות", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 0f53d1b18eb..26d9658dbdf 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ugrás a profilhoz", "account.hide_reblogs": "@{name} megtolásainak elrejtése", "account.in_memoriam": "Emlékünkben.", + "account.joined_long": "Csatlakozás ideje: {date}", "account.joined_short": "Csatlakozott", "account.languages": "Feliratkozott nyelvek módosítása", "account.link_verified_on": "A linket eredetiségét ebben az időpontban ellenőriztük: {date}", @@ -90,6 +91,8 @@ "account.unmute": "@{name} némításának feloldása", "account.unmute_notifications_short": "Értesítések némításának feloldása", "account.unmute_short": "Némitás feloldása", + "account_fields_modal.close": "Bezárás", + "account_fields_modal.title": "{name} információi", "account_note.placeholder": "Kattintás jegyzet hozzáadásához", "admin.dashboard.daily_retention": "Napi regisztráció utáni felhasználómegtartási arány", "admin.dashboard.monthly_retention": "Havi regisztráció utáni felhasználómegtartási arány", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# új elem} other {# új elem}}", "loading_indicator.label": "Betöltés…", "media_gallery.hide": "Elrejtés", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva, mert átköltöztél ide: {movedToAccount}.", "mute_modal.hide_from_notifications": "Elrejtés az értesítések közül", "mute_modal.hide_options": "Beállítások elrejtése", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 0f255dfa977..efdce412ca2 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Fara í notandasnið", "account.hide_reblogs": "Fela endurbirtingar fyrir @{name}", "account.in_memoriam": "Minning.", + "account.joined_long": "Skáði sig {date}", "account.joined_short": "Gerðist þátttakandi", "account.languages": "Breyta tungumálum í áskrift", "account.link_verified_on": "Eignarhald á þessum tengli var athugað þann {date}", @@ -90,6 +91,8 @@ "account.unmute": "Hætta að þagga niður í @{name}", "account.unmute_notifications_short": "Hætta að þagga í tilkynningum", "account.unmute_short": "Hætta að þagga niður", + "account_fields_modal.close": "Loka", + "account_fields_modal.title": "Upplýsingar um {name}", "account_note.placeholder": "Smelltu til að bæta við minnispunkti", "admin.dashboard.daily_retention": "Hlutfall virkra notenda eftir nýskráningu eftir dögum", "admin.dashboard.monthly_retention": "Hlutfall virkra notenda eftir nýskráningu eftir mánuðum", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nýtt atriði} other {# ný atriði}}", "loading_indicator.label": "Hleð inn…", "media_gallery.hide": "Fela", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Aðgangurinn þinn {disabledAccount} er óvirkur í augnablikinu vegna þess að þú fluttir þig yfir á {movedToAccount}.", "mute_modal.hide_from_notifications": "Fela úr tilkynningum", "mute_modal.hide_options": "Fela valkosti", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 7a0d542546f..364b6227c0d 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Vai al profilo", "account.hide_reblogs": "Nascondi condivisioni da @{name}", "account.in_memoriam": "In memoria.", + "account.joined_long": "Su questa istanza dal {date}", "account.joined_short": "Iscritto", "account.languages": "Modifica le lingue d'iscrizione", "account.link_verified_on": "La proprietà di questo link è stata controllata il {date}", @@ -90,6 +91,8 @@ "account.unmute": "Riattiva @{name}", "account.unmute_notifications_short": "Riattiva notifiche", "account.unmute_short": "Attiva audio", + "account_fields_modal.close": "Chiudi", + "account_fields_modal.title": "Informazioni su {name}", "account_note.placeholder": "Clicca per aggiungere una nota", "admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione", "admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione", @@ -536,7 +539,7 @@ "keyboard_shortcuts.open_media": "Apre i multimedia", "keyboard_shortcuts.pinned": "Apre l'elenco dei post fissati", "keyboard_shortcuts.profile": "Apre il profilo dell'autore", - "keyboard_shortcuts.quote": "Cita il post", + "keyboard_shortcuts.quote": "Cita post", "keyboard_shortcuts.reply": "Risponde al post", "keyboard_shortcuts.requests": "Apre l'elenco delle richieste di seguirti", "keyboard_shortcuts.search": "Focalizza sulla barra di ricerca", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}", "loading_indicator.label": "Caricamento…", "media_gallery.hide": "Nascondi", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Il tuo profilo {disabledAccount} è correntemente disabilitato perché ti sei spostato a {movedToAccount}.", "mute_modal.hide_from_notifications": "Nascondi dalle notifiche", "mute_modal.hide_options": "Nascondi le opzioni", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index cfbb355bb30..3b6957b0c26 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -69,7 +69,7 @@ "account.mute_short": "Nutildyti", "account.muted": "Nutildytas", "account.muting": "Užtildymas", - "account.mutual": "Jūs sekate vienas kitą", + "account.mutual": "Sekate vienas kitą", "account.no_bio": "Nėra pateikto aprašymo.", "account.open_original_page": "Atidaryti originalų puslapį", "account.posts": "Įrašai", @@ -685,7 +685,7 @@ "notifications.filter.follows": "Sekimai", "notifications.filter.mentions": "Paminėjimai", "notifications.filter.polls": "Balsavimo rezultatai", - "notifications.filter.statuses": "Naujinimai iš žmonių, kuriuos seki", + "notifications.filter.statuses": "Naujienos iš žmonių, kuriuos sekate", "notifications.grant_permission": "Suteikti leidimą.", "notifications.group": "{count} pranešimai", "notifications.mark_as_read": "Pažymėti kiekvieną pranešimą kaip perskaitytą", @@ -821,7 +821,7 @@ "report.thanks.title": "Nenori to matyti?", "report.thanks.title_actionable": "Ačiū, kad pranešei, mes tai išnagrinėsime.", "report.unfollow": "Nebesekti @{name}", - "report.unfollow_explanation": "Tu seki šią paskyrą. Jei nori nebematyti jų įrašų savo pagrindiniame sraute, nebesek jų.", + "report.unfollow_explanation": "Jūs sekate šią paskyrą. Kad nebematytumėte jų įrašų savo pagrindiniame sraute, nebesekite.", "report_notification.attached_statuses": "Pridėti {count, plural, one {{count} įrašas} few {{count} įrašai} many {{count} įrašo} other {{count} įrašų}}", "report_notification.categories.legal": "Teisinės", "report_notification.categories.legal_sentence": "nelegalus turinys", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index bbe3af8bc76..92b12958fe3 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -1,6 +1,7 @@ { "about.blocks": "Pelayan yang diselaraskan", "about.contact": "Hubungi:", + "about.default_locale": "Lalai", "about.disclaimer": "Mastodon ialah perisian sumber terbuka percuma, dan merupakan tanda dagangan Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Sebab tidak tersedia", "about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan pengguna daripada mana-mana pelayan dalam alam bersekutu dan berinteraksi dengan mereka. Berikut ialah pengecualian yang khusus pada pelayan ini.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Terhad", "about.domain_blocks.suspended.explanation": "Tiada data daripada pelayan ini yang akan diproses, disimpan atau ditukar, menjadikan sebarang interaksi atau perhubungan dengan pengguna daripada pelayan ini adalah mustahil.", "about.domain_blocks.suspended.title": "Digantung", + "about.language_label": "Bahasa", "about.not_available": "Maklumat ini belum tersedia pada pelayan ini.", "about.powered_by": "Media sosial terpencar yang dikuasakan oleh {mastodon}", "about.rules": "Peraturan pelayan", @@ -26,6 +28,7 @@ "account.disable_notifications": "Berhenti maklumkan saya apabila @{name} mengirim hantaran", "account.domain_blocking": "Blocking domain", "account.edit_profile": "Sunting profil", + "account.edit_profile_short": "Sunting", "account.enable_notifications": "Maklumi saya apabila @{name} mengirim hantaran", "account.endorse": "Tampilkan di profil", "account.familiar_followers_one": "melayuikutikut{name1}", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index e22d0f0b524..7bf3e42234e 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Gøym framhevingar frå @{name}", "account.in_memoriam": "Til minne om.", + "account.joined_long": "Vart med {date}", "account.joined_short": "Vart med", "account.languages": "Endre språktingingar", "account.link_verified_on": "Eigarskap for denne lenkja vart sist sjekka {date}", @@ -90,6 +91,8 @@ "account.unmute": "Opphev demping av @{name}", "account.unmute_notifications_short": "Opphev demping av varslingar", "account.unmute_short": "Opphev demping", + "account_fields_modal.close": "Lukk", + "account_fields_modal.title": "{name} sine opplysingar", "account_note.placeholder": "Klikk for å leggja til merknad", "admin.dashboard.daily_retention": "Mengda brukarar aktive ved dagar etter registrering", "admin.dashboard.monthly_retention": "Mengda brukarar aktive ved månader etter registrering", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nytt element} other {# nye element}}", "loading_indicator.label": "Lastar…", "media_gallery.hide": "Gøym", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.", "mute_modal.hide_from_notifications": "Ikkje vis varslingar", "mute_modal.hide_options": "Gøym val", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 2520939eb77..e0ced02cbd3 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -49,6 +49,7 @@ "account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ", "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ", "account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ", + "account.joined_long": "{date} ਨੂੰ ਜੁਆਇਨ ਕੀਤਾ", "account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ", "account.media": "ਮੀਡੀਆ", "account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ", @@ -76,6 +77,8 @@ "account.unmute": "@{name} ਲਈ ਮੌਨ ਹਟਾਓ", "account.unmute_notifications_short": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਅਣ-ਮੌਨ ਕਰੋ", "account.unmute_short": "ਮੌਨ-ਰਹਿਤ ਕਰੋ", + "account_fields_modal.close": "ਬੰਦ ਕਰੋ", + "account_fields_modal.title": "{name} ਦੀ ਜਾਣਕਾਰੀ", "account_note.placeholder": "Click to add a note", "admin.dashboard.retention.average": "ਔਸਤ", "admin.dashboard.retention.cohort_size": "ਨਵੇਂ ਵਰਤੋਂਕਾਰ", @@ -85,6 +88,13 @@ "alt_text_modal.cancel": "ਰੱਦ ਕਰੋ", "alt_text_modal.done": "ਮੁਕੰਮਲ", "announcement.announcement": "ਹੋਕਾ", + "annual_report.announcement.action_dismiss": "ਨਹੀਂ, ਧੰਨਵਾਦ", + "annual_report.nav_item.badge": "ਨਵਾਂ", + "annual_report.shared_page.donate": "ਦਾਨ ਕਰੋ", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.close": "ਬੰਦ ਕਰੋ", + "annual_report.summary.copy_link": "ਲਿੰਕ ਨੂੰ ਕਾਪੀ ਕਰੋ", + "annual_report.summary.highlighted_post.title": "ਸਭ ਤੋਂ ਵੱਧ ਹਰਮਨਪਿਆਰੀ ਪੋਸਟ", "annual_report.summary.most_used_app.most_used_app": "ਸਭ ਤੋਂ ਵੱਧ ਵਰਤੀ ਐਪ", "annual_report.summary.new_posts.new_posts": "ਨਵੀਆਂ ਪੋਸਟਾਂ", "audio.hide": "ਆਡੀਓ ਨੂੰ ਲੁਕਾਓ", @@ -273,6 +283,8 @@ "follow_suggestions.who_to_follow": "ਕਿਸ ਨੂੰ ਫ਼ਾਲੋ ਕਰੀਏ", "followed_tags": "ਫ਼ਾਲੋ ਕੀਤੇ ਹੈਸ਼ਟੈਗ", "footer.about": "ਸਾਡੇ ਬਾਰੇ", + "footer.about_mastodon": "Mastodon ਬਾਰੇ", + "footer.about_server": "{domain} ਬਾਰੇ", "footer.about_this_server": "ਇਸ ਬਾਰੇ", "footer.directory": "ਪਰੋਫਾਇਲ ਡਾਇਰੈਕਟਰੀ", "footer.get_app": "ਐਪ ਲਵੋ", @@ -293,6 +305,7 @@ "hashtag.column_settings.tag_mode.any": "ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ", "hashtag.column_settings.tag_mode.none": "ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਨਹੀਂ", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.feature": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ", "hashtag.follow": "ਹੈਸ਼ਟੈਗ ਨੂੰ ਫ਼ਾਲੋ ਕਰੋ", "hashtag.mute": "#{hashtag} ਨੂੰ ਮੌਨ ਕਰੋ", "hashtag.unfollow": "ਹੈਸ਼ਟੈਗ ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰੋ", @@ -438,6 +451,7 @@ "notification_requests.exit_selection": "ਮੁਕੰਮਲ", "notification_requests.notifications_from": "{name} ਵਲੋਂ ਨੋਟੀਫਿਕੇਸ਼ਨ", "notification_requests.title": "ਫਿਲਟਰ ਕੀਤੇ ਨੋਟੀਫਿਕੇਸ਼ਨ", + "notification_requests.view": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਵੇਖੋ", "notifications.clear": "ਸੂਚਨਾਵਾਂ ਨੂੰ ਮਿਟਾਓ", "notifications.clear_confirmation": "ਕੀ ਤੁਸੀਂ ਆਪਣੇ ਸਾਰੇ ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?", "notifications.clear_title": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਮਿਟਾਉਣਾ ਹੈ?", @@ -481,6 +495,7 @@ "onboarding.follows.back": "ਪਿੱਛੇ", "onboarding.follows.done": "ਮੁਕੰਮਲ", "onboarding.follows.search": "ਖੋਜੋ", + "onboarding.profile.display_name": "ਦਿਖਾਇਆ ਜਾਣ ਵਾਲਾ ਨਾਂ", "onboarding.profile.note": "ਜਾਣਕਾਰੀ", "onboarding.profile.save_and_continue": "ਸੰਭਾਲੋ ਅਤੇ ਜਾਰੀ ਰੱਖੋ", "onboarding.profile.title": "ਪਰੋਫਾਈਲ ਸੈਟਅੱਪ", @@ -584,6 +599,7 @@ "status.edited": "ਆਖਰੀ ਸੋਧ ਦੀ ਤਾਰੀਖ {date}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.favourite": "ਪਸੰਦ", + "status.filter": "ਇਸ ਪੋਸਟ ਨੂੰ ਫਿਲਟਰ ਕਰੋ", "status.history.created": "{name} ਨੇ {date} ਨੂੰ ਬਣਾਇਆ", "status.history.edited": "{name} ਨੇ {date} ਨੂੰ ਸੋਧਿਆ", "status.load_more": "ਹੋਰ ਦਿਖਾਓ", @@ -598,6 +614,7 @@ "status.pin": "ਪਰੋਫਾਈਲ ਉੱਤੇ ਟੰਗੋ", "status.quote": "ਹਵਾਲਾ", "status.quote.cancel": "ਹਵਾਲੇ ਨੂੰ ਰੱਦ ਕਰੋ", + "status.quote_noun": "ਹਵਾਲਾ", "status.quotes_count": "{count, plural, one {{counter} ਹਵਾਲਾ} other {{counter} ਹਵਾਲੇ}}", "status.read_more": "ਹੋਰ ਪੜ੍ਹੋ", "status.reblog": "ਬੂਸਟ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index dfeae82d4b6..f7aa78d1289 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -113,11 +113,37 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Opisz to dla osób niedowidzących…", "alt_text_modal.done": "Gotowe", "announcement.announcement": "Ogłoszenie", + "annual_report.announcement.action_build": "Zbuduj mój Wrapstodon", + "annual_report.announcement.action_dismiss": "Nie, dziękuję", + "annual_report.announcement.action_view": "Zobacz mój Wrapstodon", + "annual_report.announcement.description": "Odkryj więcej na temat swojego zaangażowania w Mastodon w ciągu ostatniego roku.", + "annual_report.announcement.title": "Wrapstodon roku {year} przybył", + "annual_report.nav_item.badge": "Nowe", + "annual_report.shared_page.donate": "Wesprzyj nas", + "annual_report.shared_page.footer": "Wygenerowane przez {heart} przez zespół Mastodon", + "annual_report.shared_page.footer_server_info": "{username} używa {domain}, jednej z wielu społeczności wspieranych przez Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} został na polowaniu na posty do wzmocnienia, wzmacniając innych twórców doskonałym celem.", + "annual_report.summary.archetype.booster.desc_self": "Pozostałeś na polowaniu na posty do wzmocnienia i wzmocnienia innych twórców doskonałym celem.", + "annual_report.summary.archetype.booster.name": "Łucznik", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.name": "Stoik", + "annual_report.summary.archetype.oracle.name": "Wyrocznia", + "annual_report.summary.archetype.pollster.name": "Zastanawiacz", + "annual_report.summary.archetype.replier.name": "Motylek", + "annual_report.summary.archetype.reveal": "Pokaż mój archetyp", + "annual_report.summary.archetype.title_public": "Archetyp {name}", + "annual_report.summary.archetype.title_self": "Twój archetyp", + "annual_report.summary.close": "Zamknij", + "annual_report.summary.copy_link": "Skopiuj adres", + "annual_report.summary.followers.new_followers": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}", "annual_report.summary.most_used_app.most_used_app": "najczęściej używana aplikacja", "annual_report.summary.most_used_hashtag.most_used_hashtag": "najczęściej używany hashtag", "annual_report.summary.new_posts.new_posts": "nowe wpisy", "annual_report.summary.percentile.text": "To plasuje cię w czołówce użytkowników {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nie powiemy Berniemu.", + "annual_report.summary.share_elsewhere": "Udostępnij gdziekolwiek", + "annual_report.summary.share_message": "Mam archetyp {archetype}!", + "annual_report.summary.share_on_mastodon": "Udostępnij na Mastodonie", "attachments_list.unprocessed": "(nieprzetworzone)", "audio.hide": "Ukryj dźwięk", "block_modal.remote_users_caveat": "Poprosimy serwer {domain} o uszanowanie twojej decyzji. Nie jest to jednak gwarantowane, bo niektóre serwery mogą obsługiwać blokady w inny sposób. Publiczne wpisy mogą być nadal widoczne dla niezalogowanych użytkowników.", @@ -159,6 +185,7 @@ "column.edit_list": "Edytuj listę", "column.favourites": "Ulubione", "column.firehose": "Aktualności", + "column.firehose_local": "Kanał na żywo dla tego serwera", "column.firehose_singular": "Na żywo", "column.follow_requests": "Prośby o obserwację", "column.home": "Strona główna", @@ -179,6 +206,7 @@ "community.column_settings.local_only": "Tylko lokalne", "community.column_settings.media_only": "Tylko multimedia", "community.column_settings.remote_only": "Tylko zdalne", + "compose.error.blank_post": "Post nie może być pusty.", "compose.language.change": "Zmień język", "compose.language.search": "Wyszukaj języki...", "compose.published.body": "Wpis został opublikowany.", @@ -231,6 +259,11 @@ "confirmations.missing_alt_text.secondary": "Opublikuj mimo wszystko", "confirmations.missing_alt_text.title": "Dodać tekst pomocniczy?", "confirmations.mute.confirm": "Wycisz", + "confirmations.private_quote_notify.cancel": "Wróć do edycji", + "confirmations.private_quote_notify.confirm": "Opublikuj wpis", + "confirmations.private_quote_notify.do_not_show_again": "Nie pokazuj tej wiadomości ponownie", + "confirmations.private_quote_notify.message": "Osoba, którą cytujesz, a inne wzmianki zostaną powiadomione i będą mogły zobaczyć Twój post, nawet jeśli nie obserwują Ciebie.", + "confirmations.private_quote_notify.title": "Udostępnić obserwującym i wspomnianym użytkownikom?", "confirmations.quiet_post_quote_info.dismiss": "Nie przypominaj mi ponownie", "confirmations.quiet_post_quote_info.got_it": "Rozumiem", "confirmations.quiet_post_quote_info.message": "Kiedy cytujesz niewidoczny wpis publiczny, twój wpis zostanie ukryty z popularnych osi czasu.", @@ -388,6 +421,9 @@ "follow_suggestions.who_to_follow": "Kogo warto obserwować", "followed_tags": "Obserwowane hasztagi", "footer.about": "O serwerze", + "footer.about_mastodon": "O Mastodonie", + "footer.about_server": "O {domain}", + "footer.about_this_server": "O nas", "footer.directory": "Katalog profili", "footer.get_app": "Pobierz aplikację", "footer.keyboard_shortcuts": "Skróty klawiszowe", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 66710d1172b..2738e364100 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir para o perfil", "account.hide_reblogs": "Esconder partilhas de @{name}", "account.in_memoriam": "Em Memória.", + "account.joined_long": "Juntou-se em {date}", "account.joined_short": "Juntou-se a", "account.languages": "Alterar idiomas subscritos", "account.link_verified_on": "O proprietário desta hiperligação foi verificado em {date}", @@ -90,6 +91,8 @@ "account.unmute": "Desocultar @{name}", "account.unmute_notifications_short": "Desocultar notificações", "account.unmute_short": "Desocultar", + "account_fields_modal.close": "Fechar", + "account_fields_modal.title": "Info de {name}", "account_note.placeholder": "Clicar para adicionar nota", "admin.dashboard.daily_retention": "Taxa de retenção de utilizadores por dia após a inscrição", "admin.dashboard.monthly_retention": "Taxa de retenção de utilizadores por mês após a inscrição", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# novo item} other {# novos itens}}", "loading_indicator.label": "A carregar…", "media_gallery.hide": "Esconder", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A tua conta {disabledAccount} está neste momento desativada porque migraste para {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar das notificações", "mute_modal.hide_options": "Ocultar opções", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 4e401c5c882..c7f2b15d12d 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -38,8 +38,10 @@ "account.follow": "Sledi", "account.follow_back": "Sledi nazaj", "account.follow_back_short": "Sledi nazaj", + "account.follow_request": "Zaprosi za sledenje", "account.follow_request_cancel": "Prekliči zahtevo", "account.follow_request_cancel_short": "Prekliči", + "account.follow_request_short": "Zaprosi", "account.followers": "Sledilci", "account.followers.empty": "Nihče še ne sledi temu uporabniku.", "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}", @@ -50,6 +52,7 @@ "account.go_to_profile": "Pojdi na profil", "account.hide_reblogs": "Skrij izpostavitve od @{name}", "account.in_memoriam": "V spomin.", + "account.joined_long": "Pridružen/a {date}", "account.joined_short": "Pridružil/a", "account.languages": "Spremeni naročene jezike", "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}", @@ -61,13 +64,16 @@ "account.mute_notifications_short": "Utišaj obvestila", "account.mute_short": "Utišaj", "account.muted": "Utišan", + "account.muting": "Izklop zvoka", "account.mutual": "Drug drugemu sledita", "account.no_bio": "Ni opisa.", "account.open_original_page": "Odpri izvirno stran", "account.posts": "Objave", "account.posts_with_replies": "Objave in odgovori", + "account.remove_from_followers": "Odstrani {name} iz sledilcev", "account.report": "Prijavi @{name}", "account.requested_follow": "{name} vam želi slediti", + "account.requests_to_follow_you": "Vas prosi za sledenje", "account.share": "Deli profil osebe @{name}", "account.show_reblogs": "Pokaži izpostavitve osebe @{name}", "account.statuses_counter": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}", @@ -80,6 +86,7 @@ "account.unmute": "Povrni glas @{name}", "account.unmute_notifications_short": "Izklopi utišanje obvestil", "account.unmute_short": "Povrni glas", + "account_fields_modal.close": "Zapri", "account_note.placeholder": "Kliknite, da dodate opombo", "admin.dashboard.daily_retention": "Mera ohranjanja uporabnikov po dnevih od registracije", "admin.dashboard.monthly_retention": "Mera ohranjanja uporabnikov po mesecih od registracije", @@ -103,11 +110,24 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...", "alt_text_modal.done": "Opravljeno", "announcement.announcement": "Oznanilo", + "annual_report.announcement.action_dismiss": "Ne, hvala", + "annual_report.nav_item.badge": "Nov", + "annual_report.shared_page.donate": "Prispevaj", + "annual_report.summary.archetype.replier.name": "Metulj", + "annual_report.summary.archetype.reveal": "Razkrij moj arhetip", + "annual_report.summary.archetype.title_public": "Arhetip {name}", + "annual_report.summary.archetype.title_self": "Vaš arhetip", + "annual_report.summary.close": "Zapri", + "annual_report.summary.copy_link": "Kopiraj povezavo", + "annual_report.summary.followers.new_followers": "{count, plural, one {{counter} nov sledilec} two {{counter} nova sledilca} few {{counter} novi sledilci} other {{counter} novih sledilcev}}", + "annual_report.summary.highlighted_post.title": "Najbolj priljubljena objava", "annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija", "annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik", "annual_report.summary.new_posts.new_posts": "nove objave", "annual_report.summary.percentile.text": "S tem ste se uvrstili med zgornjih uporabnikov domene {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.", + "annual_report.summary.share_elsewhere": "Deli drugje", + "annual_report.summary.share_on_mastodon": "Deli na Mastodonu", "attachments_list.unprocessed": "(neobdelano)", "audio.hide": "Skrij zvok", "block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.", @@ -368,6 +388,8 @@ "follow_suggestions.who_to_follow": "Komu slediti", "followed_tags": "Sledeni ključniki", "footer.about": "O Mastodonu", + "footer.about_mastodon": "O Mastodonu", + "footer.about_server": "O {domain}", "footer.directory": "Imenik profilov", "footer.get_app": "Prenesite aplikacijo", "footer.keyboard_shortcuts": "Tipkovne bližnjice", @@ -812,6 +834,10 @@ "status.cancel_reblog_private": "Prekliči izpostavitev", "status.cannot_reblog": "Te objave ni mogoče izpostaviti", "status.contains_quote": "Vsebuje citat", + "status.context.loading": "Nalaganje več odgovorov", + "status.context.loading_error": "Novih odgovorov ni bilo možno naložiti", + "status.context.loading_success": "Novi odgovori naloženi", + "status.context.more_replies_found": "Najdenih več odgovorov", "status.context.retry": "Poskusi znova", "status.context.show": "Pokaži", "status.continued_thread": "Nadaljevanje niti", @@ -847,6 +873,9 @@ "status.quote_followers_only": "Samo sledilci lahko citirajo to objavo", "status.quote_policy_change": "Spremenite, kdo lahko citira", "status.quote_private": "Zasebnih objav ni možno citirati", + "status.quotes.empty": "Nihče še ni citiral te objave. Ko se bo to zgodilo, se bodo pojavile tukaj.", + "status.quotes.local_other_disclaimer": "Citati, ki jih je avtor zavrnil, ne bodo prikazani.", + "status.quotes_count": "{count, plural, one {{counter} citat} two {{counter} citata} few {{counter} citati} other {{counter} citatov}}", "status.read_more": "Preberi več", "status.reblog": "Izpostavi", "status.reblogged_by": "{name} je izpostavil/a", @@ -920,7 +949,9 @@ "video.unmute": "Odtišaj", "video.volume_down": "Zmanjšaj glasnost", "video.volume_up": "Povečaj glasnost", + "visibility_modal.button_title": "Določi vidnost", "visibility_modal.header": "Vidnost in interakcija", + "visibility_modal.helper.privacy_editing": "Vidnosti ni moč spremeniti, ko je objava objavljena.", "visibility_modal.privacy_label": "Vidnost", "visibility_modal.quote_followers": "Samo sledilci", "visibility_modal.quote_label": "Kdo lahko citira", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 300966b2093..b1bf08af1bc 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Kalo te profili", "account.hide_reblogs": "Fshih përforcime nga @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "U bë pjesë më {date}", "account.joined_short": "U bë pjesë", "account.languages": "Ndryshoni gjuhë pajtimesh", "account.link_verified_on": "Pronësia e kësaj lidhjeje qe kontrolluar më {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ktheji zërin @{name}", "account.unmute_notifications_short": "Shfaqi njoftimet", "account.unmute_short": "Çheshtoje", + "account_fields_modal.close": "Mbylle", + "account_fields_modal.title": "Hollësi për {name}", "account_note.placeholder": "Klikoni për të shtuar shënim", "admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit", "admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit", @@ -587,6 +590,7 @@ "load_pending": "{count, plural,one {# objekt i ri }other {# objekte të rinj }}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.hide": "Fshihe", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Llogaria juaj {disabledAccount} aktualisht është e çaktivizuar, ngaqë kaluat te {movedToAccount}.", "mute_modal.hide_from_notifications": "Fshihe prej njoftimeve", "mute_modal.hide_options": "Fshihi mundësitë", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 461d7284084..67b8aea8c1b 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -122,6 +122,8 @@ "annual_report.shared_page.donate": "Donera", "annual_report.shared_page.footer": "Genererad med {heart} av Mastodon-teamet", "annual_report.shared_page.footer_server_info": "{username} använder {domain}, en av många forum som drivs av Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} jagade efter inlägg att boosta och förstärka andra skapare med perfekt sikte.", + "annual_report.summary.archetype.booster.desc_self": "Du jagade efter inlägg att boosta och förstärka andra skapare med perfekt sikte.", "annual_report.summary.archetype.booster.name": "Bågskytten", "annual_report.summary.archetype.die_drei_fragezeichen": "???", "annual_report.summary.archetype.lurker.desc_public": "Vi vet att {name} var där ute någonstans och njuter av Mastodon på sitt egna tysta sätt.", @@ -131,13 +133,31 @@ "annual_report.summary.archetype.oracle.desc_self": "Du skapade nya inlägg mer än svar och höll Mastodon fräscht och framtidsinriktat.", "annual_report.summary.archetype.oracle.name": "Oraklet", "annual_report.summary.archetype.pollster.desc_public": "{name} skapade fler undersökningar än andra inläggstyper och skapade nyfikenhet på Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Du skapade fler undersökningar än andra inläggstyper och skapade nyfikenhet på Mastodon.", + "annual_report.summary.archetype.pollster.name": "Undraren", + "annual_report.summary.archetype.replier.desc_public": "{name} svarade ofta på andras inlägg och pollinerade Mastodon med nya diskussioner.", + "annual_report.summary.archetype.replier.desc_self": "Du svarade ofta på andras inlägg och pollinerade Mastodon med nya diskussioner.", + "annual_report.summary.archetype.replier.name": "Fjärilen", + "annual_report.summary.archetype.reveal": "Avslöja min arketyp", + "annual_report.summary.archetype.reveal_description": "Tack för att du är en del av Mastodon! Dags att ta reda på vilken arketyp du förkroppsligade under {year}.", + "annual_report.summary.archetype.title_public": "{name}s arketyp", + "annual_report.summary.archetype.title_self": "Din arketyp", + "annual_report.summary.close": "Stäng", "annual_report.summary.copy_link": "Kopiera länk", + "annual_report.summary.followers.new_followers": "{count, plural, one {ny följare} other {nya följare}}", + "annual_report.summary.highlighted_post.boost_count": "Det här inlägget förstärktes {count, plural, one {en gång} other {# gånger}}.", + "annual_report.summary.highlighted_post.favourite_count": "Det här inlägget favoriserades {count, plural, one {en gång} other {# gånger}}.", + "annual_report.summary.highlighted_post.reply_count": "Det här inlägget fick {count, plural, one {ett svar} other {# svar}}.", + "annual_report.summary.highlighted_post.title": "Mest populära inlägg", "annual_report.summary.most_used_app.most_used_app": "mest använda app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest använda hashtag", + "annual_report.summary.most_used_hashtag.used_count": "Du inkluderade denna hashtag i {count, plural, one {ett inlägg} other {# inlägg}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} inkluderade denna hashtag i {count, plural, one {ett inlägg} other {# inlägg}}.", "annual_report.summary.new_posts.new_posts": "nya inlägg", "annual_report.summary.percentile.text": "Det placerar dig i toppbland {domain} användare.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vi berättar inte för Bernie.", "annual_report.summary.share_elsewhere": "Dela någon annanstans", + "annual_report.summary.share_message": "Jag fick {archetype}-arketypen!", "annual_report.summary.share_on_mastodon": "Dela på Mastodon", "attachments_list.unprocessed": "(obehandlad)", "audio.hide": "Dölj audio", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 4356fce6c5d..1d8a0a247f4 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Profile git", "account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle", "account.in_memoriam": "Hatırasına.", + "account.joined_long": "{date} tarihinde katıldı", "account.joined_short": "Katıldı", "account.languages": "Abone olunan dilleri değiştir", "account.link_verified_on": "Bu bağlantının sahipliği {date} tarihinde denetlendi", @@ -90,6 +91,8 @@ "account.unmute": "@{name} adlı kişinin sesini aç", "account.unmute_notifications_short": "Bildirimlerin sesini aç", "account.unmute_short": "Susturmayı kaldır", + "account_fields_modal.close": "Kapat", + "account_fields_modal.title": "{name} bilgileri", "account_note.placeholder": "Not eklemek için tıklayın", "admin.dashboard.daily_retention": "Kayıttan sonra günlük kullanıcı saklama oranı", "admin.dashboard.monthly_retention": "Kayıttan sonra aylık kullanıcı saklama oranı", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}", "loading_indicator.label": "Yükleniyor…", "media_gallery.hide": "Gizle", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "{disabledAccount} hesabınız, {movedToAccount} hesabına taşıdığınız için şu an devre dışı.", "mute_modal.hide_from_notifications": "Bildirimlerde gizle", "mute_modal.hide_options": "Seçenekleri gizle", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 4a9158e5b94..59bd543b9d2 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Xem hồ sơ", "account.hide_reblogs": "Ẩn tút @{name} đăng lại", "account.in_memoriam": "Tưởng Niệm.", + "account.joined_long": "Tham gia {date}", "account.joined_short": "Tham gia", "account.languages": "Đổi ngôn ngữ mong muốn", "account.link_verified_on": "Liên kết này đã được xác minh vào {date}", @@ -90,6 +91,8 @@ "account.unmute": "Bỏ ẩn @{name}", "account.unmute_notifications_short": "Mở lại thông báo", "account.unmute_short": "Bỏ ẩn", + "account_fields_modal.close": "Đóng", + "account_fields_modal.title": "Thông tin {name}", "account_note.placeholder": "Nhấn để thêm", "admin.dashboard.daily_retention": "Tỉ lệ người dùng sau đăng ký ở lại theo ngày", "admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# tút mới} other {# tút mới}}", "loading_indicator.label": "Đang tải…", "media_gallery.hide": "Ẩn", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.", "mute_modal.hide_from_notifications": "Ẩn thông báo", "mute_modal.hide_options": "Ẩn tùy chọn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index d69f490b5b4..fecfde2c375 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -57,6 +57,7 @@ "account.go_to_profile": "前往个人资料页", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.in_memoriam": "谨此悼念。", + "account.joined_long": "加入于 {date}", "account.joined_short": "加入于", "account.languages": "更改订阅语言", "account.link_verified_on": "此链接的所有权已在 {date} 检查", @@ -90,6 +91,8 @@ "account.unmute": "不再隐藏 @{name}", "account.unmute_notifications_short": "恢复通知", "account.unmute_short": "取消隐藏", + "account_fields_modal.close": "关闭", + "account_fields_modal.title": "{name} 的信息", "account_note.placeholder": "点击添加备注", "admin.dashboard.daily_retention": "注册后用户留存率(按日计算)", "admin.dashboard.monthly_retention": "注册后用户留存率(按月计算)", @@ -589,6 +592,7 @@ "load_pending": "{count} 项", "loading_indicator.label": "加载中…", "media_gallery.hide": "隐藏", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "你的账号 {disabledAccount} 已禁用,因为你已迁移到 {movedToAccount}。", "mute_modal.hide_from_notifications": "从通知中隐藏", "mute_modal.hide_options": "隐藏选项", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 78d191efa38..d9799ee4393 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -57,6 +57,7 @@ "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏來自 @{name} 之轉嘟", "account.in_memoriam": "謹此悼念。", + "account.joined_long": "加入於 {date}", "account.joined_short": "加入時間", "account.languages": "變更訂閱的語言", "account.link_verified_on": "已於 {date} 檢查此連結的擁有者權限", @@ -90,6 +91,8 @@ "account.unmute": "解除靜音 @{name}", "account.unmute_notifications_short": "解除靜音推播通知", "account.unmute_short": "解除靜音", + "account_fields_modal.close": "關閉", + "account_fields_modal.title": "{name} 之資訊", "account_note.placeholder": "點擊以新增備註", "admin.dashboard.daily_retention": "註冊後使用者存留率(日)", "admin.dashboard.monthly_retention": "註冊後使用者存留率(月)", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, other {# 個新項目}}", "loading_indicator.label": "正在載入...", "media_gallery.hide": "隱藏", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。", "mute_modal.hide_from_notifications": "於推播通知中隱藏", "mute_modal.hide_options": "隱藏選項", diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts index 7f9144280cf..b043edb9ca6 100644 --- a/app/javascript/mastodon/models/status.ts +++ b/app/javascript/mastodon/models/status.ts @@ -7,8 +7,6 @@ export type { StatusVisibility } from 'mastodon/api_types/statuses'; // Temporary until we type it correctly export type Status = Immutable.Map; -type CardShape = Required; - -export type Card = RecordOf; +export type Card = RecordOf; export type MediaAttachment = Immutable.Map; diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 466bfe54d65..0393c06763a 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -204,9 +204,9 @@ export default function userLists(state = initialState, action) { else if (fetchFeaturedTags.rejected.match(action)) return state.setIn(['featured_tags', action.meta.arg.accountId, 'isLoading'], false); else if (fetchDirectory.fulfilled.match(action)) - return normalizeList(state, ['directory'], action.payload.accounts, undefined); + return normalizeList(state, ['directory'], action.payload.accounts, action.payload.isLast ? null : true); else if (expandDirectory.fulfilled.match(action)) - return appendToList(state, ['directory'], action.payload.accounts, undefined); + return appendToList(state, ['directory'], action.payload.accounts, action.payload.isLast ? null : true); else if (fetchDirectory.pending.match(action) || expandDirectory.pending.match(action)) return state.setIn(['directory', 'isLoading'], true); diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 95075454f23..84767322b04 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,8 +12,21 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'fasp' | 'http_message_signatures'; +export type ServerFeatures = 'fasp'; -export function isFeatureEnabled(feature: Features) { +export function isServerFeatureEnabled(feature: ServerFeatures) { return initialState?.features.includes(feature) ?? false; } + +type ClientFeatures = 'profile_redesign'; + +export function isClientFeatureEnabled(feature: ClientFeatures) { + try { + const features = + window.localStorage.getItem('experiments')?.split(',') ?? []; + return features.includes(feature); + } catch (err) { + console.warn('Could not access localStorage to get client features', err); + return false; + } +} diff --git a/app/javascript/mastodon/utils/theme.ts b/app/javascript/mastodon/utils/theme.ts index 767d97cf5c9..494ee3cb53b 100644 --- a/app/javascript/mastodon/utils/theme.ts +++ b/app/javascript/mastodon/utils/theme.ts @@ -1,13 +1,9 @@ -export function getUserTheme() { - const { userTheme } = document.documentElement.dataset; - return userTheme; +export function getIsSystemTheme() { + const { systemTheme } = document.documentElement.dataset; + return systemTheme === 'true'; } export function isDarkMode() { - const { userTheme } = document.documentElement.dataset; - return ( - (userTheme === 'system' && - window.matchMedia('(prefers-color-scheme: dark)').matches) || - userTheme !== 'mastodon-light' - ); + const { colorScheme } = document.documentElement.dataset; + return colorScheme === 'dark'; } diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss index 93bbfed0805..cc23627a15a 100644 --- a/app/javascript/styles/contrast.scss +++ b/app/javascript/styles/contrast.scss @@ -1,2 +1 @@ @use 'common'; -@use 'mastodon/high-contrast'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 95460764030..6526b380df3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -39,8 +39,8 @@ color: var(--color-text-error); } - &:hover, - &:active { + &:not(:disabled):hover, + &:not(:disabled):active { text-decoration: underline; } @@ -1081,20 +1081,36 @@ body > [data-popper-placement] { } a { + --text-decoration-default: none; + --text-decoration-hover: underline; + + [data-contrast='high'] & { + --text-decoration-default: underline; + --text-decoration-hover: none; + } + color: var(--color-text-status-links); - text-decoration: none; + text-decoration: var(--text-decoration-default); unicode-bidi: isolate; - &:hover { - text-decoration: underline; + &:hover, + &:focus, + &:active { + text-decoration: var(--text-decoration-hover); } &.mention { - &:hover { - text-decoration: none; + text-decoration: none; + span { + text-decoration: var(--text-decoration-default); + } + + &:hover, + &:focus, + &:active { span { - text-decoration: underline; + text-decoration: var(--text-decoration-hover); } } } @@ -1356,6 +1372,15 @@ body > [data-popper-placement] { text-decoration: underline; } + [data-contrast='high'] & { + text-decoration: underline; + + &:hover, + &:active { + text-decoration: none; + } + } + .icon { width: 15px; height: 15px; diff --git a/app/javascript/styles/mastodon/high-contrast.scss b/app/javascript/styles/mastodon/high-contrast.scss deleted file mode 100644 index f55e7fae3b8..00000000000 --- a/app/javascript/styles/mastodon/high-contrast.scss +++ /dev/null @@ -1,40 +0,0 @@ -.status__content a, -.reply-indicator__content a, -.edit-indicator__content a, -.link-footer a, -.status__content__read-more-button, -.status__content__translate-button { - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - - &.mention { - text-decoration: none; - - span { - text-decoration: underline; - } - - &:hover, - &:focus, - &:active { - span { - text-decoration: none; - } - } - } -} - -.link-button:disabled { - cursor: not-allowed; - - &:hover, - &:focus, - &:active { - text-decoration: none !important; - } -} diff --git a/app/javascript/styles/mastodon/theme/_dark.scss b/app/javascript/styles/mastodon/theme/_dark.scss index e6fd6d3cc14..9485464e099 100644 --- a/app/javascript/styles/mastodon/theme/_dark.scss +++ b/app/javascript/styles/mastodon/theme/_dark.scss @@ -142,6 +142,7 @@ var(--border-strength-primary) )}; --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: #{utils.css-alpha( var(--color-indigo-200), calc(var(--border-strength-primary) / 1.5) diff --git a/app/javascript/styles/mastodon/theme/_light.scss b/app/javascript/styles/mastodon/theme/_light.scss index f0dc1bdfbc3..534a18367ca 100644 --- a/app/javascript/styles/mastodon/theme/_light.scss +++ b/app/javascript/styles/mastodon/theme/_light.scss @@ -140,6 +140,7 @@ var(--color-grey-950) var(--border-strength-primary) ); --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: var(--color-grey-200); --color-border-on-bg-brand-softer: var(--color-indigo-200); --color-border-on-bg-error-softer: #{utils.css-alpha( diff --git a/app/javascript/styles/mastodon/theme/index.scss b/app/javascript/styles/mastodon/theme/index.scss index a907299887d..a84b8b80da2 100644 --- a/app/javascript/styles/mastodon/theme/index.scss +++ b/app/javascript/styles/mastodon/theme/index.scss @@ -5,49 +5,29 @@ html { @include base.palette; - - &:where([data-user-theme='system']) { - color-scheme: dark light; - - @media (prefers-color-scheme: dark) { - @include dark.tokens; - @include utils.invert-on-dark; - - @media (prefers-contrast: more) { - @include dark.contrast-overrides; - } - } - - @media (prefers-color-scheme: light) { - @include light.tokens; - @include utils.invert-on-light; - - @media (prefers-contrast: more) { - @include light.contrast-overrides; - } - } - } } -.theme-dark, -html:where( - :not([data-user-theme='mastodon-light'], [data-user-theme='system']) -) { +[data-color-scheme='dark'], +html:not([data-color-scheme]) { color-scheme: dark; @include dark.tokens; @include utils.invert-on-dark; + + &[data-contrast='high'], + [data-contrast='high'] & { + @include dark.contrast-overrides; + } } -html[data-user-theme='contrast'], -html[data-user-theme='contrast'] .theme-dark { - @include dark.contrast-overrides; -} - -.theme-light, -html:where([data-user-theme='mastodon-light']) { +[data-color-scheme='light'] { color-scheme: light; @include light.tokens; @include utils.invert-on-light; + + &[data-contrast='high'], + [data-contrast='high'] & { + @include light.contrast-overrides; + } } diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 26b020d8c26..6f2a45e58f2 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -119,6 +119,9 @@ export function unicodeEmojiFactory( label: 'Test', unicode: '🧪', shortcodes: ['test_emoji'], + tokens: ['emoji', 'test'], + group: 1, + order: 1, ...data, }; } @@ -131,6 +134,7 @@ export function customEmojiFactory( static_url: '/custom-emoji/logo.svg', url: '/custom-emoji/logo.svg', visible_in_picker: true, + tokens: ['custom'], ...data, }; } diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 64ee9acd052..eab345ce457 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -5,6 +5,7 @@ class ActivityPub::Activity include Redisable include Lockable + MAX_JSON_SIZE = 1.megabyte SUPPORTED_TYPES = %w(Note Question).freeze CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze @@ -21,14 +22,13 @@ class ActivityPub::Activity class << self def factory(json, account, **) - @json = json - klass&.new(json, account, **) + klass_for(json)&.new(json, account, **) end private - def klass - case @json['type'] + def klass_for(json) + case json['type'] when 'Create' ActivityPub::Activity::Create when 'Announce' diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 144ba9645c5..92a8190c038 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -46,7 +46,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def accept_quote!(quote) approval_uri = value_or_id(first_of_value(@json['result'])) - return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? + return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? || !quote.pending? # NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative # as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 43c7bb1fe71..a7d2be35ed0 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -379,6 +379,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) + return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri) begin Conversation.find_or_create_by!(uri: uri) diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 3e77f9b9556..f606d9520f0 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -56,7 +56,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity end def revoke_quote - @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account) + @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account, state: [:pending, :accepted]) return if @quote.nil? ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present? diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 12f48ebb2b3..46c45cde276 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -47,7 +47,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity # NOTE: Replacing the object's context by that of the parent activity is # not sound, but it's consistent with the rest of the codebase instrument = @json['instrument'].merge({ '@context' => @json['@context'] }) - return if non_matching_uri_hosts?(instrument['id'], @account.uri) + return if non_matching_uri_hosts?(@account.uri, instrument['id']) ActivityPub::FetchRemoteStatusService.new.call(instrument['id'], prefetched_body: instrument, on_behalf_of: quoted_status.account, request_id: @options[:request_id]) end diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index d94f8767618..e22bea2c64f 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -30,7 +30,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity @status = Status.find_by(uri: object_uri, account_id: @account.id) # Ignore updates for old unknown objects, since those are updates we are not interested in - return if @status.nil? && object_too_old? + # Also ignore unknown objects from suspended users for the same reasons + return if @status.nil? && (@account.suspended? || object_too_old?) # We may be getting `Create` and `Update` out of order @status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform diff --git a/app/lib/activitypub/parser/poll_parser.rb b/app/lib/activitypub/parser/poll_parser.rb index 758c03f07ec..d43eaf6cfb4 100644 --- a/app/lib/activitypub/parser/poll_parser.rb +++ b/app/lib/activitypub/parser/poll_parser.rb @@ -3,6 +3,10 @@ class ActivityPub::Parser::PollParser include JsonLdHelper + # Limit the number of items for performance purposes. + # We truncate rather than error out to avoid missing the post entirely. + MAX_ITEMS = 500 + def initialize(json) @json = json end @@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser private def items - @json['anyOf'] || @json['oneOf'] + (@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS) end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 3174d1792e0..f9cb90f548c 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -62,6 +62,8 @@ class ActivityPub::TagManager emoji_url(target) when :flag target.uri + when :featured_collection + ap_account_collection_url(target.account.id, target) end end @@ -133,7 +135,7 @@ class ActivityPub::TagManager def collection_uri_for(target, ...) raise ArgumentError, 'target must be a local account' unless target.local? - target.numeric_ap_id? ? ap_account_collection_url(target.id, ...) : account_collection_url(target, ...) + target.numeric_ap_id? ? ap_account_actor_collection_url(target.id, ...) : account_actor_collection_url(target, ...) end def inbox_uri_for(target) @@ -241,12 +243,6 @@ class ActivityPub::TagManager !host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host)) end - def uri_to_local_id(uri, param = :id) - path_params = Rails.application.routes.recognize_path(uri) - path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' - path_params[param] - end - def uris_to_local_accounts(uris) usernames = [] ids = [] @@ -264,6 +260,14 @@ class ActivityPub::TagManager uri_to_resource(uri, Account) end + def uri_to_local_conversation(uri) + path_params = Rails.application.routes.recognize_path(uri) + return unless path_params[:controller] == 'activitypub/contexts' + + account_id, conversation_id = path_params[:id].split('-') + Conversation.find_by(parent_account_id: account_id, id: conversation_id) + end + def uri_to_resource(uri, klass) return if uri.nil? @@ -271,6 +275,8 @@ class ActivityPub::TagManager case klass.name when 'Account' uris_to_local_accounts([uri]).first + when 'Conversation' + uri_to_local_conversation(uri) else StatusFinder.new(uri).status end diff --git a/app/lib/connection_pool/shared_connection_pool.rb b/app/lib/connection_pool/shared_connection_pool.rb index 1cfcc5823b2..c7dd747edab 100644 --- a/app/lib/connection_pool/shared_connection_pool.rb +++ b/app/lib/connection_pool/shared_connection_pool.rb @@ -41,12 +41,17 @@ class ConnectionPool::SharedConnectionPool < ConnectionPool # ConnectionPool 2.4+ calls `checkin(force: true)` after fork. # When this happens, we should remove all connections from Thread.current - ::Thread.current.keys.each do |name| # rubocop:disable Style/HashEachMethods - next unless name.to_s.start_with?("#{@key}-") + connection_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key}-") && !key.to_s.start_with?("#{@key_count}-") } + count_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key_count}-") } - @available.push(::Thread.current[name]) - ::Thread.current[name] = nil + connection_keys.each do |key| + @available.push(::Thread.current[key]) + ::Thread.current[key] = nil end + count_keys.each do |key| + ::Thread.current[key] = nil + end + elsif ::Thread.current[key(preferred_tag)] if ::Thread.current[key_count(preferred_tag)] == 1 @available.push(::Thread.current[key(preferred_tag)]) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 9c5c306e966..ab5ee106c7e 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -450,6 +450,7 @@ class FeedManager return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present? return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) + return :filter if status.reblog? && status.reblog.blank? check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.push(status.account_id) diff --git a/app/lib/inline_script_manager.rb b/app/lib/inline_script_manager.rb new file mode 100644 index 00000000000..bca7c98f6b7 --- /dev/null +++ b/app/lib/inline_script_manager.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'singleton' + +class InlineScriptManager + include Singleton + include ActionView::Helpers::TagHelper + include ActionView::Helpers::JavaScriptHelper + + def initialize + @cached_files = {} + end + + def file(name) + @cached_files[name] ||= load_file(name) + end + + private + + def load_file(name) + path = Pathname.new(name).cleanpath + raise ArgumentError, "Invalid inline javascript path: #{path}" if path.to_s.start_with?('..') + + path = Rails.root.join('app', 'javascript', 'inline', path) + + contents = javascript_cdata_section(path.read) + digest = Digest::SHA256.base64digest(contents) + + { contents:, digest: } + end +end diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb index cb0c9f89668..7d0f23c4dc1 100644 --- a/app/lib/ostatus/tag_manager.rb +++ b/app/lib/ostatus/tag_manager.rb @@ -11,16 +11,12 @@ class OStatus::TagManager def unique_tag_to_local_id(tag, expected_type) return nil unless local_id?(tag) - if ActivityPub::TagManager.instance.local_uri?(tag) - ActivityPub::TagManager.instance.uri_to_local_id(tag) - else - matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) - matches[1] unless matches.nil? - end + matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) + matches[1] unless matches.nil? end def local_id?(id) - id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) + id.start_with?("tag:#{Rails.configuration.x.local_domain}") end def uri_for(target) diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index c1bd2973ed1..5a6284cc5b6 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -18,7 +18,7 @@ class TagManager return if domain.nil? uri = Addressable::URI.new - uri.host = domain.delete_suffix('/') + uri.host = domain.strip.delete_suffix('/') uri.normalized_host end diff --git a/app/models/account.rb b/app/models/account.rb index deb1589a090..32f6e39840f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -81,6 +81,13 @@ class Account < ApplicationRecord DISPLAY_NAME_LENGTH_LIMIT = 30 NOTE_LENGTH_LIMIT = 500 + # Hard limits for federated content + USERNAME_LENGTH_HARD_LIMIT = 2048 + DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048 + NOTE_LENGTH_HARD_LIMIT = 20.kilobytes + ATTRIBUTION_DOMAINS_HARD_LIMIT = 256 + ALSO_KNOWN_AS_HARD_LIMIT = 256 + AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze include Attachmentable # Load prior to Avatar & Header concerns @@ -114,7 +121,7 @@ class Account < ApplicationRecord validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? } # Remote user validations, also applies to internal actors - validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? } + validates :username, format: { with: USERNAME_ONLY_RE }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? } # Remote user validations validates :uri, presence: true, unless: :local?, on: :create diff --git a/app/models/collection.rb b/app/models/collection.rb index 2e352cbe876..3681c41d84f 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,6 +8,7 @@ # description :text not null # discoverable :boolean not null # item_count :integer default(0), not null +# language :string # local :boolean not null # name :string not null # original_number_of_items :integer @@ -36,11 +37,13 @@ class Collection < ApplicationRecord presence: true, numericality: { greater_than_or_equal: 0 }, if: :remote? + validates :language, language: { if: :local?, allow_nil: true } validate :tag_is_usable validate :items_do_not_exceed_limit scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } scope :with_tag, -> { includes(:tag) } + scope :discoverable, -> { where(discoverable: true) } def remote? !local? @@ -60,6 +63,10 @@ class Collection < ApplicationRecord self.tag = Tag.find_or_create_by_names(new_name).first end + def object_type + :featured_collection + end + private def tag_is_usable diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index c51ccf1229e..d4a415ee31b 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -164,6 +164,13 @@ module Account::Interactions end end + def blocking_or_domain_blocking?(other_account) + return true if blocking?(other_account) + return false if other_account.domain.blank? + + domain_blocking?(other_account.domain) + end + def muting?(other_account) other_id = other_account.is_a?(Account) ? other_account.id : other_account diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 65a2faa9fd5..5c39e053be1 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -26,6 +26,8 @@ class CustomEmoji < ApplicationRecord LIMIT = 256.kilobytes MINIMUM_SHORTCODE_SIZE = 2 + MAX_SHORTCODE_SIZE = 128 + MAX_FEDERATED_SHORTCODE_SIZE = 2048 SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' @@ -45,7 +47,8 @@ class CustomEmoji < ApplicationRecord normalizes :domain, with: ->(domain) { domain.downcase.strip } validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT } - validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE } + validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE } + validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local? scope :local, -> { where(domain: nil) } scope :remote, -> { where.not(domain: nil) } diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 07bbfd43733..1151c7de985 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze + TITLE_LENGTH_LIMIT = 256 + include Expireable include Redisable @@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true validates :title, :context, presence: true + validates :title, length: { maximum: TITLE_LENGTH_LIMIT } validate :context_must_be_valid normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) } diff --git a/app/models/custom_filter_keyword.rb b/app/models/custom_filter_keyword.rb index 112798b10a9..1abec4ddc4d 100644 --- a/app/models/custom_filter_keyword.rb +++ b/app/models/custom_filter_keyword.rb @@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord belongs_to :custom_filter - validates :keyword, presence: true + KEYWORD_LENGTH_LIMIT = 512 + + validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT } alias_attribute :phrase, :keyword diff --git a/app/models/list.rb b/app/models/list.rb index 8fd1953ab31..49ead642ac9 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -17,6 +17,7 @@ class List < ApplicationRecord include Paginable PER_ACCOUNT_LIMIT = 50 + TITLE_LENGTH_LIMIT = 256 enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true @@ -26,7 +27,7 @@ class List < ApplicationRecord has_many :accounts, through: :list_accounts has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account - validates :title, presence: true + validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT } validate :validate_account_lists_limit, on: :create diff --git a/app/models/quote.rb b/app/models/quote.rb index e81d427089d..4ad393e3a57 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -51,9 +51,9 @@ class Quote < ApplicationRecord def reject! if accepted? - update!(state: :revoked) + update!(state: :revoked, approval_uri: nil) elsif !revoked? - update!(state: :rejected) + update!(state: :rejected, approval_uri: nil) end end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index ab3b41d6280..1fef35714cd 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -68,4 +68,8 @@ class AccountPolicy < ApplicationPolicy def feature? record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record) end + + def index_collections? + current_account.nil? || !record.blocking_or_domain_blocking?(current_account) + end end diff --git a/app/policies/collection_policy.rb b/app/policies/collection_policy.rb index 12adfbcad1a..4d100c0e32f 100644 --- a/app/policies/collection_policy.rb +++ b/app/policies/collection_policy.rb @@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy end def show? - true + current_account.nil? || !owner.blocking_or_domain_blocking?(current_account) end def create? @@ -24,6 +24,10 @@ class CollectionPolicy < ApplicationPolicy private def owner? - current_account == record.account + current_account == owner + end + + def owner + record.account end end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index c19d42bfb43..ff1a70104b8 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -19,6 +19,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :discoverable, :indexable, :published, :memorial attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } + attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? } has_one :public_key, serializer: ActivityPub::PublicKeySerializer @@ -177,6 +178,12 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer } end + def featured_collections + return nil if instance_actor? + + ap_account_featured_collections_url(object.id) + end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index 1b410cecaef..ba0d17f5408 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -18,6 +18,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer ActivityPub::HashtagSerializer when 'ActivityPub::CollectionPresenter' ActivityPub::CollectionSerializer + when 'Collection' + ActivityPub::FeaturedCollectionSerializer when 'String' StringSerializer else diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb new file mode 100644 index 00000000000..af4c5548514 --- /dev/null +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer + class FeaturedItemSerializer < ActivityPub::Serializer + attributes :type, :featured_object, :featured_object_type + + def type + 'FeaturedItem' + end + + def featured_object + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def featured_object_type + object.account.actor_type || 'Person' + end + end + + attributes :id, :type, :total_items, :name, :attributed_to, + :sensitive, :discoverable, :published, :updated + + attribute :summary, unless: :language_present? + attribute :summary_map, if: :language_present? + + has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer + + has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer + + def id + ActivityPub::TagManager.instance.uri_for(object) + end + + def type + 'FeaturedCollection' + end + + def summary + object.description + end + + def summary_map + { object.language => object.description } + end + + def attributed_to + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def total_items + object.collection_items.size + end + + def published + object.created_at.iso8601 + end + + def updated + object.updated_at.iso8601 + end + + def language_present? + object.language.present? + end +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index fe2a857d509..a8e4b1d7f79 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -140,7 +140,7 @@ class InitialStateSerializer < ActiveModel::Serializer end def serialized_account(account) - ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer) + ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer, scope_name: :current_user, scope: object.current_account&.user) end def instance_presenter diff --git a/app/serializers/rest/base_collection_serializer.rb b/app/serializers/rest/base_collection_serializer.rb index be26aac6fe2..6bb75e99a39 100644 --- a/app/serializers/rest/base_collection_serializer.rb +++ b/app/serializers/rest/base_collection_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::BaseCollectionSerializer < ActiveModel::Serializer - attributes :id, :uri, :name, :description, :local, :sensitive, + attributes :id, :uri, :name, :description, :language, :local, :sensitive, :discoverable, :item_count, :created_at, :updated_at belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 75d3acfea58..1900330475c 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -71,6 +71,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer accounts: { max_featured_tags: FeaturedTag::LIMIT, max_pinned_statuses: StatusPinValidator::PIN_LIMIT, + max_profile_fields: Account::DEFAULT_FIELDS_SIZE, + profile_field_name_limit: Account::Field::MAX_CHARACTERS_LOCAL, + profile_field_value_limit: Account::Field::MAX_CHARACTERS_LOCAL, }, statuses: { diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 0473bb5939f..e08f82f7d9a 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -92,7 +92,6 @@ class ActivityPub::FetchRemoteStatusService < BaseService existing_status = Status.remote.find_by(uri: uri) if existing_status&.distributable? Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" } - Tombstone.find_or_create_by(uri: uri, account: existing_status.account) RemoveStatusService.new.call(existing_status, redraft: false) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f133fbc84ae..6f4aa2fdb6e 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService include Redisable include Lockable + MAX_PROFILE_FIELDS = 50 SUBDOMAINS_RATELIMIT = 10 DISCOVERIES_PER_REQUEST = 400 @@ -124,15 +125,15 @@ class ActivityPub::ProcessAccountService < BaseService def set_immediate_attributes! @account.featured_collection_url = valid_collection_uri(@json['featured']) - @account.display_name = @json['name'] || '' - @account.note = @json['summary'] || '' + @account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)] + @account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)] @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} - @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } + @account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) } @account.discoverable = @json['discoverable'] || false @account.indexable = @json['indexable'] || false @account.memorial = @json['memorial'] || false - @account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) } + @account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) } end def set_fetchable_key! @@ -253,7 +254,10 @@ class ActivityPub::ProcessAccountService < BaseService def property_values return unless @json['attachment'].is_a?(Array) - as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') } + as_array(@json['attachment']) + .select { |attachment| attachment['type'] == 'PropertyValue' } + .take(MAX_PROFILE_FIELDS) + .map { |attachment| attachment.slice('name', 'value') } end def mismatching_origin?(url) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 5d6ea2550e4..826dbcc720e 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -31,7 +31,7 @@ class BatchedRemoveStatusService < BaseService # transaction lock the database, but we use the delete method instead # of destroy to avoid all callbacks. We rely on foreign keys to # cascade the delete faster without loading the associations. - statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all } + statuses_and_reblogs.each_slice(50) { |slice| Status.unscoped.where(id: slice.pluck(:id)).delete_all } # Since we skipped all callbacks, we also need to manually # deindex the statuses diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 64769230b7c..428077b11fb 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService @account = status.account @options = options + return if @status.proper.account.suspended? + check_race_condition! warm_payload_cache! diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 666eccb9ce5..514c838d648 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -58,13 +58,7 @@ class FetchResourceService < BaseService [@url, { prefetched_body: body }] elsif !terminal - link_header = response['Link'] && parse_link_header(response) - - if link_header&.find_link(%w(rel alternate)) - process_link_headers(link_header) - elsif response.mime_type == 'text/html' - process_html(response) - end + process_link_headers(response) || process_html(response) end end @@ -73,13 +67,18 @@ class FetchResourceService < BaseService end def process_html(response) + return unless response.mime_type == 'text/html' + page = Nokogiri::HTML5(response.body_with_limit) json_link = page.xpath('//link[nokogiri:link_rel_include(@rel, "alternate")]', NokogiriHandler).find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) } process(json_link['href'], terminal: true) unless json_link.nil? end - def process_link_headers(link_header) + def process_link_headers(response) + link_header = response['Link'] && parse_link_header(response) + return if link_header.nil? + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) process(json_link.href, terminal: true) unless json_link.nil? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 47e602f0f3f..15f1e961ed6 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: I18n.locale, class: html_classes, 'data-user-theme': current_theme.parameterize, 'data-contrast': contrast.parameterize, 'data-mode': color_scheme.parameterize } +%html{ html_attributes } %head %meta{ charset: 'utf-8' }/ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }/ @@ -20,7 +20,8 @@ - if use_mask_icon? %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ - = theme_color_tags current_theme + = javascript_inline_tag 'theme-selection.js' + = theme_color_tags color_scheme %meta{ name: 'mobile-web-app-capable', content: 'yes' }/ %title= html_title diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index b802854ae72..d815b59140e 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: I18n.locale } +%html{ lang: I18n.locale, 'data-contrast': 'auto', 'data-color-scheme': 'light' } %head %meta{ charset: 'utf-8' }/ %meta{ name: 'robots', content: 'noindex' }/ @@ -11,10 +11,11 @@ - if storage_host? %link{ rel: 'dns-prefetch', href: storage_host }/ + = javascript_inline_tag 'theme-selection.js' = vite_client_tag = vite_react_refresh_tag = vite_polyfills_tag - = theme_style_tags 'mastodon-light' + = theme_style_tags 'system' = vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" = render_initial_state = vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous' diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml index f85a671a531..03cb93dba7b 100644 --- a/app/views/layouts/error.html.haml +++ b/app/views/layouts/error.html.haml @@ -1,5 +1,5 @@ !!! -%html{ lang: I18n.locale } +%html{ lang: I18n.locale, 'data-contrast': 'auto', 'data-color-scheme': 'auto' } %head %meta{ 'content' => 'text/html; charset=UTF-8', 'http-equiv' => 'Content-Type' }/ %meta{ charset: 'utf-8' }/ @@ -8,6 +8,7 @@ = vite_client_tag = vite_react_refresh_tag = vite_polyfills_tag + = vite_typescript_tag 'theme-selection.ts', crossorigin: 'anonymous', blocking: 'render' = theme_style_tags Setting.default_settings['theme'] = vite_typescript_tag 'error.ts', crossorigin: 'anonymous' %body.error diff --git a/app/views/wrapstodon/show.html.haml b/app/views/wrapstodon/show.html.haml index ed1e64d4665..8a20001ad55 100644 --- a/app/views/wrapstodon/show.html.haml +++ b/app/views/wrapstodon/show.html.haml @@ -13,7 +13,7 @@ = vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous' -- content_for :html_classes, 'theme-dark' +- content_for :force_color_scheme, 'dark' #wrapstodon = render_wrapstodon_share_data @generated_annual_report diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index ade7175c9d5..8cd39f700ca 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -38,7 +38,7 @@ class ActivityPub::DeliveryWorker if @inbox_url.present? if @performed failure_tracker.track_success! - else + elsif !@unsalvageable failure_tracker.track_failure! end end @@ -62,9 +62,13 @@ class ActivityPub::DeliveryWorker stoplight_wrapper.run do request_pool.with(@host) do |http_client| build_request(http_client).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) - - @performed = true + if response_successful?(response) + @performed = true + elsif response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) + @unsalvageable = true + else + raise Mastodon::UnexpectedResponseError, response + end end end end diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 1a5745a86ae..43238b72b20 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -45,30 +45,34 @@ class MoveWorker end # Then handle accounts that follow both the old and new account - @source_account.passive_relationships - .where(account: Account.local) - .where(account: @target_account.followers.local) - .in_batches do |follows| - ListAccount.where(follow: follows).includes(:list).find_each do |list_account| - list_account.list.accounts << @target_account - rescue ActiveRecord::RecordInvalid - nil - end + source_local_followers + .where(account: @target_account.followers.local) + .in_batches do |follows| + ListAccount.where(follow: follows).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end end # Finally, handle the common case of accounts not following the new account - @source_account.passive_relationships - .where(account: Account.local) - .where.not(account: @target_account.followers.local) - .where.not(account_id: @target_account.id) - .in_batches do |follows| - ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) - num_moved += follows.update_all(target_account_id: @target_account.id) + source_local_followers + .where.not(account: @target_account.followers.local) + .where.not(account_id: @target_account.id) + .in_batches do |follows| + ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) + num_moved += follows.update_all(target_account_id: @target_account.id) end num_moved end + def source_local_followers + @source_account + .passive_relationships + .where(account: Account.local) + end + def queue_follow_unfollows! bypass_locked = @target_account.local? diff --git a/config/locales/activerecord.gd.yml b/config/locales/activerecord.gd.yml index 6b068d04dc6..572fcc83df5 100644 --- a/config/locales/activerecord.gd.yml +++ b/config/locales/activerecord.gd.yml @@ -32,6 +32,12 @@ gd: attributes: url: invalid: "– chan eil seo ’na URL dligheach" + collection: + attributes: + collection_items: + too_many: "– tha cus dhiubh ann, chan eil còrr is %{count} ceadaichte" + tag: + unusable: "– chan fhaodar seo a chleachdadh" doorkeeper/application: attributes: website: diff --git a/config/locales/activerecord.ms.yml b/config/locales/activerecord.ms.yml index 5f282702f10..b31ccf9dfe7 100644 --- a/config/locales/activerecord.ms.yml +++ b/config/locales/activerecord.ms.yml @@ -15,9 +15,16 @@ ms: user/invite_request: text: Sebab errors: + attributes: + domain: + invalid: bukanlah nama domain yang sah + messages: + invalid_domain_on_line: "%{value} bukanlah nama domain yang sah" models: account: attributes: + fields: + fields_with_values_missing_labels: mengandungi nilai dengan label yang hilang username: invalid: hanya mengandungi aksara, nombor dan garis bawah sahaja reserved: dikhaskan @@ -25,6 +32,12 @@ ms: attributes: url: invalid: bukanlah URL yang sah + collection: + attributes: + collection_items: + too_many: terlalu banyak, tidak lebih daripada %{count} dibenarkan + tag: + unusable: mungkin tidak digunakan doorkeeper/application: attributes: website: @@ -33,12 +46,23 @@ ms: attributes: data: malformed: tersalah bentuk + list_account: + attributes: + account_id: + taken: sudah dalam senarai + must_be_following: hendaklah akaun yang diikuti status: attributes: reblog: taken: hantaran sudah wujud + terms_of_service: + attributes: + effective_date: + too_soon: terlalu awal, hendaklah selepas %{date} user: attributes: + date_of_birth: + below_limit: di bawah had umur email: blocked: menggunakan pembekal e-mel yang tidak dibenarkan unreachable: nampaknya tidak wujud diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml index e4c4fe598f9..cecd0444d72 100644 --- a/config/locales/activerecord.sl.yml +++ b/config/locales/activerecord.sl.yml @@ -32,6 +32,12 @@ sl: attributes: url: invalid: ni veljaven URL + collection: + attributes: + collection_items: + too_many: je preveč, dovoljenih je največ %{count} + tag: + unusable: ni možno uporabiti doorkeeper/application: attributes: website: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 0cbbb08f83e..6adcf1871c7 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -2162,6 +2162,7 @@ ca: error: Hi ha hagut un problema al esborrar la teva clau de seguretat. Tornau-ho a provar. success: La teva clau de seguretat s'ha esborrat correctament. invalid_credential: Clau de seguretat invàlida + nickname: Sobrenom nickname_hint: Introdueix el sobrenom de la teva clau de seguretat nova not_enabled: Encara no has activat WebAuthn not_supported: Aquest navegador no suporta claus de seguretat diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 9c3975cb1c8..acbbf0cd440 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -2278,6 +2278,7 @@ cs: error: Při odstraňování bezpečnostního klíče došlo k chybě. Zkuste to prosím znovu. success: Váš bezpečnostní klíč byl úspěšně odstraněn. invalid_credential: Neplatný bezpečnostní klíč + nickname: Přezdívka nickname_hint: Zadejte přezdívku nového bezpečnostního klíče not_enabled: Zatím jste nepovolili WebAuthn not_supported: Tento prohlížeč nepodporuje bezpečnostní klíče diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 83a9ab46c00..1f38ea6eeea 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -904,6 +904,7 @@ cy: publish_statistics: Cyhoeddi ystadegau title: Darganfod trends: Tueddiadau + wrapstodon: Wrapstodon domain_blocks: all: I bawb disabled: I neb @@ -2144,9 +2145,11 @@ cy: enabled: Dileu hen bostiadau'n awtomatig enabled_hint: Yn dileu eich postiadau yn awtomatig ar ôl iddyn nhw gyrraedd trothwy oed penodedig, oni bai eu bod yn cyfateb i un o'r eithriadau isod exceptions: Eithriadau + explanation: Caiff dileu awtomataidd ei berfformio â blaenoriaeth isel. Gall fod oedi rhwng cyrraedd y trothwy oed a chael ei ddileu. ignore_favs: Anwybyddu ffefrynnau ignore_reblogs: Anwybyddu hybiau interaction_exceptions: Eithriadau yn seiliedig ar ryngweithio + interaction_exceptions_explanation: Maen bosib cadw postiadau sy'n mynd dros dro dros y trothwy ffefryn neu hwb hyd yn oed os cawn nhw eu gostwng yn ddiweddarach. keep_direct: Cadw negeseuon uniongyrchol keep_direct_hint: Nid yw'n dileu unrhyw un o'ch negeseuon uniongyrchol keep_media: Cadw postiadau gydag atodiadau cyfryngau @@ -2363,6 +2366,7 @@ cy: error: Bu anhawster wrth ddileu eich allwedd ddiogelwch. Ceisiwch eto, os gwelwch yn dda. success: Cafodd eich allwedd ddiogelwch ei dileu'n llwyddiannus. invalid_credential: Allwedd ddiogelwch annilys + nickname: Llysenw nickname_hint: Rhowch lysenw eich allwedd ddiogelwch newydd not_enabled: Nid ydych wedi galluogi WebAuthn eto not_supported: Nid yw'r porwr hwn yn cynnal allweddi diogelwch diff --git a/config/locales/devise.el.yml b/config/locales/devise.el.yml index 86134b9491f..3708d70ce6d 100644 --- a/config/locales/devise.el.yml +++ b/config/locales/devise.el.yml @@ -14,7 +14,7 @@ el: locked: Ο λογαριασμός σου κλειδώθηκε. not_found_in_database: Λάθος %{authentication_keys} ή συνθηματικό. omniauth_user_creation_failure: Σφάλμα δημιουργίας λογαριασμού για αυτήν την ταυτότητα. - pending: Εκκρεμεί η έγκριση του λογαριασμού σου. + pending: Εκκρεμεί ο έλεγχος του λογαριασμού σου. timeout: Η τρέχουσα σύνδεσή σου έληξε. Παρακαλούμε συνδέσου ξανά για να συνεχίσεις. unauthenticated: Πρέπει να συνδεθείς ή να εγγραφείς για να συνεχίσεις. unconfirmed: Πρέπει να επιβεβαιώσεις τη διεύθυνση email σου για να συνεχίσεις. @@ -23,7 +23,7 @@ el: action: Επιβεβαίωση διεύθυνσης email action_with_app: Επιβεβαίωση και επιστροφή στο %{app} explanation: Δημιούργησες έναν λογαριασμό στο %{host} με αυτή τη διεύθυνση email. Με ένα κλικ θα τον ενεργοποιήσεις. Αν δεν το έκανες εσύ, παρακαλούμε αγνόησε αυτό το email. - explanation_when_pending: Έχεις υποβάλλει αίτηση πρόσκλησης στο %{host} με αυτή την ηλεκτρονική διεύθυνση email. Μόλις επιβεβαιώσεις το email σου, θα ελέγξουμε την αίτηση σου. Μέχρι τότε δε θα μπορείς να συνδεθείς. Αν απορριφθεί η αίτησή σου, τα στοιχεία σου θα αφαιρεθούν, άρα δε θα χρειαστεί να κάνεις κάτι επιπλέον. Αν δεν υπέβαλες εσύ την αίτηση, αγνόησε αυτό το email. + explanation_when_pending: Έχεις υποβάλλει αίτηση πρόσκλησης στο %{host} με αυτή την διεύθυνση email. Μόλις επιβεβαιώσεις το email σου, θα ελέγξουμε την αίτηση σου. Μέχρι τότε δε θα μπορείς να συνδεθείς. Αν απορριφθεί η αίτησή σου, τα στοιχεία σου θα αφαιρεθούν, άρα δε θα χρειαστεί να κάνεις κάτι επιπλέον. Αν δεν υπέβαλες εσύ την αίτηση, αγνόησε αυτό το email. extra_html: Παρακαλούμε να διαβάσεις του κανόνες αυτού του κόμβου και τους όρους χρήσης της υπηρεσίας μας. subject: 'Mastodon: Οδηγίες επιβεβαίωσης για %{instance}' title: Επιβεβαίωσε διεύθυνση email diff --git a/config/locales/devise.et.yml b/config/locales/devise.et.yml index 5843761ddba..6ed4c2dd701 100644 --- a/config/locales/devise.et.yml +++ b/config/locales/devise.et.yml @@ -29,23 +29,23 @@ et: title: Kinnita e-postiaadress email_changed: explanation: 'Sinu konto e-postiaadress muudetakse:' - extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontost välja lukustatud, võta ühendust oma serveri administraatoriga. + extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontole ligipääsu kaotanud, palun võta kohe ühendust oma serveri haldajaga. subject: 'Mastodon: e-post muudetud' title: Uus e-postiaadress password_change: explanation: Konto salasõna on vahetatud. extra: Kui sa ei muutnud oma salasõna, on tõenäoline, et keegi on su kontole ligi pääsenud. Palun muuda viivitamata oma salasõna. Kui sa oma kontole ligi ei pääse, võta ühendust serveri haldajaga. - subject: 'Mastodon: salasõna muudetud' - title: Salasõna muudetud + subject: 'Mastodon: salasõna on muudetud' + title: Salasõna on muudetud reconfirmation_instructions: explanation: Kinnita uus aadress, et oma e-posti aadress muuta. extra: Kui see muudatus pole sinu poolt algatatud, palun eira seda kirja. Selle Mastodoni konto e-postiaadress ei muutu enne, kui vajutad üleval olevale lingile. subject: 'Mastodon: kinnita e-postiaadress %{instance} jaoks' title: Kinnita e-postiaadress reset_password_instructions: - action: Salasõna muutmine - explanation: Kontole on küsitud uut salasõna. - extra: Kui see tuleb üllatusena, võib seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna määramist. + action: Muuda salasõna + explanation: Sa palusid oma kasutajakontole luua uus salasõna. + extra: Kui see tuleb üllatusena, võid seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna sisestamist. subject: 'Mastodon: salasõna lähtestamisjuhendid' title: Salasõna lähtestamine two_factor_disabled: diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index c2147fcf9fc..4df8d60dadc 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -24,7 +24,7 @@ fr: action_with_app: Confirmer et retourner à %{app} explanation: Vous avez créé un compte sur %{host} avec cette adresse courriel. Vous êtes à un clic de l’activer. Si ce n’était pas vous, veuillez ignorer ce courriel. explanation_when_pending: Vous avez demandé à vous inscrire à %{host} avec cette adresse de courriel. Une fois que vous aurez confirmé cette adresse, nous étudierons votre demande. Vous ne pourrez pas vous connecter d’ici-là. Si votre demande est refusée, vos données seront supprimées du serveur, aucune action supplémentaire de votre part n’est donc requise. Si vous n’êtes pas à l’origine de cette demande, veuillez ignorer ce message. - extra_html: Merci de consultez également les règles du serveur et nos conditions d’utilisation. + extra_html: Merci de consulter également les règles du serveur et nos conditions d’utilisation. subject: 'Mastodon : Merci de confirmer votre inscription sur %{instance}' title: Vérifiez l’adresse de courriel email_changed: diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml index 114194f6df4..1632767bc79 100644 --- a/config/locales/doorkeeper.el.yml +++ b/config/locales/doorkeeper.el.yml @@ -192,7 +192,7 @@ el: write:follows: ακολούθηση ατόμων write:lists: δημιουργία λιστών write:media: να ανεβάζει πολυμέσα - write:mutes: σίγαση ατόμων και συζητήσεων + write:mutes: σίγαση ατόμων και συνομιλιών write:notifications: καθαρισμός των ειδοποιήσεων σου write:reports: αναφορά άλλων ατόμων write:statuses: δημοσίευση αναρτήσεων diff --git a/config/locales/el.yml b/config/locales/el.yml index 10a88b36b58..1a32ecd57f1 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -108,7 +108,7 @@ el: no_limits_imposed: Χωρίς όρια no_role_assigned: Δεν έχει ανατεθεί ρόλος not_subscribed: Δεν έγινε εγγραφή - pending: Εκκρεμεί αξιολόγηση + pending: Εκκρεμεί έλεγχος perform_full_suspension: Αναστολή previous_strikes: Προηγούμενα παραπτώματα previous_strikes_description_html: @@ -327,7 +327,7 @@ el: create: Δημιουργία ανακοίνωσης title: Νέα ανακοίνωση preview: - disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω ηλεκτρονικού ταχυδρομείου θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή. + disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω email θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή. explanation_html: 'Το email θα αποσταλεί σε %{display_count} χρήστες. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:' title: Προεπισκόπηση ειδοποίησης ανακοίνωσης publish: Δημοσίευση @@ -449,7 +449,7 @@ el: reject_media: Απόρριψη αρχείων πολυμέσων reject_media_hint: Αφαιρεί τα τοπικά αποθηκευμένα αρχεία πολυμέσων και αποτρέπει τη λήψη άλλων στο μέλλον. Δεν έχει σημασία για τις αναστολές reject_reports: Απόρριψη αναφορών - reject_reports_hint: Αγνόησε όσων αναφορών που προέρχονται από αυτό τον τομέα. Δεν σχετίζεται με τις παύσεις + reject_reports_hint: Αγνόηση όσων αναφορών προέρχονται από αυτό τον τομέα. Δεν σχετίζεται με τις παύσεις undo: Αναίρεση αποκλεισμού τομέα view: Εμφάνιση αποκλεισμού τομέα email_domain_blocks: @@ -597,7 +597,7 @@ el: public_comment: Δημόσιο σχόλιο purge: Εκκαθάριση purge_description_html: Εάν πιστεύεις ότι αυτός ο τομέας είναι εκτός σύνδεσης μόνιμα, μπορείς να διαγράψεις όλες τις καταχωρήσεις λογαριασμών και τα σχετικά δεδομένα από αυτόν τον τομέα από τον αποθηκευτικό σου χώρο. Αυτό μπορεί να διαρκέσει λίγη ώρα. - title: Συναλλαγές + title: Ομοσπονδία total_blocked_by_us: Αποκλεισμένοι από εμάς total_followed_by_them: Ακολουθούνται από εκείνους total_followed_by_us: Ακολουθούνται από εμάς @@ -727,7 +727,7 @@ el: close_report: 'Επισήμανση αναφοράς #%{id} ως επιλυμένη' close_reports_html: Επισήμανε όλες τις αναφορές ενάντια στον λογαριασμό @%{acct} ως επιλυμένες delete_data_html: Διάγραψε το προφίλ και το περιεχόμενο του @%{acct} σε 30 ημέρες από τώρα εκτός αν, εν τω μεταξύ, ανακληθεί η αναστολή - preview_preamble_html: 'Ο @%{acct} θα λάβει μια προειδοποίηση με τα ακόλουθο περιεχόμενο:' + preview_preamble_html: 'Ο/Η @%{acct} θα λάβει μια προειδοποίηση με τα ακόλουθα περιεχόμενα:' record_strike_html: Κατάγραψε ένα παράπτωμα εναντίον του @%{acct} για να σε βοηθήσει να αποφασίσεις σε μελλοντικές παραβιάσεις από αυτόν τον λογαριασμό send_email_html: Στείλε στον λογαριασμό @%{acct} ένα προειδοποιητικό email warning_placeholder: Προαιρετικές επιπλέον εξηγήσεις για αυτή την ενέργεια από την ομάδα συντονισμού. @@ -772,7 +772,7 @@ el: manage_blocks_description: Επιτρέπει στους χρήστες να αποκλείουν παρόχους email και διευθύνσεις IP manage_custom_emojis: Διαχείριση Προσαρμοσμένων Emojis manage_custom_emojis_description: Επιτρέπει στους χρήστες να διαχειρίζονται προσαρμοσμένα emojis στον διακομιστή - manage_federation: Διαχείριση Συναλλαγών + manage_federation: Διαχείριση Ομοσπονδίας manage_federation_description: Επιτρέπει στους χρήστες να αποκλείουν ή να επιτρέπουν τις συναλλαγές με άλλους τομείς και να ελέγχουν την παράδοση manage_invites: Διαχείριση Προσκλήσεων manage_invites_description: Επιτρέπει στους χρήστες να περιηγούνται και να απενεργοποιούν τους συνδέσμους πρόσκλησης @@ -949,7 +949,7 @@ el: elasticsearch_health_yellow: message_html: Το σύμπλεγμα Elasticsearch δεν είναι υγιές (κίτρινη κατάσταση), ίσως θες να διαπιστώσεις την αιτία elasticsearch_index_mismatch: - message_html: Οι αντιστοιχές δείκτη του Elasticsearch δεν είναι ενημερωμένες. Παρακαλώ εκτέλεσε το tootctl search deploy --only=%{value} + message_html: Οι αντιστοιχήσεις του δείκτη Elasticsearch δεν είναι ενημερωμένες. Παρακαλώ εκτέλεσε το tootctl search deploy --only=%{value} elasticsearch_preset: action: Δες το εγχειρίδιο message_html: Το σύμπλεγμα Elasticsearch σου, έχει παραπάνω από ένα κόμβο, το Mastodon δεν είναι ρυθμισμένο για να τους χρησιμοποιεί. @@ -987,9 +987,9 @@ el: moderation: not_trendable: Δε δημιουργεί τάσεις not_usable: Μη χρησιμοποιήσιμη - pending_review: Εκκρεμεί αξιολόγηση - review_requested: Αιτήθηκε αξιολόγηση - reviewed: Αξιολογήθηκε + pending_review: Εκκρεμεί έλεγχος + review_requested: Αιτήθηκε έλεγχος + reviewed: Ελέγχθηκε title: Κατάσταση trendable: Πιθανό για τάσεις unreviewed: Μη ελεγμένη @@ -999,7 +999,7 @@ el: oldest: Παλαιότερη όλων open: Προβολή Δημόσια reset: Επαναφορά - review: Κατάσταση αξιολόγησης + review: Κατάσταση ελέγχου search: Αναζήτηση title: Ετικέτες updated_msg: Οι ρυθμίσεις των ετικετών ενημερώθηκαν επιτυχώς @@ -1012,7 +1012,7 @@ el: generate: Χρήση προτύπου generates: action: Δημιουργία - chance_to_review_html: "Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα. Θα έχεις την ευκαιρία να εξετάσεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις." + chance_to_review_html: "Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα. Θα έχεις την ευκαιρία να ελέγξεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις." explanation_html: Το πρότυπο όρων υπηρεσίας που παρέχονται είναι μόνο για ενημερωτικούς σκοπούς και δε θα πρέπει να ερμηνεύονται ως νομικές συμβουλές για οποιοδήποτε θέμα. Παρακαλούμε συμβουλέψου τον νομικό σου σύμβουλο σχετικά με την περίπτωσή σου και τις συγκεκριμένες νομικές ερωτήσεις που έχεις. title: Ρύθμιση Όρων Παροχής Υπηρεσιών going_live_on_html: Ενεργό, σε ισχύ από %{date} @@ -1060,7 +1060,7 @@ el: usage_comparison: Κοινοποιήθηκε %{today} φορές σήμερα, σε σύγκριση με %{yesterday} χθες not_allowed_to_trend: Δεν επιτρέπεται να γίνει δημοφιλές only_allowed: Μόνο επιτρεπόμενα - pending_review: Εκκρεμεί αξιολόγηση + pending_review: Εκκρεμεί έλεγχος preview_card_providers: allowed: Σύνδεσμοι από αυτόν τον εκδότη μπορούν να γίνουν δημοφιλείς description_html: Αυτοί είναι τομείς από τους οποίους οι σύνδεσμοι συχνά κοινοποιούνται στον διακομιστή σας. Οι σύνδεσμοι δεν θα γίνουν δημοφιλείς δημοσίως εκτός και αν ο τομέας του συνδέσμου εγκριθεί. Η έγκρισή σας (ή απόρριψη) περιλαμβάνει και τους υποτομείς. @@ -1155,7 +1155,7 @@ el: webhook: Webhook admin_mailer: auto_close_registrations: - body: Λόγω έλλειψης πρόσφατης δραστηριότητας συντονιστών, οι εγγραφές στο %{instance} έχουν αλλάξει αυτόματα στην απαίτηση χειροκίνητης αξιολόγησης, για να αποτρέψει το %{instance} από το να χρησιμοποιηθεί ως πλατφόρμα για πιθανούς κακούς ηθοποιούς. Μπορείς να το αλλάξεις ξανά για να ανοίξετε εγγραφές ανά πάσα στιγμή. + body: Λόγω έλλειψης πρόσφατης δραστηριότητας συντονιστών, οι εγγραφές στο %{instance} έχουν αλλάξει αυτόματα στην απαίτηση χειροκίνητου ελέγχου, για να αποτρέψει το %{instance} από το να χρησιμοποιηθεί ως πλατφόρμα για πιθανούς κακούς παράγοντες. Μπορείς να το αλλάξεις ξανά για να ανοίξετε εγγραφές ανά πάσα στιγμή. subject: Οι εγγραφές για το %{instance} έχουν αλλάξει αυτόματα σε απαίτηση έγκρισης new_appeal: actions: @@ -1174,7 +1174,7 @@ el: subject: Κρίσιμες ενημερώσεις Mastodon είναι διαθέσιμες για το %{instance}! new_pending_account: body: Τα στοιχεία του νέου λογαριασμού είναι παρακάτω. Μπορείς να εγκρίνεις ή να απορρίψεις αυτή την αίτηση. - subject: Νέος λογαριασμός προς έγκριση στο %{instance} (%{username}) + subject: Νέος λογαριασμός προς έλεγχο στο %{instance} (%{username}) new_report: body: Ο/Η %{reporter} ανέφερε τον/την %{target} body_remote: Κάποιος/α από τον τομέα %{domain} ανέφερε τον/την %{target} @@ -1183,14 +1183,14 @@ el: body: Έχουν κυκλοφορήσει νέες εκδόσεις Mastodon, ίσως θέλεις να ενημερώσεις! subject: Νέες εκδόσεις Mastodon είναι διαθέσιμες για το %{instance}! new_trends: - body: 'Τα ακόλουθα στοιχεία χρειάζονται αξιολόγηση για να μπορούν να προβληθούν δημόσια:' + body: 'Τα ακόλουθα στοιχεία χρειάζονται έλεγχο για να μπορούν να προβληθούν δημόσια:' new_trending_links: title: Σύνδεσμοι σε τάση new_trending_statuses: title: Αναρτήσεις σε τάση new_trending_tags: title: Ετικέτες σε τάση - subject: Νέες τάσεις προς αξιολόγηση στο %{instance} + subject: Νέες τάσεις προς έλεγχο στο %{instance} aliases: add_new: Δημιουργία ψευδώνυμου created_msg: Δημιουργήθηκε νέο ψευδώνυμο. Τώρα μπορείς να ξεκινήσεις τη μεταφορά από τον παλιό λογαριασμό. @@ -1291,14 +1291,14 @@ el: preamble_html: Συνδεθείτε με τα διαπιστευτήριά σας στον %{domain}. Αν ο λογαριασμός σας φιλοξενείται σε διαφορετικό διακομιστή, δε θα μπορείτε να συνδεθείτε εδώ. title: Συνδεθείτε στο %{domain} sign_up: - manual_review: Οι εγγραφές στο %{domain} περνούν από χειροκίνητη αξιολόγηση από τους συντονιστές μας. Για να μας βοηθήσεις να επεξεργαστούμε την εγγραφή σου, γράψε λίγα λόγια για τον εαυτό σου και γιατί θέλεις έναν λογαριασμό στο %{domain}. + manual_review: Οι εγγραφές στο %{domain} περνούν από χειροκίνητο έλεγχο από τους συντονιστές μας. Για να μας βοηθήσεις να επεξεργαστούμε την εγγραφή σου, γράψε λίγα λόγια για τον εαυτό σου και γιατί θέλεις έναν λογαριασμό στο %{domain}. preamble: Με έναν λογαριασμό σ' αυτόν τον διακομιστή Mastodon, θα μπορείς να ακολουθήσεις οποιοδήποτε άλλο άτομο στο δίκτυο, ανεξάρτητα από το πού φιλοξενείται ο λογαριασμός του. title: Ας ξεκινήσουμε τις ρυθμίσεις στο %{domain}. status: account_status: Κατάσταση λογαριασμού confirming: Αναμονή για ολοκλήρωση επιβεβαίωσης του email. functional: Ο λογαριασμός σας είναι πλήρως λειτουργικός. - pending: Η εφαρμογή σου εκκρεμεί έγκρισης. Ίσως θα διαρκέσει κάποιο χρόνο. Θα λάβεις email αν εγκριθεί. + pending: Η εφαρμογή σου εκκρεμεί έλεγχο από το προσωπικό μας. Ίσως θα διαρκέσει κάποιο χρόνο. Θα λάβεις email αν εγκριθεί. redirecting_to: Ο λογαριασμός σου είναι ανενεργός γιατί επί του παρόντος ανακατευθύνει στον %{acct}. self_destruct: Καθώς το %{domain} κλείνει, θα έχεις μόνο περιορισμένη πρόσβαση στον λογαριασμό σου. view_strikes: Προβολή προηγούμενων ποινών εναντίον του λογαριασμού σας @@ -1446,7 +1446,7 @@ el: home: Αρχική σελίδα και λίστες notifications: Ειδοποιήσεις public: Δημόσιες ροές - thread: Συζητήσεις + thread: Συνομιλίες edit: add_keyword: Προσθήκη λέξης-κλειδιού keywords: Λέξεις-κλειδιά @@ -1579,7 +1579,7 @@ el: type: Τύπος εισαγωγής type_groups: constructive: Ακολουθείς & Σελιδοδείκτες - destructive: Μπλοκ & σίγαση + destructive: Αποκλεισμοί & σιγάσεις types: blocking: Λίστα αποκλεισμού bookmarks: Σελιδοδείκτες @@ -1688,7 +1688,7 @@ el: title: Συντονισμός move_handler: carry_blocks_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες αποκλείσει. - carry_mutes_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες αποσιωπήσει. + carry_mutes_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες σε σίγαση. copy_account_note_text: 'Ο χρήστης μετακόμισε από τον %{acct}, ορίστε οι προηγούμενες σημειώσεις σου για εκείνον:' navigation: toggle_menu: Εμφάνιση/Απόκρυψη μενού @@ -1948,7 +1948,7 @@ el: quoted_user_not_mentioned: Δεν είναι δυνατή η παράθεση ενός μη επισημασμένου χρήστη σε μια ανάρτηση Ιδιωτικής επισήμανσης. over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων pin_errors: - direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν + direct: Αναρτήσεις που είναι ορατές μόνο στους επισημασμένους χρήστες δεν μπορούν να καρφιτσωθούν limit: Έχεις ήδη καρφιτσώσει το μέγιστο αριθμό επιτρεπτών αναρτήσεων ownership: Δεν μπορείς να καρφιτσώσεις ανάρτηση κάποιου άλλου reblog: Οι ενισχύσεις δεν καρφιτσώνονται diff --git a/config/locales/et.yml b/config/locales/et.yml index 91b34492989..58df54c8dfb 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -38,7 +38,7 @@ et: add_email_domain_block: Blokeeri e-posti domeen approve: Võta vastu approved_msg: Kasutaja %{username} liitumisavaldus rahuldatud - are_you_sure: Oled kindel? + are_you_sure: Kas oled kindel? avatar: Profiilipilt by_domain: Domeen change_email: @@ -47,7 +47,7 @@ et: label: Muuda e-posti aadressi new_email: Uus е-posti aadress submit: Muuda e-posti aadressi - title: Muuda e-postiaadressi kasutajale %{username} + title: Muuda kasutaja %{username} e-posti aadressi change_role: changed_msg: Roll on muudetud! edit_roles: Halda kasutaja rolle @@ -142,7 +142,7 @@ et: security: Turvalisus security_measures: only_password: Ainult salasõna - password_and_2fa: Salasõna ja 2-etapine autentimine (2FA) + password_and_2fa: Salasõna ja kahefaktoriline autentimine (2FA) sensitive: Tundlik sisu sensitized: Märgitud kui tundlik sisu shared_inbox_url: Jagatud sisendkausta URL @@ -292,7 +292,7 @@ et: remove_avatar_user_html: "%{name} eemaldas %{target} avatari" reopen_report_html: "%{name} taasavas raporti %{target}" resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti" - reset_password_user_html: "%{name} lähtestas %{target} salasõna" + reset_password_user_html: "%{name} lähtestas %{target} kasutaja salasõna" resolve_report_html: "%{name} lahendas raporti %{target}" sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu" silence_account_html: "%{name} piiras %{target} konto" @@ -787,7 +787,7 @@ et: manage_taxonomies: Halda taksonoomiaid manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada teemaviidete seadistusi manage_user_access: Halda kasutajate ligipääsu - manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta oma e-posti aadressi ja lähtestada oma parooli + manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta nende e-posti aadressi ja lähtestada oma salasõna manage_users: Kasutajate haldamine manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid manage_webhooks: Halda webhook'e @@ -1249,7 +1249,7 @@ et: suffix: Kasutajakontoga saad jälgida inimesi, postitada uudiseid ning pidada kirjavahetust ükskõik millise Mastodoni serveri kasutajatega ja muudki! didnt_get_confirmation: Ei saanud kinnituslinki? dont_have_your_security_key: Pole turvavõtit? - forgot_password: Salasõna ununenud? + forgot_password: Kas unustasid oma salasõna? invalid_reset_password_token: Salasõna lähtestusvõti on vale või aegunud. Palun taotle uus. link_to_otp: Kaheastmeline kood telefonist või taastekood link_to_webauth: Turvavõtmete seadme kasutamine @@ -1270,7 +1270,7 @@ et: register: Loo konto registration_closed: "%{instance} ei võta vastu uusi liikmeid" resend_confirmation: Saada kinnituslink uuesti - reset_password: Salasõna lähtestamine + reset_password: Lähtesta salasõna rules: accept: Nõus back: Tagasi @@ -1280,7 +1280,7 @@ et: title: Mõned põhireeglid. title_invited: Oled kutsutud. security: Turvalisus - set_new_password: Uue salasõna määramine + set_new_password: Sisesta uus salasõna setup: email_below_hint_html: Kontrolli rämpsposti kausta või palu uue kirja saatmist. Kui sinu e-posti aadress on vale, siis saad seda parandada. email_settings_hint_html: Klõpsa aadressile %{email} saadetud linki, et alustada Mastodoni kasutamist. Me oleme ootel. @@ -1316,9 +1316,9 @@ et: title: Autori tunnustamine challenge: confirm: Jätka - hint_html: "Nõuanne: Me ei küsi salasõna uuesti järgmise tunni jooksul." + hint_html: "Nõuanne: Me ei küsi sinu salasõna uuesti järgmise tunni jooksul." invalid_password: Vigane salasõna - prompt: Jätkamiseks salasõna veelkord + prompt: Jätkamiseks korda salasõna color_scheme: auto: Auto dark: Tume @@ -1627,7 +1627,7 @@ et: password: salasõna sign_in_token: e-posti turvvakood webauthn: turvavõtmed - description_html: Kui paistab tundmatuid tegevusi, tuleks vahetada salasõna ja aktiveerida kaheastmeline autentimine. + description_html: Kui paistab tundmatuid tegevusi, palun vaheta salasõna ja aktiveeri kaheastmeline autentimine. empty: Autentimisajalugu pole saadaval failed_sign_in_html: Nurjunud sisenemine meetodiga %{method} aadressilt %{ip} (%{browser}) successful_sign_in_html: Edukas sisenemine meetodiga %{method} aadressilt %{ip} (%{browser}) @@ -1973,9 +1973,11 @@ et: enabled: Vanade postituste automaatne kustutamine enabled_hint: Kustutab automaatselt postitused, mis ületavad määratud ajalimiiti, välja arvatud allpool toodud erandite puhul exceptions: Erandid + explanation: Automaatne kustutamine toimib madala prioriteetusega protsessi abil. Vanuse ülempiiri ja tegeliku kustutamise vahel võib seetõttu tekkida viivitus. ignore_favs: Eira lemmikuid ignore_reblogs: Eira jagamisi interaction_exceptions: Interaktsioonidel põhinevad erandid + interaction_exceptions_explanation: Postitused, mis ajutiselt ületavad lemmikuks lisamise või hooandmise künnise, võivad jääda alles isegi siis, kui nimetatud tegevuste arv hiljem väheneb. keep_direct: Säilita otsesõnumid keep_direct_hint: Ei kustuta otsesõnumeid keep_media: Meedialisanditega postituste säilitamine @@ -2190,6 +2192,7 @@ et: error: Turvavõtme kustutamisel tekkis tõrge. Palun proovi uuesti. success: Turvavõtmed edukalt kustutatud. invalid_credential: Vigane turvavõti + nickname: Hüüdnimi nickname_hint: Uue turvavõtme hüüdnimi not_enabled: Veebiautentimine pole sisse lülitatud not_supported: See veebilehitseja ei toeta turvavõtmeid diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 514a7f77836..c2e5ef0750e 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1973,9 +1973,11 @@ fi: enabled: Poista vanhat julkaisut automaattisesti enabled_hint: Poistaa julkaisusi automaattisesti, kun ne saavuttavat valitun ikäkynnyksen, ellei jokin alla olevista poikkeuksista tule kyseeseen exceptions: Poikkeukset + explanation: Automaattipoisto suoritetaan pienellä prioriteetilla. Ikäkynnyksen saavuttamisen ja poistetuksi tulemisen välillä saattaa olla viive. ignore_favs: Ohita suosikit ignore_reblogs: Ohita tehostukset interaction_exceptions: Vuorovaikutuksiin perustuvat poikkeukset + interaction_exceptions_explanation: Julkaisut, jotka ylittävät väliaikaisesti suosikki- tai tehostusrajan, voidaan säilyttää, vaikka rajat myöhemmin alittuisivat. keep_direct: Säilytä yksityisviestit keep_direct_hint: Ei poista yksityisviestejäsi keep_media: Säilytä julkaisut, joissa on medialiitteitä diff --git a/config/locales/fo.yml b/config/locales/fo.yml index c69fd8306ca..a531e0bc13c 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -2190,6 +2190,7 @@ fo: error: Ein trupulleiki var við at strika trygdarlykilin hjá tær. Vinarliga royn aftur. success: Trygdarlykilin hjá tær varð strikaður. invalid_credential: Ógyldugur trygdarlykil + nickname: Kallinavn nickname_hint: Skriva eyknevni á tínum nýggja trygdarlykli not_enabled: Tú hevur ikki gjørt WebAuthn virkið enn not_supported: Hesin kagin stuðlar ikki uppundir trygdarlyklar diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 0349e1795ed..9024a66eb9c 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -2104,9 +2104,11 @@ ga: enabled: Scrios seanphostálacha go huathoibríoch enabled_hint: Scriostar do phostálacha go huathoibríoch nuair a shroicheann siad tairseach aoise sonraithe, ach amháin má thagann siad le ceann de na heisceachtaí thíos exceptions: Eisceachtaí + explanation: Déantar scriosadh uathoibrithe le tosaíocht íseal. D’fhéadfadh moill a bheith ann idir an aois a shroichtear agus an bhaint. ignore_favs: Tabhair neamhaird ar toghanna ignore_reblogs: Déan neamhaird de boosts interaction_exceptions: Eisceachtaí bunaithe ar idirghníomhaíochtaí + interaction_exceptions_explanation: Féadfar poist a sháraíonn an tairseach is fearr leat nó an tairseach borrtha a choinneáil fiú má laghdaítear iad níos déanaí. keep_direct: Coinnigh teachtaireachtaí díreacha keep_direct_hint: Ní scriosann sé aon cheann de do theachtaireachtaí díreacha keep_media: Coinnigh postálacha le ceangaltáin meán @@ -2322,6 +2324,7 @@ ga: error: Bhí fadhb ann agus d'eochair shlándála á scriosadh. Arís, le d'thoil. success: Scriosadh d'eochair shlándála go rathúil. invalid_credential: Eochair shlándála neamhbhailí + nickname: Leasainm nickname_hint: Cuir isteach leasainm d'eochair shlándála nua not_enabled: Níl WebAuthn cumasaithe agat fós not_supported: Ní thacaíonn an brabhsálaí seo le heochracha slándála diff --git a/config/locales/gd.yml b/config/locales/gd.yml index b637a9eb71f..543cd0cb460 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -7,6 +7,8 @@ gd: hosted_on: Mastodon ’ga òstadh air %{domain} title: Mu dhèidhinn accounts: + errors: + cannot_be_added_to_collections: Cha ghabh an cunntas seo a chur ri cruinneachadh. followers: few: Luchd-leantainn one: Neach-leantainn @@ -874,6 +876,7 @@ gd: publish_statistics: Foillsich an stadastaireachd title: Rùrachadh trends: Treandaichean + wrapstodon: Wrapstodon domain_blocks: all: Dhan a h-uile duine disabled: Na seall idir @@ -1354,6 +1357,13 @@ gd: hint_html: "Gliocas: Chan iarr sinn am facal-faire agad ort a-rithist fad uair a thìde." invalid_password: Facal-faire mì-dhligheach prompt: Dearbh am facal-faire airson leantainn air adhart + color_scheme: + auto: Fèin-obrachail + dark: Dorcha + light: Soilleir + contrast: + auto: Fèin-obrachail + high: Àrd crypto: errors: invalid_key: "– chan e iuchair Ed25519 no Curve25519 dhligheach a th’ ann" @@ -1784,16 +1794,22 @@ gd: body: 'Thug %{name} iomradh ort an-seo:' subject: Thug %{name} iomradh ort title: Iomradh ùr + moderation_warning: + subject: Fhuair thu rabhadh on mhaorsainneachd poll: subject: Thàinig cunntas-bheachd le %{name} gu crìoch quote: body: 'Chaidh post a luaidh le %{name}:' subject: Luaidh %{name} am post agad title: Luaidh ùr + quoted_update: + subject: Dheasaich %{name} post a luaidh thu reblog: body: 'Chaidh am post agad a bhrosnachadh le %{name}:' subject: Bhrosnaich %{name} am post agad title: Brosnachadh ùr + severed_relationships: + subject: Chaill thu dàimhean ri linn co-dhùnadh na maorsainneachd status: subject: Tha %{name} air post a sgrìobhadh update: @@ -2038,14 +2054,16 @@ gd: public: Poblach public_long: Neach sam bith taobh a-staigh no a-muigh Mhastodon unlisted: Sàmhach - unlisted_long: Falaichte o na toraidhean-luirg, na treandaichean ’s na loichnichean-ama poblach + unlisted_long: Falaichte o na toraidhean-luirg, na treandaichean ’s na loidhnichean-ama poblach statuses_cleanup: enabled: Sguab às seann-phostaichean gu fèin-obrachail enabled_hint: Sguabaidh seo às na seann-phostaichean agad gu fèin-obrachail nuair a ruigeas iad stairsneach aoise sònraichte ach ma fhreagras iad ri gin dhe na h-eisgeachdan gu h-ìosal exceptions: Eisgeachdan + explanation: Tha prìomhachas ìosal air an sguabadh às fhèin-obrachail. Dh’fhaoidte gum bi dàil eadar ruigsinn stairsneach na h-aoise agus an toirt air falbh. ignore_favs: Leig seachad na h-annsachdan ignore_reblogs: Leig seachad na brosnachaidhean interaction_exceptions: Eisgeachdan stèidhichte air eadar-ghnìomhan + interaction_exceptions_explanation: Faodaidh postaichean mairsinn ma thèid iad thar stairsneach nan annsachdan no brosnachaidhean rè seal fiù ma bhios an cunntas as ìsle an uairsin. keep_direct: Cùm na teachdaireachdan dìreach keep_direct_hint: Cha dèid gin dhe na teachdaireachdan dìreach agad a sguabadh às keep_media: Cùm postaichean le ceanglachan meadhain @@ -2260,8 +2278,12 @@ gd: error: Bha duilgheadas ann le bhith a’ sguabadh às an iuchair tèarainteachd agad. Feuch ris a-rithist. success: Chaidh an iuchair tèarainteachd agad a sguabadh às. invalid_credential: Iuchair tèarainteachd mì-dhligheach + nickname: Far-ainm nickname_hint: Cuir a-steach far-ainm na h-iuchrach tèarainteachd ùir agad not_enabled: Cha do chuir thu WebAuthn an comas fhathast not_supported: Cha chuir am brabhsair seo taic ri iuchraichean tèarainteachd otp_required: Mus cleachd thu iuchraichean tèarainteachd, feumaidh tu an dearbhadh dà-cheumnach a chur an comas. registered_on: Air a chlàradh %{date} + wrapstodon: + description: Seall mar a chleachd %{name} Mastodon am bliadhna! + title: Wrapstodon %{year} dha %{name} diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 3de76f110fb..6c9ed9b6c98 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -2190,6 +2190,7 @@ gl: error: Houbo un problema ó eliminar a túa chave de seguridade, inténtao outra vez. success: Eliminouse correctamente a chave de seguridade. invalid_credential: Chave de seguridade non válida + nickname: Sobrenome nickname_hint: Escribe un alcume para a túa nova chave de seguridade not_enabled: Aínda non tes activado WebAuthn not_supported: Este navegador non ten soporte para chaves de seguridade diff --git a/config/locales/it.yml b/config/locales/it.yml index 9e6e91e0a4b..49719320e3f 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -2190,6 +2190,7 @@ it: error: Si è verificato un problema durante la cancellazione della chiave di sicurezza. Dovresti riprovare. success: La chiave di sicurezza è stata cancellata. invalid_credential: Chiave di sicurezza non valida + nickname: Soprannome nickname_hint: Inserisci il soprannome della tua nuova chiave di sicurezza not_enabled: Non hai ancora abilitato WebAuthn not_supported: Questo browser non supporta le chiavi di sicurezza diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 62346c68597..f0ede70b9d5 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1748,7 +1748,7 @@ ko: unrecognized_emoji: 인식 되지 않은 에모지입니다 redirects: prompt: 이 링크를 믿을 수 있다면, 클릭해서 계속하세요. - title: "%{instance}를 떠나려고 합니다." + title: "%{instance}을(를) 떠나려고 합니다." relationships: activity: 계정 활동 confirm_follow_selected_followers: 정말로 선택된 팔로워들을 팔로우하시겠습니까? diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 33028404502..6666a4d7bd8 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -1033,7 +1033,7 @@ lt: merge_long: Išsaugoti esančius įrašus ir pridėti naujus overwrite: Perrašyti overwrite_long: Pakeisti senus įrašus naujais - preface: Gali importuoti duomenis, kuriuos eksportavai iš kito serverio, pavyzdžiui, sekamų arba blokuojamų žmonių sąrašą. + preface: Galite importuoti duomenis, kuriuos eksportavote iš kito serverio kaip sekamų arba blokuojamų žmonių sąrašą. success: Jūsų informacija sėkmingai įkelta ir bus apdorota kaip įmanoma greičiau types: blocking: Blokuojamų sąrašas diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 5fdeb84200a..c19edeeed10 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -1681,6 +1681,74 @@ nan: subject: Lí有收著管理ê警告 poll: subject: "%{name} 舉辦ê投票suah ah" + quote: + body: Lí ê PO文hōo %{name} 引用: + subject: "%{name} 引用lí ê PO文" + title: 新ê引用 + quoted_update: + subject: "%{name} 編輯lí有引用ê PO文" + reblog: + body: Lí ê PO文hōo %{name} 轉送: + subject: "%{name} 轉送lí ê PO文" + title: 新ê轉送 + severed_relationships: + subject: 因為管理ê決定,lí失去聯絡ah + status: + subject: "%{name} 頭tú-á PO文" + update: + subject: "%{name} 有編輯PO文" + notifications: + administration_emails: 管理員ê電子phue通知 + email_events: 電子phue通知ê事件 + email_events_hint: 揀lí想beh收ê通知ê事件: + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T + otp_authentication: + code_hint: 請輸入lí ê驗證應用程式生成ê code,來確認 + description_html: Nā lí啟用用驗證應用程式ê雙因素驗證,登入需要lí有手機á,伊ê產生token hōo lí輸入。 + enable: 啟用 + instructions_html: "請用lí手機á ê TOTP應用程式(親像Google Authenticator)掃描tsit ê QR code。對tsit-má起,hit ê應用程式ê生token,佇lí登入ê時陣著輸入。" + manual_instructions: 若lí bē當掃描 QR code,需要手動輸入下kha ê光碼: + setup: 設定 + wrong_code: Lí輸入ê碼無效!服侍器時間kap設備時間kám lóng正確? + pagination: + newer: 較新ê + next: 後一頁 + older: 較舊ê + prev: 頂一頁 + truncate: "…" + polls: + errors: + already_voted: Lí有投票ah + duplicate_options: 包含重覆ê項目 + duration_too_long: 傷久ah + duration_too_short: 傷緊ah + expired: 投票結束ah + invalid_choice: 所揀ê選項無佇leh + over_character_limit: Bē當比 %{max} ê字元tsē + self_vote: Lí bē當佇lí開ê投票投 + too_few_options: 項目定著愛超過一ê + too_many_options: Bē當包含超過 %{max} ê項目 + vote: 投票 + posting_defaults: + explanation: Tsiah ê設定ē用做預設ê值,若lí開新ê PO文,毋過lí會當佇編輯器內底,個別編輯PO文。 + preferences: + other: 其他 + posting_defaults: PO文預設值 + public_timelines: 公共ê時間線 + privacy: + hint_html: "自訂lí beh án-nuá hōo lí ê個人資料kap PO文受lâng發現。Mastodon ê tsē-tsē功能nā是拍開,通幫tsān lí接觸kàu較闊ê觀眾。請開一寡時間重看tsiah ê設定,確認in合lí ê使用例。" + privacy: 隱私權 + privacy_hint_html: 控制lí想欲為別lâng ê利益,公開guā tsē內容。Lâng用瀏覽別lâng ê跟tuè koh看in用啥物應用程式PO文,來發現心適ê個人資料kap時行ê應用程式,但是lí凡勢beh保持khàm起來。 + reach: 接觸 scheduled_statuses: too_soon: Tio̍h用未來ê日期。 statuses: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 9d92e42a541..792a4e28666 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -2190,6 +2190,7 @@ nn: error: Det oppsto et problem med å slette sikkerhetsnøkkelen. Prøv igjen. success: Sikkerhetsnøkkelen din ble vellykket slettet. invalid_credential: Ugyldig sikkerhetsnøkkel + nickname: Kallenamn nickname_hint: Skriv inn kallenavnet til din nye sikkerhetsnøkkel not_enabled: Du har ikke aktivert WebAuthn ennå not_supported: Denne nettleseren støtter ikke sikkerhetsnøkler diff --git a/config/locales/pl.yml b/config/locales/pl.yml index ce998071be9..86f015878ac 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -866,10 +866,21 @@ pl: publish_statistics: Publikuj statystyki title: Odkrywanie trends: Trendy + wrapstodon: Wrapstodon domain_blocks: all: Każdemu disabled: Nikomu users: Zalogowanym lokalnym użytkownikom + feed_access: + modes: + authenticated: tylko zalogowani użytkownicy + disabled: Wymagaj określonej roli użytkownika + public: Wszyscy + landing_page: + values: + about: O nas + local_feed: Lokalny kanał + trends: Trendy registrations: moderation_recommandation: Upewnij się, że masz adekwatny i szybko reagujący zespół moderacyjny przed otwarciem rejestracji! preamble: Kontroluj, kto może utworzyć konto na Twoim serwerze. @@ -923,6 +934,7 @@ pl: no_status_selected: Żaden wpis nie został zmieniony, bo żaden nie został wybrany open: Otwarty post original_status: Oryginalny post + quotes: Cytaty reblogs: Podbicia replied_to_html: Odpowiedziano na %{acct_link} status_changed: Post zmieniony @@ -1209,6 +1221,7 @@ pl: hint_html: Jeżeli chcesz przenieść się z innego konta na to, możesz utworzyć alias, który jest wymagany zanim zaczniesz przenoszenie obserwacji z poprzedniego konta na to. To działanie nie wyrządzi szkód i jest odwracalne. Migracja konta jest inicjowana ze starego konta. remove: Odłącz alias appearance: + advanced_settings: Ustawienia zaawansowane animations_and_accessibility: Animacje i dostępność discovery: Odkrywanie localization: @@ -1326,6 +1339,13 @@ pl: hint_html: "Informacja: Nie będziemy prosić Cię o ponowne podanie hasła przez następną godzinę." invalid_password: Nieprawidłowe hasło prompt: Potwierdź hasło, aby kontynuować + color_scheme: + auto: Automatyczne + dark: Ciemny + light: Jasny + contrast: + auto: Automatyczny + high: Wysoki crypto: errors: invalid_key: nie jest prawidłowym kluczem Ed25519 lub Curve25519 @@ -1651,6 +1671,10 @@ pl: expires_at: Wygaśnie po uses: Użycia title: Zaproś użytkowników + link_preview: + author_html: Przez %{name} + potentially_sensitive_content: + action: Kliknij, aby pokazać lists: errors: limit: Przekroczono maksymalną liczbę utworzonych list diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index b850cabb6e4..080603d66c0 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -243,6 +243,8 @@ cy: setting_always_send_emails: Anfonwch hysbysiadau e-bost bob amser setting_auto_play_gif: Chwarae GIFs wedi'u hanimeiddio yn awtomatig setting_boost_modal: Rheoli hybu gwelededd + setting_color_scheme: Modd + setting_contrast: Cyferbyniad setting_default_language: Iaith postio setting_default_privacy: Gwelededd postio setting_default_quote_policy: Pwy sy'n gallu dyfynnu @@ -317,6 +319,7 @@ cy: thumbnail: Bawdlun y gweinydd trendable_by_default: Caniatáu pynciau llosg heb adolygiad trends: Galluogi pynciau llosg + wrapstodon: Galluogi Wrapstodon interactions: must_be_follower: Blocio hysbysiadau o bobl nad ydynt yn eich dilyn must_be_following: Blocio hysbysiadau o bobl nad ydych yn eu dilyn diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 98defb79cdb..a39cf443594 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -175,7 +175,7 @@ de: labels: account: attribution_domains: Websites, die auf dich verweisen dürfen - discoverable: Profil und Beiträge in Suchalgorithmen berücksichtigen + discoverable: Profil und Beiträge in Empfehlungsalgorithmen berücksichtigen fields: name: Beschriftung value: Inhalt diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index 9c77f798daa..3c06421aee9 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -16,7 +16,7 @@ el: account_migration: acct: Όρισε το username@domain του λογαριασμού στον οποίο θέλεις να μετακινηθείς account_warning_preset: - text: Μπορείς να χρησιμοποιήσεις το ίδιο συντακτικό των αναρτήσεων όπως URL, ετικέτες και αναφορές + text: Μπορείς να χρησιμοποιήσεις το ίδιο συντακτικό των αναρτήσεων όπως URL, ετικέτες και επισημάνσεις title: Προαιρετικό. Δεν εμφανίζεται στον παραλήπτη admin_account_action: include_statuses: Ο χρήστης θα δει ποιες αναρτήσεις προκάλεσαν την προειδοποίηση ή την ενέργεια των συντονιστών @@ -90,7 +90,7 @@ el: backups_retention_period: Οι χρήστες έχουν τη δυνατότητα να δημιουργήσουν αρχεία των αναρτήσεων τους για να κατεβάσουν αργότερα. Όταν οριστεί μια θετική τιμή, αυτά τα αρχεία θα διαγράφονται αυτόματα από τον αποθηκευτικό σου χώρο μετά τον καθορισμένο αριθμό ημερών. bootstrap_timeline_accounts: Αυτοί οι λογαριασμοί θα καρφιτσωθούν στην κορυφή των προτεινόμενων ακολουθήσεων για νέους χρήστες. Παρέχετε μια λίστα λογαριασμών χωρισμένη με κόμμα. closed_registrations_message: Εμφανίζεται όταν κλείνουν οι εγγραφές - content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές αναφορές μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. + content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές επισημάνσεις μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστοσελίδας του Mastodon. favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. landing_page: Επιλέγει ποια σελίδα βλέπουν οι νέοι επισκέπτες όταν φτάνουν για πρώτη φορά στο διακομιστή σας. Αν επιλέξετε "Τάσεις", τότε οι τάσεις πρέπει να είναι ενεργοποιημένες στις Ρυθμίσεις Ανακάλυψης. Αν επιλέξετε "Τοπική ροή", τότε το "Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις" πρέπει να οριστεί σε "Όλοι" στις Ρυθμίσεις Ανακάλυψης. @@ -117,7 +117,7 @@ el: imports: data: Αρχείο CSV που έχει εξαχθεί από διαφορετικό διακομιστή Mastodon invite_request: - text: Αυτό θα μας βοηθήσει να επιθεωρήσουμε την αίτησή σου + text: Αυτό θα μας βοηθήσει να ελέγξουμε την αίτησή σου ip_block: comment: Προαιρετικό. Θυμηθείτε γιατί προσθέσατε αυτόν τον κανόνα. expires_in: Οι διευθύνσεις IP είναι ένας πεπερασμένος πόρος, μερικές φορές μοιράζονται και συχνά αλλάζουν χέρια. Για το λόγο αυτό, δεν συνιστώνται αόριστοι αποκλεισμοί διευθύνσεων IP. @@ -190,7 +190,7 @@ el: text: Προκαθορισμένο κείμενο title: Τίτλος admin_account_action: - include_statuses: Συμπερίληψη των καταγγελλομένων τουτ στο email + include_statuses: Συμπερίληψη των αναφερόμενων αναρτήσεων στο email send_email_notification: Ενημέρωση χρήστη μέσω email text: Προσαρμοσμένη προειδοποίηση type: Ενέργεια @@ -313,7 +313,7 @@ el: status_page_url: URL σελίδας κατάστασης theme: Προεπιλεγμένο θέμα thumbnail: Μικρογραφία διακομιστή - trendable_by_default: Επίτρεψε τις τάσεις χωρίς προηγούμενη αξιολόγηση + trendable_by_default: Επίτρεψε τις τάσεις χωρίς προηγούμενο έλεγχο trends: Ενεργοποίηση τάσεων wrapstodon: Ενεργοποίηση Wrapstodon interactions: @@ -339,7 +339,7 @@ el: follow: Κάποιος σε ακολούθησε follow_request: Κάποιος ζήτησε να σε ακολουθήσει mention: Κάποιος σε επισήμανε - pending_account: Νέος λογαριασμός χρειάζεται αναθεώρηση + pending_account: Νέος λογαριασμός χρειάζεται έλεγχο quote: Κάποιος σε παρέθεσε reblog: Κάποιος ενίσχυσε την ανάρτηση σου report: Υποβλήθηκε νέα αναφορά @@ -349,7 +349,7 @@ el: label: Μια νέα έκδοση του Mastodon είναι διαθέσιμη none: Να μην ειδοποιούμαι ποτέ για ενημερώσεις (δεν συνιστάται) patch: Ειδοποίηση για ενημερώσεις σφαλμάτων - trending_tag: Νέο περιεχόμενο προς τάση απαιτεί αξιολόγηση + trending_tag: Νέο περιεχόμενο προς τάση απαιτεί έλεγχο rule: hint: Επιπρόσθετες πληροφορίες text: Κανόνας diff --git a/config/locales/simple_form.et.yml b/config/locales/simple_form.et.yml index d51e816d2e5..2e45c6ffa07 100644 --- a/config/locales/simple_form.et.yml +++ b/config/locales/simple_form.et.yml @@ -51,7 +51,7 @@ et: inbox_url: Kopeeri soovitud sõnumivahendusserveri avalehe võrguaadress irreversible: Filtreeritud postitused kaovad taastamatult, isegi kui filter on hiljem eemaldatud locale: Kasutajaliidese, e-kirjade ja tõuketeadete keel - password: Vajalik on vähemalt 8 märki + password: Vajalik on vähemalt 8 tähemärki phrase: Kattub olenemata postituse teksti suurtähtedest või sisuhoiatusest scopes: Milliseid API-sid see rakendus tohib kasutada. Kui valid kõrgeima taseme, ei pea üksikuid eraldi valima. setting_advanced_layout: Näita Mastodoni mitme veeruga paigutuses, mispuhul näed korraga nii ajajoont, teavitusi, kui sinu valitud kolmandat veergu. Ei sobi kasutamiseks väikeste ekraanide puhul. @@ -113,7 +113,7 @@ et: trends: Trendid näitavad, millised postitused, teemaviited ja uudislood koguvad sinu serveris tähelepanu. wrapstodon: Paku kohalikele kasutajatele luua nende Mastodoni kasutamise aastast mänguline kokkuvõte. See võimalus on saadaval igal aastal 10. ja 31. detsembri vahel ja seda pakutakse kasutajatele, kes tegid vähemalt ühe avaliku või vaikse avaliku postituse ja kes kasutas aasta jooksul vähemalt ühte silti. form_challenge: - current_password: Turvalisse alasse sisenemine + current_password: Sisened turvalisse alasse imports: data: CSV fail eksporditi teisest Mastodoni serverist invite_request: @@ -214,8 +214,8 @@ et: avatar: Profiilipilt bot: See konto on robot chosen_languages: Keelte filtreerimine - confirm_new_password: Uue salasõna kinnitamine - confirm_password: Salasõna kinnitamine + confirm_new_password: Korda uut salasõna + confirm_password: Korda salasõna context: Filtreeri kontekste current_password: Kehtiv salasõna data: Andmed diff --git a/config/locales/simple_form.gd.yml b/config/locales/simple_form.gd.yml index e11b300b33a..2673988df10 100644 --- a/config/locales/simple_form.gd.yml +++ b/config/locales/simple_form.gd.yml @@ -88,6 +88,7 @@ gd: activity_api_enabled: Cunntasan nam postaichean a chaidh fhoillseachadh gu h-ionadail, nan cleachdaichean gnìomhach ’s nan clàraidhean ùra an am bucaidean seachdaineil app_icon: WEBP, PNG, GIF no JPG. Tar-àithnidh seo ìomhaigheag bhunaiteach na h-aplacaid air uidheaman mobile le ìomhaigheag ghnàthaichte. backups_retention_period: "’S urrainn do chleachdaichean tasg-lannan dhe na postaichean aca a gintinn airson an luchdadh a-nuas an uairsin. Nuair a bhios luach dearbh air, thèid na tasg-lannan a sguabadh às on stòras agad gu fèin-obrachail às dèidh an àireamh de làithean a shònraich thu." + bootstrap_timeline_accounts: Thèid na cunntasan seo a phrìneachadh air bàrr nam molaidhean leantainn dhan luchd-cleachdaidh ùr. Solar liosta de chunntasan sgaraichte le cromagan. closed_registrations_message: Thèid seo a shealltainn nuair a bhios an clàradh dùinte content_cache_retention_period: Thèid a h-uile post o fhrithealaiche sam bith eile (a’ gabhail a-staigh brosnachaidhean is freagairtean) a sguabadh às às dèidh na h-àireimh de làithean a shònraich thu ’s gun diù a chon air eadar-ghabhail ionadail air na postaichean ud. Gabhaidh seo a-steach na postaichean a chuir cleachdaiche ionadail ris na h-annsachdan aca no comharran-lìn riutha. Thèid iomraidhean prìobhaideach eadar cleachdaichean o ionstansan diofraichte air chall cuideachd agus cha ghabh an aiseag idir. Tha an roghainn seo do dh’ionstansan sònraichte a-mhàin agus briseadh e dùilean an luchd-cleachdaidh nuair a rachadh a chleachdadh gu coitcheann. custom_css: "’S urrainn dhut stoidhlean gnàthaichte a chur an sàs air an tionndadh-lìn de Mhastodon." @@ -110,6 +111,7 @@ gd: thumbnail: Dealbh mu 2:1 a thèid a shealltainn ri taobh fiosrachadh an fhrithealaiche agad. trendable_by_default: Geàrr leum thar lèirmheas a làimh na susbainte a’ treandadh. Gabhaidh nithean fa leth a thoirt far nan treandaichean fhathast an uairsin. trends: Seallaidh na treandaichean na postaichean, tagaichean hais is naidheachdan a tha fèill mhòr orra air an fhrithealaiche agad. + wrapstodon: Tairg gintinn geàrr-chunntais àbhaich air mar a chleachd iad Mastodon rè a’ bhliadhna dhan luchd-cleachdaidh ionadail. Bidh an gleus seo ri fhaighinn eadar an 10mh is 31mh dhen Dùbhlachd gach bliadhna ’s thèid a thairgsinn dhan luchd-cleachdaidh a rinn co-dhiù aon post poblach no sàmhach ’s a chleachd co-dhiù aon taga hais rè a’ bhliadhna. form_challenge: current_password: Tha thu a’ tighinn a-steach gu raon tèarainte imports: @@ -239,6 +241,8 @@ gd: setting_always_send_emails: Cuir brathan puist-d an-còmhnaidh setting_auto_play_gif: Cluich GIFs beòthaichte gu fèin-obrachail setting_boost_modal: Smachd air faicsinneachd nam brosnachaidhean + setting_color_scheme: Modh + setting_contrast: Iomsgaradh setting_default_language: Cànan postaidh setting_default_privacy: Faicsinneachd nam post setting_default_quote_policy: Cò dh’fhaodas luaidh @@ -313,6 +317,7 @@ gd: thumbnail: Dealbhag an fhrithealaiche trendable_by_default: Ceadaich treandaichean gun lèirmheas ro làimh trends: Cuir na treandaichean an comas + wrapstodon: Cuir Wrapstodon an comas interactions: must_be_follower: Bac na brathan nach eil o luchd-leantainn must_be_following: Bac na brathan o dhaoine nach lean thu @@ -373,7 +378,9 @@ gd: jurisdiction: Uachdranas laghail min_age: An aois as lugha user: + date_of_birth_1i: Bliadhna date_of_birth_2i: Mìos + date_of_birth_3i: Latha role: Dreuchd time_zone: Roinn-tìde user_role: diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 192a30a53ae..3f186e1c0ba 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -237,6 +237,8 @@ pl: setting_always_send_emails: Zawsze wysyłaj powiadomienia e-mail setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy setting_boost_modal: Kontroluj widoczność podbić + setting_color_scheme: Tryb + setting_contrast: Kontrast setting_default_language: Język wpisów setting_default_privacy: Widoczność wpisów setting_default_quote_policy: Kto może cytować @@ -286,6 +288,7 @@ pl: content_cache_retention_period: Okres zachowywania zdalnych treści custom_css: Niestandardowy CSS favicon: Favicon + landing_page: Strona docelowa dla nowych odwiedzających local_live_feed_access: Uzyskaj dostęp do kanałów zawierających lokalne wpisy local_topic_feed_access: Uzyskaj dostęp do hashtagów i linków zawierających lokalne wpisy mascot: Własna ikona @@ -310,6 +313,7 @@ pl: thumbnail: Miniaturka serwera trendable_by_default: Zezwalaj na trendy bez wcześniejszego przeglądu trends: Włącz trendy + wrapstodon: Włącz Wrapstodon interactions: must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie obserwują must_be_following: Nie wyświetlaj powiadomień od osób, których nie obserwujesz @@ -370,7 +374,9 @@ pl: jurisdiction: Jurysdykcja min_age: Wiek minimalny user: + date_of_birth_1i: Rok date_of_birth_2i: Miesiąc + date_of_birth_3i: Dzień role: Rola time_zone: Strefa czasowa user_role: diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index e87dd799d2a..7cddf46aaf8 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -73,6 +73,7 @@ sl: featured_tag: name: 'Tukaj je nekaj ključnikov, ki ste jih nedavno uporabili:' filters: + action: Izberite, kako naj se program vede, ko se objava sklada s filtrom actions: hide: Povsem skrij filtrirano vsebino, kot da ne bi obstajala warn: Skrij filtrirano vsebino za opozorilom, ki pomenja naslov filtra @@ -220,6 +221,8 @@ sl: setting_always_send_emails: Vedno pošlji e-obvestila setting_auto_play_gif: Samodejno predvajanje animiranih GIF-ov setting_boost_modal: Nadziraj vidnost objav + setting_color_scheme: Način + setting_contrast: Kontrast setting_default_language: Jezik objavljanja setting_default_privacy: Vidnost objav setting_default_quote_policy: Kdo lahko citira @@ -231,6 +234,7 @@ sl: setting_display_media_default: Privzeto setting_display_media_hide_all: Skrij vse setting_display_media_show_all: Prikaži vse + setting_emoji_style: Slog čustvenih simbolov setting_expand_spoilers: Vedno razširi objave, označene z opozorili o vsebini setting_hide_network: Skrij svoje omrežje setting_reduce_motion: Zmanjšanje premikanja v animacijah @@ -285,6 +289,7 @@ sl: thumbnail: Sličica strežnika trendable_by_default: Dovoli trende brez predhodnega pregleda trends: Omogoči trende + wrapstodon: Omogoči Wrapstodon interactions: must_be_follower: Blokiraj obvestila nesledilcev must_be_following: Blokiraj obvestila oseb, ki jim ne sledite diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index f3997a91c14..524ec6c8b1a 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -65,6 +65,7 @@ sv: setting_display_media_hide_all: Dölj alltid all media setting_display_media_show_all: Visa alltid media markerad som känslig setting_emoji_style: Hur emojier visas. "Automatiskt" kommer att försöka använda webbläsarens emojier, men faller tillbaka till Twemoji för äldre webbläsare. + setting_quick_boosting_html: När aktiverad, klicka på %{boost_icon} Boost-ikonen för att omedelbart boosta istället för att öppna boost/citera-rullgardinsmenyn. Flyttar citering till %{options_icon} (Alternativ)-menyn. setting_system_scrollbars_ui: Gäller endast för webbläsare som är baserade på Safari och Chrome setting_use_blurhash: Gradienter är baserade på färgerna av de dolda objekten men fördunklar alla detaljer setting_use_pending_items: Dölj tidslinjeuppdateringar bakom ett klick istället för att automatiskt bläddra i flödet @@ -78,6 +79,7 @@ sv: featured_tag: name: 'Här är några av de hashtaggar du använt nyligen:' filters: + action: Välj vilken åtgärd som ska utföras när ett inlägg matchar filtret actions: blur: Dölj media bakom en varning utan att dölja själva texten hide: Dölj det filtrerade innehållet helt, beter sig som om det inte fanns @@ -86,10 +88,12 @@ sv: activity_api_enabled: Antalet lokalt publicerade inlägg, aktiva användare och nya registrerade konton per vecka app_icon: WEBP, PNG, GIF eller JPG. Använd istället för appens egna ikon på mobila enheter. backups_retention_period: Användare har möjlighet att generera arkiv av sina inlägg för att ladda ned senare. När det sätts till ett positivt värde raderas dessa arkiv automatiskt från din lagring efter det angivna antalet dagar. + bootstrap_timeline_accounts: Dessa konton kommer att fästas på toppen av nya användares följ-rekommendationer. Ange en kommaseparerad lista över konton. closed_registrations_message: Visas när nyregistreringar är avstängda content_cache_retention_period: Alla inlägg från andra servrar (inklusive booster och svar) kommer att raderas efter det angivna antalet dagar, utan hänsyn till någon lokal användarinteraktion med dessa inlägg. Detta inkluderar inlägg där en lokal användare har markerat det som bokmärke eller favoriter. Privata omnämnanden mellan användare från olika instanser kommer också att gå förlorade och blir omöjliga att återställa. Användningen av denna inställning är avsedd för specialfall och bryter många användarförväntningar när de implementeras för allmänt bruk. custom_css: Du kan använda anpassade stilar på webbversionen av Mastodon. favicon: WEBP, PNG, GIF eller JPG. Används på mobila enheter istället för appens egen ikon. + landing_page: Väljer vilken sida nya besökare ser när de först anländer till din server. Om du väljer "Trender" måste trenderna aktiveras i Upptäckningsinställningarna. Om du väljer "Lokalt flöde" måste "Åtkomst till live-flöden med lokala inlägg" sättas till "Alla" i Upptäckningsinställningarna. mascot: Åsidosätter illustrationen i det avancerade webbgränssnittet. media_cache_retention_period: Mediafiler från inlägg som gjorts av fjärranvändare cachas på din server. När inställd på ett positivt värde kommer media att raderas efter det angivna antalet dagar. Om mediadatat begärs efter att det har raderats, kommer det att laddas ned igen om källinnehållet fortfarande är tillgängligt. På grund av begränsningar för hur ofta förhandsgranskningskort för länkar hämtas från tredjepartswebbplatser, rekommenderas det att ange detta värde till minst 14 dagar, annars kommer förhandsgranskningskorten inte att uppdateras på begäran före den tiden. min_age: Användare kommer att bli ombedda att bekräfta sitt födelsedatum under registreringen @@ -107,6 +111,7 @@ sv: thumbnail: En bild i cirka 2:1-proportioner som visas tillsammans med din serverinformation. trendable_by_default: Hoppa över manuell granskning av trendande innehåll. Enskilda objekt kan ändå raderas från trender retroaktivt. trends: Trender visar vilka inlägg, hashtaggar och nyheter det pratas om på din server. + wrapstodon: Erbjud lokala användare att generera en lekfull sammanfattning av deras Mastodon-användning under året. Denna funktion är tillgänglig mellan den 10 och 31 december varje år, och erbjuds till användare som gjort minst ett Offentligt eller Tyst Offentligt inlägg och använt minst en hashtag under året. form_challenge: current_password: Du går in i ett säkert område imports: @@ -233,6 +238,7 @@ sv: setting_aggregate_reblogs: Gruppera boostar i tidslinjer setting_always_send_emails: Skicka alltid e-postnotiser setting_auto_play_gif: Spela upp GIF:ar automatiskt + setting_boost_modal: Kontrollera boost-synlighet setting_color_scheme: Läge setting_contrast: Kontrast setting_default_language: Inläggsspråk diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 2fc26113588..2abd3600e70 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -7,6 +7,8 @@ sl: hosted_on: Mastodon gostuje na %{domain} title: O programu accounts: + errors: + cannot_be_added_to_collections: Tega računa ni možno dodati v zbirke. followers: few: Sledilci one: Sledilec @@ -196,6 +198,7 @@ sl: create_relay: Ustvari rele create_unavailable_domain: Ustvari domeno, ki ni na voljo create_user_role: Ustvari vlogo + create_username_block: Ustvari pravilo uporabniškega imena demote_user: Ponižaj uporabnika destroy_announcement: Izbriši obvestilo destroy_canonical_email_block: Izbriši blokado e-naslova @@ -209,6 +212,7 @@ sl: destroy_status: Izbriši objavo destroy_unavailable_domain: Izbriši nedosegljivo domeno destroy_user_role: Uniči vlogo + destroy_username_block: Izbriši pravilo uporabniškega imena disable_2fa_user: Onemogoči disable_custom_emoji: Onemogoči emotikon po meri disable_relay: Onemogoči rele @@ -243,6 +247,7 @@ sl: update_report: Posodobi poročilo update_status: Posodobi objavo update_user_role: Posodobi vlogo + update_username_block: Posodobi pravilo uporabniškega imena actions: approve_appeal_html: "%{name} je ugodil pritožbi uporabnika %{target} na moderatorsko odločitev" approve_user_html: "%{name} je odobril/a registracijo iz %{target}" @@ -498,21 +503,30 @@ sl: fasp: debug: callbacks: + created_at: Ustvarjeno delete: Izbriši ip: Naslov IP + request_body: Telo zahteve providers: + active: Dejaven base_url: Osnovna povezava callback: Povratni klic delete: Izbriši edit: Uredi ponudnika finish_registration: Dokončaj registracijo name: Ime + providers: Ponudniki + public_key_fingerprint: Prstni odtis javnega ključa + registration_requested: Registracija je obvezna registrations: confirm: Potrdi reject: Zavrni + title: Potrdri registracijo FASP save: Shrani sign_in: Prijava status: Stanje + title: Ponudniki dodatnih storitev Fediverse + title: FASP follow_recommendations: description_html: "Sledi priporočilom pomaga novim uporabnikom, da hitro najdejo zanimivo vsebino. Če uporabnik ni dovolj komuniciral z drugimi, da bi oblikoval prilagojena priporočila za sledenje, se namesto tega priporočajo ti računi. Dnevno se ponovno izračunajo iz kombinacije računov z najvišjimi nedavnimi angažiranostmi in najvišjim številom krajevnih sledilcev za določen jezik." language: Za jezik @@ -844,10 +858,15 @@ sl: publish_statistics: Objavi statistiko title: Razkrivanje trends: Trendi + wrapstodon: Wrapstodon domain_blocks: all: Vsem disabled: Nikomur users: Prijavljenim krajevnim uporabnikom + feed_access: + modes: + authenticated: Samo overjeni uporabniki + public: Vsi landing_page: values: trends: Trendi @@ -1103,13 +1122,23 @@ sl: trending: V porastu username_blocks: add_new: Dodaj novo + block_registrations: Blokiraj registracije comparison: contains: Vsebuje + equals: Je enako contains_html: Vsebuje %{string} + created_msg: Pravilo uporabniškega imena uspešno ustvarjeno delete: Izbriši + edit: + title: Uredi pravilo uporabniškega imena + matches_exactly_html: Je enako %{string} new: create: Ustvari pravilo + title: Ustvari novo pravilo uporabniškega imena + no_username_block_selected: Nobeno pravilo uporabniškega imena ni bilo spremenjeno, ker nobeno ni bilo izbrano not_permitted: Ni dovoljeno + title: Pravila uporabniškega imena + updated_msg: Pravilo uporabniškega imena uspešno posodobljeno warning_presets: add_new: Dodaj novo delete: Izbriši @@ -1302,6 +1331,10 @@ sl: hint_html: "Namig: naslednjo uro vas ne bomo več vprašali po vašem geslu." invalid_password: Neveljavno geslo prompt: Potrdite geslo za nadaljevanje + color_scheme: + auto: Samodejno + contrast: + auto: Samodejno crypto: errors: invalid_key: ni veljaven ključ Ed25519 ali Curve25519 @@ -1629,6 +1662,7 @@ sl: author_html: Avtor/ica %{name} potentially_sensitive_content: action: Kliknite za prikaz + confirm_visit: Ali ste prepričani, da želite odpreti to povezavo? hide_button: Skrij lists: errors: @@ -1727,10 +1761,13 @@ sl: body: "%{name} vas je omenil/a v:" subject: "%{name} vas je omenil/a" title: Nova omemba + moderation_warning: + subject: Prejeli ste opozorilo moderatorjev poll: subject: Anketa, ki jo je pripravil/a %{name}, se je iztekla quote: body: 'Vašo objavo je citiral/a %{name}:' + subject: "%{name} je citiral/a vašo objavo" title: Nov citat reblog: body: 'Vašo objavo je izpostavil/a %{name}:' @@ -1962,6 +1999,7 @@ sl: reblog: Izpostavitev ne more biti pripeta quote_error: not_available: Objava ni na voljo + revoked: Avtor je umaknil objavo quote_policies: followers: Samo sledilci nobody: Samo jaz @@ -2191,8 +2229,11 @@ sl: error: Pri brisanju vašega varnostnega ključa je prišlo do težav. Poskusite znova. success: Vaš varnostni ključ je bil uspešno izbrisan. invalid_credential: Neveljaven varnostni ključ + nickname: Vzdevek nickname_hint: Vnesite vzdevek svojega novega varnostnega ključa not_enabled: Niste še omogočili WebAuthn not_supported: Ta brskalnik ne podpira varnostnih ključev otp_required: Za uporabo varnostnih ključev morate najprej omogočiti 2FA (dvostopenjsko overjanje). registered_on: Datum registracije %{date} + wrapstodon: + title: Wrapstodon %{year} za %{name} diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 5b50f5846d7..d390baf720e 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -7,6 +7,8 @@ sv: hosted_on: Mastodon-värd på %{domain} title: Om accounts: + errors: + cannot_be_added_to_collections: Detta konto kan inte läggas till i samlingar. followers: one: Följare other: Följare @@ -1714,12 +1716,16 @@ sv: body: 'Du nämndes av %{name} i:' subject: Du nämndes av %{name} title: Ny omnämning + moderation_warning: + subject: Du har mottagit en modereringsvarning poll: subject: En undersökning av %{name} har avslutats quote: body: 'Ditt inlägg citerades av %{name}:' subject: "%{name} citerade ditt inlägg" title: Nytt citat + quoted_update: + subject: "%{name} redigerade ett inlägg du har citerat" reblog: body: 'Ditt inlägg boostades av %{name}:' subject: "%{name} boostade ditt inlägg" @@ -2192,3 +2198,4 @@ sv: registered_on: Registrerad den %{date} wrapstodon: description: Se hur %{name} använde Mastodon i år! + title: Wrapstodon %{year} för %{name} diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 22afa3ab19f..4f1c03a46a9 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -2146,6 +2146,7 @@ zh-CN: error: 删除你的安全密钥时出错。请重试。 success: 你的安全密钥已成功删除。 invalid_credential: 无效的安全密钥 + nickname: 昵称 nickname_hint: 输入你的新安全密钥的昵称 not_enabled: 你尚未启用WebAuthn not_supported: 此浏览器不支持安全密钥 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index c9609eb10e3..100c57ab1ca 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1437,7 +1437,7 @@ zh-TW: statuses_hint_html: 此過濾器會套用至所選之各別嘟文,無論其是否符合下列關鍵字。審閱或自過濾條件移除嘟文。 title: 編輯過濾條件 errors: - deprecated_api_multiple_keywords: 這些參數無法自此應用程式中更改,因為它們適用於一或多個過濾器關鍵字。請使用較新的應用程式或是網頁介面。 + deprecated_api_multiple_keywords: 這些參數無法自此應用程式中更改,因為它們適用於一個以上之過濾器關鍵字。請使用較新的應用程式或是網頁介面。 invalid_context: 沒有提供內文或內文無效 index: contexts: "%{contexts} 中的過濾器" @@ -1737,7 +1737,7 @@ zh-TW: invalid_choice: 您所選的投票選項並不存在 over_character_limit: 不能多於 %{max} 個字元 self_vote: 您無法於您的嘟文投票 - too_few_options: 必須包含至少一個項目 + too_few_options: 必須包含一個項目以上 too_many_options: 不能包含多於 %{max} 個項目 vote: 投票 posting_defaults: diff --git a/config/routes.rb b/config/routes.rb index 3685e695f91..bf50b67fe13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -96,13 +96,14 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } concern :account_resources do + resources :collections, only: [:show], constraints: { id: /\d+/ } 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] + resources :collections, only: [:show], as: :actor_collections, constraints: { id: Regexp.union(ActivityPub::CollectionsController::SUPPORTED_COLLECTIONS) } resource :followers_synchronization, only: [:show] resources :quote_authorizations, only: [:show] end @@ -123,6 +124,8 @@ Rails.application.routes.draw do scope path: 'ap', as: 'ap' do resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do + resources :featured_collections, only: [:index], module: :activitypub + resources :statuses, only: [:show] do member do get :activity diff --git a/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb b/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb new file mode 100644 index 00000000000..25aad0732ed --- /dev/null +++ b/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class UseSnowflakeIdsForCollections < ActiveRecord::Migration[8.0] + def up + safety_assured do + execute(<<~SQL.squish) + ALTER TABLE collections ALTER COLUMN id SET DEFAULT timestamp_id('collections'); + ALTER TABLE collection_items ALTER COLUMN id SET DEFAULT timestamp_id('collection_items'); + SQL + end + + Mastodon::Snowflake.ensure_id_sequences_exist + end + + def down + execute(<<~SQL.squish) + LOCK collections; + SELECT setval('collections_id_seq', (SELECT MAX(id) FROM collections)); + ALTER TABLE collections ALTER COLUMN id SET DEFAULT nextval('collections_id_seq'); + LOCK collection_items; + SELECT setval('collection_items_id_seq', (SELECT MAX(id) FROM collection_items)); + ALTER TABLE collection_items ALTER COLUMN id SET DEFAULT nextval('collection_items_id_seq'); + SQL + end +end diff --git a/db/migrate/20260119153538_add_language_to_collections.rb b/db/migrate/20260119153538_add_language_to_collections.rb new file mode 100644 index 00000000000..066288b070a --- /dev/null +++ b/db/migrate/20260119153538_add_language_to_collections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddLanguageToCollections < ActiveRecord::Migration[8.0] + def change + add_column :collections, :language, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 78d7ef68261..8801882808d 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_12_17_091936) do +ActiveRecord::Schema[8.0].define(version: 2026_01_19_153538) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -352,7 +352,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do t.index ["reference_account_id"], name: "index_canonical_email_blocks_on_reference_account_id" end - create_table "collection_items", force: :cascade do |t| + create_table "collection_items", id: :bigint, default: -> { "timestamp_id('collection_items'::text)" }, force: :cascade do |t| t.bigint "collection_id", null: false t.bigint "account_id" t.integer "position", default: 1, null: false @@ -369,7 +369,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do t.index ["object_uri"], name: "index_collection_items_on_object_uri", unique: true, where: "(activity_uri IS NOT NULL)" end - create_table "collections", force: :cascade do |t| + create_table "collections", id: :bigint, default: -> { "timestamp_id('collections'::text)" }, force: :cascade do |t| t.bigint "account_id", null: false t.string "name", null: false t.text "description", null: false @@ -382,6 +382,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "item_count", default: 0, null: false + t.string "language" t.index ["account_id"], name: "index_collections_on_account_id" t.index ["tag_id"], name: "index_collections_on_tag_id" end diff --git a/docker-compose.yml b/docker-compose.yml index d4974eb1bdd..52d2a83f443 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.4 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.5.4 + image: ghcr.io/mastodon/mastodon-streaming:v4.5.5 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.4 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 235ac92cbd6..f532276f85e 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'alpha.2' + 'alpha.3' end def prerelease diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 011e165ed74..7141adc9edc 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -16,8 +16,7 @@ module Paperclip # if we're processing the original, close + unlink the source tempfile intermediate_files << original if name == :original - @queued_for_write[name] = style.processors - .inject(original) do |file, processor| + @queued_for_write[name] = style.processors.inject(original) do |file, processor| file = Paperclip.processor(processor).make(file, style.processor_options, self) intermediate_files << file unless file == original file diff --git a/package.json b/package.json index 8a1ecc77374..acdd57e3fd6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "private": true, "dependencies": { - "@csstools/stylelint-formatter-github": "^1.0.0", + "@csstools/stylelint-formatter-github": "^2.0.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -85,7 +85,7 @@ "lodash": "^4.17.21", "marky": "^1.2.5", "path-complete-extname": "^1.0.0", - "postcss-preset-env": "^10.1.5", + "postcss-preset-env": "^11.0.0", "prop-types": "^15.8.1", "punycode": "^2.3.0", "react": "^18.2.0", @@ -175,7 +175,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-storybook": "^10.0.2", "fake-indexeddb": "^6.0.1", - "globals": "^16.0.0", + "globals": "^17.0.0", "husky": "^9.0.11", "lint-staged": "^16.2.6", "msw": "^2.12.1", @@ -186,7 +186,7 @@ "storybook": "^10.0.5", "stylelint": "^16.19.1", "stylelint-config-prettier-scss": "^1.0.0", - "stylelint-config-standard-scss": "^16.0.0", + "stylelint-config-standard-scss": "^17.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.45.0", "typescript-plugin-css-modules": "^5.2.0", diff --git a/spec/fabricators/collection_fabricator.rb b/spec/fabricators/collection_fabricator.rb index a6a8411ba00..7e0e14a765f 100644 --- a/spec/fabricators/collection_fabricator.rb +++ b/spec/fabricators/collection_fabricator.rb @@ -8,3 +8,10 @@ Fabricator(:collection) do sensitive false discoverable true end + +Fabricator(:remote_collection, from: :collection) do + account { Fabricate.build(:remote_account) } + local false + uri { sequence(:uri) { |i| "https://example.com/collections/#{i}" } } + original_number_of_items 0 +end diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 9eefa01f902..a4028857114 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -9,17 +9,10 @@ RSpec.describe ThemeHelper do context 'when using "system" theme' do let(:theme) { 'system' } - it 'returns the mastodon-light and application stylesheets with correct color schemes' do + it 'returns the default theme' do expect(html_links.first.attributes.symbolize_keys) .to include( - # This is now identical to the default theme & will be unified very soon - href: have_attributes(value: match(/default/)), - media: have_attributes(value: 'not all and (prefers-color-scheme: dark)') - ) - expect(html_links.last.attributes.symbolize_keys) - .to include( - href: have_attributes(value: match(/default/)), - media: have_attributes(value: '(prefers-color-scheme: dark)') + href: have_attributes(value: match(/default/)) ) end end @@ -41,20 +34,19 @@ RSpec.describe ThemeHelper do it 'returns the theme stylesheet without color scheme information' do expect(html_links.first.attributes.symbolize_keys) .to include( - href: have_attributes(value: match(/contrast/)), - media: have_attributes(value: 'all') + href: have_attributes(value: match(/default/)) ) end end end describe 'theme_color_tags' do - let(:result) { helper.theme_color_tags(theme) } + let(:result) { helper.theme_color_tags(color_scheme) } context 'when using system theme' do - let(:theme) { 'system' } + let(:color_scheme) { 'auto' } - it 'returns the mastodon-light and default stylesheets with correct color schemes' do + it 'returns both color schemes with appropriate media queries' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:dark]), @@ -68,10 +60,10 @@ RSpec.describe ThemeHelper do end end - context 'when using mastodon-light theme' do - let(:theme) { 'mastodon-light' } + context 'when light color scheme' do + let(:color_scheme) { 'light' } - it 'returns the theme stylesheet without color scheme information' do + it 'returns the light color' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:light]) @@ -79,10 +71,10 @@ RSpec.describe ThemeHelper do end end - context 'when using other theme' do - let(:theme) { 'contrast' } + context 'when using dark color scheme' do + let(:color_scheme) { 'dark' } - it 'returns the theme stylesheet without color scheme information' do + it 'returns the dark color' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:dark]) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1e8a2a29db4..19b6014af15 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with a reply' do + context 'with a reply without explicitly setting a conversation' do let(:original_status) { Fabricate(:status) } let(:object_json) do @@ -493,6 +493,30 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'with a reply explicitly setting a conversation' do + let(:original_status) { Fabricate(:status) } + + let(:object_json) do + build_object( + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation), + context: ActivityPub::TagManager.instance.uri_for(original_status.conversation) + ) + end + + it 'creates status' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.thread).to eq original_status + expect(status.reply?).to be true + expect(status.in_reply_to_account).to eq original_status.account + expect(status.conversation).to eq original_status.conversation + end + end + context 'with mentions' do let(:recipient) { Fabricate(:account) } diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 6cbb58055e5..a15529057cb 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -192,6 +192,23 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_for(status.conversation)).to eq status.conversation.uri end end + + context 'with a local collection' do + let(:collection) { Fabricate(:collection) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(collection)) + .to eq("#{host_prefix}/ap/users/#{collection.account.id}/collections/#{collection.id}") + end + end + + context 'with a remote collection' do + let(:collection) { Fabricate(:remote_collection) } + + it 'returns the expected URL' do + expect(subject.uri_for(collection)).to eq collection.uri + end + end end describe '#key_uri_for' do @@ -612,14 +629,6 @@ RSpec.describe ActivityPub::TagManager do end end - describe '#uri_to_local_id' do - let(:account) { Fabricate(:account, id_scheme: :username_ap_id) } - - it 'returns the local ID' do - expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username - end - end - describe '#uris_to_local_accounts' do it 'returns the expected local accounts' do account = Fabricate(:account) diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 38203a55f70..927214bb40c 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -54,12 +54,44 @@ RSpec.describe TagManager do end describe '#normalize_domain' do - it 'returns nil if the given parameter is nil' do - expect(described_class.instance.normalize_domain(nil)).to be_nil + subject { described_class.instance.normalize_domain(domain) } + + context 'with a nil value' do + let(:domain) { nil } + + it { is_expected.to be_nil } end - it 'returns normalized domain' do - expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' + context 'with a blank value' do + let(:domain) { '' } + + it { is_expected.to be_blank } + end + + context 'with a mixed case string' do + let(:domain) { 'DoMaIn.Example.com' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with a trailing slash string' do + let(:domain) { 'domain.example.com/' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with a space padded string' do + let(:domain) { ' domain.example.com ' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with an invalid domain string' do + let(:domain) { ' !@#$@#$@$@# ' } + + it 'raises invalid uri error' do + expect { subject }.to raise_error(Addressable::URI::InvalidURIError) + end end end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 659e0178694..b50969b68a9 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -16,12 +16,18 @@ RSpec.describe Collection do it { is_expected.to_not allow_value(nil).for(:discoverable) } + it { is_expected.to allow_value('en').for(:language) } + + it { is_expected.to_not allow_value('randomstuff').for(:language) } + context 'when collection is remote' do subject { Fabricate.build :collection, local: false } it { is_expected.to validate_presence_of(:uri) } it { is_expected.to validate_presence_of(:original_number_of_items) } + + it { is_expected.to allow_value('randomstuff').for(:language) } end context 'when using a hashtag as category' do @@ -126,4 +132,10 @@ RSpec.describe Collection do end end end + + describe '#object_type' do + it 'returns `:featured_collection`' do + expect(subject.object_type).to eq :featured_collection + end + end end diff --git a/spec/models/concerns/account/interactions_spec.rb b/spec/models/concerns/account/interactions_spec.rb index cc50c465517..5bca7959080 100644 --- a/spec/models/concerns/account/interactions_spec.rb +++ b/spec/models/concerns/account/interactions_spec.rb @@ -450,6 +450,44 @@ RSpec.describe Account::Interactions do end end + describe '#blocking_or_domain_blocking?' do + subject { account.blocking_or_domain_blocking?(target_account) } + + context 'when blocking target_account' do + before do + account.block_relationships.create(target_account: target_account) + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + + expect(result).to be true + end + end + + context 'when blocking the domain' do + let(:target_account) { Fabricate(:remote_account) } + + before do + account_domain_block = Fabricate(:account_domain_block, domain: target_account.domain) + account.domain_blocks << account_domain_block + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + expect(result).to be true + end + end + + context 'when blocking neither target_account nor its domain' do + it 'returns false' do + expect(subject).to be false + end + end + end + describe '#muting?' do subject { account.muting?(target_account) } diff --git a/spec/models/instance_moderation_note_spec.rb b/spec/models/instance_moderation_note_spec.rb index 4d77d497122..011b001cc72 100644 --- a/spec/models/instance_moderation_note_spec.rb +++ b/spec/models/instance_moderation_note_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe InstanceModerationNote do describe 'chronological' do it 'returns the instance notes sorted by oldest first' do - instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain('mastodon.example')) + instance = Instance.find_or_initialize_by(domain: 'mastodon.example') note1 = Fabricate(:instance_moderation_note, domain: instance.domain) note2 = Fabricate(:instance_moderation_note, domain: instance.domain) diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index f877bded252..96fcbdb4d82 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -188,4 +188,24 @@ RSpec.describe AccountPolicy do end end end + + permissions :index_collections? do + it 'permits when no user is given' do + expect(subject).to permit(nil, john) + end + + it 'permits unblocked users' do + expect(subject).to permit(john, john) + expect(subject).to permit(alice, john) + end + + it 'denies blocked users' do + domain_blocked_user = Fabricate(:remote_account) + john.block_domain!(domain_blocked_user.domain) + john.block!(alice) + + expect(subject).to_not permit(domain_blocked_user, john) + expect(subject).to_not permit(alice, john) + end + end end diff --git a/spec/policies/announcement_policy_spec.rb b/spec/policies/announcement_policy_spec.rb index 2fec34f8e4b..a78b016a61c 100644 --- a/spec/policies/announcement_policy_spec.rb +++ b/spec/policies/announcement_policy_spec.rb @@ -3,20 +3,45 @@ require 'rails_helper' RSpec.describe AnnouncementPolicy do - let(:policy) { described_class } + subject { described_class } + let(:admin) { Fabricate(:admin_user).account } let(:john) { Fabricate(:account) } permissions :index?, :create?, :update?, :destroy? do context 'with an admin' do - it 'permits' do - expect(policy).to permit(admin, Announcement) - end + it { is_expected.to permit(admin, Announcement) } end context 'with a non-admin' do - it 'denies' do - expect(policy).to_not permit(john, Announcement) + it { is_expected.to_not permit(john, Announcement) } + end + end + + permissions :distribute? do + let(:announcement) { Fabricate :announcement } + + context 'with non admin role' do + it { is_expected.to_not permit(john, announcement) } + end + + context 'with admin role' do + context 'with unpublished announcement' do + let(:announcement) { Fabricate :announcement, published: false, scheduled_at: 5.days.from_now } + + it { is_expected.to_not permit(admin, announcement) } + end + + context 'with published already sent announcement' do + let(:announcement) { Fabricate :announcement, notification_sent_at: 3.days.ago } + + it { is_expected.to_not permit(admin, announcement) } + end + + context 'with published not sent announcement' do + let(:announcement) { Fabricate :announcement } + + it { is_expected.to permit(admin, announcement) } end end end diff --git a/spec/policies/collection_policy_spec.rb b/spec/policies/collection_policy_spec.rb index 156e1a76572..ecef6e899d9 100644 --- a/spec/policies/collection_policy_spec.rb +++ b/spec/policies/collection_policy_spec.rb @@ -16,11 +16,23 @@ RSpec.describe CollectionPolicy do end permissions :show? do - it 'permits everyone to show' do + it 'permits when no user is given' do expect(policy).to permit(nil, collection) + end + + it 'permits unblocked users' do expect(policy).to permit(owner, collection) expect(policy).to permit(other_user, collection) end + + it 'denies blocked users' do + domain_blocked_user = Fabricate(:remote_account) + owner.block_domain!(domain_blocked_user.domain) + owner.block!(other_user) + + expect(policy).to_not permit(domain_blocked_user, collection) + expect(policy).to_not permit(other_user, collection) + end end permissions :create? do diff --git a/spec/policies/instance_moderation_note_policy_spec.rb b/spec/policies/instance_moderation_note_policy_spec.rb new file mode 100644 index 00000000000..66b4e7f937f --- /dev/null +++ b/spec/policies/instance_moderation_note_policy_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe InstanceModerationNotePolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :create? do + context 'when admin' do + it { is_expected.to permit(admin, InstanceModerationNote.new) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, InstanceModerationNote.new) } + end + end + + permissions :destroy? do + context 'when owner of note' do + let(:note) { Fabricate :instance_moderation_note, account: account } + + it { is_expected.to permit(account, note) } + end + + context 'when not owner of note' do + context 'when admin and overrides' do + let(:note) { Fabricate :instance_moderation_note } + + it { is_expected.to permit(admin, note) } + end + + context 'when admin and does not override' do + let(:note) { Fabricate :instance_moderation_note, account: Fabricate(:admin_user).account } + + it { is_expected.to_not permit(admin, note) } + end + end + end +end diff --git a/spec/policies/media_attachment_policy_spec.rb b/spec/policies/media_attachment_policy_spec.rb new file mode 100644 index 00000000000..d194cd819c6 --- /dev/null +++ b/spec/policies/media_attachment_policy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe MediaAttachmentPolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :download? do + context 'when attachment is on private discarded status' do + let(:media_attachment) { Fabricate.build :media_attachment, status: Fabricate.build(:status, deleted_at: 5.days.ago, visibility: :private) } + + context 'when admin' do + it { is_expected.to permit(admin, media_attachment) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, media_attachment) } + end + end + + context 'when attachment is on public status' do + let(:media_attachment) { Fabricate.build :media_attachment, status: Fabricate.build(:status, visibility: :public) } + + context 'when admin' do + it { is_expected.to permit(admin, media_attachment) } + end + + context 'when not admin' do + it { is_expected.to permit(account, media_attachment) } + end + end + end +end diff --git a/spec/policies/quote_policy_spec.rb b/spec/policies/quote_policy_spec.rb new file mode 100644 index 00000000000..71708a9b715 --- /dev/null +++ b/spec/policies/quote_policy_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe QuotePolicy do + subject { described_class } + + let(:account) { Fabricate(:account) } + + permissions :revoke? do + context 'when quote matches the revoking account' do + let(:quote) { Fabricate.build :quote, quoted_account_id: account.id } + + it { is_expected.to permit(account, quote) } + end + + context 'when quote does not match the revoking account' do + let(:quote) { Fabricate.build :quote, quoted_account_id: Fabricate(:account).id } + + it { is_expected.to_not permit(account, quote) } + end + + context 'when quote does not have quoted account id' do + let(:quote) { Fabricate.build :quote } + + it { is_expected.to_not permit(account, quote) } + end + end +end diff --git a/spec/policies/username_block_policy_spec.rb b/spec/policies/username_block_policy_spec.rb new file mode 100644 index 00000000000..5092f71274d --- /dev/null +++ b/spec/policies/username_block_policy_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UsernameBlockPolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :index?, :create?, :update?, :destroy? do + context 'when admin' do + it { is_expected.to permit(admin, UsernameBlock.new) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, UsernameBlock.new) } + end + end +end diff --git a/spec/requests/activitypub/collections_spec.rb b/spec/requests/activitypub/collections_spec.rb index d2761f98ea3..39bd2252e78 100644 --- a/spec/requests/activitypub/collections_spec.rb +++ b/spec/requests/activitypub/collections_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'ActivityPub Collections' do end describe 'GET #show' do - subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } + subject { get account_actor_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } context 'when id is "featured"' do let(:id) { 'featured' } @@ -131,16 +131,5 @@ RSpec.describe 'ActivityPub Collections' do end end end - - context 'when id is not "featured"' do - let(:id) { 'hoge' } - - it 'returns http not found' do - subject - - expect(response) - .to have_http_status(404) - end - end end end diff --git a/spec/requests/activitypub/featured_collections_spec.rb b/spec/requests/activitypub/featured_collections_spec.rb new file mode 100644 index 00000000000..09a17c53bea --- /dev/null +++ b/spec/requests/activitypub/featured_collections_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Collections' do + describe 'GET /ap/users/@:account_id/featured_collections', feature: :collections do + subject { get ap_account_featured_collections_path(account.id, format: :json) } + + let(:collection) { Fabricate(:collection) } + let(:account) { collection.account } + + context 'when signed out' do + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http forbidden' do + subject + + expect(response) + .to have_http_status(403) + end + end + + context 'when account is accessible' do + it 'renders ActivityPub Collection successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'Collection', + 'totalItems' => 1, + 'first' => match(%r{^https://.*page=1.*$}), + }) + end + + context 'when requesting the first page' do + subject { get ap_account_featured_collections_path(account.id, page: 1, format: :json) } + + context 'when account has many collections' do + before do + Fabricate.times(5, :collection, account:) + end + + it 'includes a link to the next page', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + + expect(response.parsed_body) + .to include({ + 'type' => 'CollectionPage', + 'totalItems' => 6, + 'next' => match(%r{^https://.*page=2.*$}), + }) + end + end + end + end + end + + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + post user_session_path, params: { user: { email: user.email, password: user.password } } + end + + context 'when account blocks user' do + before { account.block!(user.account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + end + + context 'with "HTTP Signature" access signed by a remote account' do + subject do + get ap_account_featured_collections_path(account.id, format: :json), + headers: nil, + sign_with: remote_account + end + + let(:remote_account) { Fabricate(:account, domain: 'host.example') } + + context 'when account blocks the remote account' do + before { account.block!(remote_account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'when account domain blocks the domain of the remote account' do + before { account.block_domain!(remote_account.domain) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'with JSON' do + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'Collection', + 'totalItems' => 1, + }) + end + end + end + end +end diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 5db9889e2d5..3fbf26c54a7 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') end end + + context 'when status has non-default quote policy and param is omitted' do + let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') } + + it 'preserves existing quote approval policy' do + expect { subject } + .to_not(change { status.reload.quote_approval_policy }) + end + end end end diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1_alpha/collections_spec.rb index 3921fabfde8..de79dcf7230 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -55,6 +55,32 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do ) end end + + context 'when some collections are not discoverable' do + before do + Fabricate(:collection, account:, discoverable: false) + end + + context 'when requesting user is a third party' do + it 'hides the collections that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 3 + end + end + + context 'when requesting user owns the collection' do + let(:account) { user.account } + + it 'returns all collections, including the ones that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 4 + end + end + end end describe 'GET /api/v1_alpha/collections/:id' do @@ -115,6 +141,7 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do { name: 'Low-traffic bots', description: 'Really nice bots, please follow', + language: 'en', sensitive: '0', discoverable: '1', } diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 21830d1b1c1..88c0302f860 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -163,9 +163,10 @@ RSpec.describe 'API Web Push Subscriptions' do end describe 'PUT /api/web/push_subscriptions/:id' do - before { sign_in Fabricate :user } + before { sign_in user } - let(:subscription) { Fabricate :web_push_subscription } + let(:user) { Fabricate(:user) } + let(:subscription) { Fabricate(:web_push_subscription, user: user) } it 'gracefully handles invalid nested params' do put api_web_push_subscription_path(subscription), params: { data: 'invalid' } diff --git a/spec/requests/collections_spec.rb b/spec/requests/collections_spec.rb new file mode 100644 index 00000000000..fece4b62b82 --- /dev/null +++ b/spec/requests/collections_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Collections' do + describe 'GET /@:account_username/collections/:id', feature: :collections do + subject { get account_collection_path(account, collection, format: :json) } + + let(:collection) { Fabricate(:collection) } + let(:account) { collection.account } + + context 'when signed out' do + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http forbidden' do + subject + + expect(response) + .to have_http_status(403) + end + end + + context 'when account is accessible' do + context 'with JSON' do + subject { get ap_account_collection_path(account.id, collection, format: :json) } + + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'FeaturedCollection', + 'name' => collection.name, + }) + end + end + end + end + + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + post user_session_path, params: { user: { email: user.email, password: user.password } } + end + + context 'when account blocks user' do + before { account.block!(user.account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + end + + context 'with "HTTP Signature" access signed by a remote account' do + subject do + get account_collection_path(account, collection, format: :json), + headers: nil, + sign_with: remote_account + end + + let(:remote_account) { Fabricate(:account, domain: 'host.example') } + + context 'when account blocks the remote account' do + before { account.block!(remote_account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'when account domain blocks the domain of the remote account' do + before { account.block_domain!(remote_account.domain) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'with JSON' do + subject do + get ap_account_collection_path(account.id, collection, format: :json), + headers: nil, + sign_with: remote_account + end + + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'FeaturedCollection', + 'name' => collection.name, + }) + end + end + end + end +end diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb index 0a58a03ffad..c84c3802f24 100644 --- a/spec/requests/content_security_policy_spec.rb +++ b/spec/requests/content_security_policy_spec.rb @@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do img-src 'self' data: blob: #{local_domain} manifest-src 'self' #{local_domain} media-src 'self' data: #{local_domain} - script-src 'self' #{local_domain} 'wasm-unsafe-eval' + script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Z5KW83D+6/pygIQS3h9XDpF52xW3l3BHc7JL9tj3uMs=' style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ==' worker-src 'self' blob: #{local_domain} CSP diff --git a/spec/serializers/activitypub/collection_serializer_spec.rb b/spec/serializers/activitypub/collection_serializer_spec.rb index 7726df914f2..d7099ba3d5a 100644 --- a/spec/serializers/activitypub/collection_serializer_spec.rb +++ b/spec/serializers/activitypub/collection_serializer_spec.rb @@ -35,5 +35,11 @@ RSpec.describe ActivityPub::CollectionSerializer do it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } end + + context 'with a Collection' do + let(:model) { Collection.new } + + it { is_expected.to eq(ActivityPub::FeaturedCollectionSerializer) } + end end end diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb new file mode 100644 index 00000000000..e6bb4ea4b0f --- /dev/null +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FeaturedCollectionSerializer do + subject { serialized_record_json(collection, described_class, adapter: ActivityPub::Adapter) } + + let(:collection) do + Fabricate(:collection, + name: 'Incredible people', + description: 'These are really amazing', + tag_name: '#people', + discoverable: false) + end + let!(:collection_items) { Fabricate.times(2, :collection_item, collection:) } + + it 'serializes to the expected structure' do + expect(subject).to include({ + 'type' => 'FeaturedCollection', + 'id' => ActivityPub::TagManager.instance.uri_for(collection), + 'name' => 'Incredible people', + 'summary' => 'These are really amazing', + 'attributedTo' => ActivityPub::TagManager.instance.uri_for(collection.account), + 'sensitive' => false, + 'discoverable' => false, + 'topic' => { + 'href' => match(%r{/tags/people$}), + 'type' => 'Hashtag', + 'name' => '#people', + }, + 'totalItems' => 2, + 'orderedItems' => [ + { + 'type' => 'FeaturedItem', + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.first.account), + 'featuredObjectType' => 'Person', + }, + { + 'type' => 'FeaturedItem', + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.last.account), + 'featuredObjectType' => 'Person', + }, + ], + 'published' => match_api_datetime_format, + 'updated' => match_api_datetime_format, + }) + end + + context 'when a language is set' do + before do + collection.language = 'en' + end + + it 'uses "summaryMap" to include the language' do + expect(subject).to include({ + 'summaryMap' => { + 'en' => 'These are really amazing', + }, + }) + + expect(subject).to_not have_key('summary') + end + end +end diff --git a/spec/serializers/rest/collection_serializer_spec.rb b/spec/serializers/rest/collection_serializer_spec.rb index f0baf7dff87..80ed6a559ed 100644 --- a/spec/serializers/rest/collection_serializer_spec.rb +++ b/spec/serializers/rest/collection_serializer_spec.rb @@ -18,6 +18,7 @@ RSpec.describe REST::CollectionSerializer do id: 2342, name: 'Exquisite follows', description: 'Always worth a follow', + language: 'en', local: true, sensitive: true, discoverable: false, @@ -31,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do 'id' => '2342', 'name' => 'Exquisite follows', 'description' => 'Always worth a follow', + 'language' => 'en', 'local' => true, 'sensitive' => true, 'discoverable' => false, diff --git a/spec/support/browser_errors.rb b/spec/support/browser_errors.rb index 6c101540a3a..860c8bd7316 100644 --- a/spec/support/browser_errors.rb +++ b/spec/support/browser_errors.rb @@ -4,6 +4,10 @@ module BrowserErrorsHelpers def ignore_js_error(error) @ignored_js_errors_for_spec << error end + + def error_message(error) + error.keys.map { |key| "#{key.to_s.titleize}: #{error[key]}" }.join("\n") + end end RSpec.configure do |config| @@ -15,7 +19,7 @@ RSpec.configure do |config| example.metadata[:js_console_messages] ||= [] Capybara.current_session.driver.with_playwright_page do |page| page.on('console', lambda { |msg| - example.metadata[:js_console_messages] << { type: msg.type, text: msg.text, location: msg.location } + example.metadata[:js_console_messages] << { type: msg.type, text: msg.text, location: msg.location, page: msg.page.url } }) end end @@ -34,7 +38,7 @@ RSpec.configure do |config| if errors.present? aggregate_failures 'browser errrors' do errors.each do |error| - expect(error[:type]).to_not eq('error'), error[:text] + expect(error[:type]).to_not eq('error'), error_message(error) next unless error[:type] == 'warning' warn 'WARN: browser warning' diff --git a/streaming/package.json b/streaming/package.json index 0f6651b741a..7684ed7cc85 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -36,7 +36,7 @@ "@types/express": "^5.0.5", "@types/pg": "^8.6.6", "@types/ws": "^8.5.9", - "globals": "^16.0.0", + "globals": "^17.0.0", "pino-pretty": "^13.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.28.0" diff --git a/yarn.lock b/yarn.lock index 233b83d0cbb..9c14c9e85e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1248,13 +1248,13 @@ __metadata: languageName: node linkType: hard -"@csstools/cascade-layer-name-parser@npm:^2.0.5": - version: 2.0.5 - resolution: "@csstools/cascade-layer-name-parser@npm:2.0.5" +"@csstools/cascade-layer-name-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/cascade-layer-name-parser@npm:3.0.0" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/b6c73d5c8132f922edc88b9df5272c93c9753945f1e1077b80d03b314076ffe03c2cc9bf6cbc85501ee7c7f27e477263df96997c9125fd2fd0cfe82fe2d7c141 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/657b325261dfa567e26b56a1a6a00fcfc877b9e95ce8ae3595ec95c89323fc2123dad7a4ca79bede8dec475ccc41d9d528470b0be01c4f22104f6962f2bfb40c languageName: node linkType: hard @@ -1265,6 +1265,13 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/color-helpers@npm:6.0.0" + checksum: 10c0/784447fa6ba2f5fec30f8676c48f9f66bff30a7a16582e3c3b3e9aa2574df1ac4e5f8e7455dfa6d991fd84ceceb22ff4016e2ea3a8116e76772b3972dbab7bec + languageName: node + linkType: hard + "@csstools/css-calc@npm:^2.1.4": version: 2.1.4 resolution: "@csstools/css-calc@npm:2.1.4" @@ -1275,6 +1282,16 @@ __metadata: languageName: node linkType: hard +"@csstools/css-calc@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/css-calc@npm:3.0.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/2f062db206dcdcb561a802d791aaf8b410f0e4d91ded89eb4075f75f1eafe8f5392c431a8d602d6ae660fca6299e02fcc555efdb235f3403ffc56ac6f14a1c2b + languageName: node + linkType: hard + "@csstools/css-color-parser@npm:^3.1.0": version: 3.1.0 resolution: "@csstools/css-color-parser@npm:3.1.0" @@ -1288,6 +1305,19 @@ __metadata: languageName: node linkType: hard +"@csstools/css-color-parser@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-color-parser@npm:4.0.0" + dependencies: + "@csstools/color-helpers": "npm:^6.0.0" + "@csstools/css-calc": "npm:^3.0.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/bbfa3855bd53d5f31e2e9d40d8bc7b1143c7efd3dd6fa43c0ef2222a66bdc94f7ffedc8932a4eb4f10c5cbc38d1ed110fda99240037e0a2dae61c6b25527b0a2 + languageName: node + linkType: hard + "@csstools/css-parser-algorithms@npm:^3.0.5": version: 3.0.5 resolution: "@csstools/css-parser-algorithms@npm:3.0.5" @@ -1297,6 +1327,15 @@ __metadata: languageName: node linkType: hard +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d + languageName: node + linkType: hard + "@csstools/css-syntax-patches-for-csstree@npm:1.0.14": version: 1.0.14 resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14" @@ -1320,6 +1359,13 @@ __metadata: languageName: node linkType: hard +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f + languageName: node + linkType: hard + "@csstools/media-query-list-parser@npm:^4.0.3": version: 4.0.3 resolution: "@csstools/media-query-list-parser@npm:4.0.3" @@ -1330,513 +1376,535 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-alpha-function@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-alpha-function@npm:1.0.1" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" +"@csstools/media-query-list-parser@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/media-query-list-parser@npm:5.0.0" peerDependencies: - postcss: ^8.4 - checksum: 10c0/35ca209e572534ade21ac5c18aad702aa492eb39e2d0e475f441371063418fe9650554e6a59b1318d3a615da83ef54d9a588faa27063ecc0a568ef7290a6b488 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/dbc22654769eca02c182f3a57be02cd5b8d0b958adc8397e66770b64b0e8fcd32faa93a3f6a99e1457bde11862485de3cd83a31dac7b03925d32f9891b31ccfd languageName: node linkType: hard -"@csstools/postcss-cascade-layers@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-cascade-layers@npm:5.0.2" - dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/dd8e29cfd3a93932fa35e3a59aa62fd2e720772d450f40f38f65ce1e736e2fe839635eb6f033abcc8ee8bc2856161a297f4458b352b26d2216856feb03176612 - languageName: node - linkType: hard - -"@csstools/postcss-color-function-display-p3-linear@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-color-function-display-p3-linear@npm:1.0.1" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d02d45410c9257f5620c766f861f8fa3762b74ef01fdba8060b33a4c98f929e2219cd476b25bd4181ac186158a4d99a0da555c0b6ba45a7ac4a3a5885baad1f5 - languageName: node - linkType: hard - -"@csstools/postcss-color-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-color-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/a355b04d90f89c8e37a4a23543151558060acc68fb2e7d1c3549bebeeae2b147eec26af1fbc6ee690f0ba4830263f2d181f5331d16d3483b5542be46996fa755 - languageName: node - linkType: hard - -"@csstools/postcss-color-mix-function@npm:^3.0.12": - version: 3.0.12 - resolution: "@csstools/postcss-color-mix-function@npm:3.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/3e98a5118852083d1f87a3f842f78088192b1f9f08fdf1f3b3ef1e8969e18fdadc1e3bcac3d113a07c8917a7e8fa65fdec55a31df9a1b726c8d7ae89db86e8e5 - languageName: node - linkType: hard - -"@csstools/postcss-color-mix-variadic-function-arguments@npm:^1.0.2": - version: 1.0.2 - resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:1.0.2" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/34073f0f0d33e4958f90763e692955a8e8c678b74284234497c4aa0d2143756e1b3616e0c09832caad498870e227ca0a681316afe3a71224fc40ade0ead1bdd9 - languageName: node - linkType: hard - -"@csstools/postcss-content-alt-text@npm:^2.0.8": - version: 2.0.8 - resolution: "@csstools/postcss-content-alt-text@npm:2.0.8" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/4c330cc2a1e434688a62613ecceb1434cd725ce024c1ad8d4a4c76b9839d1f3ea8566a8c6494921e2b46ec7feef6af8ed6548c216dcb8f0feab4b1d52c96228e - languageName: node - linkType: hard - -"@csstools/postcss-contrast-color-function@npm:^2.0.12": - version: 2.0.12 - resolution: "@csstools/postcss-contrast-color-function@npm:2.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/b783ce948cdf1513ee238e9115b42881a8d3e5d13c16038601b1c470d661cfaeeece4eea29904fb9fcae878bad86f766810fa798a703ab9ad4b0cf276b173f8f - languageName: node - linkType: hard - -"@csstools/postcss-exponential-functions@npm:^2.0.9": - version: 2.0.9 - resolution: "@csstools/postcss-exponential-functions@npm:2.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/78ea627a87fb23e12616c4e54150363b0e8793064634983dbe0368a0aca1ff73206c2d1f29845773daaf42787e7d1f180ce1b57c43e2b0d10da450101f9f34b6 - languageName: node - linkType: hard - -"@csstools/postcss-font-format-keywords@npm:^4.0.0": - version: 4.0.0 - resolution: "@csstools/postcss-font-format-keywords@npm:4.0.0" - dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/eb794fb95fefcac75e606d185255e601636af177866a317b0c6b6c375055e7240be53918229fd8d4bba00df01bedd2256bdac2b0ad4a4c2ec64f9d27cd6ff639 - languageName: node - linkType: hard - -"@csstools/postcss-gamut-mapping@npm:^2.0.11": - version: 2.0.11 - resolution: "@csstools/postcss-gamut-mapping@npm:2.0.11" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/490b8ccf10e30879a4415afbdd3646e1cdac3671586b7916855cf47a536f3be75eed014396056bde6528e0cb76d904e79bad78afc0b499e837264cf22519d145 - languageName: node - linkType: hard - -"@csstools/postcss-gradients-interpolation-method@npm:^5.0.12": - version: 5.0.12 - resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/70b3d6c7050ce882ed2281e71eb4493531ae8d55d21899920eeeb6c205d90aaf430419a66235484ccce3a1a1891367dfc0ef772f3866ae3a9d8ec5ddd0cfe894 - languageName: node - linkType: hard - -"@csstools/postcss-hwb-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-hwb-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d0dac34da9d7ac654060b6b27690a419718e990b21ff3e63266ea59934a865bc6aeae8eb8e1ca3e227a8b2a208657e3ab70ccdf0437f1f09d21ab848bbffcaa2 - languageName: node - linkType: hard - -"@csstools/postcss-ic-unit@npm:^4.0.4": - version: 4.0.4 - resolution: "@csstools/postcss-ic-unit@npm:4.0.4" - dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/20168e70ecb4abf7a69e407d653b6c7c9c82f2c7b1da0920e1d035f62b5ef8552cc7f1b62e0dca318df13c348e79fba862e1a4bb0e9432119a82b10aeb511752 - languageName: node - linkType: hard - -"@csstools/postcss-initial@npm:^2.0.1": +"@csstools/postcss-alpha-function@npm:^2.0.1": version: 2.0.1 - resolution: "@csstools/postcss-initial@npm:2.0.1" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/dbff7084ef4f1c4647efe2b147001daf172003c15b5e22689f0540d03c8d362f2a332cd9cf136e6c8dcda7564ee30492a4267ea188f72cb9c1000fb9bcfbfef8 - languageName: node - linkType: hard - -"@csstools/postcss-is-pseudo-class@npm:^5.0.3": - version: 5.0.3 - resolution: "@csstools/postcss-is-pseudo-class@npm:5.0.3" + resolution: "@csstools/postcss-alpha-function@npm:2.0.1" dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/7980f1cabf32850bac72552e4e9de47412359e36e259a92b9b9af25dae4cce42bbcc5fdca8f384a589565bf383ecb23dec3af9f084d8df18b82552318b2841b6 + checksum: 10c0/7e38e245eb4a4d76dcaafd9ee1f6d1cdfc8fceabf835dd1615fbb9fb0944224d29c8d601e55db4b3ba1dd37a541c8568fd130106171ca11b012eb512ff1ec7a9 languageName: node linkType: hard -"@csstools/postcss-light-dark-function@npm:^2.0.11": - version: 2.0.11 - resolution: "@csstools/postcss-light-dark-function@npm:2.0.11" +"@csstools/postcss-cascade-layers@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-cascade-layers@npm:6.0.0" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/0175be41bb0044a48bc98d5c55cce41ed6b9ada88253c5f20d0ca17287cba4b429742b458ac5744675b9a286109e13ac51d64e226ab16040d7b051ba64c0c77b + checksum: 10c0/f9cf3fa52d0f2615a69ad52067cc2229573151525792272eb82a28f0feca64f9ca54e8459c0ae5c254807507e4630aa9866690a4b22e57250c353ee28db86a50 languageName: node linkType: hard -"@csstools/postcss-logical-float-and-clear@npm:^3.0.0": - version: 3.0.0 - resolution: "@csstools/postcss-logical-float-and-clear@npm:3.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/71a20e8c37877bf68ae615d7bb93fc11b4f8da8be8b1dc1a6e0fc69e27f189712ed71436b8ed51fa69fdb98b8e6718df2b5f42f246c4d39badaf0e43020fcfd4 - languageName: node - linkType: hard - -"@csstools/postcss-logical-overflow@npm:^2.0.0": +"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.0": version: 2.0.0 - resolution: "@csstools/postcss-logical-overflow@npm:2.0.0" + resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/0e103343d3ff8b34eef01b02355c5e010d272fd12d149a242026bb13ab1577b7f3a11fd4514be9342d96f73d61dac1f093a9bd36ece591753ed09a84eb7fca0a + checksum: 10c0/c2d007dfa7500b6b54bca3a43f92f272cbdd8acaa9d4924eafa320c448f1cee77e7c00a229d5a6ffcc5211664b1dcfd7d54bb6622f2e7956e21ac51ea883165c languageName: node linkType: hard -"@csstools/postcss-logical-overscroll-behavior@npm:^2.0.0": +"@csstools/postcss-color-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-color-function@npm:5.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/b01b0e86df5cca222b9859601a4da18ee7a3bade52d6c7fada739cc6c6a4741173f0dccce05f4b307776bc8c0e156923930ce869faae9231e59aa2abab522581 + languageName: node + linkType: hard + +"@csstools/postcss-color-mix-function@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-color-mix-function@npm:4.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/60fa7fbc5e97dc295fd01a90e866d3923bfe47a6e2e3317f01ec8ab0aca687901ef569cdda867780b3aa37a02efa44531bfa0d0d602b90803333baa4cfbf6063 + languageName: node + linkType: hard + +"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.0": version: 2.0.0 - resolution: "@csstools/postcss-logical-overscroll-behavior@npm:2.0.0" + resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/1649601bb26f04d760fb5ebc42cdf414fa2a380b8ec22fe1c117f664c286665a786bd7bbda01b7e7567eaf3cc018a4f36a5c9805f6751cc497da223e0ffe9524 + checksum: 10c0/1206543ec6472f9dd7b67d3311128b66e3be2c6f1bdf9da1b6e4b4f9d69e9388e2896128bb3d1834d825ae4de9455524d7749501a60be740c52c0f7b67287263 languageName: node linkType: hard -"@csstools/postcss-logical-resize@npm:^3.0.0": +"@csstools/postcss-content-alt-text@npm:^3.0.0": version: 3.0.0 - resolution: "@csstools/postcss-logical-resize@npm:3.0.0" + resolution: "@csstools/postcss-content-alt-text@npm:3.0.0" dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/88f62ade9fa8af8b3292d9437e4df2002d79c139fc63e3030a72276a56cc5a13904fb18d20f07b09de45904da1a37eee4b448cdad07487fc28f96a0e3209bb9d + languageName: node + linkType: hard + +"@csstools/postcss-contrast-color-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-contrast-color-function@npm:3.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/7dec6e3bac9d45ae22b5dfcaec9ce8894a1ad80d1e62eb58634eebaa4a02a4f43f5b795a65224442b90b2285c0a0e1d7e80d9528fc1480348ab9a03bf9be2fe0 + languageName: node + linkType: hard + +"@csstools/postcss-exponential-functions@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-exponential-functions@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/911101ec98ac89f56cb7ea52ab2d836646b48e13163d46e1a1f70b680dcc903d314ddf80eeeecfa7b561a99fad2f4b6ce2b0702fbcaa6d379b2c047bde20d398 + languageName: node + linkType: hard + +"@csstools/postcss-font-format-keywords@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-font-format-keywords@npm:5.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/4f12efcaf5468ff359bb3f32f0f66034b9acc9b3ac21fcd2f30a1c8998fc653ebac0091f35c8b7e8dbfe6ccf595aee67f9b06a67adf45a8844e49a82d98b4386 + checksum: 10c0/ae776eb164a4501549924a0b153c47cac31b4d288867c71775ff34941653bfe38d3d5a27888ab76fbef0289163e1687daae74e0b317547b7bd281e63b75afa98 languageName: node linkType: hard -"@csstools/postcss-logical-viewport-units@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/postcss-logical-viewport-units@npm:3.0.4" +"@csstools/postcss-gamut-mapping@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-gamut-mapping@npm:3.0.0" dependencies: - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f0b5ba38acde3bf0ca880c6e0a883950c99fa9919b0e6290c894d5716569663590f26aa1170fd9483ce14544e46afac006ab3b02781410d5e7c8dd1467c674ce + checksum: 10c0/da138bee13c3af9e25962a425c70e242581722881fa52faa090dbb62d22adeced112d3589304651c759bae5fc0060eefb7eb62114798e96d3925bb01febc5a99 languageName: node linkType: hard -"@csstools/postcss-media-minmax@npm:^2.0.9": - version: 2.0.9 - resolution: "@csstools/postcss-media-minmax@npm:2.0.9" +"@csstools/postcss-gradients-interpolation-method@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.0" dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d82622ee9de6eacba1abbf31718cd58759d158ed8a575f36f08e982d07a7d83e51fb184178b96c6f7b76cb333bb33cac04d06a750b6b9c5c43ae1c56232880f9 + checksum: 10c0/33da282f65480d1a4e8a2ce336bf5e0948d47e9fdab0847d3c9e839ae640cb4af96defdcf4cb3d59e43019dec83a5e48fcd0fb96be927c43c7369e8be316fc79 languageName: node linkType: hard -"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^3.0.5": - version: 3.0.5 - resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:3.0.5" +"@csstools/postcss-hwb-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-hwb-function@npm:5.0.0" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/a47abdaa7f4b26596bd9d6bb77aed872a232fc12bd144d2c062d9da626e8dfd8336e2fff67617dba61a1666c2b8027145b390d70d5cd4d4f608604e077cfb04e + checksum: 10c0/7f3e6d46531334621cc2c525df5c76cf916a53d0d9c82008b97b4b6b42f1b07b5729b4c3a8e5ba7b77c51ca44bbd216f83549e978f1aaf75e060335f754aacb2 languageName: node linkType: hard -"@csstools/postcss-nested-calc@npm:^4.0.0": +"@csstools/postcss-ic-unit@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-ic-unit@npm:5.0.0" + dependencies: + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/04148390bcdd0722af7bab6e05dbc573d8d1b5a216ca1f1021ea0cec955a8c3489e4788b3649f3a7be4a9267e0b3a6012f5e9d80d6f3ac3f1f52e585e5ce0c6e + languageName: node + linkType: hard + +"@csstools/postcss-initial@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-initial@npm:3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/4c59994c1ff6443f69ba54d2177adf3441756f067876500f281bd3021da1d9a18ae36cf7264a7ef7ca720508cac936f1120dc872f57fb3f6f28ac523c111a890 + languageName: node + linkType: hard + +"@csstools/postcss-is-pseudo-class@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-is-pseudo-class@npm:6.0.0" + dependencies: + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/55dfb215843258167426eecf7cb8dd2361c2502722469ffc4f3aa7bad6987a08c5ff819ef6bca427c59ee9ad7b3942b89b17bde1d0288ece03f8760c56df7542 + languageName: node + linkType: hard + +"@csstools/postcss-light-dark-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-light-dark-function@npm:3.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/769324f8acda2ce759cad959f43d49ccbc578ccb326362260019a7f7a126408c925d49f437c85d28fb2696cfe1405740a271c250184d9f34ea5603a0b8220936 + languageName: node + linkType: hard + +"@csstools/postcss-logical-float-and-clear@npm:^4.0.0": version: 4.0.0 - resolution: "@csstools/postcss-nested-calc@npm:4.0.0" - dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + resolution: "@csstools/postcss-logical-float-and-clear@npm:4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/fb61512fa4909bdf0ee32a23e771145086c445f2208a737b52093c8adfab7362c56d3aeaf2a6e33ffcec067e99a07219775465d2fbb1a3ac30cdcfb278b218b7 + checksum: 10c0/bbb2e69878965943fc9686c4827a2f363f7a42c051e6df288e68d68741b424fb3cbdc2aa3e2d2169bcf1a4cfd6a24f0aa06c3061ea02ac03e0ad0bfae8fdc999 languageName: node linkType: hard -"@csstools/postcss-normalize-display-values@npm:^4.0.0": +"@csstools/postcss-logical-overflow@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-logical-overflow@npm:3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/cca3397de39eb858a216d0566598ff24a8c94d20c12630568437d8145d78c0d5f36533c72dab72f50702dabbcada87fd34b1129aa4d9c24b427aaa211df124c1 + languageName: node + linkType: hard + +"@csstools/postcss-logical-overscroll-behavior@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-logical-overscroll-behavior@npm:3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0138d89c739dae70b3496d68e08109d97c82d0038fe9bcc5b40f6efb136428def62e81c316f1902d2e4e5611c90c66f70e2008e99fdd0c7358dcfdffb4e326cc + languageName: node + linkType: hard + +"@csstools/postcss-logical-resize@npm:^4.0.0": version: 4.0.0 - resolution: "@csstools/postcss-normalize-display-values@npm:4.0.0" + resolution: "@csstools/postcss-logical-resize@npm:4.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d3a3a362b532163bd791f97348ef28b7a43baf01987c7702b06285e751cdc5ea3e3a2553f088260515b4d28263d5c475923d4d4780ecb4078ec66dff50c9e638 + checksum: 10c0/b9a7eb1ecb1637dac76c957e05f3579e390514d3e93245b5e0a3764542e7875c89811c3526548d466a2fdf76f1cd1a8fadb62c536b3d2521d4310a2f143e405e languageName: node linkType: hard -"@csstools/postcss-oklab-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-oklab-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/40d4f51b568c8299c054f8971d0e85fa7da609ba23ce6c84dc17e16bc3838640ed6da75c3886dc9a96a11005773c6e23cba13a5510c781b2d633d07ad7bda6b7 - languageName: node - linkType: hard - -"@csstools/postcss-position-area-property@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-position-area-property@npm:1.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/38f770454d46bfed01d43a3f5e7ac07d3111399b374a7198ae6503cdb6288e410c7b4199f5a7af8f16aeb688216445ade97be417c084313d6c56f55e50d34559 - languageName: node - linkType: hard - -"@csstools/postcss-progressive-custom-properties@npm:^4.2.1": - version: 4.2.1 - resolution: "@csstools/postcss-progressive-custom-properties@npm:4.2.1" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/56e9a147799719fd5c550c035437693dd50cdfef46d66a4f2ce8f196e1006a096aa47d412710a89c3dc9808068a0a101c7f607a507ed68e925580c6f921e84d5 - languageName: node - linkType: hard - -"@csstools/postcss-property-rule-prelude-list@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-property-rule-prelude-list@npm:1.0.0" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/ae8bbca3a77ca59c21c11899a904f9d9417a19a3359d01dee042e0489b7ddfe7cea13ae275b7e7936d9b0b99c0a13f7f685f962cd63ca3d3d2b6e5eacc293a0d - languageName: node - linkType: hard - -"@csstools/postcss-random-function@npm:^2.0.1": - version: 2.0.1 - resolution: "@csstools/postcss-random-function@npm:2.0.1" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/475bacf685b8bb82942d388e9e3b95f4156800f370299f19f5acc490475dc2813100de81a5a6bf48b696b4d83247622005b616af3166a668556b4b1aceded70d - languageName: node - linkType: hard - -"@csstools/postcss-relative-color-syntax@npm:^3.0.12": - version: 3.0.12 - resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/11af386c8193e22c148ac034eee94c56da3060bdbde3196d2d641b088e12de35bef187bcd7d421f9e4d49c4f1cfc28b24e136e62107e02ed7007a3a28f635d06 - languageName: node - linkType: hard - -"@csstools/postcss-scope-pseudo-class@npm:^4.0.1": - version: 4.0.1 - resolution: "@csstools/postcss-scope-pseudo-class@npm:4.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6a0ca50fae655f4498200d1ce298ca794c85fbe2e3fd5d6419843254f055df5007a973e09b5f1e78e376c02b54278e411516c8d824300c68b265d3e5b311d7ee - languageName: node - linkType: hard - -"@csstools/postcss-sign-functions@npm:^1.1.4": - version: 1.1.4 - resolution: "@csstools/postcss-sign-functions@npm:1.1.4" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/ff58108b2527832a84c571a1f40224b5c8d2afa8db2fe3b1e3599ff6f3469d9f4c528a70eb3c25c5d7801e30474fabfec04e7c23bfdad8572ad492053cd4f899 - languageName: node - linkType: hard - -"@csstools/postcss-stepped-value-functions@npm:^4.0.9": - version: 4.0.9 - resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/f143ca06338c30abb2aa37adc3d7e43a78f3b4493093160cb5babe3ec8cf6b86d83876746ee8e162db87b5e9af6e0066958d89fe8b4a503a29568e5c57c1bf8a - languageName: node - linkType: hard - -"@csstools/postcss-syntax-descriptor-syntax-production@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-syntax-descriptor-syntax-production@npm:1.0.1" - dependencies: - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/b9b3d84a50b86b1af1b8b7e56a64d5eebc1c89c323a5263306c5c69ddb05a4d468d7072a7786b0ea6601629035df0089565e9d98d55d0f4eb7201cf7ed1bb3e9 - languageName: node - linkType: hard - -"@csstools/postcss-system-ui-font-family@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-system-ui-font-family@npm:1.0.0" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6a81761ae3cae643659b1416a7a892cf1505474896193b8abc26cff319cb6b1a20b64c5330d64019fba458e058da3abc9407d0ebf0c102289c0b79ef99b4c6d6 - languageName: node - linkType: hard - -"@csstools/postcss-text-decoration-shorthand@npm:^4.0.3": - version: 4.0.3 - resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.3" - dependencies: - "@csstools/color-helpers": "npm:^5.1.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/f6af7d5dcf599edcf76c5e396ef2d372bbe1c1f3fbaaccd91e91049e64b6ff68b44f459277aef0a8110baca3eaa21275012adc52ccb8c0fc526a4c35577f8fce - languageName: node - linkType: hard - -"@csstools/postcss-trigonometric-functions@npm:^4.0.9": - version: 4.0.9 - resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6ba3d381c977c224f01d47a36f78c9b99d3b89d060a357a9f8840537fdf497d9587a28165dc74e96abdf02f8db0a277d3558646355085a74c8915ee73c6780d1 - languageName: node - linkType: hard - -"@csstools/postcss-unset-value@npm:^4.0.0": +"@csstools/postcss-logical-viewport-units@npm:^4.0.0": version: 4.0.0 - resolution: "@csstools/postcss-unset-value@npm:4.0.0" + resolution: "@csstools/postcss-logical-viewport-units@npm:4.0.0" + dependencies: + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/8424ac700ded5bf59d49310335896f10c069e2c3fc6a676b5d13ca5a6fb78689b948f50494df875da284c4c76651deb005eafba70d87e693274628c5a685abfa + checksum: 10c0/1213ee92b5d9aad68c65478d4243dbd1bd75b88090edec18e1ebaf0aa38459a53609b9aa0e8c48070d546a1c873b1de94a5449fa173a3440334a3ba366c14549 languageName: node linkType: hard -"@csstools/selector-resolve-nested@npm:^3.1.0": - version: 3.1.0 - resolution: "@csstools/selector-resolve-nested@npm:3.1.0" +"@csstools/postcss-media-minmax@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-media-minmax@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" peerDependencies: - postcss-selector-parser: ^7.0.0 - checksum: 10c0/c2b1a930ad03c1427ab90b28c4940424fb39e8175130148f16209be3a3937f7a146d5483ca1da1dfc100aa7ae86df713f0ee82d4bbaa9b986e7f47f35cb67cca + postcss: ^8.4 + checksum: 10c0/0b03ab1ad162d53eef69470246025d01390cc84d2cb0983ffaf09c47bb3d27951e4794638430e778c737cf57c891f834adf3be08fcd5a84c4f6698a03ee9be4a + languageName: node + linkType: hard + +"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:4.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/80397e21d16f443445d03b79171c5c5444ef0e921a6eb1e0419eec24150bd9112469e2e9825aaf0612fa98d05fb0a50cf09f09c2b86d01a238229f205c15d2b2 + languageName: node + linkType: hard + +"@csstools/postcss-mixins@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-mixins@npm:1.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/3160f87f8c307ccdb97fe9ef3507c36e59bec11c0cfa7e486fbe7b0df96cb41b071d7e475d8ef52974bbcc4bf8a2243d5c2c849c0ba8a171aa384ba4b6c0b768 + languageName: node + linkType: hard + +"@csstools/postcss-nested-calc@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-nested-calc@npm:5.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/57764637a4855e941ab9360e1b4603e1fa0a29043148b67b80629d12ae4c93b4d2f156069826c4b8100ad3a90552744424b9a1e05710fe9b6958e1a817e6e07e + languageName: node + linkType: hard + +"@csstools/postcss-normalize-display-values@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-normalize-display-values@npm:5.0.0" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/d43a2658604bdd037ab1516bffadea4db3224390838f5096fccdfbae12a2db08e56ca576724a0279968e6af39419e5fe2963632754dd0956fda2d01b848cf97e + languageName: node + linkType: hard + +"@csstools/postcss-oklab-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-oklab-function@npm:5.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/af3413b1667be101f39b15b9528a57ff764e5abcf80578590b95c27c42a493ed9dcd2927834371d211b025c5d5df103cded6f2def98c9c79301f76ca379d7dbf + languageName: node + linkType: hard + +"@csstools/postcss-position-area-property@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-position-area-property@npm:2.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/54b9a97a9ef636dfc8b62cdabb7d194438c6f510effdff4339073626446c078033d475dfec8c82fdbcf25745fb05caafffc9512e3be7ff53ced55f1d38d2da6a + languageName: node + linkType: hard + +"@csstools/postcss-progressive-custom-properties@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-progressive-custom-properties@npm:5.0.0" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/8476a58b777e7015f40fea53829d31429c8bab114b884b87c1706b5df095102232fb46b275a76c719f73399d3dbdfec2c172fb68c2fb362dbe68569446366a1a + languageName: node + linkType: hard + +"@csstools/postcss-property-rule-prelude-list@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-property-rule-prelude-list@npm:2.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/6ce0c962391b27966fba1623bca2dacc61fb7b1c9916f08d17256a800b0f8476d34835d50cb2ed77bdc4e920e4be5dddc1569c7e5140895e7b0076694d769182 + languageName: node + linkType: hard + +"@csstools/postcss-random-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-random-function@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/bf9b8836ce3eafd670bfb9fa16d3befde8827ad220edc80eb4301fa716c1a4ff444254ab7612a6f6f952d16d3ac6d6e594448dcdd860f6ca8b575ffe59946864 + languageName: node + linkType: hard + +"@csstools/postcss-relative-color-syntax@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/52f21acc66a0aa591d99173b9813f0b9f375de462bc657e909916ed176590b8d530a2eff83e71da488e2c8f476f0397d4a79bb7b7ef13664176d9e744a49510a + languageName: node + linkType: hard + +"@csstools/postcss-scope-pseudo-class@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-scope-pseudo-class@npm:5.0.0" + dependencies: + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/2b67bd6af9a1175ca47c6593ee73564d70cb39c1adc9ffc5099ba513b49055994786c60687b44d88beccd09f1a6196e5e9157fd439632ae34381a60adaef5246 + languageName: node + linkType: hard + +"@csstools/postcss-sign-functions@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-sign-functions@npm:2.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/e5239e012e78cfcb244e8599d93700bbbcd15fd78e4a71f8bd5296022bd1d1ead750689bdacc35a47d2061729dd727e2fb1a185bee0620a62b64e4ee3434861e + languageName: node + linkType: hard + +"@csstools/postcss-stepped-value-functions@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/a8354fa0f4b74def1c258bf267c00d029f65618ccebb9c14837522f11eeb6b673717b56720588d5fec7bd07036ed84826dea01c44e9a085ae7af78a2b4ea7ec0 + languageName: node + linkType: hard + +"@csstools/postcss-syntax-descriptor-syntax-production@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-syntax-descriptor-syntax-production@npm:2.0.0" + dependencies: + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0c9ef2f5cd21079db1df497adf99a3d66c912f69c471dd89283a1b8ed3a9c4e7670bfa439b615ac27acd86ce2e1adf4a5eb7b551113ecf5e95ee77269446bbbe + languageName: node + linkType: hard + +"@csstools/postcss-system-ui-font-family@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-system-ui-font-family@npm:2.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/e4f59c093565ed3753f3561a754fe3c0c8152a3b25f184cb370fa5cb1e9b54ad62fe53f7886ed840d445201120197ae6d42c6266ef4d5cfd063226f95c433c06 + languageName: node + linkType: hard + +"@csstools/postcss-text-decoration-shorthand@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-text-decoration-shorthand@npm:5.0.0" + dependencies: + "@csstools/color-helpers": "npm:^6.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0d1dc47da13adc1f0863017a3448f3a79774f00f1952fc2bfc09404b00b03d125b54dedce036e0e5c73a6f2a64cdabe702e9bc03dc6b88d3b71a6424cd0cec80 + languageName: node + linkType: hard + +"@csstools/postcss-trigonometric-functions@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/8b2e149f4fb8ac1bc7a642c99265df7f54729fbace29cde24f65078c0d97e9050e75576ef55b869ab78fbb94a2408adae2bc6f3ced759fca96519c6ceb92cc20 + languageName: node + linkType: hard + +"@csstools/postcss-unset-value@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-unset-value@npm:5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/21e26cccf7824e4366378a20bc043034105db654632b941f77a0cbbf4fe0de291d1ae94845280c08c8d851ae14ce6d07bc09a33c5fbbcb7ce1f21b10212df1f1 + languageName: node + linkType: hard + +"@csstools/selector-resolve-nested@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/selector-resolve-nested@npm:4.0.0" + peerDependencies: + postcss-selector-parser: ^7.1.1 + checksum: 10c0/f6bccfe24a47c3e55710d421740550b868bbe7f0820f32d14d1eb737eefe7ec26261fb726cc5cfdf8236b3173d0a657ebdc9602e0c53824603f719b14a76bcaf languageName: node linkType: hard @@ -1849,21 +1917,30 @@ __metadata: languageName: node linkType: hard -"@csstools/stylelint-formatter-github@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/stylelint-formatter-github@npm:1.0.0" +"@csstools/selector-specificity@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/selector-specificity@npm:6.0.0" peerDependencies: - stylelint: ^16.6.0 - checksum: 10c0/2052c4e4d89656b2b4176a6d07508ef73278d33c24a7408a3555d07f26ec853f85da95525590c51751fb3150a2ebb5e3083d8200dc6597af2cd8e93198695269 + postcss-selector-parser: ^7.1.1 + checksum: 10c0/7a93973f9054f2e1f03c8543cde68e0b0c65e5e72da6e4e959974d28fe809e11bd2afa1ff2ca11a1690a4c9a2f2bbe00d00e2b07fb2108bf89c5e48fe441c432 languageName: node linkType: hard -"@csstools/utilities@npm:^2.0.0": +"@csstools/stylelint-formatter-github@npm:^2.0.0": version: 2.0.0 - resolution: "@csstools/utilities@npm:2.0.0" + resolution: "@csstools/stylelint-formatter-github@npm:2.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/1eddcb749eb93efff2e2d7edb4405459bf558ceaa6d90e792408802f30c55e3482a4cead9e69fd651f04a927e863782fc6cf813c37433da9ff1f068910080a06 + languageName: node + linkType: hard + +"@csstools/utilities@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/utilities@npm:3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/be5c31437b726928f64cd4bb3e47f5b90bfd2e2a69a8eaabd8e89cc6c0977e4f0f7ee48de50c8ed8b07e04e3956a02293247e0da3236d521fb2e836f88f65822 + checksum: 10c0/65b6f6aa4ea899777bea710aab5f17028aa5e1aa41f7fe892c2e916b188da2b929c76be643d4a5d8d9ca5a7853df94f05c0247dd6b63540b360055d36db3104f languageName: node linkType: hard @@ -2095,13 +2172,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/aix-ppc64@npm:0.25.5" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/aix-ppc64@npm:0.27.2" @@ -2109,13 +2179,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm64@npm:0.25.5" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-arm64@npm:0.27.2" @@ -2123,13 +2186,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm@npm:0.25.5" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-arm@npm:0.27.2" @@ -2137,13 +2193,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-x64@npm:0.25.5" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-x64@npm:0.27.2" @@ -2151,13 +2200,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-arm64@npm:0.25.5" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/darwin-arm64@npm:0.27.2" @@ -2165,13 +2207,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-x64@npm:0.25.5" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/darwin-x64@npm:0.27.2" @@ -2179,13 +2214,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-arm64@npm:0.25.5" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/freebsd-arm64@npm:0.27.2" @@ -2193,13 +2221,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-x64@npm:0.25.5" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/freebsd-x64@npm:0.27.2" @@ -2207,13 +2228,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm64@npm:0.25.5" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-arm64@npm:0.27.2" @@ -2221,13 +2235,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm@npm:0.25.5" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-arm@npm:0.27.2" @@ -2235,13 +2242,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ia32@npm:0.25.5" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-ia32@npm:0.27.2" @@ -2249,13 +2249,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-loong64@npm:0.25.5" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-loong64@npm:0.27.2" @@ -2263,13 +2256,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-mips64el@npm:0.25.5" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-mips64el@npm:0.27.2" @@ -2277,13 +2263,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ppc64@npm:0.25.5" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-ppc64@npm:0.27.2" @@ -2291,13 +2270,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-riscv64@npm:0.25.5" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-riscv64@npm:0.27.2" @@ -2305,13 +2277,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-s390x@npm:0.25.5" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-s390x@npm:0.27.2" @@ -2319,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-x64@npm:0.25.5" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-x64@npm:0.27.2" @@ -2333,13 +2291,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-arm64@npm:0.25.5" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/netbsd-arm64@npm:0.27.2" @@ -2347,13 +2298,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-x64@npm:0.25.5" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/netbsd-x64@npm:0.27.2" @@ -2361,13 +2305,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-arm64@npm:0.25.5" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/openbsd-arm64@npm:0.27.2" @@ -2375,13 +2312,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-x64@npm:0.25.5" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/openbsd-x64@npm:0.27.2" @@ -2396,13 +2326,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/sunos-x64@npm:0.25.5" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/sunos-x64@npm:0.27.2" @@ -2410,13 +2333,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-arm64@npm:0.25.5" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-arm64@npm:0.27.2" @@ -2424,13 +2340,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-ia32@npm:0.25.5" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-ia32@npm:0.27.2" @@ -2438,13 +2347,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-x64@npm:0.25.5" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-x64@npm:0.27.2" @@ -2819,10 +2721,10 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:1.4.0": - version: 1.4.0 - resolution: "@ioredis/commands@npm:1.4.0" - checksum: 10c0/99afe21fba794f84a2b84cceabcc370a7622e7b8b97a6589456c07c9fa62a15d54c5546f6f7214fb9a2458b1fa87579d5c531aaf48e06cc9be156d5923892c8d +"@ioredis/commands@npm:1.5.0": + version: 1.5.0 + resolution: "@ioredis/commands@npm:1.5.0" + checksum: 10c0/2d192d967a21f0192e17310d27ead02b0bdd504e834c782714abe641190ebfb548ad307fd89fd2d80db97c462afdc69ab4a4383831ab64ce61fe92f130d8b466 languageName: node linkType: hard @@ -2959,7 +2861,7 @@ __metadata: version: 0.0.0-use.local resolution: "@mastodon/mastodon@workspace:." dependencies: - "@csstools/stylelint-formatter-github": "npm:^1.0.0" + "@csstools/stylelint-formatter-github": "npm:^2.0.0" "@dnd-kit/core": "npm:^6.1.0" "@dnd-kit/sortable": "npm:^10.0.0" "@dnd-kit/utilities": "npm:^3.2.2" @@ -3039,7 +2941,7 @@ __metadata: fake-indexeddb: "npm:^6.0.1" fast-glob: "npm:^3.3.3" fuzzysort: "npm:^3.0.0" - globals: "npm:^16.0.0" + globals: "npm:^17.0.0" history: "npm:^4.10.1" hoist-non-react-statics: "npm:^3.3.2" http-link-header: "npm:^1.1.1" @@ -3056,7 +2958,7 @@ __metadata: msw-storybook-addon: "npm:^2.0.6" path-complete-extname: "npm:^1.0.0" playwright: "npm:^1.57.0" - postcss-preset-env: "npm:^10.1.5" + postcss-preset-env: "npm:^11.0.0" prettier: "npm:^3.3.3" prop-types: "npm:^15.8.1" punycode: "npm:^2.3.0" @@ -3088,7 +2990,7 @@ __metadata: stringz: "npm:^2.1.0" stylelint: "npm:^16.19.1" stylelint-config-prettier-scss: "npm:^1.0.0" - stylelint-config-standard-scss: "npm:^16.0.0" + stylelint-config-standard-scss: "npm:^17.0.0" substring-trie: "npm:^1.0.2" tesseract.js: "npm:^7.0.0" tiny-queue: "npm:^0.2.1" @@ -3131,7 +3033,7 @@ __metadata: cors: "npm:^2.8.5" dotenv: "npm:^16.0.3" express: "npm:^5.1.0" - globals: "npm:^16.0.0" + globals: "npm:^17.0.0" ioredis: "npm:^5.3.2" jsdom: "npm:^27.0.0" pg: "npm:^8.5.0" @@ -4452,9 +4354,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.195": - version: 4.17.21 - resolution: "@types/lodash@npm:4.17.21" - checksum: 10c0/73cb006e047d8871e9d63f3a165543bf16c44a5b6fe3f9f6299e37cb8d67a7b1d55ac730959a81f9def510fd07232ff7e30e05413e5d5a12793baad84ebe36c3 + version: 4.17.23 + resolution: "@types/lodash@npm:4.17.23" + checksum: 10c0/9d9cbfb684e064a2b78aab9e220d398c9c2a7d36bc51a07b184ff382fa043a99b3d00c16c7f109b4eb8614118f4869678dbae7d5c6700ed16fb9340e26cc0bf6 languageName: node linkType: hard @@ -4503,13 +4405,13 @@ __metadata: linkType: hard "@types/pg@npm:^8.6.6": - version: 8.15.6 - resolution: "@types/pg@npm:8.15.6" + version: 8.16.0 + resolution: "@types/pg@npm:8.16.0" dependencies: "@types/node": "npm:*" pg-protocol: "npm:*" pg-types: "npm:^2.2.0" - checksum: 10c0/7f93f83a4da0dc6133918f824d826fa34e78fb8cf86392d28a0e095c836c6910c014ced5d4b364d83e8485a65ce369adeb9663b14ba301241d4c0f80073007f3 + checksum: 10c0/421fe7c07d5c0226835d362414a63653f86251ee966150d807ed60174c13921d1b8a3e2f1c2bfba9659ec0282ca50974030c4c1efcd575003eb922ea12ca7d05 languageName: node linkType: hard @@ -6394,14 +6296,14 @@ __metadata: languageName: node linkType: hard -"css-blank-pseudo@npm:^7.0.1": - version: 7.0.1 - resolution: "css-blank-pseudo@npm:7.0.1" +"css-blank-pseudo@npm:^8.0.1": + version: 8.0.1 + resolution: "css-blank-pseudo@npm:8.0.1" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/46c3d3a611972fdb0c264db7c0b34fe437bc4300961d11945145cf04962f52a545a6ef55bc8ff4afd82b605bd692b4970f2b54582616dea00441105e725d4618 + checksum: 10c0/851d38cb3e11a63db4a0f44751cde99d6825bb11adc78418e8e83f32f0a3dcc3245e6e4b5053e796635560a1224af8ae63dfe330f87175fcc7bf34a1af3ecddc languageName: node linkType: hard @@ -6412,25 +6314,25 @@ __metadata: languageName: node linkType: hard -"css-has-pseudo@npm:^7.0.3": - version: 7.0.3 - resolution: "css-has-pseudo@npm:7.0.3" +"css-has-pseudo@npm:^8.0.0": + version: 8.0.0 + resolution: "css-has-pseudo@npm:8.0.0" dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/c89f68e17bed229e9a3e98da5032e1360c83d45d974bc3fb8d6b5358399bca80cce7929e4a621a516a75536edb78678dc486eb41841eeed28cca79e3be4bdc27 + checksum: 10c0/fb623c09d3cb1d5ec029c61b5a24484ee712fbd20bffae37a20a2843917706cadb1c4d695585382e4839dde136922ffee6924ec83f633dde2e2afc6d77a26be8 languageName: node linkType: hard -"css-prefers-color-scheme@npm:^10.0.0": - version: 10.0.0 - resolution: "css-prefers-color-scheme@npm:10.0.0" +"css-prefers-color-scheme@npm:^11.0.0": + version: 11.0.0 + resolution: "css-prefers-color-scheme@npm:11.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/a66c727bb2455328b18862f720819fc98ff5c1486b69f758bdb5c66f46cc6d484f9fc0bfa4f00f2693c5da6707ad136ca789496982f713ade693f08af624930e + checksum: 10c0/dea9f17cc33b5bedb660ed3925a53c9034bd5f4f94222d3b6df1dffd011061c124800dfc3bbeb1136fd69e84be577c63ac3da9220e175ea10b49275d7d0ada40 languageName: node linkType: hard @@ -6451,10 +6353,10 @@ __metadata: languageName: node linkType: hard -"cssdb@npm:^8.6.0": - version: 8.6.0 - resolution: "cssdb@npm:8.6.0" - checksum: 10c0/4bb7b77ba24902e8d481e9514ec0be56e205186a2b7d9f5027fedfe718952c559c62acfd2859f92869f8090da7c2170f83d68170db5058a6ba8d9d5e8ded3b3e +"cssdb@npm:^8.7.0": + version: 8.7.0 + resolution: "cssdb@npm:8.7.0" + checksum: 10c0/8db722877a68732d378e6f734dcf343f5d62670a1df630a74f5ae4a4c1ec482223730128825c5bcd99307fcd2128160b3705a6b1508d446e80e6ab2eebc7e8bc languageName: node linkType: hard @@ -7118,7 +7020,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0, esbuild@npm:^0.27.0": version: 0.27.2 resolution: "esbuild@npm:0.27.2" dependencies: @@ -7207,92 +7109,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.25.0": - version: 0.25.5 - resolution: "esbuild@npm:0.25.5" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.5" - "@esbuild/android-arm": "npm:0.25.5" - "@esbuild/android-arm64": "npm:0.25.5" - "@esbuild/android-x64": "npm:0.25.5" - "@esbuild/darwin-arm64": "npm:0.25.5" - "@esbuild/darwin-x64": "npm:0.25.5" - "@esbuild/freebsd-arm64": "npm:0.25.5" - "@esbuild/freebsd-x64": "npm:0.25.5" - "@esbuild/linux-arm": "npm:0.25.5" - "@esbuild/linux-arm64": "npm:0.25.5" - "@esbuild/linux-ia32": "npm:0.25.5" - "@esbuild/linux-loong64": "npm:0.25.5" - "@esbuild/linux-mips64el": "npm:0.25.5" - "@esbuild/linux-ppc64": "npm:0.25.5" - "@esbuild/linux-riscv64": "npm:0.25.5" - "@esbuild/linux-s390x": "npm:0.25.5" - "@esbuild/linux-x64": "npm:0.25.5" - "@esbuild/netbsd-arm64": "npm:0.25.5" - "@esbuild/netbsd-x64": "npm:0.25.5" - "@esbuild/openbsd-arm64": "npm:0.25.5" - "@esbuild/openbsd-x64": "npm:0.25.5" - "@esbuild/sunos-x64": "npm:0.25.5" - "@esbuild/win32-arm64": "npm:0.25.5" - "@esbuild/win32-ia32": "npm:0.25.5" - "@esbuild/win32-x64": "npm:0.25.5" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/aba8cbc11927fa77562722ed5e95541ce2853f67ad7bdc40382b558abc2e0ec57d92ffb820f082ba2047b4ef9f3bc3da068cdebe30dfd3850cfa3827a78d604e - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -8275,10 +8091,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^16.0.0": - version: 16.5.0 - resolution: "globals@npm:16.5.0" - checksum: 10c0/615241dae7851c8012f5aa0223005b1ed6607713d6813de0741768bd4ddc39353117648f1a7086b4b0fa45eae733f1c0a0fe369aa4e543bb63f8de8990178ea9 +"globals@npm:^17.0.0": + version: 17.0.0 + resolution: "globals@npm:17.0.0" + checksum: 10c0/e3c169fdcb0fc6755707b697afb367bea483eb29992cfc0de1637382eb893146e17f8f96db6d7453e3696b478a7863ae2000e6c71cd2f4061410285106d3847a languageName: node linkType: hard @@ -8747,10 +8563,10 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.8.2 - resolution: "ioredis@npm:5.8.2" + version: 5.9.2 + resolution: "ioredis@npm:5.9.2" dependencies: - "@ioredis/commands": "npm:1.4.0" + "@ioredis/commands": "npm:1.5.0" cluster-key-slot: "npm:^1.1.0" debug: "npm:^4.3.4" denque: "npm:^2.1.0" @@ -8759,7 +8575,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88 + checksum: 10c0/9732a3adab56c63a0e76bd1a1c891e4201448c44ab31e7e9aee8e83005be07597a4969199593c0d59642fb3fcd64ce00e9a8fad6c92d80e648df20b4b2b184ce languageName: node linkType: hard @@ -9495,13 +9311,6 @@ __metadata: languageName: node linkType: hard -"known-css-properties@npm:^0.36.0": - version: 0.36.0 - resolution: "known-css-properties@npm:0.36.0" - checksum: 10c0/098c8f956408a7ce26a639c2354e0184fb2bb2772bb7d1ba23192b6b6cf5818cbb8a0acfb4049705ea103d9916065703bc540fa084a6349fdb41bf745aada4bc - languageName: node - linkType: hard - "known-css-properties@npm:^0.37.0": version: 0.37.0 resolution: "known-css-properties@npm:0.37.0" @@ -9863,10 +9672,10 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:^2.21.0": - version: 2.21.0 - resolution: "mdn-data@npm:2.21.0" - checksum: 10c0/cd26902551af2cc29f06f130893cb04bca9ee278939fce3ffbcb759497cc80d53a6f4abdef2ae2f3ed3c95ac8d651f53fc141defd580ebf4ae2f93aea325957b +"mdn-data@npm:^2.25.0": + version: 2.26.0 + resolution: "mdn-data@npm:2.26.0" + checksum: 10c0/e5f17f4dac247f3e260c081761628d371e23659a7ff13413f83f5bd7fd0f2d8317e72277bb77f0e13115041334ff728a5363db64aabaf376c0e1b0b31016d0b8 languageName: node linkType: hard @@ -10695,17 +10504,17 @@ __metadata: languageName: node linkType: hard -"pg-cloudflare@npm:^1.2.7": - version: 1.2.7 - resolution: "pg-cloudflare@npm:1.2.7" - checksum: 10c0/8a52713dbdecc9d389dc4e65e3b7ede2e199ec3715f7491ee80a15db171f2d75677a102e9c2cef0cb91a2f310e91f976eaec0dd6ef5d8bf357de0b948f9d9431 +"pg-cloudflare@npm:^1.3.0": + version: 1.3.0 + resolution: "pg-cloudflare@npm:1.3.0" + checksum: 10c0/b0866c88af8e54c7b3ed510719d92df37714b3af5e3a3a10d9f761fcec99483e222f5b78a1f2de590368127648087c45c01aaf66fadbe46edb25673eedc4f8fc languageName: node linkType: hard -"pg-connection-string@npm:^2.6.0, pg-connection-string@npm:^2.9.1": - version: 2.9.1 - resolution: "pg-connection-string@npm:2.9.1" - checksum: 10c0/9a646529bbc0843806fc5de98ce93735a4612b571f11867178a85665d11989a827e6fd157388ca0e34ec948098564fce836c178cfd499b9f0e8cd9972b8e2e5c +"pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": + version: 2.10.0 + resolution: "pg-connection-string@npm:2.10.0" + checksum: 10c0/6639d3e12f66bc3cc5fa1e5794b4b22a4212e30424cbf001422e041c77c598ee0cd19395a9d79cb99a962da612b03c021e8148c3437cf01a29a18437bad193eb languageName: node linkType: hard @@ -10716,19 +10525,19 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.10.1": - version: 3.10.1 - resolution: "pg-pool@npm:3.10.1" +"pg-pool@npm:^3.11.0": + version: 3.11.0 + resolution: "pg-pool@npm:3.11.0" peerDependencies: pg: ">=8.0" - checksum: 10c0/a00916b7df64226cc597fe769e3a757ff9b11562dc87ce5b0a54101a18c1fe282daaa2accaf27221e81e1e4cdf4da6a33dab09614734d32904d6c4e11c44a079 + checksum: 10c0/4b104b48a47257a0edad0c62e5ea1908b72cb79386270264b452e69895e9e4c589d00cdbf6e46d4e9c05bc7e7d191656b66814b5282d65f33b12648a21df3c7f languageName: node linkType: hard -"pg-protocol@npm:*, pg-protocol@npm:^1.10.3": - version: 1.10.3 - resolution: "pg-protocol@npm:1.10.3" - checksum: 10c0/f7ef54708c93ee6d271e37678296fc5097e4337fca91a88a3d99359b78633dbdbf6e983f0adb34b7cdd261b7ec7266deb20c3233bf3dfdb498b3e1098e8750b9 +"pg-protocol@npm:*, pg-protocol@npm:^1.11.0": + version: 1.11.0 + resolution: "pg-protocol@npm:1.11.0" + checksum: 10c0/93e83581781418c9173eba4e4545f73392cfe66b78dd1d3624d7339fbd37e7f4abebaf2615e68e0701a9bf0edf5b81a4ad533836f388f775fe25fa24a691c464 languageName: node linkType: hard @@ -10746,13 +10555,13 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.16.3 - resolution: "pg@npm:8.16.3" + version: 8.17.1 + resolution: "pg@npm:8.17.1" dependencies: - pg-cloudflare: "npm:^1.2.7" - pg-connection-string: "npm:^2.9.1" - pg-pool: "npm:^3.10.1" - pg-protocol: "npm:^1.10.3" + pg-cloudflare: "npm:^1.3.0" + pg-connection-string: "npm:^2.10.0" + pg-pool: "npm:^3.11.0" + pg-protocol: "npm:^1.11.0" pg-types: "npm:2.2.0" pgpass: "npm:1.0.5" peerDependencies: @@ -10763,7 +10572,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/a6a407ff0efb7599760d72ffdcda47a74c34c0fd71d896623caac45cf2cfb0f49a10973cce23110f182b9810639a1e9f6904454d7358c7001574ee0ffdcbce2a + checksum: 10c0/39a92391adfc73f793d195b4062bc2d21aa3537073e3973f3979d72901d92a59e129f31f42577ff916038a6c3f9fe423b6024717529609ae8548fda21248cfe7 languageName: node linkType: hard @@ -10813,15 +10622,6 @@ __metadata: languageName: node linkType: hard -"pino-abstract-transport@npm:^2.0.0": - version: 2.0.0 - resolution: "pino-abstract-transport@npm:2.0.0" - dependencies: - split2: "npm:^4.0.0" - checksum: 10c0/02c05b8f2ffce0d7c774c8e588f61e8b77de8ccb5f8125afd4a7325c9ea0e6af7fb78168999657712ae843e4462bb70ac550dfd6284f930ee57f17f486f25a9f - languageName: node - linkType: hard - "pino-abstract-transport@npm:^3.0.0": version: 3.0.0 resolution: "pino-abstract-transport@npm:3.0.0" @@ -10874,23 +10674,23 @@ __metadata: linkType: hard "pino@npm:^10.0.0": - version: 10.1.0 - resolution: "pino@npm:10.1.0" + version: 10.2.1 + resolution: "pino@npm:10.2.1" dependencies: "@pinojs/redact": "npm:^0.4.0" atomic-sleep: "npm:^1.0.0" on-exit-leak-free: "npm:^2.1.0" - pino-abstract-transport: "npm:^2.0.0" + pino-abstract-transport: "npm:^3.0.0" pino-std-serializers: "npm:^7.0.0" process-warning: "npm:^5.0.0" quick-format-unescaped: "npm:^4.0.3" real-require: "npm:^0.2.0" safe-stable-stringify: "npm:^2.3.1" sonic-boom: "npm:^4.0.1" - thread-stream: "npm:^3.0.0" + thread-stream: "npm:^4.0.0" bin: pino: bin.js - checksum: 10c0/49c1dd80d5f99f02bde1acf2f60cef7686948a937f751f6cb368c2868c7e82e54aeabac63a34587e16019965cbf0eb6e609edf92c439a98a0a4fcb0add277eaf + checksum: 10c0/2eaed48bb7fb8865e27ac6d6709383f5c117f1e59c818734c7cc22b362e9aa5846a0547e7fd9cde64088a3b48aa314e1dab07ee16da8dc3b87897970eb56843e languageName: node linkType: hard @@ -10943,14 +10743,14 @@ __metadata: languageName: node linkType: hard -"postcss-attribute-case-insensitive@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-attribute-case-insensitive@npm:7.0.1" +"postcss-attribute-case-insensitive@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-attribute-case-insensitive@npm:8.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/48945abe2024e2d2e4c37d30b8c1aaf37af720f24f6a996f7ea7e7ed33621f5c22cf247ed22028c0c922de040c58c0802729bc39b903cb1693f4b63c0b49da34 + checksum: 10c0/d6442d1580a3ec5083ff41435e8423dfe9c5b3b65b610e082769d27f11b98e0699d5361f7e9a9e499805c0a78d83a8aec3dd9f9d9280636cca82ec1aea79e928 languageName: node linkType: hard @@ -10965,131 +10765,131 @@ __metadata: languageName: node linkType: hard -"postcss-color-functional-notation@npm:^7.0.12": - version: 7.0.12 - resolution: "postcss-color-functional-notation@npm:7.0.12" +"postcss-color-functional-notation@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-color-functional-notation@npm:8.0.0" dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/dc80ba1a956ae9b396596bda72d9bdb92de96874378a38ba4e2177ffa35339dc76d894920bb013b6f10c9b75cfb41778e09956a438c2e9ea41b684f766c55f4a + checksum: 10c0/4b23633475b57f5d0076340ab4f434506891b5a25254d077215c08d08bbdab369484488bbc249595618730afea3546329e5633cc9e119f31c756b1c224dd0300 languageName: node linkType: hard -"postcss-color-hex-alpha@npm:^10.0.0": +"postcss-color-hex-alpha@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-color-hex-alpha@npm:11.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/375509c404b0ac496f66bf0910d12e5aabca058c0d9de9d10719e9a0729013d5f4fafd06d8c8073b2d68613b3346e8ff79ef223a5ce4ddf0d4c4f645fd7a5436 + languageName: node + linkType: hard + +"postcss-color-rebeccapurple@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-color-rebeccapurple@npm:11.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/9f22fff4ce2f0d7ce07be2ba2d07697e24f7db83a6252f02f36aa3a556cf69e1346a3cbab3908264e267306cc5a79b5d95c21e6fa4257a68c13c7411224543b9 + languageName: node + linkType: hard + +"postcss-custom-media@npm:^12.0.0": + version: 12.0.0 + resolution: "postcss-custom-media@npm:12.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/bf1f3939d83a24eb7ef86f9fc2cb799026d4dc4620d3769eef9234d23fd528c73e907c2b88cb239290d169e5ddf8f0624cf8b22084874953c224e6000d55bf50 + languageName: node + linkType: hard + +"postcss-custom-properties@npm:^15.0.0": + version: 15.0.0 + resolution: "postcss-custom-properties@npm:15.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/d766f1632075f8db2cd5075967cc4397c46b59699dd7a70762d9bd3e1ff91d3ada167b4f79e7e254518cdaae8d71645cd92faeb89559175ed50337c0acf5b0b4 + languageName: node + linkType: hard + +"postcss-custom-selectors@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-custom-selectors@npm:9.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/a99d87639e8bca0a3eebd910356cecd67a4153789a77e15604db68caee415e64bc90d3e58a8322ed3d37addb80574a1180e58d603ac6541364b373b322e7de9f + languageName: node + linkType: hard + +"postcss-dir-pseudo-class@npm:^10.0.0": version: 10.0.0 - resolution: "postcss-color-hex-alpha@npm:10.0.0" + resolution: "postcss-dir-pseudo-class@npm:10.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/8a6dcb27403d04b55d6de88bf3074622bcea537fc4436bbcb346e92289c4d17059444e2e6c3554c325e7a777bb4cdc711e764a83123b4000aec211052e957d5b + checksum: 10c0/c7cae0b4e96bf0e51845c20b6fae7eec7b428b569b4fbae639b4f1f3b16a0ced4f5610ffb0209484ba4dfc8858abc64a9577cc8d2e3f1dbf3154f4ed0eb07669 languageName: node linkType: hard -"postcss-color-rebeccapurple@npm:^10.0.0": +"postcss-double-position-gradients@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-double-position-gradients@npm:7.0.0" + dependencies: + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0a9a1c567c79ddbe7bbeafe9770f2393d4d0eb344a79a2823dc5944690d74c311841ff6fd533b823d282029eff07418eeb671fabc85db6414ace48cce37223e8 + languageName: node + linkType: hard + +"postcss-focus-visible@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-focus-visible@npm:11.0.0" + dependencies: + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/001cdfb200fe83b25a0fdbb7315b70fcf91c71d0134e16d48334fc7eab3d41c7cd3b8fe7bb8201a0ef9f242afb10f71aed238715a1d6923f68a02e6dc4219916 + languageName: node + linkType: hard + +"postcss-focus-within@npm:^10.0.0": version: 10.0.0 - resolution: "postcss-color-rebeccapurple@npm:10.0.0" + resolution: "postcss-focus-within@npm:10.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/308e33f76f2b48c1c2121d4502fc053e869f3415898de7d30314353df680e79b37497e7b628e3447edc1049091da3672f7d891e45604f238598e846e06b893ed - languageName: node - linkType: hard - -"postcss-custom-media@npm:^11.0.6": - version: 11.0.6 - resolution: "postcss-custom-media@npm:11.0.6" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/62dcb2858fd490d90aab32062621d58892a7b2a54948ee63af81a2cd61807a11815d28d4ef6bc800c5e142ac73098f7e56822c7cc63192eb20d5b16071543a73 - languageName: node - linkType: hard - -"postcss-custom-properties@npm:^14.0.6": - version: 14.0.6 - resolution: "postcss-custom-properties@npm:14.0.6" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/0eeef77bc713551f5cb8fa5982d24da4e854075f3af020f1c94366c47a23a4cc225ebfecc978bdb17f00ee0bdee9d2c784e0d01adc64a447321e408abbe2c83b - languageName: node - linkType: hard - -"postcss-custom-selectors@npm:^8.0.5": - version: 8.0.5 - resolution: "postcss-custom-selectors@npm:8.0.5" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/bd8f2f85bbec4bd56ff408cb699d9fe649e2af0db82d5752eee05481ae522f06f5a47950ca22fcb4c8601071c03346df67cf20b0b0bcade32ce58d07ebaf9b32 - languageName: node - linkType: hard - -"postcss-dir-pseudo-class@npm:^9.0.1": - version: 9.0.1 - resolution: "postcss-dir-pseudo-class@npm:9.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/da9d3387648c5c3161a653d354c8f3e70a299108df3977e8aa65cf10793e4dd58a2711b3426cd63716245b13584ca8d95adcd6e10e3c9adbc61d08743e2d8690 - languageName: node - linkType: hard - -"postcss-double-position-gradients@npm:^6.0.4": - version: 6.0.4 - resolution: "postcss-double-position-gradients@npm:6.0.4" - dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6dbbe7a3855e84a9319df434e210225f6dfa7262e5959611355f1769c2c9d30d37a19737712f20eac6354876fff4ba556d8d0b12a90c78d8ab97c9a8da534a7c - languageName: node - linkType: hard - -"postcss-focus-visible@npm:^10.0.1": - version: 10.0.1 - resolution: "postcss-focus-visible@npm:10.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/c5ecc8536a708a49a99d0abd68a88a160664e6c832c808db8edd9f0221e7017a258daa87e49daf2cb098cb037005d46cf492403c8c9c92ad8835d30adaccf665 - languageName: node - linkType: hard - -"postcss-focus-within@npm:^9.0.1": - version: 9.0.1 - resolution: "postcss-focus-within@npm:9.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d6ab49d2a7f33485a9e137dc77ec92c5619a3ec92e1e672734fc604853ff1f3c0c189085c12461614be4fcb03ea0347d91791a45986a18d50b5228d161eda57a + checksum: 10c0/297ba52c07ba9284e7bf862e23ce90312bc2068d005a3c95833d832462691fac276850dd5508228fc808ec5f2963cb61a8b510645e08b78a74a9aaff9a79b02b languageName: node linkType: hard @@ -11102,39 +10902,39 @@ __metadata: languageName: node linkType: hard -"postcss-gap-properties@npm:^6.0.0": - version: 6.0.0 - resolution: "postcss-gap-properties@npm:6.0.0" +"postcss-gap-properties@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-gap-properties@npm:7.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/4e07e0d3927d0e65d67eaf047ac39e08d39cb1bf74e16e10c7df7f0d01b184a77ea59f63fd5691b5ed6df159970b972db28cb784d883e26e981137696460897d + checksum: 10c0/c93a0d15d40a7ae474a1bf05bd8f77410c6a353c7ea3bbad3aa61c38f6471d9bf23bac738aa1b3ca58805a33a730a4e6d15b7d03e0d2f1a29ef70969c14b483d languageName: node linkType: hard -"postcss-image-set-function@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-image-set-function@npm:7.0.0" +"postcss-image-set-function@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-image-set-function@npm:8.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" + "@csstools/utilities": "npm:^3.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/913fd9492f00122aa0c2550fb0d72130428cbe1e6465bc65e8fe71e9deb10ac0c01d7caceb68b560da759139e8cbc6c90ed22dfe6cf34949af49bb86bcbf4d3a + checksum: 10c0/b2c527d8731c97f09e79ab477069138eba994a656473f57e1afd2480b18cd54cdb6600888e44d208b11efae01c123bc81629ecb77d8ed19397bc2a4b8da6da52 languageName: node linkType: hard -"postcss-lab-function@npm:^7.0.12": - version: 7.0.12 - resolution: "postcss-lab-function@npm:7.0.12" +"postcss-lab-function@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-lab-function@npm:8.0.0" dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/de39b59da3b97c18d055d81fba68993e93253184ed76f103c888273584f868c551d047814dd54445980a1bdc5987e8f8af141383d84ecc641e5a6ee7bd901095 + checksum: 10c0/8805a853016c5919afd95f7cbee9ba624f398d25c81a2a7987edc0d0031fee08732b02f64cb51f537f364b0de07fc837bb4bd375e0ea96a489860c59a94c0590 languageName: node linkType: hard @@ -11156,14 +10956,14 @@ __metadata: languageName: node linkType: hard -"postcss-logical@npm:^8.1.0": - version: 8.1.0 - resolution: "postcss-logical@npm:8.1.0" +"postcss-logical@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-logical@npm:9.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/0e2e9e901d8a550db7f682d46b1f7e4f363c1ada061dc8e4548e2b563c5e39f3684a2d7c3f11fe061188782bca37874e34967fc6179fa6d98a49ff66a0076d27 + checksum: 10c0/058394f2234464b043249317b2c0a2271fa5194ada0f39afdb0f5b7e2ad172b1f24da7ae2641dfbd44d1ee98f5438844bc85d6005b881f691630381673707c32 languageName: node linkType: hard @@ -11207,16 +11007,16 @@ __metadata: languageName: node linkType: hard -"postcss-nesting@npm:^13.0.2": - version: 13.0.2 - resolution: "postcss-nesting@npm:13.0.2" +"postcss-nesting@npm:^14.0.0": + version: 14.0.0 + resolution: "postcss-nesting@npm:14.0.0" dependencies: - "@csstools/selector-resolve-nested": "npm:^3.1.0" - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" + "@csstools/selector-resolve-nested": "npm:^4.0.0" + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/bfa0578b3b686c6374f5a7b2f6ef955cb7e13400de95a919975a982ae43c1e25db37385618f210715ff15393dc7ff8c26c7b156f06b8fb3118a426099cf7f1f2 + checksum: 10c0/ffa1799ed49759be7f09940614a8cfd06f15bd13e01a7eaabf788939c23a6b617ef281ae383f5b3b3078ddd6d78fd90624af164ea5a421698a1b0972cf14253c languageName: node linkType: hard @@ -11229,14 +11029,14 @@ __metadata: languageName: node linkType: hard -"postcss-overflow-shorthand@npm:^6.0.0": - version: 6.0.0 - resolution: "postcss-overflow-shorthand@npm:6.0.0" +"postcss-overflow-shorthand@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-overflow-shorthand@npm:7.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/6598321b2ed0b68461135395bba9c7f76a4672617770df1e8487f459bc975f4ded6c3d37b6f72a44f4f77f7b6789e0c6f927e66dbbf1bcde1537167dbea39968 + checksum: 10c0/706430a2b1c28f361aad1d90e618c04c7ad7861971f2447fbd0e34c1d095bec0e5f4ed2857c98e05f5a7bcc694b7b763d08d5f515d5d45dd942f4b046593d638 languageName: node linkType: hard @@ -11249,106 +11049,107 @@ __metadata: languageName: node linkType: hard -"postcss-place@npm:^10.0.0": - version: 10.0.0 - resolution: "postcss-place@npm:10.0.0" +"postcss-place@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-place@npm:11.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/ebb13deaac7648ba6042622375a31f78fbcc5209b7d196e478debbdf94525963fe621c932f4737a5b6b3d487af3b5ed6d059ed6193fdcbff6d3d5b150886ccc1 + checksum: 10c0/0b6b27426b198507583388a0e4f27552a136e5b9ccd1fc356dbd9a04c91e2bf6dcd680c3ad540ffc5aa1a63779467b9c7dc43d2164f83a0658505ca2408d79a9 languageName: node linkType: hard -"postcss-preset-env@npm:^10.1.5": - version: 10.6.0 - resolution: "postcss-preset-env@npm:10.6.0" +"postcss-preset-env@npm:^11.0.0": + version: 11.1.1 + resolution: "postcss-preset-env@npm:11.1.1" dependencies: - "@csstools/postcss-alpha-function": "npm:^1.0.1" - "@csstools/postcss-cascade-layers": "npm:^5.0.2" - "@csstools/postcss-color-function": "npm:^4.0.12" - "@csstools/postcss-color-function-display-p3-linear": "npm:^1.0.1" - "@csstools/postcss-color-mix-function": "npm:^3.0.12" - "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^1.0.2" - "@csstools/postcss-content-alt-text": "npm:^2.0.8" - "@csstools/postcss-contrast-color-function": "npm:^2.0.12" - "@csstools/postcss-exponential-functions": "npm:^2.0.9" - "@csstools/postcss-font-format-keywords": "npm:^4.0.0" - "@csstools/postcss-gamut-mapping": "npm:^2.0.11" - "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.12" - "@csstools/postcss-hwb-function": "npm:^4.0.12" - "@csstools/postcss-ic-unit": "npm:^4.0.4" - "@csstools/postcss-initial": "npm:^2.0.1" - "@csstools/postcss-is-pseudo-class": "npm:^5.0.3" - "@csstools/postcss-light-dark-function": "npm:^2.0.11" - "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" - "@csstools/postcss-logical-overflow": "npm:^2.0.0" - "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0" - "@csstools/postcss-logical-resize": "npm:^3.0.0" - "@csstools/postcss-logical-viewport-units": "npm:^3.0.4" - "@csstools/postcss-media-minmax": "npm:^2.0.9" - "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.5" - "@csstools/postcss-nested-calc": "npm:^4.0.0" - "@csstools/postcss-normalize-display-values": "npm:^4.0.0" - "@csstools/postcss-oklab-function": "npm:^4.0.12" - "@csstools/postcss-position-area-property": "npm:^1.0.0" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/postcss-property-rule-prelude-list": "npm:^1.0.0" - "@csstools/postcss-random-function": "npm:^2.0.1" - "@csstools/postcss-relative-color-syntax": "npm:^3.0.12" - "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" - "@csstools/postcss-sign-functions": "npm:^1.1.4" - "@csstools/postcss-stepped-value-functions": "npm:^4.0.9" - "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^1.0.1" - "@csstools/postcss-system-ui-font-family": "npm:^1.0.0" - "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.3" - "@csstools/postcss-trigonometric-functions": "npm:^4.0.9" - "@csstools/postcss-unset-value": "npm:^4.0.0" + "@csstools/postcss-alpha-function": "npm:^2.0.1" + "@csstools/postcss-cascade-layers": "npm:^6.0.0" + "@csstools/postcss-color-function": "npm:^5.0.0" + "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.0" + "@csstools/postcss-color-mix-function": "npm:^4.0.0" + "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.0" + "@csstools/postcss-content-alt-text": "npm:^3.0.0" + "@csstools/postcss-contrast-color-function": "npm:^3.0.0" + "@csstools/postcss-exponential-functions": "npm:^3.0.0" + "@csstools/postcss-font-format-keywords": "npm:^5.0.0" + "@csstools/postcss-gamut-mapping": "npm:^3.0.0" + "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.0" + "@csstools/postcss-hwb-function": "npm:^5.0.0" + "@csstools/postcss-ic-unit": "npm:^5.0.0" + "@csstools/postcss-initial": "npm:^3.0.0" + "@csstools/postcss-is-pseudo-class": "npm:^6.0.0" + "@csstools/postcss-light-dark-function": "npm:^3.0.0" + "@csstools/postcss-logical-float-and-clear": "npm:^4.0.0" + "@csstools/postcss-logical-overflow": "npm:^3.0.0" + "@csstools/postcss-logical-overscroll-behavior": "npm:^3.0.0" + "@csstools/postcss-logical-resize": "npm:^4.0.0" + "@csstools/postcss-logical-viewport-units": "npm:^4.0.0" + "@csstools/postcss-media-minmax": "npm:^3.0.0" + "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^4.0.0" + "@csstools/postcss-mixins": "npm:^1.0.0" + "@csstools/postcss-nested-calc": "npm:^5.0.0" + "@csstools/postcss-normalize-display-values": "npm:^5.0.0" + "@csstools/postcss-oklab-function": "npm:^5.0.0" + "@csstools/postcss-position-area-property": "npm:^2.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/postcss-property-rule-prelude-list": "npm:^2.0.0" + "@csstools/postcss-random-function": "npm:^3.0.0" + "@csstools/postcss-relative-color-syntax": "npm:^4.0.0" + "@csstools/postcss-scope-pseudo-class": "npm:^5.0.0" + "@csstools/postcss-sign-functions": "npm:^2.0.0" + "@csstools/postcss-stepped-value-functions": "npm:^5.0.0" + "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^2.0.0" + "@csstools/postcss-system-ui-font-family": "npm:^2.0.0" + "@csstools/postcss-text-decoration-shorthand": "npm:^5.0.0" + "@csstools/postcss-trigonometric-functions": "npm:^5.0.0" + "@csstools/postcss-unset-value": "npm:^5.0.0" autoprefixer: "npm:^10.4.23" browserslist: "npm:^4.28.1" - css-blank-pseudo: "npm:^7.0.1" - css-has-pseudo: "npm:^7.0.3" - css-prefers-color-scheme: "npm:^10.0.0" - cssdb: "npm:^8.6.0" - postcss-attribute-case-insensitive: "npm:^7.0.1" + css-blank-pseudo: "npm:^8.0.1" + css-has-pseudo: "npm:^8.0.0" + css-prefers-color-scheme: "npm:^11.0.0" + cssdb: "npm:^8.7.0" + postcss-attribute-case-insensitive: "npm:^8.0.0" postcss-clamp: "npm:^4.1.0" - postcss-color-functional-notation: "npm:^7.0.12" - postcss-color-hex-alpha: "npm:^10.0.0" - postcss-color-rebeccapurple: "npm:^10.0.0" - postcss-custom-media: "npm:^11.0.6" - postcss-custom-properties: "npm:^14.0.6" - postcss-custom-selectors: "npm:^8.0.5" - postcss-dir-pseudo-class: "npm:^9.0.1" - postcss-double-position-gradients: "npm:^6.0.4" - postcss-focus-visible: "npm:^10.0.1" - postcss-focus-within: "npm:^9.0.1" + postcss-color-functional-notation: "npm:^8.0.0" + postcss-color-hex-alpha: "npm:^11.0.0" + postcss-color-rebeccapurple: "npm:^11.0.0" + postcss-custom-media: "npm:^12.0.0" + postcss-custom-properties: "npm:^15.0.0" + postcss-custom-selectors: "npm:^9.0.0" + postcss-dir-pseudo-class: "npm:^10.0.0" + postcss-double-position-gradients: "npm:^7.0.0" + postcss-focus-visible: "npm:^11.0.0" + postcss-focus-within: "npm:^10.0.0" postcss-font-variant: "npm:^5.0.0" - postcss-gap-properties: "npm:^6.0.0" - postcss-image-set-function: "npm:^7.0.0" - postcss-lab-function: "npm:^7.0.12" - postcss-logical: "npm:^8.1.0" - postcss-nesting: "npm:^13.0.2" + postcss-gap-properties: "npm:^7.0.0" + postcss-image-set-function: "npm:^8.0.0" + postcss-lab-function: "npm:^8.0.0" + postcss-logical: "npm:^9.0.0" + postcss-nesting: "npm:^14.0.0" postcss-opacity-percentage: "npm:^3.0.0" - postcss-overflow-shorthand: "npm:^6.0.0" + postcss-overflow-shorthand: "npm:^7.0.0" postcss-page-break: "npm:^3.0.4" - postcss-place: "npm:^10.0.0" - postcss-pseudo-class-any-link: "npm:^10.0.1" + postcss-place: "npm:^11.0.0" + postcss-pseudo-class-any-link: "npm:^11.0.0" postcss-replace-overflow-wrap: "npm:^4.0.0" - postcss-selector-not: "npm:^8.0.1" + postcss-selector-not: "npm:^9.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/61162c9d675004db842d58829605c3c9ee81ed1a15684793a419b94c2c28e3be2ff9a7373f0996a1a255caf208d8f3d5dd907e61af1bbb0c7634e3215e87fc56 + checksum: 10c0/ed9e0b07c82368defe1ab7f776e8b163610c9db3a79e36cf95291f65959d912261aa8c4b94734bba047fe875fcf3eb6e0dd71dec93640dfd94578cba33833c5a languageName: node linkType: hard -"postcss-pseudo-class-any-link@npm:^10.0.1": - version: 10.0.1 - resolution: "postcss-pseudo-class-any-link@npm:10.0.1" +"postcss-pseudo-class-any-link@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-pseudo-class-any-link@npm:11.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/95e883996e87baf14fc09d25f9a763a2e9d599eb3b9c6b736e83a8c3d0b55841bcb886bccdf51b5b7fefc128cbd0187ad8841f59878f85bd1613642e592d7673 + checksum: 10c0/bd7e1daa582bc19fe0e30c3ab8b13f8cf75ae2f116124960e9d28092255ede77455cd982bef8010d7ea64fdffc66a14624e63fe438e6d957415f5a4bc16b59a4 languageName: node linkType: hard @@ -11386,24 +11187,24 @@ __metadata: languageName: node linkType: hard -"postcss-selector-not@npm:^8.0.1": - version: 8.0.1 - resolution: "postcss-selector-not@npm:8.0.1" +"postcss-selector-not@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-selector-not@npm:9.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/491ea3dcc421cd90135be786078521605e2062fb93624ea8813cfd5ba0d35143f931e2e608d5f20effd5ea7d3f4786d2afea2afa42d117779a0288e135f132b6 + checksum: 10c0/090ce6d65667b8b6b9c1c26b7bd7740a31f3e7f36c29574e622d5fac5ab2ce291ac11622914745f22dda531d75e36fcdd395339fbe8fac0fc04fa7d750bb5248 languageName: node linkType: hard -"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0": - version: 7.1.0 - resolution: "postcss-selector-parser@npm:7.1.0" +"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0, postcss-selector-parser@npm:^7.1.1": + version: 7.1.1 + resolution: "postcss-selector-parser@npm:7.1.1" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10c0/0fef257cfd1c0fe93c18a3f8a6e739b4438b527054fd77e9a62730a89b2d0ded1b59314a7e4aaa55bc256204f40830fecd2eb50f20f8cb7ab3a10b52aa06c8aa + checksum: 10c0/02d3b1589ddcddceed4b583b098b95a7266dacd5135f041e5d913ebb48e874fd333a36e564cc9a2ec426a464cb18db11cb192ac76247aced5eba8c951bf59507 languageName: node linkType: hard @@ -13377,74 +13178,74 @@ __metadata: languageName: node linkType: hard -"stylelint-config-recommended-scss@npm:^16.0.1": - version: 16.0.2 - resolution: "stylelint-config-recommended-scss@npm:16.0.2" +"stylelint-config-recommended-scss@npm:^17.0.0": + version: 17.0.0 + resolution: "stylelint-config-recommended-scss@npm:17.0.0" dependencies: postcss-scss: "npm:^4.0.9" - stylelint-config-recommended: "npm:^17.0.0" - stylelint-scss: "npm:^6.12.1" + stylelint-config-recommended: "npm:^18.0.0" + stylelint-scss: "npm:^7.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.24.0 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/d4e30a881e248d8b039347bf967526f6afe6d6a07f18e2747e14568de32273e819ba478be7a61a0dd63178931b4e891050a34e73d296ab533aa434209a7f3146 + checksum: 10c0/05b2e8d4316c2a8cc66eed0a2a8f01237e0ee8966a2e73d0b3c6706694f7630be165daa5a0cef511bc51f7e3fcb07a84c55d948c15fe6193a7e13cf9bb67c913 languageName: node linkType: hard -"stylelint-config-recommended@npm:^17.0.0": +"stylelint-config-recommended@npm:^18.0.0": + version: 18.0.0 + resolution: "stylelint-config-recommended@npm:18.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/c7f8ff45c76ec23f4c8c0438894726976fd5e872c59d489f959b728d9879bba20dbf0040cd29ad3bbc00eb32befd95f5b6ca150002bb8aea74b0797bc42ccc17 + languageName: node + linkType: hard + +"stylelint-config-standard-scss@npm:^17.0.0": version: 17.0.0 - resolution: "stylelint-config-recommended@npm:17.0.0" - peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/49e5d1c0f58197b2c5585b85fad814fed9bdec44c9870368c46a762664c5ff158c1145b6337456ae194409d692992b5b87421d62880422f71d8a3360417f5ad1 - languageName: node - linkType: hard - -"stylelint-config-standard-scss@npm:^16.0.0": - version: 16.0.0 - resolution: "stylelint-config-standard-scss@npm:16.0.0" + resolution: "stylelint-config-standard-scss@npm:17.0.0" dependencies: - stylelint-config-recommended-scss: "npm:^16.0.1" - stylelint-config-standard: "npm:^39.0.0" + stylelint-config-recommended-scss: "npm:^17.0.0" + stylelint-config-standard: "npm:^40.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.23.1 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/eb77f23824c5d649b193cb71d7f9b538b32b8cc1769451b2993270361127243d4011baf891ec265711b8e34e69ce28acb57ab6c3947b51fa3713ac26f4276439 + checksum: 10c0/0506537ba896f3d5e0fb002608090fcb41aa8ba7b65f1de8533702ce7c70e3f92b275782788a8356b5b687c86c53468c223e082226dda62780294b1cba324a36 languageName: node linkType: hard -"stylelint-config-standard@npm:^39.0.0": - version: 39.0.1 - resolution: "stylelint-config-standard@npm:39.0.1" +"stylelint-config-standard@npm:^40.0.0": + version: 40.0.0 + resolution: "stylelint-config-standard@npm:40.0.0" dependencies: - stylelint-config-recommended: "npm:^17.0.0" + stylelint-config-recommended: "npm:^18.0.0" peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/70a9862a2cedcc2a1807bd92fc91c40877270cf8a39576b91ae056d6de51d3b68104b26f71056ff22461b4319e9ec988d009abf10ead513b2ec15569d82e865a + stylelint: ^17.0.0 + checksum: 10c0/d8942552d53a3afda59b64d0c49503bb626fe5cef39a9e8c9583fcd60869f21431125ef4480ff27a59f7f2cf0da8af810d377129ef1d670ddc5def4defe2880c languageName: node linkType: hard -"stylelint-scss@npm:^6.12.1": - version: 6.12.1 - resolution: "stylelint-scss@npm:6.12.1" +"stylelint-scss@npm:^7.0.0": + version: 7.0.0 + resolution: "stylelint-scss@npm:7.0.0" dependencies: css-tree: "npm:^3.0.1" is-plain-object: "npm:^5.0.0" - known-css-properties: "npm:^0.36.0" - mdn-data: "npm:^2.21.0" + known-css-properties: "npm:^0.37.0" + mdn-data: "npm:^2.25.0" postcss-media-query-parser: "npm:^0.2.3" postcss-resolve-nested-selector: "npm:^0.1.6" - postcss-selector-parser: "npm:^7.1.0" + postcss-selector-parser: "npm:^7.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - stylelint: ^16.0.2 - checksum: 10c0/9a0903d34be3c75a72bef32402899db5f6b94c0823c5944fdf1acb2c3dc61c1f70fbb322558f8cb7e42dd01ed5e0dec22ed298f03b7bacc9f467c28330acae71 + stylelint: ^16.8.2 || ^17.0.0 + checksum: 10c0/07d0f20c6bcb34b8b0b6bfb1d4367b4825b52a7eef7dde2adfbaec11ebc67242e6b99dccf70dfbef1eb0a9bf8712fe0ab49d183ff6e4cca9c7f89752f7e27027 languageName: node linkType: hard @@ -13673,12 +13474,12 @@ __metadata: languageName: node linkType: hard -"thread-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "thread-stream@npm:3.0.0" +"thread-stream@npm:^4.0.0": + version: 4.0.0 + resolution: "thread-stream@npm:4.0.0" dependencies: real-require: "npm:^0.2.0" - checksum: 10c0/1f4da5a8c93b170cdc7c1ad774af49bb2af43f73cfd9a7f8fb02b766255b483eb6d0b734502c880397baa95c0ce3490088b9a487cff32d4e481aab6fe76560f5 + checksum: 10c0/f0a47a673af574062df20140ec3e857d679365253fcaa98a76c167c9a053ee03291f4b25bd89b078c7f6a48f07f49d5a49e4f5598bb1c8a263ec15955a018fbd languageName: node linkType: hard @@ -14317,11 +14118,11 @@ __metadata: linkType: hard "use-debounce@npm:^10.0.0": - version: 10.0.6 - resolution: "use-debounce@npm:10.0.6" + version: 10.1.0 + resolution: "use-debounce@npm:10.1.0" peerDependencies: react: "*" - checksum: 10c0/f0745de48fc344e6f90ea24384f3c79bf0733d06649827241135847abd67ee8db32f523d490c3276bce9f5a6867194ab1c0187bf6b84cabe1dd679037f4a527e + checksum: 10c0/1d2c9ab71be283f7ea9f9c78f3574aeb6ff6fbcb18a9c5daf7f633521a8978f14d190016d39fd773227e40e9929e223677bb311343dadf33ee0763ef24bff510 languageName: node linkType: hard @@ -14458,10 +14259,10 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.1.1": - version: 7.2.7 - resolution: "vite@npm:7.2.7" + version: 7.3.1 + resolution: "vite@npm:7.3.1" dependencies: - esbuild: "npm:^0.25.0" + esbuild: "npm:^0.27.0" fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.3" @@ -14508,7 +14309,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/0c502d9eb898d9c05061dbd8fd199f280b524bbb4c12ab5f88c7b12779947386684a269e4dd0aa424aa35bcd857f1aa44aadb9ea764702a5043af433052455b5 + checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 languageName: node linkType: hard