diff --git a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml index 004e74bf42eb67..5278933ecefed9 100644 --- a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml +++ b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml @@ -61,7 +61,7 @@ body: value: | Please at least include those informations: - Operating system: (eg. Ubuntu 24.04.2) - - Ruby version: (from `ruby --version`, eg. v3.4.4) + - Ruby version: (from `ruby --version`, eg. v3.4.9) - Node.js version: (from `node --version`, eg. v22.16.0) validations: required: false diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml index 0085aa875cb7d8..fed79add6d8ba6 100644 --- a/.github/actions/setup-ruby/action.yml +++ b/.github/actions/setup-ruby/action.yml @@ -17,7 +17,7 @@ runs: sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }} - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 + uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1 with: ruby-version: ${{ inputs.ruby-version }} bundler-cache: true diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml index bf4fbc547973ad..d0f88dcd90c4ea 100644 --- a/.github/workflows/bundler-audit.yml +++ b/.github/workflows/bundler-audit.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 + uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1 with: bundler-cache: true diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index faa963384fd9f9..0f8fd6d46ab697 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 + uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1 with: bundler-cache: true diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index e00031a4a18cc4..7be23472975436 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Ruby - uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 + uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1 with: bundler-cache: true diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml index 2722db09bbd43f..3c9601a108a9de 100644 --- a/.rubocop/rspec.yml +++ b/.rubocop/rspec.yml @@ -10,6 +10,7 @@ RSpec/MultipleMemoizedHelpers: Max: 20 # Overrides default of 5 Exclude: - 'spec/services/delete_account_service_spec.rb' + - 'spec/services/activitypub/process_status_update_service_spec.rb' RSpec/NamedSubject: EnforcedStyle: named_only diff --git a/.ruby-version b/.ruby-version index 7921bd0c892723..7bcbb3808b5089 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.8 +3.4.9 diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 1067b0e0414a13..b8b9bd385efa9c 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -26,9 +26,10 @@ import { modes } from './modes'; import '../app/javascript/styles/application.scss'; import './styles.css'; -const localeFiles = import.meta.glob('@/mastodon/locales/*.json', { - query: { as: 'json' }, -}); +// Disabling locales in Storybook as it's breaking with Vite 8. +// const localeFiles = import.meta.glob('@/mastodon/locales/*.json', { +// query: { as: 'json' }, +// }); // Initialize MSW initialize({ @@ -39,17 +40,17 @@ const preview: Preview = { // Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], globalTypes: { - locale: { - description: 'Locale for the story', - toolbar: { - title: 'Locale', - icon: 'globe', - items: Object.keys(localeFiles).map((path) => - path.replace('/mastodon/locales/', '').replace('.json', ''), - ), - dynamicTitle: true, - }, - }, + // locale: { + // description: 'Locale for the story', + // toolbar: { + // title: 'Locale', + // icon: 'globe', + // items: Object.keys(localeFiles).map((path) => + // path.replace('/mastodon/locales/', '').replace('.json', ''), + // ), + // dynamicTitle: true, + // }, + // }, theme: { description: 'Theme for the story', toolbar: { diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts deleted file mode 100644 index a08badd02f85e1..00000000000000 --- a/.storybook/vitest.setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; -import { setProjectAnnotations } from '@storybook/react-vite'; - -import * as projectAnnotations from './preview'; - -// This is an important step to apply the right configuration when testing your stories. -// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c0ff20d1aba3..56b6fbc92cf2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. +## [4.5.8] - 2026-03-24 + +### Security + +- Fix insufficient checks on quote authorizations ([GHSA-q4g8-82c5-9h33](https://github.com/mastodon/mastodon/security/advisories/GHSA-q4g8-82c5-9h33)) +- Fix open redirect in legacy path handler ([GHSA-xqw8-4j56-5hj6](https://github.com/mastodon/mastodon/security/advisories/GHSA-xqw8-4j56-5hj6)) +- Updated dependencies + +### Added + +- Add for searching already-known private GtS posts (#38057 by @ClearlyClaire) + +### Changed + +- Change media description length limit for remote media attachments from 1500 to 10000 characters (#37921 by @ClearlyClaire) +- Change HTTP signatures to skip the `Accept` header (#38132 by @ClearlyClaire) +- Change numeric AP endpoints to redirect to short account URLs when HTML is requested (#38056 by @ClearlyClaire) + +### Fixed + +- Fix some model definitions in `tootctl maintenance fix-duplicates` (#38214 by @ClearlyClaire) +- Fix overly strict checks for current username on account migration page (#38183 by @mjankowski) +- Fix OpenStack Swift Keystone token rate limiting (#38145 by @hugogameiro) +- Fix poll expiration notification being re-triggered on implicit updates (#38078 by @ClearlyClaire) +- Fix incorrect translation string in webauthn mailers (#38062 by @mjankowski) +- Fix “Unblock” and “Unmute” actions being disabled when blocked (#38075 by @ClearlyClaire) +- Fix username availability check being wrongly applied on race conditions (#37975 by @ClearlyClaire) +- Fix hover card unintentionally being shown in some cases (#38039 and #38112 by @diondiondion) +- Fix existing posts not being removed from lists when a list member is unfollowed (#38048 by @ClearlyClaire) + ## [4.5.7] - 2026-02-24 ### Security @@ -1245,1154 +1275,4 @@ The following changelog entries focus on changes visible to users, administrator - Fix empty environment variables not using default nil value (#27400 by @renchap) - Fix language sorting in settings (#27158 by @gunchleoc) -## [4.2.11] - 2024-08-16 - -### Added - -- Add support for incoming `` tag ([mediaformat](https://github.com/mastodon/mastodon/pull/31375)) - -### Changed - -- Change logic of block/mute bypass for mentions from moderators to only apply to visible roles with moderation powers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31271)) - -### Fixed - -- Fix incorrect rate limit on PUT requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31356)) -- Fix presence of `ß` in adjacent word preventing mention and hashtag matching ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31122)) -- Fix processing of webfinger responses with multiple `self` links ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31110)) -- Fix duplicate `orderedItems` in user archive's `outbox.json` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31099)) -- Fix click event handling when clicking outside of an open dropdown menu ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31251)) -- Fix status processing failing halfway when a remote post has a malformed `replies` attribute ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31246)) -- Fix `--verbose` option of `tootctl media remove`, which was previously erroneously removed ([mjankowski](https://github.com/mastodon/mastodon/pull/30536)) -- Fix division by zero on some video/GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30600)) -- Fix Web UI trying to save user settings despite being logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30324)) -- Fix hashtag regexp matching some link anchors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30190)) -- Fix local account search on LDAP login being case-sensitive ([raucao](https://github.com/mastodon/mastodon/pull/30113)) -- Fix development environment admin account not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29958)) -- Fix report reason selector in moderation interface not unselecting rules when changing category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29026)) -- Fix already-invalid reports failing to resolve ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29027)) -- Fix OCR when using S3/CDN for assets ([vmstan](https://github.com/mastodon/mastodon/pull/28551)) -- Fix error when encountering malformed `Tag` objects from Kbin ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28235)) -- Fix not all allowed image formats showing in file picker when uploading custom emoji ([june128](https://github.com/mastodon/mastodon/pull/28076)) -- Fix search popout listing unusable search options when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27918)) -- Fix processing of featured collections lacking an `items` attribute ([tribela](https://github.com/mastodon/mastodon/pull/27581)) -- Fix `mastodon:stats` decoration of stats rake task ([mjankowski](https://github.com/mastodon/mastodon/pull/31104)) - -## [4.2.10] - 2024-07-04 - -### Security - -- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7)) -- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3)) -- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx)) -- Update dependencies - -### Added - -- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4 - -### Changed - -- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854)) -- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865)) -- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691)) -- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377)) - -### Removed - -- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559)) - -### Fixed - -- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584)) -- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780)) -- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819)) -- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653)) - -## [4.2.9] - 2024-05-30 - -### Security - -- Update dependencies -- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf)) -- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh)) -- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553)) - -### Added - -- Add rate-limit on OAuth application registration ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) -- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592)) -- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092)) - -### Removed - -- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862)) -- Remove aggressive OAuth application vacuuming ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) - -### Fixed - -- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450)) -- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403)) -- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306)) -- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125)) -- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119)) -- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084)) -- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022)) -- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838)) -- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597)) -- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530)) -- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379)) -- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363)) - -## [4.2.8] - 2024-02-23 - -### Added - -- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355)) - In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week. - When this happens, users with the permission to change server settings will receive an email notification. - This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`. - -### Changed - -- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280)) - If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations. - Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again. - -### Fixed - -- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335)) -- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358)) - -## [4.2.7] - 2024-02-16 - -### Fixed - -- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207)) -- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065)) - -### Security - -- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36)) - -## [4.2.6] - 2024-02-14 - -### Security - -- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38)) - In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution. - If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`. - If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`. -- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j)) -- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187)) -- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x)) - In some rare cases, the streaming server was not notified of access tokens revocation on application deletion. -- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3)) - Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address. - This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another. - However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider. - For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable. - In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account. - -## [4.2.5] - 2024-02-01 - -### Security - -- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw)) - -## [4.2.4] - 2024-01-24 - -### Fixed - -- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823)) -- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816)) -- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788)) -- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748)) -- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476)) -- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665)) -- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558)) -- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252)) -- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035)) -- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763)) -- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479)) -- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127)) -- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482)) -- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339)) -- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337)) -- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268)) -- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367)) - -### Security - -- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801)) - -## [4.2.3] - 2023-12-05 - -### Fixed - -- Fix dependency on `json-canonicalization` version that has been made unavailable since last release - -## [4.2.2] - 2023-12-04 - -### Changed - -- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055)) -- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927)) -- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586)) -- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476)) -- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889)) -- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207)) - -### Fixed - -- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890)) -- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081)) -- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653)) -- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620)) -- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569)) -- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554)) -- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474)) -- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459)) -- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442)) -- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423)) -- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391)) -- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584)) -- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634)) - -## [4.2.1] - 2023-10-10 - -### Added - -- Add redirection on `/deck` URLs for logged-out users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27128)) -- Add support for v4.2.0 migrations to `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27147)) - -### Changed - -- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246)) -- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200)) - -### Fixed - -- Fix duplicate reports being sent when reporting some remote posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27355)) -- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350)) -- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307)) -- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286)) -- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272)) -- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656)) -- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986)) -- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187)) -- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204)) -- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253)) -- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258)) -- Fix incorrectly keeping outdated update notices absent from the API endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27021)) -- Fix import progress not updating on certain failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27247)) -- Fix websocket connections being incorrectly decremented twice on errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/27238)) -- Fix explore prompt appearing because of posts being received out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27211)) -- Fix explore prompt sometimes showing up when the home TL is loading ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27062)) -- Fix link handling of mentions in user profiles when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27185)) -- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186)) -- Fix notification toasts not respecting reduce-motion ([c960657](https://github.com/mastodon/mastodon/pull/27178)) -- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180)) -- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111)) -- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129)) -- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306)) -- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061)) -- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036)) - -## [4.2.0] - 2023-09-21 - -The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki). - -### Added - -- **Add full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26927), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27014)) - This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag). - This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with). - Results are now ordered chronologically. -- **Add admin notifications for new Mastodon versions** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26582)) - This is done by querying `https://api.joinmastodon.org/update-check` every 30 minutes in a background job. - That URL can be changed using the `UPDATE_CHECK_URL` environment variable, and the feature outright disabled by setting that variable to an empty string (`UPDATE_CHECK_URL=`). -- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508)) - This reorganized scattered privacy and reach settings to a single place, as well as improve their wording. -- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26606), [Gargron](https://github.com/mastodon/mastodon/pull/26666), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26960)) -- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281)) -- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26636)) - The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained. - The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account. - The forwarded-to domains can only include that of the original author and people being replied to. -- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189)) -- Add `ONE_CLICK_SSO_LOGIN` environment variable to directly link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901)) -- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289)) -- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211)) -- **Add exclusive lists** ([dariusk, necropolina](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324)) -- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603)) -- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372)) -- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388)) -- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807)) -- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561)) -- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510)) -- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658)) -- Add `hide_collections`, `discoverable` and `indexable` attributes to credentials API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26998)) -- Add `S3_ENABLE_CHECKSUM_MODE` environment variable to enable checksum verification on compatible S3-providers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435)) -- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872)) -- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724)) -- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822)) -- Add `authorized_fetch` server setting in addition to env var ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26958)) -- Add avatar image to webfinger responses ([tvler](https://github.com/mastodon/mastodon/pull/26558)) -- Add debug logging on signature verification failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26637), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26812)) -- Add explicit error messages when DeepL quota is exceeded ([lutoma](https://github.com/mastodon/mastodon/pull/26704)) -- Add Elasticsearch/OpenSearch version to “Software” in admin dashboard ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26652)) -- Add `data-nosnippet` attribute to remote posts and local posts with `noindex` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26648)) -- Add support for federating `memorial` attribute ([rrgeorge](https://github.com/mastodon/mastodon/pull/26583)) -- Add Cherokee and Kalmyk to languages dropdown ([gunchleoc](https://github.com/mastodon/mastodon/pull/26012), [gunchleoc](https://github.com/mastodon/mastodon/pull/26013)) -- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573)) -- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489)) - This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards). -- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542)) -- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295)) -- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443)) -- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26979)) -- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300)) -- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155)) -- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149)) -- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937)) -- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080)) -- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919), [Gargron](https://github.com/mastodon/mastodon/pull/26664)) -- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715)) -- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726)) -- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684)) -- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702)) -- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670)) -- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647)) -- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917), [Gargron](https://github.com/mastodon/mastodon/pull/26829), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26935)) -- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509)) -- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524)) -- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085)) -- Add logging of websocket send errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25280)) -- Add time zone preference ([Gargron](https://github.com/mastodon/mastodon/pull/25342), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26025)) -- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26509)) -- Add `data-nosnippet` so Google doesn't use trending posts in snippets for `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25279)) -- Add card with who invited you to join when displaying rules on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23475)) -- Add missing primary keys to `accounts_tags` and `statuses_tags` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25210)) -- Add support for custom sign-up URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25014), [renchap](https://github.com/mastodon/mastodon/pull/25108), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25190), [mgmn](https://github.com/mastodon/mastodon/pull/25531)) - This is set using `SSO_ACCOUNT_SIGN_UP` and reflected in the REST API by adding `registrations.sign_up_url` to the `/api/v2/instance` endpoint. -- Add polling and automatic redirection to `/start` on email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25013)) -- Add ability to block sign-ups from IP using the CLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24870)) -- Add ALT badges to media that has alternative text in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24782), [c960657](https://github.com/mastodon/mastodon/pull/26166) -- Add ability to include accounts with pending follow requests in lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19727), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24810)) -- Add trend management to admin API ([rrgeorge](https://github.com/mastodon/mastodon/pull/24257)) - - `POST /api/v1/admin/trends/statuses/:id/approve` - - `POST /api/v1/admin/trends/statuses/:id/reject` - - `POST /api/v1/admin/trends/links/:id/approve` - - `POST /api/v1/admin/trends/links/:id/reject` - - `POST /api/v1/admin/trends/tags/:id/approve` - - `POST /api/v1/admin/trends/tags/:id/reject` - - `GET /api/v1/admin/trends/links/publishers` - - `POST /api/v1/admin/trends/links/publishers/:id/approve` - - `POST /api/v1/admin/trends/links/publishers/:id/reject` -- Add user handle to notification mail recipient address ([HeitorMC](https://github.com/mastodon/mastodon/pull/24240)) -- Add progress indicator to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/24545)) -- Add client-side validation for taken username in sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24546)) -- Add `--approve` option to `tootctl accounts create` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24533)) -- Add “In Memoriam” banner back to profiles ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23591), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23614)) - This adds the `memorial` attribute to the `Account` REST API entity. -- Add colour to follow button when hashtag is being followed ([c960657](https://github.com/mastodon/mastodon/pull/24361)) -- Add further explanations to the profile link verification instructions ([drzax](https://github.com/mastodon/mastodon/pull/19723)) -- Add a link to Identity provider's account settings from the account settings ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24628)) -- Add support for streaming server to connect to postgres with self-signed certs through the `sslmode` URL parameter ([ramuuns](https://github.com/mastodon/mastodon/pull/21431)) -- Add support for specifying S3 storage classes through the `S3_STORAGE_CLASS` environment variable ([hyl](https://github.com/mastodon/mastodon/pull/22480)) -- Add support for incoming rich text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23913)) -- Add support for Ruby 3.2 ([tenderlove](https://github.com/mastodon/mastodon/pull/22928), [casperisfine](https://github.com/mastodon/mastodon/pull/24142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24202), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26934)) -- Add API parameter to safeguard unexpected mentions in new posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18350)) - -### Changed - -- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499), [renchap](https://github.com/mastodon/mastodon/pull/26614), [renchap](https://github.com/mastodon/mastodon/pull/26615)) -- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302)) -- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459), [tribela](https://github.com/mastodon/mastodon/pull/26461), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26593), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26795)) -- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184)) -- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248)) -- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452)) -- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378)) -- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874)) -- **Change local and federated timelines to be tabs of a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26633)) -- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034)) -- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751)) -- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310)) - This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`. - This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead. - Later versions of Mastodon will have other ways to get the same metrics. -- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26856)) - This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas. - To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`. -- Change DCT method used for JPEG encoding to float ([electroCutie](https://github.com/mastodon/mastodon/pull/26675)) -- Change from `node-redis` to `ioredis` for streaming ([gmemstr](https://github.com/mastodon/mastodon/pull/26581)) -- Change private statuses index to index without crutches ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26713)) -- Change video compression parameters ([Gargron](https://github.com/mastodon/mastodon/pull/26631), [Gargron](https://github.com/mastodon/mastodon/pull/26745), [Gargron](https://github.com/mastodon/mastodon/pull/26766), [Gargron](https://github.com/mastodon/mastodon/pull/26970)) -- Change admin e-mail notification settings to be their own settings group ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26596)) -- Change opacity of the delete icon in the search field to be more visible ([AntoninDelFabbro](https://github.com/mastodon/mastodon/pull/26449)) -- Change Account Search to prioritize username over display name ([jsgoldstein](https://github.com/mastodon/mastodon/pull/26623)) -- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545)) -- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396)) -- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416)) -- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26945)) -- Change indexing frequency from 5 minutes to 1 minute, add locks to schedulers ([Gargron](https://github.com/mastodon/mastodon/pull/26304)) -- Change column link to add a better keyboard focus indicator ([teeerevor](https://github.com/mastodon/mastodon/pull/26278)) -- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164)) -- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109)) -- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276)) -- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125), [Gargron](https://github.com/mastodon/mastodon/pull/26767)) -- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685)) -- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973), [Signez](https://github.com/mastodon/mastodon/pull/26019), [Signez](https://github.com/mastodon/mastodon/pull/26759)) -- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638)) -- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330)) -- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679)) -- Change dropdown icon above compose form from ellipsis to bars in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25661)) -- Change header backgrounds to use fewer different colors in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25577)) -- Change files to be deleted in batches instead of one-by-one ([Gargron](https://github.com/mastodon/mastodon/pull/23302), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25586), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25587)) -- Change emoji picker icon ([iparr](https://github.com/mastodon/mastodon/pull/25479)) -- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413), [c960657](https://github.com/mastodon/mastodon/pull/26538)) -- Change "bot" label to "automated" ([Gargron](https://github.com/mastodon/mastodon/pull/25356)) -- Change design of dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25107)) -- Change wording of “Content cache retention period” setting to highlight destructive implications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23261)) -- Change autolinking to allow carets in URL search params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) -- Change share action from being in action bar to being in dropdown in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25105)) -- Change sessions to be ordered from most-recent to least-recently updated ([frankieroberto](https://github.com/mastodon/mastodon/pull/25005)) -- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871)) -- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942)) -- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535)) -- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26801)) -- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707)) -- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706)) -- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708)) -- Change unauthenticated responses to be cached in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/24348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24665)) -- Change HTTP caching logic ([Gargron](https://github.com/mastodon/mastodon/pull/24347), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24604)) -- Change hashtags and mentions in bios to open in-app in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24643)) -- Change styling of the recommended accounts to allow bio to be more visible ([chike00](https://github.com/mastodon/mastodon/pull/24480)) -- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242)) -- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512)) -- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305)) -- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27012)) -- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726)) -- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131)) -- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020)) -- Change sidekiq-bulk's batch size from 10,000 to 1,000 jobs in one Redis call ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24034)) -- Change translation to only be offered for supported languages ([c960657](https://github.com/mastodon/mastodon/pull/23879), [c960657](https://github.com/mastodon/mastodon/pull/24037)) - This adds the `/api/v1/instance/translation_languages` REST API endpoint that returns an object with the supported translation language pairs in the form: - ```json - { - "fr": ["en", "de"] - } - ``` - (where `fr` is a supported source language and `en` and `de` or supported output language when translating a `fr` string) -- Change compose form checkbox to native input with `appearance: none` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22949)) -- Change posts' clickable area to be larger ([c960657](https://github.com/mastodon/mastodon/pull/23621)) -- Change `followed_by` link to `location=all` if account is local on /admin/accounts/:id page ([tribela](https://github.com/mastodon/mastodon/pull/23467)) - -### Removed - -- **Remove support for Node.js 14** ([renchap](https://github.com/mastodon/mastodon/pull/25198)) -- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237)) -- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655)) -- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989)) -- Remove obfuscation of reply count in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26768)) -- Remove `kmr` from language selection, as it was a duplicate for `ku` ([gunchleoc](https://github.com/mastodon/mastodon/pull/26014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26787)) -- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132)) -- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126)) -- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704)) -- Remove `tai` locale ([c960657](https://github.com/mastodon/mastodon/pull/23880)) -- Remove empty Kushubian (csb) local files ([nschonni](https://github.com/mastodon/mastodon/pull/24151)) -- Remove `Permissions-Policy` header from all responses ([Gargron](https://github.com/mastodon/mastodon/pull/24124)) - -### Fixed - -- **Fix filters not being applying in the explore page** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25887)) -- **Fix being unable to load past a full page of filtered posts in Home timeline** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24930)) -- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073)) -- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218)) -- **Fix migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808)) -- Fix crash when viewing a moderation appeal and the moderator account has been deleted ([xrobau](https://github.com/mastodon/mastodon/pull/25900)) -- Fix error in Web UI when server rules cannot be fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26957)) -- Fix paragraph margins resulting in irregular read-more cut-off in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26828)) -- Fix notification permissions being requested immediately after login ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26472)) -- Fix performances of profile directory ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26840), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26842)) -- Fix mute button and volume slider feeling disconnected in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26827), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26860)) -- Fix “Scoped order is ignored, it's forced to be batch order.” warnings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26793)) -- Fix blocked domain appearing in account feeds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26823)) -- Fix invalid `Content-Type` header for WebP images ([c960657](https://github.com/mastodon/mastodon/pull/26773)) -- Fix minor inefficiencies in `tootctl search deploy` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26721)) -- Fix filter form in profiles directory overflowing instead of wrapping ([arbolitoloco1](https://github.com/mastodon/mastodon/pull/26682)) -- Fix sign up steps progress layout in right-to-left locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26728)) -- Fix bug with “favorited by” and “reblogged by“ view on posts only showing up to 40 items ([timothyjrogers](https://github.com/mastodon/mastodon/pull/26577), [timothyjrogers](https://github.com/mastodon/mastodon/pull/26574)) -- Fix bad search type heuristic ([Gargron](https://github.com/mastodon/mastodon/pull/26673)) -- Fix not being able to negate prefix clauses in search ([Gargron](https://github.com/mastodon/mastodon/pull/26672)) -- Fix timeout on invalid set of exclusionary parameters in `/api/v1/timelines/public` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26239)) -- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375)) -- Fix light theme select option for hashtags ([teeerevor](https://github.com/mastodon/mastodon/pull/26311)) -- Fix AVIF attachments ([c960657](https://github.com/mastodon/mastodon/pull/26264)) -- Fix incorrect URL normalization when fetching remote resources ([c960657](https://github.com/mastodon/mastodon/pull/26219), [c960657](https://github.com/mastodon/mastodon/pull/26285)) -- Fix being unable to filter posts for individual Chinese languages ([gunchleoc](https://github.com/mastodon/mastodon/pull/26066)) -- Fix preview card sometimes linking to 4xx error pages ([c960657](https://github.com/mastodon/mastodon/pull/26200)) -- Fix emoji picker button scrolling with textarea content in single-column view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25304)) -- Fix missing border on error screen in light theme in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26152)) -- Fix UI overlap with the loupe icon in the Explore Tab ([gol-cha](https://github.com/mastodon/mastodon/pull/26113)) -- Fix unexpected redirection to `/explore` after sign-in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26143)) -- Fix `/api/v1/statuses/:id/unfavourite` and `/api/v1/statuses/:id/unreblog` returning non-updated counts ([c960657](https://github.com/mastodon/mastodon/pull/24365)) -- Fix clicking the “Back” button sometimes leading out of Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953), [CSFlorin](https://github.com/mastodon/mastodon/pull/24835), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/24867), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25281)) -- Fix processing of `null` ActivityPub activities ([tribela](https://github.com/mastodon/mastodon/pull/26021)) -- Fix hashtag posts not being removed from home feed on hashtag unfollow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26028)) -- Fix for "follows you" indicator in light web UI not readable ([vmstan](https://github.com/mastodon/mastodon/pull/25993)) -- Fix incorrect line break between icon and number of reposts & favourites ([edent](https://github.com/mastodon/mastodon/pull/26004)) -- Fix sounds not being loaded from assets host ([Signez](https://github.com/mastodon/mastodon/pull/25931)) -- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26482)) -- Fix trend calculation working on too many items at a time ([Gargron](https://github.com/mastodon/mastodon/pull/25835)) -- Fix dropdowns being disabled for logged out users in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25964)) -- Fix explore page being inaccessible when opted-out of trends in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25716)) -- Fix re-activated accounts possibly getting deleted by `AccountDeletionWorker` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25711)) -- Fix `/api/v2/search` not working with following query param ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25681)) -- Fix inefficient query when requesting a new confirmation email from a logged-in account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25669)) -- Fix unnecessary concurrent calls to `/api/*/instance` in web UI ([mgmn](https://github.com/mastodon/mastodon/pull/25663)) -- Fix resolving local URL for remote content ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) -- Fix search not being easily findable on smaller screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25576), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25631)) -- Fix j/k keyboard shortcuts on some status lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25554)) -- Fix missing validation on `default_privacy` setting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25513)) -- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) -- Fix non-interactive upload container being given a `button` role and tabIndex ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25462)) -- Fix always redirecting to onboarding in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25396)) -- Fix inconsistent use of middle dot (·) instead of bullet (•) to separate items ([j-f1](https://github.com/mastodon/mastodon/pull/25248)) -- Fix spacing of middle dots in the detailed status meta section ([j-f1](https://github.com/mastodon/mastodon/pull/25247)) -- Fix prev/next buttons color in media viewer ([renchap](https://github.com/mastodon/mastodon/pull/25231)) -- Fix email addresses not being properly updated in `tootctl maintenance fix-duplicates` ([mjankowski](https://github.com/mastodon/mastodon/pull/25118)) -- Fix unicode surrogate pairs sometimes being broken in page title ([eai04191](https://github.com/mastodon/mastodon/pull/25148)) -- Fix various inefficient queries against account domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25126)) -- Fix video player offering to expand in a lightbox when it's in an `iframe` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25067)) -- Fix post embed previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25071)) -- Fix inadequate error handling in several API controllers when given invalid parameters ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24947), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24958), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25063), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25072), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25386), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25595)) -- Fix uncaught `ActiveRecord::StatementInvalid` in Mastodon::IpBlocksCLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24861)) -- Fix various edge cases with local moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24812)) -- Fix `tootctl accounts cull` crashing when encountering a domain resolving to a private address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23378)) -- Fix `tootctl accounts approve --number N` not aproving the N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) -- Fix being unable to clear media description when editing posts ([c960657](https://github.com/mastodon/mastodon/pull/24720)) -- Fix unavailable translations not falling back to English ([mgmn](https://github.com/mastodon/mastodon/pull/24727)) -- Fix anonymous visitors getting a session cookie on first visit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24584), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24664)) -- Fix cutting off first letter of hashtag links sometimes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24623)) -- Fix crash in `tootctl accounts create --reattach --force` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24557), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24680)) -- Fix characters being emojified even when using Variation Selector 15 (text) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20949), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24615)) -- Fix uncaught ActiveRecord::StatementInvalid exception in `Mastodon::AccountsCLI#approve` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24590)) -- Fix email confirmation skip option in `tootctl accounts modify USERNAME --email EMAIL --confirm` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24578)) -- Fix tooltip for dates without time ([c960657](https://github.com/mastodon/mastodon/pull/24244)) -- Fix missing loading spinner and loading more on scroll in Private Mentions column ([c960657](https://github.com/mastodon/mastodon/pull/24446)) -- Fix account header image missing from `/settings/profile` on narrow screens ([c960657](https://github.com/mastodon/mastodon/pull/24433)) -- Fix height of announcements not being updated when using reduced animations ([c960657](https://github.com/mastodon/mastodon/pull/24354)) -- Fix inconsistent radius in advanced interface drawer ([thislight](https://github.com/mastodon/mastodon/pull/24407)) -- Fix loading more trending posts on scroll in the advanced interface ([OmmyZhang](https://github.com/mastodon/mastodon/pull/24314)) -- Fix poll ending notification for edited polls ([c960657](https://github.com/mastodon/mastodon/pull/24311)) -- Fix max width of media in `/about` and `/privacy-policy` ([mgmn](https://github.com/mastodon/mastodon/pull/24180)) -- Fix streaming API not being usable without `DATABASE_URL` ([Gargron](https://github.com/mastodon/mastodon/pull/23960)) -- Fix external authentication not running onboarding code for new users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23458)) - -## [4.1.8] - 2023-09-19 - -### Fixed - -- Fix post edits not being forwarded as expected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26936)) -- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729)) -- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814)) -- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409)) -- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500)) -- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608)) - -### Security - -- Fix missing HTML sanitization in translation API (CVE-2023-42452, [GHSA-2693-xr3m-jhqr](https://github.com/mastodon/mastodon/security/advisories/GHSA-2693-xr3m-jhqr)) -- Fix incorrect domain name normalization (CVE-2023-42451, [GHSA-v3xf-c9qf-j667](https://github.com/mastodon/mastodon/security/advisories/GHSA-v3xf-c9qf-j667)) - -## [4.1.7] - 2023-09-05 - -### Changed - -- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028)) - -### Fixed - -- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392)) -- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237)) -- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727)) - -## [4.1.6] - 2023-07-31 - -### Fixed - -- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228)) -- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233)) -- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116)) - -## [4.1.5] - 2023-07-21 - -### Added - -- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850)) - -### Changed - -- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055)) - -### Fixed - -- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25885)) -- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886)) -- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945)) - -### Security - -- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105)) - -## [4.1.4] - 2023-07-07 - -### Fixed - -- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794)) -- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796)) -- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788)) - -## [4.1.3] - 2023-07-06 - -### Added - -- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600)) - -### Changed - -- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058)) -- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868)) -- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852)) -- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614)) -- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510)) -- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) - -### Removed - -- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070)) - -### Fixed - -- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464)) -- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519)) -- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) -- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840)) -- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361)) -- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273)) -- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) -- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988)) -- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015)) -- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016)) -- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060)) -- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713)) -- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499)) -- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431)) -- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) -- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342)) - -### Security - -- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463)) -- Update dependencies -- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756)) -- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462) -- Fix timeout handling of outbound HTTP requests (CVE-2023-36461) -- Fix arbitrary file creation through media processing (CVE-2023-36460) -- Fix possible XSS in preview cards (CVE-2023-36459) - -## [4.1.2] - 2023-04-04 - -### Fixed - -- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377)) -- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302)) -- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200)) -- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337)) - -### Security - -- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334)) -- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379)) - -## [4.1.1] - 2023-03-16 - -### Added - -- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593)) -- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749)) -- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597)) -- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304)) -- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936)) -- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064)) -- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123)) -- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120)) - -### Changed - -- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836)) -- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320)) -- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701)) -- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956)) - -### Fixed - -- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805)) -- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520)) -- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526)) -- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566)) -- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764)) -- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801)) -- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804)) -- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787)) -- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574)) -- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567)) -- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957)) -- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953)) -- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958)) -- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803)) -- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988)) -- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029)) -- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046)) -- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975)) -- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019)) -- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751)) -- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611)) -- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568)) -- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750)) - -### Security - -- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136)) -- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137)) - -## [4.1.0] - 2023-02-10 - -### Added - -- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470)) -- **Add listing of followed hashtags** ([connorshea](https://github.com/mastodon/mastodon/pull/21773)) -- **Add support for editing media description and focus point of already-sent posts** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878)) - - Previously, you could add and remove attachments, but not edit media description of already-attached media - - REST API changes: - - `PUT /api/v1/statuses/:id` now takes an extra `media_attributes[]` array parameter with the `id` of the updated media and their updated `description`, `focus`, and `thumbnail` -- **Add follow request banner on account header** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785)) - - REST API changes: - - `Relationship` entities have an extra `requested_by` boolean attribute representing whether the represented user has requested to follow you -- **Add confirmation screen when handling reports** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156), [tribela](https://github.com/mastodon/mastodon/pull/23178)) -- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808)) -- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205)) -- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810)) -- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833)) -- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499)) - - REST API changes: - - Add `configuration.urls.status` attribute to the object returned by `GET /api/v2/instance` -- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938)) -- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131)) -- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895)) -- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149)) -- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328)) -- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330)) -- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397)) -- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063)) -- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589)) -- Add `DB_POOL` environment variable support for streaming server ([Gargron](https://github.com/mastodon/mastodon/pull/23470)) -- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048)) -- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946)) -- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423)) -- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904)) -- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022)) -- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602)) -- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013)) -- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262)) -- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667)) -- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629)) -- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025)) -- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282)) -- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081)) -- Add `lang` attribute to image description textarea and poll option field ([c960657](https://github.com/mastodon/mastodon/pull/23293)) -- Add `spellcheck` attribute to Content Warning and poll option input fields ([c960657](https://github.com/mastodon/mastodon/pull/23395)) -- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420)) -- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464)) -- Add `roles` attribute to `Account` entities in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23255), [tribela](https://github.com/mastodon/mastodon/pull/23428)) -- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706)) -- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790)) -- Add `policy` attribute to web push subscription objects in REST API at `/api/v1/push/subscriptions` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23210)) -- Add metrics endpoint to streaming API ([Gargron](https://github.com/mastodon/mastodon/pull/23388), [Gargron](https://github.com/mastodon/mastodon/pull/23469)) -- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617)) -- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929)) -- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795)) -- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549)) -- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513)) -- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568)) -- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576)) -- Add `lang` attribute to compose form inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23240)) - -### Changed - -- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315)) -- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956)) -- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897)) -- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272)) -- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850)) -- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692)) -- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541)) -- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013)) -- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491)) -- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388)) -- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411)) -- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923)) -- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490)) -- Change `POST /settings/applications/:id` to regenerate token on scopes change ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23359)) -- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553)) -- Change link previews for statuses to never use avatar as fallback ([Gargron](https://github.com/mastodon/mastodon/pull/23376)) -- Change email address input to be read-only for logged-in users when requesting a new confirmation e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23247)) -- Change notifications per page from 15 to 40 in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/23348)) -- Change number of stored items in home feed from 400 to 800 ([Gargron](https://github.com/mastodon/mastodon/pull/23349)) -- Change API rate limits from 300/5min per user to 1500/5min per user, 300/5min per app ([Gargron](https://github.com/mastodon/mastodon/pull/23347)) -- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465)) -- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037)) -- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608)) -- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864)) -- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774)) -- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285)) -- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692)) -- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028)) -- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637)) -- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385)) -- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999)) -- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759)) -- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790)) -- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479)) -- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327)) -- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316)) -- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324)) -- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555)) -- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960)) -- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517), [Akkiesoft](https://github.com/mastodon/mastodon/pull/23094)) -- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840)) -- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796)) -- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575)) -- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533)) -- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064)) -- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879)) -- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342)) - -### Removed - -- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477)) -- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693)) -- Remove `intersection-observer` polyfill for old Safari support ([shuuji3](https://github.com/mastodon/mastodon/pull/23284)) -- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078)) -- Remove post count and last posts from ActivityPub representation of hashtag collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23460)) - -### Fixed - -- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135)) -- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487)) -- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363)) -- Fix being stuck in edit mode when deleting the edited posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126)) -- Fix attached media uploads not being cleared when replying to a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23504)) -- Fix filters not being applied to some notification types ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23211)) -- Fix incorrect link in push notifications for some event types ([elizabeth-dev](https://github.com/mastodon/mastodon/pull/23286)) -- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907)) -- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091)) -- Fix moderation audit log items for warnings having incorrect links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23242)) -- Fix account activation being sometimes triggered before email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23245)) -- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979)) -- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700)) -- Fix attachments of edited posts not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565)) -- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988)) -- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134)) -- Fix expanded posts not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797)) -- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763)) -- Fix not being able to scroll in post history modal ([cadars](https://github.com/mastodon/mastodon/pull/23396)) -- Fix audio player volume control on Safari ([minacle](https://github.com/mastodon/mastodon/pull/23187)) -- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982)) -- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157)) -- Fix drag & drop upload area display in single-column mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23217)) -- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846)) -- Fix admin-set follow recommandations being case-sensitive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23500)) -- Fix unserialized `role` on account entities in admin API ([Gargron](https://github.com/mastodon/mastodon/pull/23290)) -- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861)) -- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062)) -- Fix email with empty domain name labels passing validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23246)) -- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127)) -- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270)) -- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772)) -- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741)) -- Fix error in `VerifyLinkService` when processing links with invalid URLs ([untitaker](https://github.com/mastodon/mastodon/pull/23204)) -- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191)) -- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724)) -- Fix misleading message briefly showing up when loading follow requests under some conditions ([c960657](https://github.com/mastodon/mastodon/pull/23386)) -- Fix “Share @:user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490)) -- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004)) -- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422)) -- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23318)) -- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324)) -- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655)) -- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303)) -- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006)) -- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088)) -- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060)) -- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783)) -- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462)) -- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462)) -- Fix processing error on incoming malformed JSON-LD under some situations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23416)) -- Fix potential duplicate posts in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121)) -- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120)) -- Fix styling of featured tags in light theme ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23252)) -- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302)) -- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326)) -- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117)) -- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006)) -- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994)) -- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202)) -- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231)) -- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141)) -- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275)) -- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829)) -- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932)) -- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943)) -- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926)) -- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440)) -- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814)) -- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606)) -- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463)) -- Fix minor post cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879)) -- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888)) -- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849)) -- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497)) -- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479)) -- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485)) -- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827)) -- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778)) -- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072)) -- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558)) -- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896)) -- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483)) -- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943)) -- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307)) - -### Security - -- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962)) -- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025)) -- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325)) -- Fix unbounded recursion in post discovery ([ClearlyClaire,nametoolong](https://github.com/mastodon/mastodon/pull/23506)) - -## [4.0.2] - 2022-11-15 - -### Fixed - -- Fix wrong color on mentions hidden behind content warning in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20724)) -- Fix filters from other users being used in the streaming service ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20719)) -- Fix `unsafe-eval` being used when `wasm-unsafe-eval` is enough in Content Security Policy ([Gargron](https://github.com/mastodon/mastodon/pull/20729), [prplecake](https://github.com/mastodon/mastodon/pull/20606)) - -## [4.0.1] - 2022-11-14 - -### Fixed - -- Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677)) - -## [4.0.0] - 2022-11-14 - -Some of the features in this release have been funded through the [NGI0 Discovery](https://nlnet.nl/discovery) Fund, a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825322. - -### Added - -- Add ability to filter followed accounts' posts by language ([Gargron](https://github.com/mastodon/mastodon/pull/19095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19268)) -- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924)) -- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945)) -- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245)) -- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398), [Gargron](https://github.com/mastodon/mastodon/pull/19712), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20018)) -- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335)) - - Previously, you could only see trends in your current language - - For less popular languages, that meant empty trends - - Now, trends in your preferred languages' are shown on top, with others beneath -- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296)) -- Add privacy icons to report modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19190)) -- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014)) -- Add option to open original page in dropdowns of remote content in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20299)) -- Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885)) -- Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544)) -- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506)) -- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737)) -- Add support for uploading AVIF files ([txt-file](https://github.com/mastodon/mastodon/pull/19647)) -- Add support for uploading HEIC files ([Gargron](https://github.com/mastodon/mastodon/pull/19618)) -- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209)) -- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248)) - - Set for how long remote posts or media should be cached on your server - - Hands-off alternative to `tootctl` commands -- **Add customizable user roles** ([Gargron](https://github.com/mastodon/mastodon/pull/18641), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18812), [Gargron](https://github.com/mastodon/mastodon/pull/19040), [tribela](https://github.com/mastodon/mastodon/pull/18825), [tribela](https://github.com/mastodon/mastodon/pull/18826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18776), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18777), [unextro](https://github.com/mastodon/mastodon/pull/18786), [tribela](https://github.com/mastodon/mastodon/pull/18824), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19436)) - - Previously, there were 3 hard-coded roles, user, moderator, and admin - - Create your own roles and decide which permissions they should have -- Add notifications for new reports ([Gargron](https://github.com/mastodon/mastodon/pull/18697), [Gargron](https://github.com/mastodon/mastodon/pull/19475)) -- Add ability to select all accounts matching search for batch actions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19053), [Gargron](https://github.com/mastodon/mastodon/pull/19054)) -- Add ability to view previous edits of a status in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19462)) -- Add ability to block sign-ups from IP ([Gargron](https://github.com/mastodon/mastodon/pull/19037)) -- **Add webhooks to admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18510)) -- Add admin API for managing domain allows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18668)) -- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247)) -- Add admin API for managing e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19066)) -- Add admin API for managing canonical e-mail blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19067)) -- Add admin API for managing IP blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19065), [trwnh](https://github.com/mastodon/mastodon/pull/20207)) -- Add `sensitized` attribute to accounts in admin REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20094)) -- Add `services` and `metadata` to the NodeInfo endpoint ([MFTabriz](https://github.com/mastodon/mastodon/pull/18563)) -- Add `--remove-role` option to `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/19477)) -- Add `--days` option to `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/18425)) -- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642)) -- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757)) -- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427)) -- Add `ENABLE_STARTTLS` environment variable ([erbridge](https://github.com/mastodon/mastodon/pull/20321)) -- Add caching for payload serialization during fan-out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19637), [Gargron](https://github.com/mastodon/mastodon/pull/19642), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19746), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19747), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19963)) -- Add assets from Twemoji 14.0 ([Gargron](https://github.com/mastodon/mastodon/pull/19733)) -- Add reputation and followers score boost to SQL-only account search ([Gargron](https://github.com/mastodon/mastodon/pull/19251)) -- Add Scots, Balaibalan, Láadan, Lingua Franca Nova, Lojban, Toki Pona to languages list ([VyrCossont](https://github.com/mastodon/mastodon/pull/20168)) -- Set autocomplete hints for e-mail, password and OTP fields ([rcombs](https://github.com/mastodon/mastodon/pull/19833), [offbyone](https://github.com/mastodon/mastodon/pull/19946), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20071)) -- Add support for DigitalOcean Spaces in setup wizard ([v-aisac](https://github.com/mastodon/mastodon/pull/20573)) - -### Changed - -- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710)) -- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103)) -- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273), [Gargron](https://github.com/mastodon/mastodon/pull/19801), [Gargron](https://github.com/mastodon/mastodon/pull/19790), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19773), [Gargron](https://github.com/mastodon/mastodon/pull/19798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19724), [Gargron](https://github.com/mastodon/mastodon/pull/19709), [Gargron](https://github.com/mastodon/mastodon/pull/19514), [Gargron](https://github.com/mastodon/mastodon/pull/19562), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19978), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20148), [Gargron](https://github.com/mastodon/mastodon/pull/20302), [cutls](https://github.com/mastodon/mastodon/pull/20400)) - - The web app can now be accessed without being logged in - - No more `/web` prefix on web app paths - - Profiles, posts, and other public pages now use the same interface for logged in and logged out users - - The web app displays a server information banner - - Pop-up windows for remote interaction have been replaced with a modal window - - No need to type in your username for remote interaction, copy-paste-to-search method explained - - Various hints throughout the app explain what the different timelines are - - New about page design - - New privacy policy page design shows when the policy was last updated - - All sections of the web app now have appropriate window titles - - The layout of the interface has been streamlined between different screen sizes - - Posts now use more horizontal space -- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583)) -- Change language to be carried over on reply in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18557)) -- Change "Unfollow" to "Cancel follow request" when request still pending in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/19363)) -- **Change post filtering system** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18058), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19050), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18894), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19051), [noellabo](https://github.com/mastodon/mastodon/pull/18923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18744), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20567)) - - Filtered keywords and phrases can now be grouped into named categories - - Filtered posts show which exact filter was hit - - Individual posts can be added to a filter - - You can peek inside filtered posts anyway -- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249)) -- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854)) -- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407), [Gargron](https://github.com/mastodon/mastodon/pull/19533)) -- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356)) -- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979)) -- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788)) -- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977)) -- Change admin announcements form to use single inputs for date and time in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18321)) -- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326)) -- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964)) -- Change `AUTHORIZED_FETCH` to not block unauthenticated REST API access ([Gargron](https://github.com/mastodon/mastodon/pull/19803)) -- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941)) -- Change mentions of blocked users to not be processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19725)) -- Change max. thumbnail dimensions to 640x360px (360p) ([Gargron](https://github.com/mastodon/mastodon/pull/19619)) -- Change post-processing to be deferred only for large media types ([Gargron](https://github.com/mastodon/mastodon/pull/19617)) -- Change link verification to only work for https links without unicode ([Gargron](https://github.com/mastodon/mastodon/pull/20304), [Gargron](https://github.com/mastodon/mastodon/pull/20295)) -- Change account deletion requests to spread out over time ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20222)) -- Change larger reblogs/favourites numbers to be shortened in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20303)) -- Change incoming activity processing to happen in `ingress` queue ([Gargron](https://github.com/mastodon/mastodon/pull/20264)) -- Change notifications to not link show preview cards in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20335)) -- Change amount of replies returned for logged out users in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20355)) -- Change in-app links to keep you in-app in web UI ([trwnh](https://github.com/mastodon/mastodon/pull/20540), [Gargron](https://github.com/mastodon/mastodon/pull/20628)) -- Change table header to be sticky in admin UI ([sk22](https://github.com/mastodon/mastodon/pull/20442)) - -### Removed - -- Remove setting that disables account deletes ([Gargron](https://github.com/mastodon/mastodon/pull/17683)) -- Remove digest e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/17985)) -- Remove unnecessary sections from welcome e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/19299)) -- Remove item titles from RSS feeds ([Gargron](https://github.com/mastodon/mastodon/pull/18640)) -- Remove volume number from hashtags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19253)) -- Remove Nanobox configuration ([tonyjiang](https://github.com/mastodon/mastodon/pull/17881)) - -### Fixed - -- Fix rules with same priority being sorted non-deterministically ([Gargron](https://github.com/mastodon/mastodon/pull/20623)) -- Fix error when invalid domain name is submitted ([Gargron](https://github.com/mastodon/mastodon/pull/19474)) -- Fix icons having an image role ([Gargron](https://github.com/mastodon/mastodon/pull/20600)) -- Fix connections to IPv6-only servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20108)) -- Fix unnecessary service worker registration and preloading when logged out in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20341)) -- Fix unnecessary and slow regex construction ([raggi](https://github.com/mastodon/mastodon/pull/20215)) -- Fix `mailers` queue not being used for mailers ([Gargron](https://github.com/mastodon/mastodon/pull/20274)) -- Fix error in webfinger redirect handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20260)) -- Fix report category not being set to `violation` if rule IDs are provided ([trwnh](https://github.com/mastodon/mastodon/pull/20137)) -- Fix nodeinfo metadata attribute being an array instead of an object ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20114)) -- Fix account endorsements not being idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20118)) -- Fix status and rule IDs not being strings in admin reports REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20122)) -- Fix error on invalid `replies_policy` in REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20126)) -- Fix redrafting a currently-editing post not leaving edit mode in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20023)) -- Fix performance by avoiding method cache busts ([raggi](https://github.com/mastodon/mastodon/pull/19957)) -- Fix opening the language picker scrolling the single-column view to the top in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19983)) -- Fix content warning button missing `aria-expanded` attribute in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19975)) -- Fix redundant `aria-pressed` attributes in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/19912)) -- Fix crash when external auth provider has no display name set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19962)) -- Fix followers count not being updated when migrating follows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19998)) -- Fix double button to clear emoji search input in web UI ([sunny](https://github.com/mastodon/mastodon/pull/19888)) -- Fix missing null check on applications on strike disputes ([kescherCode](https://github.com/mastodon/mastodon/pull/19851)) -- Fix featured tags not saving preferred casing ([Gargron](https://github.com/mastodon/mastodon/pull/19732)) -- Fix language not being saved when editing status ([Gargron](https://github.com/mastodon/mastodon/pull/19543)) -- Fix not being able to input featured tag with hash symbol ([Gargron](https://github.com/mastodon/mastodon/pull/19535)) -- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19629)) -- Fix being unable to withdraw follow request when confirmation modal is disabled in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19687)) -- Fix inaccurate admin log entry for re-sending confirmation e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19674)) -- Fix edits not being immediately reflected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19673)) -- Fix bookmark import stopping at the first failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19669)) -- Fix account action type validation ([Gargron](https://github.com/mastodon/mastodon/pull/19476)) -- Fix upload progress not communicating processing phase in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19530)) -- Fix wrong host being used for custom.css when asset host configured ([Gargron](https://github.com/mastodon/mastodon/pull/19521)) -- Fix account migration form ever using outdated account data ([Gargron](https://github.com/mastodon/mastodon/pull/18429), [nightpool](https://github.com/mastodon/mastodon/pull/19883)) -- Fix error when uploading malformed CSV import ([Gargron](https://github.com/mastodon/mastodon/pull/19509)) -- Fix avatars not using image tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19488)) -- Fix handling of duplicate and out-of-order notifications in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19693)) -- Fix reblogs being discarded after the reblogged status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19731)) -- Fix indexing scheduler trying to index when Elasticsearch is disabled ([Gargron](https://github.com/mastodon/mastodon/pull/19805)) -- Fix n+1 queries when rendering initial state JSON ([Gargron](https://github.com/mastodon/mastodon/pull/19795)) -- Fix n+1 query during status removal ([Gargron](https://github.com/mastodon/mastodon/pull/19753)) -- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817)) -- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455)) -- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428)) -- Fix button to dismiss suggestions not showing up in search results in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19325)) -- Fix language dropdown sometimes not appearing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19246)) -- Fix quickly switching notification filters resulting in empty or incorrect list in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18960)) -- Fix media modal link button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18877)) -- Fix error upon successful account migration ([Gargron](https://github.com/mastodon/mastodon/pull/19386)) -- Fix negatives values in search index causing queries to fail ([Gargron](https://github.com/mastodon/mastodon/pull/19464), [Gargron](https://github.com/mastodon/mastodon/pull/19481)) -- Fix error when searching for invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18580)) -- Fix IP blocks not having a unique index ([Gargron](https://github.com/mastodon/mastodon/pull/19456)) -- Fix remote account in contact account setting not being used ([Gargron](https://github.com/mastodon/mastodon/pull/19351)) -- Fix swallowing mentions of unconfirmed/unapproved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19191)) -- Fix incorrect and slow cache invalidation when blocking domain and removing media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19062)) -- Fix HTTPs redirect behaviour when running as I2P service ([gi-yt](https://github.com/mastodon/mastodon/pull/18929)) -- Fix deleted pinned posts potentially counting towards the pinned posts limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19005)) -- Fix compatibility with OpenSSL 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18449)) -- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760)) -- Fix suspicious sign-in mails never being sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18599)) -- Fix fallback locale when somehow user's locale is an empty string ([tribela](https://github.com/mastodon/mastodon/pull/18543)) -- Fix avatar/header not being deleted locally when deleted on remote account ([tribela](https://github.com/mastodon/mastodon/pull/18973)) -- Fix missing `,` in Blurhash validation ([noellabo](https://github.com/mastodon/mastodon/pull/18660)) -- Fix order by most recent not working for relationships page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/18996)) -- Fix uncaught error when invalid date is supplied to API ([Gargron](https://github.com/mastodon/mastodon/pull/19480)) -- Fix REST API sometimes returning HTML on error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19135)) -- Fix ambiguous column names in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/19206)) -- Fix ambiguous column names in `tootctl search deploy` ([mashirozx](https://github.com/mastodon/mastodon/pull/18993)) -- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662)) -- Fix `CAS_DISPLAY_NAME`, `SAML_DISPLAY_NAME` and `OIDC_DISPLAY_NAME` being ignored ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18568)) -- Fix various typos in comments throughout the codebase ([luzpaz](https://github.com/mastodon/mastodon/pull/18604)) -- Fix CSV import error when rows include unicode characters ([HamptonMakes](https://github.com/mastodon/mastodon/pull/20592)) - -### Security - -- Fix being able to spoof link verification ([Gargron](https://github.com/mastodon/mastodon/pull/20217)) -- Fix emoji substitution not applying only to text nodes in backend code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20641)) -- Fix emoji substitution not applying only to text nodes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20640)) -- Fix rate limiting for paths with formats ([Gargron](https://github.com/mastodon/mastodon/pull/20675)) -- Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) - -_For previous changes, review the [stable-3.5 branch](https://github.com/mastodon/mastodon/blob/stable-3.5/CHANGELOG.md)_ +_For previous changes, review the [stable-4.2 branch](https://github.com/mastodon/mastodon/blob/stable-4.2/CHANGELOG.md)_ diff --git a/Dockerfile b/Dockerfile index c06bc84a3395dc..c26f957cee23fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io" # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.8" +ARG RUBY_VERSION="3.4.9" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="24" @@ -181,7 +181,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.18.0 +ARG VIPS_VERSION=8.18.1 # 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 0ac44afc3cd37c..22bbb03a0c5a59 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -13,7 +13,8 @@ - [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) - [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) - [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) -- [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md): partial support for incoming quote-posts +- [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md) +- [FEP-3b86: Activity Intents](https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md): offer handlers for `Object` and `Create` (with support for the `content` parameter only), has support for the `Follow`, `Announce`, `Like` and `Object` intents ## ActivityPub in Mastodon @@ -67,4 +68,6 @@ The following table summarizes those limits. | 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 | -| Media and avatar/header descriptions (`name`/`summary`) | 1500 | Description will be truncated | +| Media and avatar/header descriptions (`name`/`summary`) | 10000 | Description will be truncated | +| Collection name (`FeaturedCollection` `name`) | 256 | Name will be truncated | +| Collection description (`FeaturedCollection` `summary`) | 2048 | Description will be truncated | diff --git a/Gemfile b/Gemfile index 1ff1ebf7de56df..40f95d56e9f27c 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'addressable', '~> 2.8' gem 'bootsnap', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'chewy', '~> 7.3' +gem 'chewy' gem 'devise' gem 'devise-two-factor' @@ -67,7 +67,6 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar' gem 'mutex_m' gem 'nokogiri', '~> 1.15' -gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' gem 'premailer-rails' @@ -96,27 +95,28 @@ gem 'tzinfo-data', '~> 1.2023' gem 'webauthn', '~> 3.0' gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' +gem 'json' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' gem 'prometheus_exporter', '~> 2.2', require: false -gem 'opentelemetry-api', '~> 1.7.0' +gem 'opentelemetry-api', '~> 1.8.0' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.32.0', require: false 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.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-excon', '~> 0.28.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.32.0', require: false + gem 'opentelemetry-instrumentation-http', '~> 0.29.0', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.28.0', require: false + gem 'opentelemetry-instrumentation-net_http', '~> 0.28.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-rack', '~> 0.30.0', require: false + gem 'opentelemetry-instrumentation-rails', '~> 0.40.0', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 4096fb39d1d08f..fc2beaa711124b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,31 +10,31 @@ GIT GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.16) + action_text-trix (2.1.17) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.2.1) + actionpack (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activesupport (= 8.1.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.2.1) + actionview (= 8.1.2.1) + activesupport (= 8.1.2.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -42,16 +42,16 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.2.1) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.2.1) + activesupport (= 8.1.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -61,22 +61,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.2.1) + activesupport (= 8.1.2.1) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.2.1) + activesupport (= 8.1.2.1) + activerecord (8.1.2.1) + activemodel (= 8.1.2.1) + activesupport (= 8.1.2.1) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activesupport (= 8.1.2.1) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.2.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -99,8 +99,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1222.0) - aws-sdk-core (3.243.0) + aws-partitions (1.1227.0) + aws-sdk-core (3.244.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -108,11 +108,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.122.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (1.123.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.213.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-s3 (1.217.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -150,18 +150,18 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-playwright-driver (0.5.8) + capybara-playwright-driver (0.5.9) addressable capybara playwright-ruby-client (>= 1.16.0) case_transform (0.2) activesupport cbor (0.5.10.1) - cgi (0.4.2) + cgi (0.5.1) charlock_holmes (0.7.9) - chewy (7.6.0) - activesupport (>= 5.2) - elasticsearch (>= 7.14.0, < 8) + chewy (8.0.1) + activesupport (>= 7.2) + elasticsearch (>= 8.14, < 9.0) elasticsearch-dsl childprocess (5.1.0) logger (~> 1.5) @@ -170,7 +170,7 @@ GEM cocoon (1.2.15) color_diff (0.2) concurrent-ruby (1.3.6) - connection_pool (2.5.5) + connection_pool (3.0.2) cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -190,7 +190,7 @@ GEM irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) - devise (5.0.2) + devise (5.0.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 7.0) @@ -209,31 +209,31 @@ GEM activerecord (>= 4.2, < 9.0) docile (1.4.1) domain_name (0.6.20240107) - doorkeeper (5.8.2) + doorkeeper (5.9.0) railties (>= 5) dotenv (3.2.0) drb (2.2.3) dry-cli (1.4.1) - elasticsearch (7.17.11) - elasticsearch-api (= 7.17.11) - elasticsearch-transport (= 7.17.11) - elasticsearch-api (7.17.11) + elastic-transport (8.4.1) + faraday (< 3) multi_json - elasticsearch-dsl (0.1.10) - elasticsearch-transport (7.17.11) - base64 - faraday (>= 1, < 3) + elasticsearch (8.19.3) + elastic-transport (~> 8.3) + elasticsearch-api (= 8.19.3) + ostruct + elasticsearch-api (8.19.3) multi_json + elasticsearch-dsl (0.1.10) email_validator (2.2.4) activemodel erb (6.0.2) erubi (1.13.1) et-orbi (1.4.0) tzinfo - excon (1.3.2) + excon (1.4.0) logger fabrication (3.0.0) - faker (3.6.0) + faker (3.6.1) i18n (>= 1.8.11, < 2) faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) @@ -246,17 +246,19 @@ GEM faraday-net_http (3.4.2) net-http (~> 0.5) fast_blank (1.0.1) - fastimage (2.4.0) + fastimage (2.4.1) ffi (1.17.3) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake - flatware (2.3.4) + flatware (2.4.0) + benchmark drb + logger thor (< 2.0) - flatware-rspec (2.3.4) - flatware (= 2.3.4) - rspec (>= 3.6) + flatware-rspec (2.4.0) + flatware (= 2.4.0) + rspec (>= 3.8) fog-core (2.6.0) builder excon (~> 1.0) @@ -276,9 +278,9 @@ GEM raabro (~> 1.4) globalid (1.3.0) activesupport (>= 6.1) - google-protobuf (4.33.5) + google-protobuf (4.34.0) bigdecimal - rake (>= 13) + rake (~> 13.3) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) haml (7.2.0) @@ -304,8 +306,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.26.4) - redis-client (= 0.26.4) + hiredis-client (0.28.0) + redis-client (= 0.28.0) hkdf (0.3.0) htmlentities (4.4.2) http (5.3.1) @@ -352,7 +354,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.18.1) + json (2.19.2) json-canonicalization (1.0.0) json-jwt (1.17.0) activesupport (>= 4.2) @@ -372,7 +374,7 @@ GEM json-ld-preloaded (3.3.2) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (6.1.0) + json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) jsonapi-renderer (0.2.2) @@ -429,7 +431,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.25.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) @@ -446,7 +448,7 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2026.0224) + mime-types-data (3.2026.0317) mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (6.0.2) @@ -470,12 +472,9 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.1) + nokogiri (1.19.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.15) - bigdecimal (>= 3.0) - ostruct (>= 0.2) omniauth (2.1.4) hashie (>= 3.4.6) logger @@ -507,13 +506,14 @@ GEM tzinfo validate_url webfinger (~> 2.0) - openssl (3.3.2) + openssl (4.0.1) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.7.0) + opentelemetry-api (1.8.0) + logger opentelemetry-common (0.23.0) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.31.1) + opentelemetry-exporter-otlp (0.32.0) google-protobuf (>= 3.18) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) @@ -527,7 +527,7 @@ GEM opentelemetry-common (~> 0.21) opentelemetry-instrumentation-action_mailer (0.6.1) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-action_pack (0.15.1) + opentelemetry-instrumentation-action_pack (0.16.0) opentelemetry-instrumentation-rack (~> 0.29) opentelemetry-instrumentation-action_view (0.11.2) opentelemetry-instrumentation-active_support (~> 0.10) @@ -547,23 +547,23 @@ GEM opentelemetry-registry (~> 0.1) opentelemetry-instrumentation-concurrent_ruby (0.24.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.27.0) + opentelemetry-instrumentation-excon (0.28.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-faraday (0.31.0) + opentelemetry-instrumentation-faraday (0.32.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http (0.28.0) + opentelemetry-instrumentation-http (0.29.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.27.0) + opentelemetry-instrumentation-http_client (0.28.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-net_http (0.27.0) + opentelemetry-instrumentation-net_http (0.28.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-pg (0.35.0) opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rack (0.29.0) + opentelemetry-instrumentation-rack (0.30.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rails (0.39.1) + opentelemetry-instrumentation-rails (0.40.0) opentelemetry-instrumentation-action_mailer (~> 0.6) opentelemetry-instrumentation-action_pack (~> 0.15) opentelemetry-instrumentation-action_view (~> 0.11) @@ -657,20 +657,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.2.1) + actioncable (= 8.1.2.1) + actionmailbox (= 8.1.2.1) + actionmailer (= 8.1.2.1) + actionpack (= 8.1.2.1) + actiontext (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activemodel (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.2.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -681,9 +681,9 @@ GEM rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -709,7 +709,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.26.4) + redis-client (0.28.0) connection_pool regexp_parser (2.11.3) reline (0.6.3) @@ -738,17 +738,17 @@ GEM rspec-support (~> 3.13.0) rspec-github (3.0.0) rspec-core (~> 3.0) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.3) + rspec-rails (8.0.4) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) rspec-sidekiq (5.3.0) rspec-core (~> 3.0) rspec-expectations (~> 3.0) @@ -766,7 +766,7 @@ GEM rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-capybara (2.22.1) @@ -792,7 +792,7 @@ GEM lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) - ruby-prof (2.0.2) + ruby-prof (2.0.4) base64 ostruct ruby-progressbar (1.13.0) @@ -847,7 +847,7 @@ GEM stackprof (0.2.28) starry (0.2.0) base64 - stoplight (5.7.0) + stoplight (5.8.0) concurrent-ruby zeitwerk stringio (3.2.0) @@ -864,10 +864,10 @@ GEM unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) climate_control - test-prof (1.5.2) + test-prof (1.6.0) thor (1.5.0) tilt (2.7.0) - timeout (0.6.0) + timeout (0.6.1) tpm-key_attestation (0.14.1) bindata (~> 2.4) openssl (> 2.0) @@ -924,7 +924,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.26.1) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -958,7 +958,7 @@ DEPENDENCIES capybara (~> 3.39) capybara-playwright-driver charlock_holmes (~> 0.7.7) - chewy (~> 7.3) + chewy climate_control cocoon (~> 1.2) color_diff (~> 0.1) @@ -996,6 +996,7 @@ DEPENDENCIES inline_svg irb (~> 1.8) jd-paperclip-azure (~> 3.0) + json json-ld json-ld-preloaded (~> 3.2) json-schema (~> 6.0) @@ -1014,25 +1015,24 @@ DEPENDENCIES net-http (~> 0.6.0) net-ldap (~> 0.18) nokogiri (~> 1.15) - oj (~> 3.14) omniauth (~> 2.0) omniauth-cas (~> 3.0.0.beta.1) omniauth-rails_csrf_protection (~> 2.0) omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.8.0) - opentelemetry-api (~> 1.7.0) - opentelemetry-exporter-otlp (~> 0.31.0) + opentelemetry-api (~> 1.8.0) + opentelemetry-exporter-otlp (~> 0.32.0) 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.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-excon (~> 0.28.0) + opentelemetry-instrumentation-faraday (~> 0.32.0) + opentelemetry-instrumentation-http (~> 0.29.0) + opentelemetry-instrumentation-http_client (~> 0.28.0) + opentelemetry-instrumentation-net_http (~> 0.28.0) opentelemetry-instrumentation-pg (~> 0.35.0) - opentelemetry-instrumentation-rack (~> 0.29.0) - opentelemetry-instrumentation-rails (~> 0.39.0) + opentelemetry-instrumentation-rack (~> 0.30.0) + opentelemetry-instrumentation-rails (~> 0.40.0) opentelemetry-instrumentation-redis (~> 0.28.0) opentelemetry-instrumentation-sidekiq (~> 0.28.0) opentelemetry-sdk (~> 1.4) @@ -1097,7 +1097,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.4.8 + ruby 3.4.9 BUNDLED WITH - 4.0.7 + 4.0.8 diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index cf46bf21b5e44c..b5926d94fdaf1f 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -26,9 +26,9 @@ def skip_unknown_actor_activity end def unknown_affected_account? - json = Oj.load(body, mode: :strict) + json = JSON.parse(body) json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor']) - rescue Oj::ParseError + rescue JSON::ParserError false end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 84a1680f9c263d..74a5d806aee815 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -121,7 +121,11 @@ def set_status end def set_statuses - @statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE) + @statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(*preload_columns, reblog: [:account, *preload_columns]).page(params[:page]).per(PER_PAGE) + end + + def preload_columns + [:application, :preloadable_poll, :media_attachments, active_mentions: :account] end def edit_status_account_id diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 9f9216f9b3c7b0..16042616a5108b 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -38,7 +38,7 @@ def create headers.merge!(response.headers) - self.response_body = Oj.dump(response.body) + self.response_body = response.body.to_json self.status = response.status rescue ActiveRecord::RecordInvalid => e render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422 diff --git a/app/controllers/api/v1/donation_campaigns_controller.rb b/app/controllers/api/v1/donation_campaigns_controller.rb index cdd7503b304659..43df1fdc3eb88a 100644 --- a/app/controllers/api/v1/donation_campaigns_controller.rb +++ b/app/controllers/api/v1/donation_campaigns_controller.rb @@ -35,7 +35,7 @@ def from_cache return if key.blank? campaign = Rails.cache.read("donation_campaign:#{key}", raw: true) - Oj.load(campaign) if campaign.present? + JSON.parse(campaign) if campaign.present? end def save_to_cache!(campaign) @@ -44,7 +44,7 @@ def save_to_cache!(campaign) Rails.cache.write_multi( { request_key => campaign_key(campaign), - "donation_campaign:#{campaign_key(campaign)}" => Oj.dump(campaign), + "donation_campaign:#{campaign_key(campaign)}" => campaign.to_json, }, expires_in: 1.hour, raw: true @@ -57,10 +57,10 @@ def fetch_campaign url.query_values = { platform: 'web', seed: seed, locale: locale, environment: Rails.configuration.x.donation_campaigns.environment }.compact Request.new(:get, url.to_s).perform do |res| - return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200 + return JSON.parse(res.body_with_limit) if res.code == 200 end end - rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError + rescue *Mastodon::HTTP_CONNECTION_ERRORS, JSON::ParserError nil end diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb index 8eaf7767df87e0..cfb708ff3082d6 100644 --- a/app/controllers/api/v1/markers_controller.rb +++ b/app/controllers/api/v1/markers_controller.rb @@ -32,13 +32,7 @@ def create private def serialize_map(map) - serialized = {} - - map.each_pair do |key, value| - serialized[key] = ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer).as_json - end - - Oj.dump(serialized) + map.transform_values { |value| ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer) } end def resource_params diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 196d0ef3a7012a..02907f4fb44461 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -25,7 +25,9 @@ def account_params :display_name, :note, :avatar, + :avatar_description, :header, + :header_description, :locked, :bot, :discoverable, diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 32a5f71293d537..39662eff733188 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -30,7 +30,7 @@ def distribute_add_activity! adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) + ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id) end def distribute_remove_activity! @@ -40,6 +40,6 @@ def distribute_remove_activity! adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) + ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a19fcc7a0aeb1b..deb60f69bc7f8f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base include UserTrackingConcern include SessionTrackingConcern include CacheConcern + include ErrorResponses include PreloadingConcern include DomainControlHelper include DatabaseHelper @@ -23,21 +24,6 @@ class ApplicationController < ActionController::Base helper_method :limited_federation_mode? helper_method :skip_csrf_meta_tags? - rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request - rescue_from Mastodon::NotPermittedError, with: :forbidden - rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found - rescue_from ActionController::UnknownFormat, with: :not_acceptable - rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content - rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests - - rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) - rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable - - rescue_from Seahorse::Client::NetworkingError do |e| - Rails.logger.warn "Storage server error: #{e}" - service_unavailable - end - before_action :check_self_destruct! before_action :store_referrer, except: :raise_not_found, if: :devise_controller? @@ -118,42 +104,6 @@ def truthy_param?(key) ActiveModel::Type::Boolean.new.cast(params[key]) end - def forbidden - respond_with_error(403) - end - - def not_found - respond_with_error(404) - end - - def gone - respond_with_error(410) - end - - def unprocessable_content - respond_with_error(422) - end - - def not_acceptable - respond_with_error(406) - end - - def bad_request - respond_with_error(400) - end - - def internal_server_error - respond_with_error(500) - end - - def service_unavailable - respond_with_error(503) - end - - def too_many_requests - respond_with_error(429) - end - def single_user_mode? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists? end @@ -178,13 +128,6 @@ def current_session @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end - def respond_with_error(code) - respond_to do |format| - format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } - format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } - end - end - def check_self_destruct! return unless self_destruct? diff --git a/app/controllers/concerns/error_responses.rb b/app/controllers/concerns/error_responses.rb new file mode 100644 index 00000000000000..402ade0066a098 --- /dev/null +++ b/app/controllers/concerns/error_responses.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module ErrorResponses + extend ActiveSupport::Concern + + included do + rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content + rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request + rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found + rescue_from ActionController::UnknownFormat, with: :not_acceptable + rescue_from Mastodon::NotPermittedError, with: :forbidden + rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable + rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests + rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) + + rescue_from Seahorse::Client::NetworkingError do |e| + Rails.logger.warn "Storage server error: #{e}" + service_unavailable + end + end + + protected + + def bad_request + respond_with_error(400) + end + + def forbidden + respond_with_error(403) + end + + def gone + respond_with_error(410) + end + + def internal_server_error + respond_with_error(500) + end + + def not_acceptable + respond_with_error(406) + end + + def not_found + respond_with_error(404) + end + + def service_unavailable + respond_with_error(503) + end + + def too_many_requests + respond_with_error(429) + end + + def unprocessable_content + respond_with_error(422) + end + + private + + def respond_with_error(code) + respond_to do |format| + format.any { render "errors/#{code}", layout: 'error', formats: [:html], status: code } + format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } + end + end +end diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index 8201f36e3c2d1b..ef6d33ae5ce3d4 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -11,11 +11,14 @@ def logo_as_symbol(version = :icon) end def _logo_as_symbol_wordmark - content_tag(:svg, tag.use(href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + tag.svg(viewBox: '0 0 261 66', class: 'logo logo--wordmark') do + tag.title('Mastodon') + + tag.use(href: '#logo-symbol-wordmark') + end end def _logo_as_symbol_icon - content_tag(:svg, tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + tag.svg(tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') end def render_logo diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 74702d20890451..4f0327e3704bc0 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -36,6 +36,12 @@ module ContextHelper suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' }, + profile_settings: { + 'toot' => 'http://joinmastodon.org/ns#', + 'showFeatured' => 'toot:showFeatured', + 'showMedia' => 'toot:showMedia', + 'showRepliesInMedia' => 'toot:showRepliesInMedia', + }, quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' }, quotes: { 'quote' => 'https://w3id.org/fep/044f#quote', diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb index 2bd0e6737ef08a..2e331629e4895a 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -317,12 +317,12 @@ def valid_activitypub_content_type?(response) end def body_to_json(body, compare_id: nil) - json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body + json = body.is_a?(String) ? JSON.parse(body) : body return if compare_id.present? && json['id'] != compare_id json - rescue Oj::ParseError + rescue JSON::ParserError nil end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index 821a6f1e2d451d..a9a7a8d7c9db56 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -2,33 +2,16 @@ module ReactComponentHelper def react_component(name, props = {}, &block) - data = { component: name.to_s.camelcase, props: Oj.dump(props) } - if block.nil? - div_tag_with_data(data) + data = { component: name.to_s.camelcase, props: } + if block_given? + tag.div data:, &block else - content_tag(:div, data: data, &block) + tag.div nil, data: end end def react_admin_component(name, props = {}) - data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } - div_tag_with_data(data) - end - - def serialized_media_attachments(media_attachments) - media_attachments.map { |attachment| serialized_attachment(attachment) } - end - - private - - def div_tag_with_data(data) - content_tag(:div, nil, data: data) - end - - def serialized_attachment(attachment) - ActiveModelSerializers::SerializableResource.new( - attachment, - serializer: REST::MediaAttachmentSerializer - ).as_json + data = { 'admin-component': name.to_s.camelcase, props: } + tag.div nil, data: end end diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx index 5501fb5daeb045..0331ca83ba5d15 100644 --- a/app/javascript/entrypoints/admin.tsx +++ b/app/javascript/entrypoints/admin.tsx @@ -166,6 +166,38 @@ on('change', '#domain_block_severity', ({ target }) => { if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target); }); +const onChangeInviteUsersPermission = (target: HTMLInputElement) => { + const inviteBypassApprovalCheckbox = document.querySelector( + 'input#user_role_permissions_as_keys_invite_bypass_approval', + ); + + if (inviteBypassApprovalCheckbox) { + inviteBypassApprovalCheckbox.disabled = !target.checked; + + if (target.checked) { + inviteBypassApprovalCheckbox.parentElement?.classList.remove('disabled'); + inviteBypassApprovalCheckbox.parentElement?.parentElement?.classList.remove( + 'disabled', + ); + } else { + inviteBypassApprovalCheckbox.parentElement?.classList.add('disabled'); + inviteBypassApprovalCheckbox.parentElement?.parentElement?.classList.add( + 'disabled', + ); + } + } +}; + +on( + 'change', + 'input#user_role_permissions_as_keys_invite_users', + ({ target }) => { + if (target instanceof HTMLInputElement) { + onChangeInviteUsersPermission(target); + } + }, +); + function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) { const bootstrapTimelineAccountsField = document.querySelector( @@ -363,6 +395,13 @@ ready(() => { ); if (registrationMode) onChangeRegistrationMode(registrationMode); + const inviteUsersPermissionChecbkox = + document.querySelector( + 'input#user_role_permissions_as_keys_invite_users', + ); + if (inviteUsersPermissionChecbkox) + onChangeInviteUsersPermission(inviteUsersPermissionChecbkox); + const checkAllElement = document.querySelector( '#batch_checkbox_all', ); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index f891410a3cdc48..8b67698f20e24a 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -1,14 +1,20 @@ import { createRoot } from 'react-dom/client'; import { IntlMessageFormat } from 'intl-messageformat'; -import type { MessageDescriptor, PrimitiveType } from 'react-intl'; +import type { + FormatDateOptions, + IntlShape, + MessageDescriptor, + PrimitiveType, +} from 'react-intl'; import { defineMessages } from 'react-intl'; import axios from 'axios'; import { on } from 'delegated-events'; import { throttle } from 'lodash'; -import { timeAgoString } from '../mastodon/components/relative_timestamp'; +import { formatTime } from '@/mastodon/utils/time'; + import emojify from '../mastodon/features/emoji/emoji'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; import { loadLocale, getLocale } from '../mastodon/locales'; @@ -58,7 +64,7 @@ function loaded() { const formatMessage = ( { id, defaultMessage }: MessageDescriptor, values?: Record, - ) => { + ): string => { let message: string | undefined = undefined; if (id) message = localeData[id]; @@ -126,29 +132,31 @@ function loaded() { .querySelectorAll('time.time-ago') .forEach((content) => { const datetime = new Date(content.dateTime); - const now = new Date(); const timeGiven = content.dateTime.includes('T'); content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime); - content.textContent = timeAgoString( - { - formatMessage, - formatDate: (date: Date, options) => + const now = Date.now(); + content.textContent = formatTime({ + // We don't want to show future dates. + timestamp: Math.min(datetime.getTime(), now), + now, + intl: { + formatMessage: formatMessage as IntlShape['formatMessage'], + formatDate: (date: Date, options: FormatDateOptions) => new Intl.DateTimeFormat(locale, options).format(date), }, - datetime, - now.getTime(), - now.getFullYear(), - timeGiven, - ); + noTime: !timeGiven, + }); }); updateDefaultQuotePrivacyFromPrivacy( document.querySelector('#user_settings_attributes_default_privacy'), ); + truncateRuleHints(); + const reactComponents = document.querySelectorAll('[data-component]'); if (reactComponents.length > 0) { @@ -425,21 +433,61 @@ on('submit', '#registration_new_user,#new_user', () => { }); }); +// Truncate long rule hints + +const MAX_RULE_HINT_LENGTH = 100; + +function truncateRuleHints() { + const ruleListItems = + document.querySelectorAll('.rules-list li'); + if (!ruleListItems.length) return; + + ruleListItems.forEach((item) => { + toggleRuleHint(item, true); + }); +} + +function toggleRuleHint(listItem: HTMLLIElement, isInitialSetup?: boolean) { + const hint = listItem.querySelector( + '.rules-list__hint-text', + ); + if (!hint) return; + + const hintText = hint.innerHTML; + const hintToggleButton = listItem.querySelector('button'); + + if (hintText.length > MAX_RULE_HINT_LENGTH) { + // Store full hint in a data attribute, then truncate it with an '…' + hint.dataset.fullHint = hintText; + hint.innerHTML = `${hintText.slice(0, MAX_RULE_HINT_LENGTH - 1).trim()}…`; + + if (hintToggleButton) { + // Reveal toggle button if needed + hintToggleButton.removeAttribute('hidden'); + hintToggleButton.setAttribute('aria-expanded', 'false'); + } + } else if (!isInitialSetup) { + const { fullHint } = hint.dataset; + if (fullHint) { + // Restore full hint from data attribute, then delete attribute + hint.innerHTML = fullHint; + delete hint.dataset.fullHint; + + hintToggleButton?.setAttribute('aria-expanded', 'true'); + hint.parentElement?.focus(); + } + } +} + on('click', '.rules-list button', ({ target }) => { if (!(target instanceof HTMLElement)) { return; } - const button = target.closest('button'); + const listItem = target.closest('li'); - if (!button) { - return; - } - - if (button.ariaExpanded === 'true') { - button.ariaExpanded = 'false'; - } else { - button.ariaExpanded = 'true'; + if (listItem) { + toggleRuleHint(listItem); } }); diff --git a/app/javascript/entrypoints/remote_interaction_helper.ts b/app/javascript/entrypoints/remote_interaction_helper.ts index f50203747d8f28..093f6a7ec29147 100644 --- a/app/javascript/entrypoints/remote_interaction_helper.ts +++ b/app/javascript/entrypoints/remote_interaction_helper.ts @@ -39,18 +39,65 @@ const findLink = (rel: string, data: unknown): JRDLink | undefined => { } }; -const findTemplateLink = (data: unknown) => - findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template; +const intentParams = (intent: string): [string, string] | null => { + switch (intent) { + case 'follow': + return ['https://w3id.org/fep/3b86/Follow', 'object']; + case 'reblog': + return ['https://w3id.org/fep/3b86/Announce', 'object']; + case 'favourite': + return ['https://w3id.org/fep/3b86/Like', 'object']; + case 'vote': + case 'reply': + return ['https://w3id.org/fep/3b86/Object', 'object']; + default: + return null; + } +}; + +const findTemplateLink = ( + data: unknown, + intent: string, +): [string, string] | [null, null] => { + // Find the FEP-3b86 handler for the specific intent + const [needle, param] = intentParams(intent) ?? [ + 'http://ostatus.org/schema/1.0/subscribe', + 'uri', + ]; + + const match = findLink(needle, data); + + if (match?.template) { + return [match.template, param]; + } + + // If the specific intent wasn't found, try the FEP-3b86 handler for the `Object` intent + let fallback = findLink('https://w3id.org/fep/3b86/Object', data); + if (fallback?.template) { + return [fallback.template, 'object']; + } + + // If it's still not found, try the legacy OStatus subscribe handler + fallback = findLink('http://ostatus.org/schema/1.0/subscribe', data); + + if (fallback?.template) { + return [fallback.template, 'uri']; + } + + return [null, null]; +}; const fetchInteractionURLSuccess = ( uri_or_domain: string, template: string, + param: string, ) => { window.parent.postMessage( { type: 'fetchInteractionURL-success', uri_or_domain, template, + param, }, window.origin, ); @@ -74,7 +121,7 @@ const isValidDomain = (value: unknown) => { }; // Attempt to find a remote interaction URL from a domain -const fromDomain = (domain: string) => { +const fromDomain = (domain: string, intent: string) => { const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`; axios @@ -82,17 +129,21 @@ const fromDomain = (domain: string) => { params: { resource: `https://${domain}` }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(domain, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + domain, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { - fetchInteractionURLSuccess(domain, fallbackTemplate); + fetchInteractionURLSuccess(domain, fallbackTemplate, 'uri'); }); }; // Attempt to find a remote interaction URL from an arbitrary URL -const fromURL = (url: string) => { +const fromURL = (url: string, intent: string) => { const domain = new URL(url).host; const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`; @@ -101,17 +152,21 @@ const fromURL = (url: string) => { params: { resource: url }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(url, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + url, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { - fromDomain(domain); + fromDomain(domain, intent); }); }; // Attempt to find a remote interaction URL from a `user@domain` string -const fromAcct = (acct: string) => { +const fromAcct = (acct: string, intent: string) => { acct = acct.replace(/^@/, ''); const segments = acct.split('@'); @@ -134,25 +189,29 @@ const fromAcct = (acct: string) => { params: { resource: `acct:${acct}` }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(acct, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + acct, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { // TODO: handle host-meta? - fromDomain(domain); + fromDomain(domain, intent); }); }; -const fetchInteractionURL = (uri_or_domain: string) => { +const fetchInteractionURL = (uri_or_domain: string, intent: string) => { if (uri_or_domain === '') { fetchInteractionURLFailure(); } else if (/^https?:\/\//.test(uri_or_domain)) { - fromURL(uri_or_domain); + fromURL(uri_or_domain, intent); } else if (uri_or_domain.includes('@')) { - fromAcct(uri_or_domain); + fromAcct(uri_or_domain, intent); } else { - fromDomain(uri_or_domain); + fromDomain(uri_or_domain, intent); } }; @@ -172,8 +231,10 @@ window.addEventListener('message', (event: MessageEvent) => { 'type' in event.data && event.data.type === 'fetchInteractionURL' && 'uri_or_domain' in event.data && - typeof event.data.uri_or_domain === 'string' + typeof event.data.uri_or_domain === 'string' && + 'intent' in event.data && + typeof event.data.intent === 'string' ) { - fetchInteractionURL(event.data.uri_or_domain); + fetchInteractionURL(event.data.uri_or_domain, event.data.intent); } }); diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index f944cef7f5dc40..059c389e07734b 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -274,3 +274,7 @@ export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); export const setComposeQuotePolicy = createAction( 'compose/setQuotePolicy', ); + +export const setDragUploadEnabled = createAction( + 'compose/setDragUploadEnabled', +); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 5baa47a0bac828..e6accb495586a6 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -158,7 +158,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); +export const expandAccountMediaTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}:media${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, exclude_replies: !withReplies }); export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); export const expandAntennaTimeline = (id, { maxId } = {}) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }); export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index 39617d82fe04f2..688c50d21850ed 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -155,8 +155,12 @@ export async function apiRequest< export async function apiRequestGet( url: ApiUrl, params?: RequestParamsOrData, + args: { + signal?: AbortSignal; + timeout?: number; + } = {}, ) { - return apiRequest('GET', url, { params }); + return apiRequest('GET', url, { params, ...args }); } export async function apiRequestPost( diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index da4b0e94f8637c..fc6e38fbc8d47a 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -67,5 +67,11 @@ export const apiGetFamiliarFollowers = (id: string) => export const apiGetProfile = () => apiRequestGet('v1/profile'); -export const apiPatchProfile = (params: ApiProfileUpdateParams) => +export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) => apiRequestPatch('v1/profile', params); + +export const apiDeleteProfileAvatar = () => + apiRequestDelete('v1/profile/avatar'); + +export const apiDeleteProfileHeader = () => + apiRequestDelete('v1/profile/header'); diff --git a/app/javascript/mastodon/api/collections.ts b/app/javascript/mastodon/api/collections.ts index 8e35be41c1eac8..862f98b60a688d 100644 --- a/app/javascript/mastodon/api/collections.ts +++ b/app/javascript/mastodon/api/collections.ts @@ -49,3 +49,9 @@ export const apiRemoveCollectionItem = (collectionId: string, itemId: string) => apiRequestDelete( `v1_alpha/collections/${collectionId}/items/${itemId}`, ); + +export const apiRevokeCollectionInclusion = ( + collectionId: string, + itemId: string, +) => + apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`); diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts index 79b0385fe8e9b4..497327004aec14 100644 --- a/app/javascript/mastodon/api/search.ts +++ b/app/javascript/mastodon/api/search.ts @@ -4,13 +4,22 @@ import type { ApiSearchResultsJSON, } from 'mastodon/api_types/search'; -export const apiGetSearch = (params: { - q: string; - resolve?: boolean; - type?: ApiSearchType; - limit?: number; - offset?: number; -}) => - apiRequestGet('v2/search', { - ...params, - }); +export const apiGetSearch = ( + params: { + q: string; + resolve?: boolean; + type?: ApiSearchType; + limit?: number; + offset?: number; + }, + options: { + signal?: AbortSignal; + } = {}, +) => + apiRequestGet( + 'v2/search', + { + ...params, + }, + options, + ); diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index e89516c0e47c81..273a523e690c84 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -78,6 +78,9 @@ export interface BaseApiAccountJSON { id: string; last_status_at: string; locked: boolean; + show_media: boolean; + show_media_replies: boolean; + show_featured: boolean; noindex?: boolean; note: string; other_settings: ApiAccountOtherSettingsJSON; diff --git a/app/javascript/mastodon/api_types/collections.ts b/app/javascript/mastodon/api_types/collections.ts index 23f835f5fc91c6..fae95875d1e3b3 100644 --- a/app/javascript/mastodon/api_types/collections.ts +++ b/app/javascript/mastodon/api_types/collections.ts @@ -52,7 +52,7 @@ export interface ApiCollectionWithAccountsJSON extends ApiWrappedCollectionJSON /** * Nested account item */ -interface CollectionAccountItem { +export interface CollectionAccountItem { id: string; account_id?: string; // Only present when state is 'accepted' (or the collection is your own) state: 'pending' | 'accepted' | 'rejected' | 'revoked'; diff --git a/app/javascript/mastodon/api_types/profile.ts b/app/javascript/mastodon/api_types/profile.ts index 9814bddde96006..acc3b46787d5b1 100644 --- a/app/javascript/mastodon/api_types/profile.ts +++ b/app/javascript/mastodon/api_types/profile.ts @@ -27,6 +27,8 @@ export interface ApiProfileJSON { export type ApiProfileUpdateParams = Partial< Pick< ApiProfileJSON, + | 'avatar_description' + | 'header_description' | 'display_name' | 'note' | 'locked' diff --git a/app/javascript/mastodon/components/__tests__/short_number-test.tsx b/app/javascript/mastodon/components/__tests__/short_number-test.tsx new file mode 100644 index 00000000000000..e221ca1eb8f57e --- /dev/null +++ b/app/javascript/mastodon/components/__tests__/short_number-test.tsx @@ -0,0 +1,80 @@ +import { IntlProvider } from 'react-intl'; + +import { render, screen } from '@testing-library/react'; + +import { ShortNumber } from '../short_number'; + +function renderShortNumber(value: number) { + return render( + + + , + ); +} + +describe('ShortNumber Component', () => { + it('does not abbreviate numbers under 1000', () => { + renderShortNumber(999); + expect(screen.getByText('999')).toBeDefined(); + }); + + it('formats thousands correctly for 1000', () => { + renderShortNumber(1000); + expect(screen.getByText('1K')).toBeDefined(); + }); + + it('truncates decimals for 1051', () => { + renderShortNumber(1051); + expect(screen.getByText('1K')).toBeDefined(); + }); + + it('truncates decimals for 2999', () => { + renderShortNumber(2999); + expect(screen.getByText('2.9K')).toBeDefined(); + }); + + it('truncates decimals for 9999', () => { + renderShortNumber(9999); + expect(screen.getByText('9.9K')).toBeDefined(); + }); + + it('truncates decimals for 10501', () => { + renderShortNumber(10501); + expect(screen.getByText('10K')).toBeDefined(); + }); + + it('truncates decimals for 11000', () => { + renderShortNumber(11000); + expect(screen.getByText('11K')).toBeDefined(); + }); + + it('truncates decimals for 99999', () => { + renderShortNumber(99999); + expect(screen.getByText('99K')).toBeDefined(); + }); + + it('truncates decimals for 100501', () => { + renderShortNumber(100501); + expect(screen.getByText('100K')).toBeDefined(); + }); + + it('truncates decimals for 101000', () => { + renderShortNumber(101000); + expect(screen.getByText('101K')).toBeDefined(); + }); + + it('truncates decimals for 999999', () => { + renderShortNumber(999999); + expect(screen.getByText('999K')).toBeDefined(); + }); + + it('truncates decimals for 2999999', () => { + renderShortNumber(2999999); + expect(screen.getByText('2.9M')).toBeDefined(); + }); + + it('truncates decimals for 9999999', () => { + renderShortNumber(9999999); + expect(screen.getByText('9.9M')).toBeDefined(); + }); +}); diff --git a/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx b/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx new file mode 100644 index 00000000000000..00804d685ba2ad --- /dev/null +++ b/app/javascript/mastodon/components/a11y_live_region/a11y_live_region.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { A11yLiveRegion } from '.'; + +const meta = { + title: 'Components/A11yLiveRegion', + component: A11yLiveRegion, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Polite: Story = { + args: { + children: "This field can't be empty.", + }, +}; + +export const Assertive: Story = { + args: { + ...Polite.args, + role: 'alert', + }, +}; diff --git a/app/javascript/mastodon/components/a11y_live_region/index.tsx b/app/javascript/mastodon/components/a11y_live_region/index.tsx new file mode 100644 index 00000000000000..51fee5e4b93fee --- /dev/null +++ b/app/javascript/mastodon/components/a11y_live_region/index.tsx @@ -0,0 +1,28 @@ +import { polymorphicForwardRef } from '@/types/polymorphic'; + +/** + * A live region is a content region that announces changes of its contents + * to users of assistive technology like screen readers. + * + * Dynamically added warnings, errors, or live status updates should be wrapped + * in a live region to ensure they are not missed when they appear. + * + * **Important:** + * Live regions must be present in the DOM _before_ + * the to-be announced content is rendered into it. + */ + +export const A11yLiveRegion = polymorphicForwardRef<'div'>( + ({ role = 'status', as: Component = 'div', children, ...props }, ref) => { + return ( + + {children} + + ); + }, +); diff --git a/app/javascript/mastodon/components/account/index.tsx b/app/javascript/mastodon/components/account/index.tsx index d454f201fcf9b5..bc96b41e36481a 100644 --- a/app/javascript/mastodon/components/account/index.tsx +++ b/app/javascript/mastodon/components/account/index.tsx @@ -285,7 +285,7 @@ export const Account: React.FC = ({ if (account?.mute_expires_at) { muteTimeRemaining = ( <> - · + · ); } diff --git a/app/javascript/mastodon/components/admin/Dimension.jsx b/app/javascript/mastodon/components/admin/Dimension.jsx index 5ed91bede09d24..b859c6be2b579b 100644 --- a/app/javascript/mastodon/components/admin/Dimension.jsx +++ b/app/javascript/mastodon/components/admin/Dimension.jsx @@ -99,7 +99,7 @@ export default class Dimension extends PureComponent { return (
-

{label}

+

{label}

{content}
diff --git a/app/javascript/mastodon/components/admin/ImpactReport.jsx b/app/javascript/mastodon/components/admin/ImpactReport.jsx index add54134b6fe92..5a6a5a9eca442f 100644 --- a/app/javascript/mastodon/components/admin/ImpactReport.jsx +++ b/app/javascript/mastodon/components/admin/ImpactReport.jsx @@ -49,7 +49,7 @@ export default class ImpactReport extends PureComponent { return (
-

+ diff --git a/app/javascript/mastodon/components/admin/Retention.jsx b/app/javascript/mastodon/components/admin/Retention.jsx index 87746e9f497bf6..7634815e80cb69 100644 --- a/app/javascript/mastodon/components/admin/Retention.jsx +++ b/app/javascript/mastodon/components/admin/Retention.jsx @@ -145,7 +145,7 @@ export default class Retention extends PureComponent { return (
-

{title}

+

{title}

{content}
diff --git a/app/javascript/mastodon/components/admin/Trends.jsx b/app/javascript/mastodon/components/admin/Trends.jsx index fd6db106d54183..eb2c31b10d5c0f 100644 --- a/app/javascript/mastodon/components/admin/Trends.jsx +++ b/app/javascript/mastodon/components/admin/Trends.jsx @@ -66,7 +66,7 @@ export default class Trends extends PureComponent { return (
-

+ {content}
diff --git a/app/javascript/mastodon/components/callout/index.tsx b/app/javascript/mastodon/components/callout/index.tsx index a9232ec3a7a8f3..fe088e2a830f57 100644 --- a/app/javascript/mastodon/components/callout/index.tsx +++ b/app/javascript/mastodon/components/callout/index.tsx @@ -1,4 +1,4 @@ -import type { FC, ReactNode } from 'react'; +import type { FC, ReactElement, ReactNode } from 'react'; import { useIntl } from 'react-intl'; @@ -19,7 +19,7 @@ import classes from './styles.module.css'; export interface CalloutProps { variant?: | 'default' - // | 'subtle' + | 'subtle' | 'feature' | 'inverted' | 'success' @@ -31,9 +31,9 @@ export interface CalloutProps { /** Set to false to hide the icon. */ icon?: IconProp | boolean; onPrimary?: () => void; - primaryLabel?: string; + primaryLabel?: string | ReactElement; onSecondary?: () => void; - secondaryLabel?: string; + secondaryLabel?: string | ReactElement; onClose?: () => void; id?: string; extraContent?: ReactNode; @@ -41,7 +41,7 @@ export interface CalloutProps { const variantClasses = { default: classes.variantDefault as string, - // subtle: classes.variantSubtle as string, + subtle: classes.variantSubtle as string, feature: classes.variantFeature as string, inverted: classes.variantInverted as string, success: classes.variantSuccess as string, diff --git a/app/javascript/mastodon/components/callout/styles.module.css b/app/javascript/mastodon/components/callout/styles.module.css index 7f33c96eae8736..14003ccf5dcbda 100644 --- a/app/javascript/mastodon/components/callout/styles.module.css +++ b/app/javascript/mastodon/components/callout/styles.module.css @@ -32,6 +32,10 @@ .body { flex-grow: 1; + a { + color: inherit; + } + h3 { font-weight: 500; margin-bottom: 5px; @@ -51,6 +55,7 @@ color: inherit; font-weight: 500; padding: 0; + text-wrap: nowrap; text-decoration: underline; transition: color 0.1s ease-in-out; @@ -80,14 +85,14 @@ } } -/* .variantSubtle { +.variantSubtle { border: 1px solid var(--color-bg-brand-softer); background-color: var(--color-bg-primary); .icon { background-color: var(--color-bg-brand-softer); } -} */ +} .variantFeature { background-color: var(--color-bg-brand-base); diff --git a/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx b/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx new file mode 100644 index 00000000000000..f18af41dc0d1e5 --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/callout_inline.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { CalloutInline } from '.'; + +const meta = { + title: 'Components/CalloutInline', + args: { + children: 'Contents here', + }, + component: CalloutInline, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Error: Story = { + args: { + variant: 'error', + }, +}; + +export const Warning: Story = { + args: { + variant: 'warning', + }, +}; + +export const Success: Story = { + args: { + variant: 'success', + }, +}; + +export const Info: Story = { + args: { + variant: 'info', + }, +}; diff --git a/app/javascript/mastodon/components/callout_inline/index.tsx b/app/javascript/mastodon/components/callout_inline/index.tsx new file mode 100644 index 00000000000000..e2e6791963285a --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/index.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react'; + +import classNames from 'classnames'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import ErrorIcon from '@/material-icons/400-24px/error.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.css'; + +export interface FieldStatus { + variant: 'error' | 'warning' | 'info' | 'success'; + message?: string; +} + +const iconMap: Record = { + error: ErrorIcon, + warning: WarningIcon, + info: InfoIcon, + success: CheckIcon, +}; + +export const CalloutInline: FC< + Partial & React.ComponentPropsWithoutRef<'div'> +> = ({ variant = 'error', message, className, children, ...props }) => { + return ( +
+ + {message ?? children} +
+ ); +}; diff --git a/app/javascript/mastodon/components/callout_inline/styles.module.css b/app/javascript/mastodon/components/callout_inline/styles.module.css new file mode 100644 index 00000000000000..8d32f7df9b4742 --- /dev/null +++ b/app/javascript/mastodon/components/callout_inline/styles.module.css @@ -0,0 +1,29 @@ +.wrapper { + display: flex; + align-items: start; + gap: 4px; + font-size: 13px; + font-weight: 500; + + &[data-variant='success'] { + color: var(--color-text-success); + } + + &[data-variant='warning'] { + color: var(--color-text-warning); + } + + &[data-variant='error'] { + color: var(--color-text-error); + } + + &[data-variant='info'] { + color: var(--color-text-primary); + } +} + +.icon { + width: 16px; + height: 16px; + margin-top: 1px; +} diff --git a/app/javascript/mastodon/components/character_counter/index.tsx b/app/javascript/mastodon/components/character_counter/index.tsx index dce410a7c1336f..6ffe4d02f48a9a 100644 --- a/app/javascript/mastodon/components/character_counter/index.tsx +++ b/app/javascript/mastodon/components/character_counter/index.tsx @@ -26,6 +26,7 @@ export const CharacterCounter = polymorphicForwardRef< maxLength, as: Component = 'span', recommended = false, + className, ...props }, ref, @@ -39,6 +40,7 @@ export const CharacterCounter = polymorphicForwardRef< {...props} ref={ref} className={classNames( + className, classes.counter, currentLength > maxLength && !recommended && classes.counterError, )} diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index 9f2f38e7d08ab2..49c2bb1d4a1ff5 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -267,6 +267,15 @@ export const ColumnHeader: React.FC = ({ const hasTitle = (hasIcon || backButton) && title; const columnIndex = useColumnIndexContext(); + const titleContents = ( + <> + {!backButton && hasIcon && ( + + )} + {title} + + ); + const component = (

@@ -274,21 +283,25 @@ export const ColumnHeader: React.FC = ({ <> {backButton} - + {onClick && ( + + )} + {!onClick && ( + + {titleContents} + + )} )} diff --git a/app/javascript/mastodon/components/details/details.stories.tsx b/app/javascript/mastodon/components/details/details.stories.tsx new file mode 100644 index 00000000000000..3bc833f313b8d8 --- /dev/null +++ b/app/javascript/mastodon/components/details/details.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Details } from './index'; + +const meta = { + component: Details, + title: 'Components/Details', + args: { + summary: 'Here is the summary title', + children: ( +

+ And here are the details that are hidden until you click the summary. +

+ ), + }, + render(props) { + return ( +
+
+
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Plain: Story = {}; diff --git a/app/javascript/mastodon/components/details/index.tsx b/app/javascript/mastodon/components/details/index.tsx new file mode 100644 index 00000000000000..aac92e8f77b986 --- /dev/null +++ b/app/javascript/mastodon/components/details/index.tsx @@ -0,0 +1,35 @@ +import { forwardRef } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.scss'; + +export const Details = forwardRef< + HTMLDetailsElement, + { + summary: ReactNode; + children: ReactNode; + className?: string; + } & ComponentPropsWithoutRef<'details'> +>(({ summary, children, className, ...rest }, ref) => { + return ( +
+ + {summary} + + + + {children} +
+ ); +}); +Details.displayName = 'Details'; diff --git a/app/javascript/mastodon/components/details/styles.module.scss b/app/javascript/mastodon/components/details/styles.module.scss new file mode 100644 index 00000000000000..03aace8a626b45 --- /dev/null +++ b/app/javascript/mastodon/components/details/styles.module.scss @@ -0,0 +1,25 @@ +.details { + color: var(--color-text-secondary); + font-size: 13px; + margin-top: 8px; + + summary { + cursor: pointer; + font-weight: 600; + list-style: none; + margin-bottom: 8px; + text-decoration: underline; + text-decoration-style: dotted; + } + + :global(.icon) { + width: 1.4em; + height: 1.4em; + vertical-align: middle; + transition: transform 0.2s ease-in-out; + } + + &[open] :global(.icon) { + transform: rotate(-180deg); + } +} diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx index e7483c62d4d66c..b9686fc0f91397 100644 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ b/app/javascript/mastodon/components/dropdown_menu.tsx @@ -296,6 +296,7 @@ interface DropdownProps { children?: React.ReactElement; icon?: string; iconComponent?: IconProp; + iconClassName?: string; items?: Item[]; loading?: boolean; title?: string; @@ -327,6 +328,7 @@ export const Dropdown = ({ children, icon, iconComponent, + iconClassName, items, loading, title = 'Menu', @@ -501,6 +503,7 @@ export const Dropdown = ({ iconComponent={iconComponent} title={title} active={open || active} + className={iconClassName} {...buttonProps} /> ); diff --git a/app/javascript/mastodon/components/edited_timestamp/index.tsx b/app/javascript/mastodon/components/edited_timestamp/index.tsx index 36f8db8abff159..eb07559cb259ee 100644 --- a/app/javascript/mastodon/components/edited_timestamp/index.tsx +++ b/app/javascript/mastodon/components/edited_timestamp/index.tsx @@ -60,10 +60,7 @@ export const EditedTimestamp: React.FC<{ const renderItem = useCallback( (item: HistoryItem, index: number, onClick: React.MouseEventHandler) => { const formattedDate = ( - + ); const formattedName = ( diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 000702b4d81d5f..52864b432c654c 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames'; import { Link } from 'react-router-dom'; import { useIdentity } from '@/mastodon/identity_context'; -import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { fetchRelationships, followAccount, @@ -92,6 +92,7 @@ export const FollowButton: React.FC<{ openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'follow', accountId: accountId, url: account?.url, }, @@ -174,7 +175,7 @@ export const FollowButton: React.FC<{ 'button--compact': compact, }); - if (isClientFeatureEnabled('profile_editing')) { + if (isServerFeatureEnabled('profile_redesign')) { return ( {label} diff --git a/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx b/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx index 4d208cf21b6d4e..16b3a53f0baff2 100644 --- a/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/checkbox_field.stories.tsx @@ -76,7 +76,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/checkbox_field.tsx b/app/javascript/mastodon/components/form_fields/checkbox_field.tsx index 2b6933c8473146..c08b81ca36d7f5 100644 --- a/app/javascript/mastodon/components/form_fields/checkbox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/checkbox_field.tsx @@ -13,12 +13,12 @@ type Props = Omit, 'type'> & { export const CheckboxField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 89193ed9d57ca7..057258847e7065 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -28,7 +28,10 @@ export interface ComboboxItemState { isDisabled: boolean; } -interface ComboboxProps extends TextInputProps { +interface ComboboxProps extends Omit< + TextInputProps, + 'icon' +> { /** * The value of the combobox's text input */ @@ -71,6 +74,18 @@ interface ComboboxProps extends TextInputProps { * The main selection handler, called when an option is selected or deselected. */ onSelectItem: (item: T) => void; + /** + * Icon to be displayed in the text input + */ + icon?: TextInputProps['icon'] | null; + /** + * Set to false to keep the menu open when an item is selected + */ + closeOnSelect?: boolean; + /** + * Prevent the menu from opening, e.g. to prevent the empty state from showing + */ + suppressMenu?: boolean; } interface Props @@ -86,14 +101,14 @@ interface Props */ export const ComboboxFieldWithRef = ( - { id, label, hint, hasError, required, ...otherProps }: Props, + { id, label, hint, status, required, ...otherProps }: Props, ref: React.ForwardedRef, ) => ( {(inputProps) => } @@ -124,6 +139,8 @@ const ComboboxWithRef = ( onSelectItem, onChange, onKeyDown, + closeOnSelect = true, + suppressMenu = false, icon = SearchIcon, className, ...otherProps @@ -148,7 +165,7 @@ const ComboboxWithRef = ( const showStatusMessageInMenu = !!statusMessage && value.length > 0 && items.length === 0; const hasMenuContent = - !disabled && (items.length > 0 || showStatusMessageInMenu); + !disabled && !suppressMenu && (items.length > 0 || showStatusMessageInMenu); const isMenuOpen = shouldMenuOpen && hasMenuContent; const openMenu = useCallback(() => { @@ -204,11 +221,15 @@ const ComboboxWithRef = ( const isDisabled = getIsItemDisabled?.(item) ?? false; if (!isDisabled) { onSelectItem(item); + + if (closeOnSelect) { + closeMenu(); + } } } inputRef.current?.focus(); }, - [getIsItemDisabled, items, onSelectItem], + [closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem], ); const handleSelectItem = useCallback( @@ -343,7 +364,7 @@ const ComboboxWithRef = ( value={value} onChange={handleInputChange} onKeyDown={handleInputKeyDown} - icon={icon} + icon={icon ?? undefined} className={classNames(classes.input, className)} ref={mergeRefs} /> diff --git a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx index ad93e3a065d66f..d772315adeb5ea 100644 --- a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx +++ b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx @@ -22,7 +22,7 @@ interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps { export const CopyLinkField = forwardRef( ( - { id, label, hint, hasError, value, required, className, ...otherProps }, + { id, label, hint, status, value, required, className, ...otherProps }, ref, ) => { const intl = useIntl(); @@ -48,7 +48,7 @@ export const CopyLinkField = forwardRef( label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} > {(inputProps) => ( diff --git a/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx b/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx index a57c4d1dd4861b..af9e3d5280f8c4 100644 --- a/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx +++ b/app/javascript/mastodon/components/form_fields/emoji_text_field.tsx @@ -37,7 +37,7 @@ export const EmojiTextInputField: FC< value, label, hint, - hasError, + status, maxLength, counterMax = maxLength, recommended, @@ -49,7 +49,7 @@ export const EmojiTextInputField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, @@ -84,7 +84,7 @@ export const EmojiTextAreaField: FC< recommended, disabled, hint, - hasError, + status, ...otherProps }) => { const textareaRef = useRef(null); @@ -92,7 +92,7 @@ export const EmojiTextAreaField: FC< const wrapperProps = { label, hint, - hasError, + status, counterMax, recommended, disabled, diff --git a/app/javascript/mastodon/components/form_fields/fieldset.module.scss b/app/javascript/mastodon/components/form_fields/fieldset.module.scss index f222762af51c3f..2751b3c8a01c0e 100644 --- a/app/javascript/mastodon/components/form_fields/fieldset.module.scss +++ b/app/javascript/mastodon/components/form_fields/fieldset.module.scss @@ -1,7 +1,9 @@ .fieldset { + --container-gap: 12px; + display: flex; flex-direction: column; - gap: 12px; + gap: var(--container-gap); color: var(--color-text-primary); font-size: 15px; } @@ -17,3 +19,11 @@ column-gap: 24px; } } + +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--container-gap)); + } +} diff --git a/app/javascript/mastodon/components/form_fields/fieldset.tsx b/app/javascript/mastodon/components/form_fields/fieldset.tsx index d52a95130b13ef..26381ca834ca0a 100644 --- a/app/javascript/mastodon/components/form_fields/fieldset.tsx +++ b/app/javascript/mastodon/components/form_fields/fieldset.tsx @@ -3,14 +3,19 @@ import type { ReactNode, FC } from 'react'; import { createContext, useId } from 'react'; +import { A11yLiveRegion } from 'mastodon/components/a11y_live_region'; +import type { FieldStatus } from 'mastodon/components/callout_inline'; +import { CalloutInline } from 'mastodon/components/callout_inline'; + import classes from './fieldset.module.scss'; +import { getFieldStatus } from './form_field_wrapper'; import formFieldWrapperClasses from './form_field_wrapper.module.scss'; interface FieldsetProps { legend: ReactNode; hint?: ReactNode; name?: string; - hasError?: boolean; + status?: FieldStatus | FieldStatus['variant']; layout?: 'vertical' | 'horizontal'; children: ReactNode; } @@ -26,22 +31,33 @@ export const Fieldset: FC = ({ legend, hint, name, - hasError, + status, layout, children, }) => { const uniqueId = useId(); const labelId = `${uniqueId}-label`; const hintId = `${uniqueId}-hint`; + const statusId = `${uniqueId}-status`; const fieldsetName = name || `${uniqueId}-fieldset-name`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; + + const descriptionIds = [ + hasHint ? hintId : '', + hasStatusMessage ? statusId : '', + ] + .filter((id) => !!id) + .join(' '); + return (
@@ -59,6 +75,11 @@ export const Fieldset: FC = ({ {children}
+ + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } +
); }; diff --git a/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss b/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss index faeb48aae4f62b..cff93be8a69fcd 100644 --- a/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss +++ b/app/javascript/mastodon/components/form_fields/form_field_wrapper.module.scss @@ -46,6 +46,14 @@ font-size: 13px; } +.status { + // If there's no content, we need to compensate for the parent's + // flex gap to avoid extra spacing below the field. + &:empty { + margin-top: calc(-1 * var(--form-field-label-gap)); + } +} + .inputWrapper { display: block; } diff --git a/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx b/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx index 6454153ab88c90..7cd6d676142f74 100644 --- a/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx +++ b/app/javascript/mastodon/components/form_fields/form_field_wrapper.tsx @@ -7,6 +7,10 @@ import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { A11yLiveRegion } from 'mastodon/components/a11y_live_region'; +import type { FieldStatus } from 'mastodon/components/callout_inline'; +import { CalloutInline } from 'mastodon/components/callout_inline'; + import { FieldsetNameContext } from './fieldset'; import classes from './form_field_wrapper.module.scss'; @@ -20,7 +24,7 @@ interface FieldWrapperProps { label: ReactNode; hint?: ReactNode; required?: boolean; - hasError?: boolean; + status?: FieldStatus['variant'] | FieldStatus; inputId?: string; describedById?: string; inputPlacement?: 'inline-start' | 'inline-end'; @@ -33,7 +37,7 @@ interface FieldWrapperProps { */ export type CommonFieldWrapperProps = Pick< FieldWrapperProps, - 'label' | 'hint' | 'hasError' + 'label' | 'hint' | 'status' > & { wrapperClassName?: string }; /** @@ -48,27 +52,31 @@ export const FormFieldWrapper: FC = ({ hint, describedById, required, - hasError, + status, inputPlacement, children, className, }) => { const uniqueId = useId(); const inputId = inputIdProp || `${uniqueId}-input`; + const statusId = `${inputIdProp || uniqueId}-status`; const hintId = `${inputIdProp || uniqueId}-hint`; const hasHint = !!hint; + const fieldStatus = getFieldStatus(status); + const hasStatusMessage = !!fieldStatus?.message; const hasParentFieldset = !!useContext(FieldsetNameContext); + const descriptionIds = + [hasHint ? hintId : '', hasStatusMessage ? statusId : '', describedById] + .filter((id) => !!id) + .join(' ') || undefined; + const inputProps: InputProps = { required, id: inputId, + 'aria-describedby': descriptionIds, }; - if (hasHint) { - inputProps['aria-describedby'] = describedById - ? `${describedById} ${hintId}` - : hintId; - } const input = (
{children(inputProps)}
@@ -77,7 +85,7 @@ export const FormFieldWrapper: FC = ({ return (
{inputPlacement === 'inline-start' && input} @@ -100,6 +108,11 @@ export const FormFieldWrapper: FC = ({
{inputPlacement !== 'inline-start' && input} + + {/* Live region must be rendered even when empty */} + + {hasStatusMessage && } +

); }; @@ -121,3 +134,19 @@ const RequiredMark: FC<{ required?: boolean }> = ({ required }) => ); + +export function getFieldStatus(status: FieldWrapperProps['status']) { + if (!status) { + return null; + } + + if (typeof status === 'string') { + const fieldStatus: FieldStatus = { + variant: status, + message: '', + }; + return fieldStatus; + } + + return status; +} diff --git a/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx b/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx index 95687abff324c7..1292b85724d589 100644 --- a/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/radio_button_field.stories.tsx @@ -71,7 +71,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/radio_button_field.tsx b/app/javascript/mastodon/components/form_fields/radio_button_field.tsx index 51f52168e06ec3..cbc9020ca7fe17 100644 --- a/app/javascript/mastodon/components/form_fields/radio_button_field.tsx +++ b/app/javascript/mastodon/components/form_fields/radio_button_field.tsx @@ -15,7 +15,7 @@ type Props = Omit, 'type'> & { export const RadioButtonField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => { +>(({ id, label, hint, status, required, ...otherProps }, ref) => { const fieldsetName = useContext(FieldsetNameContext); return ( @@ -23,7 +23,7 @@ export const RadioButtonField = forwardRef< label={label} hint={hint} required={required} - hasError={hasError} + status={status} inputId={id} inputPlacement='inline-start' > diff --git a/app/javascript/mastodon/components/form_fields/range_input.module.scss b/app/javascript/mastodon/components/form_fields/range_input.module.scss new file mode 100644 index 00000000000000..5aa46b52cc3242 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input.module.scss @@ -0,0 +1,129 @@ +/* + Inspired by: + https://danielstern.ca/range.css + https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ +*/ + +.input { + --color-bg-thumb: var(--color-bg-brand-base); + --color-bg-thumb-hover: var(--color-bg-brand-base-hover); + --color-bg-track: var(--color-bg-secondary); + + width: 100%; + margin: 6px 0; + background-color: transparent; + appearance: none; + display: block; + + &:focus { + outline: none; + } + + // Thumb + + &::-webkit-slider-thumb { + margin-top: -6px; + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + -webkit-appearance: none; + } + + &::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + } + + &::-ms-thumb { + width: 16px; + height: 16px; + background: var(--color-bg-thumb); + border: 0; + border-radius: 50px; + cursor: pointer; + margin-top: 0; // Needed to keep the Edge thumb centred + } + + &:focus, + &:hover { + &::-webkit-slider-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-moz-range-thumb { + background: var(--color-bg-thumb-hover); + } + + &::-ms-thumb { + background: var(--color-bg-thumb-hover); + } + } + + &:focus-visible { + &::-webkit-slider-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-moz-range-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::-ms-thumb { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + // Track + + &::-webkit-slider-runnable-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-moz-range-track { + background: var(--color-bg-track); + border: 0; + border-radius: 1.3px; + width: 100%; + height: 4px; + cursor: pointer; + } + + &::-ms-track { + background: var(--color-bg-track); + border: 0; + color: transparent; + width: 100%; + height: 4px; + cursor: pointer; + } +} + +.markers { + display: flex; + flex-direction: column; + justify-content: space-between; + writing-mode: vertical-lr; + width: 100%; + font-size: 11px; + color: var(--color-text-secondary); + user-select: none; + + option { + padding: 0; + } +} diff --git a/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx new file mode 100644 index 00000000000000..672228ab8c0795 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input_field.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { RangeInputField } from './range_input_field'; + +const meta = { + title: 'Components/Form Fields/RangeInputField', + component: RangeInputField, + args: { + label: 'Label', + hint: 'This is a description of this form field', + checked: false, + disabled: false, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Simple: Story = {}; + +export const Markers: Story = { + args: { + markers: [ + { value: 0, label: 'None' }, + { value: 25, label: 'Some' }, + { value: 50, label: 'Half' }, + { value: 75, label: 'Most' }, + { value: 100, label: 'All' }, + ], + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/range_input_field.tsx b/app/javascript/mastodon/components/form_fields/range_input_field.tsx new file mode 100644 index 00000000000000..d619d6455a7320 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/range_input_field.tsx @@ -0,0 +1,98 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { forwardRef, useId } from 'react'; + +import classNames from 'classnames'; + +import { FormFieldWrapper } from './form_field_wrapper'; +import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './range_input.module.scss'; + +export type RangeInputProps = Omit< + ComponentPropsWithoutRef<'input'>, + 'type' | 'list' +> & { + markers?: { value: number; label: string }[] | number[]; +}; + +interface Props extends RangeInputProps, CommonFieldWrapperProps { + inputPlacement?: 'inline-start' | 'inline-end'; // TODO: Move this to the common field wrapper props for other fields. +} + +/** + * A simple form field for single-line text. + * + * Accepts an optional `hint` and can be marked as required + * or optional (by explicitly setting `required={false}`) + */ + +export const RangeInputField = forwardRef( + ( + { + id, + label, + hint, + status, + required, + wrapperClassName, + inputPlacement, + ...otherProps + }, + ref, + ) => ( + + {(inputProps) => } + + ), +); + +RangeInputField.displayName = 'RangeInputField'; + +export const RangeInput = forwardRef( + ({ className, markers, id, ...otherProps }, ref) => { + const markersId = useId(); + + if (!markers) { + return ( + + ); + } + return ( + <> + + + {markers.map((marker) => { + const value = typeof marker === 'number' ? marker : marker.value; + return ( + + + ); + }, +); + +RangeInput.displayName = 'RangeInput'; diff --git a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx index 469238dd44d9f4..c215a6e04ae832 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx @@ -51,7 +51,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/select_field.tsx b/app/javascript/mastodon/components/form_fields/select_field.tsx index 59854b578e0aee..7c1bfdf47d93db 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.tsx @@ -19,12 +19,12 @@ interface Props */ export const SelectField = forwardRef( - ({ id, label, hint, required, hasError, children, ...otherProps }, ref) => ( + ({ id, label, hint, required, status, children, ...otherProps }, ref) => ( {(inputProps) => ( diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx index 190239aee2a752..f06d7bbdcf799d 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx @@ -38,7 +38,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: { variant: 'error', message: "This field can't be empty" }, + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.tsx index 1e4bacc041931e..1284aa9276a0d7 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.tsx @@ -26,14 +26,14 @@ export const TextAreaField = forwardRef< TextAreaProps & CommonFieldWrapperProps >( ( - { id, label, hint, required, hasError, wrapperClassName, ...otherProps }, + { id, label, hint, required, status, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/mastodon/components/form_fields/text_input.module.scss b/app/javascript/mastodon/components/form_fields/text_input.module.scss index 289ff1333aecaa..f432f57055d39c 100644 --- a/app/javascript/mastodon/components/form_fields/text_input.module.scss +++ b/app/javascript/mastodon/components/form_fields/text_input.module.scss @@ -29,16 +29,16 @@ color: var(--color-text-secondary); } - &:focus { - outline-color: var(--color-text-brand); - } - &:focus:user-invalid, &:required:user-invalid, [data-has-error='true'] & { outline-color: var(--color-text-error); } + &:focus { + outline-color: var(--color-text-brand); + } + &:required:user-valid { outline-color: var(--color-text-success); } diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx index 8e8d7e99230f99..702597a0c1de76 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx @@ -40,7 +40,17 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', + }, +}; + +export const WithWarning: Story = { + args: { + required: false, + status: { + variant: 'warning', + message: 'Special characters are not allowed', + }, }, }; diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.tsx index f23a5da62f0e9f..d7d07833d360ae 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.tsx @@ -25,14 +25,14 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {} export const TextInputField = forwardRef( ( - { id, label, hint, hasError, required, wrapperClassName, ...otherProps }, + { id, label, hint, status, required, wrapperClassName, ...otherProps }, ref, ) => ( diff --git a/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx b/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx index 924c18aa74c91e..295600a3fde62d 100644 --- a/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/toggle_field.stories.tsx @@ -45,7 +45,7 @@ export const Optional: Story = { export const WithError: Story = { args: { required: false, - hasError: true, + status: 'error', }, }; diff --git a/app/javascript/mastodon/components/form_fields/toggle_field.tsx b/app/javascript/mastodon/components/form_fields/toggle_field.tsx index 6cafbcdc3602d1..75fdb8f21bd770 100644 --- a/app/javascript/mastodon/components/form_fields/toggle_field.tsx +++ b/app/javascript/mastodon/components/form_fields/toggle_field.tsx @@ -14,12 +14,12 @@ type Props = Omit, 'type'> & { export const ToggleField = forwardRef< HTMLInputElement, Props & CommonFieldWrapperProps ->(({ id, label, hint, hasError, required, ...otherProps }, ref) => ( +>(({ id, label, hint, status, required, ...otherProps }, ref) => ( diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index d9352018bb8a26..a0c704a4e7c995 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -14,6 +14,10 @@ import { useTimeout } from 'mastodon/hooks/useTimeout'; const offset = [-12, 4] as OffsetValue; const enterDelay = 750; const leaveDelay = 150; +// Only open the card if the mouse was moved within this time, +// to avoid triggering the card without intentional mouse movement +// (e.g. when content changed underneath the mouse cursor) +const activeMovementThreshold = 150; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; const isHoverCardAnchor = (element: HTMLElement) => @@ -23,10 +27,10 @@ export const HoverCardController: React.FC = () => { const [open, setOpen] = useState(false); const [accountId, setAccountId] = useState(); const [anchor, setAnchor] = useState(null); - const isUsingTouchRef = useRef(false); const cardRef = useRef(null); const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); + const [setMoveTimeout, cancelMoveTimeout] = useTimeout(); const [setScrollTimeout] = useTimeout(); const handleClose = useCallback(() => { @@ -45,6 +49,8 @@ export const HoverCardController: React.FC = () => { useEffect(() => { let isScrolling = false; + let isUsingTouch = false; + let isActiveMouseMovement = false; let currentAnchor: HTMLElement | null = null; let currentTitle: string | null = null; @@ -66,7 +72,7 @@ export const HoverCardController: React.FC = () => { const handleTouchStart = () => { // Keeping track of touch events to prevent the // hover card from being displayed on touch devices - isUsingTouchRef.current = true; + isUsingTouch = true; }; const handleMouseEnter = (e: MouseEvent) => { @@ -78,13 +84,14 @@ export const HoverCardController: React.FC = () => { return; } - // Bail out if a touch is active - if (isUsingTouchRef.current) { + // Bail out if we're scrolling, a touch is active, + // or if there was no active mouse movement + if (isScrolling || !isActiveMouseMovement || isUsingTouch) { return; } // We've entered an anchor - if (!isScrolling && isHoverCardAnchor(target)) { + if (isHoverCardAnchor(target)) { cancelLeaveTimeout(); currentAnchor?.removeAttribute('aria-describedby'); @@ -99,10 +106,7 @@ export const HoverCardController: React.FC = () => { } // We've entered the hover card - if ( - !isScrolling && - (target === currentAnchor || target === cardRef.current) - ) { + if (target === currentAnchor || target === cardRef.current) { cancelLeaveTimeout(); } }; @@ -141,10 +145,17 @@ export const HoverCardController: React.FC = () => { }; const handleMouseMove = () => { - if (isUsingTouchRef.current) { - isUsingTouchRef.current = false; + if (isUsingTouch) { + isUsingTouch = false; } + delayEnterTimeout(enterDelay); + + cancelMoveTimeout(); + isActiveMouseMovement = true; + setMoveTimeout(() => { + isActiveMouseMovement = false; + }, activeMovementThreshold); }; document.body.addEventListener('touchstart', handleTouchStart, { @@ -188,6 +199,8 @@ export const HoverCardController: React.FC = () => { setOpen, setAccountId, setAnchor, + setMoveTimeout, + cancelMoveTimeout, ]); return ( diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index 2b7134185e3641..3ab31f4229679b 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -70,7 +70,7 @@ export const Poll: React.FC = ({ pollId, disabled, status }) => { if (expired) { return intl.formatMessage(messages.closed); } - return ; + return ; }, [expired, intl, poll]); const votesCount = useMemo(() => { if (!poll) { @@ -110,6 +110,7 @@ export const Poll: React.FC = ({ pollId, disabled, status }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'vote', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx deleted file mode 100644 index 6253525091b52f..00000000000000 --- a/app/javascript/mastodon/components/relative_timestamp.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { Component } from 'react'; - -import type { MessageDescriptor, PrimitiveType, IntlShape } from 'react-intl'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - today: { id: 'relative_time.today', defaultMessage: 'today' }, - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - just_now_full: { - id: 'relative_time.full.just_now', - defaultMessage: 'just now', - }, - seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - seconds_full: { - id: 'relative_time.full.seconds', - defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', - }, - minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - minutes_full: { - id: 'relative_time.full.minutes', - defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', - }, - hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - hours_full: { - id: 'relative_time.full.hours', - defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', - }, - days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - days_full: { - id: 'relative_time.full.days', - defaultMessage: '{number, plural, one {# day} other {# days}} ago', - }, - moments_remaining: { - id: 'time_remaining.moments', - defaultMessage: 'Moments remaining', - }, - seconds_remaining: { - id: 'time_remaining.seconds', - defaultMessage: '{number, plural, one {# second} other {# seconds}} left', - }, - minutes_remaining: { - id: 'time_remaining.minutes', - defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', - }, - hours_remaining: { - id: 'time_remaining.hours', - defaultMessage: '{number, plural, one {# hour} other {# hours}} left', - }, - days_remaining: { - id: 'time_remaining.days', - defaultMessage: '{number, plural, one {# day} other {# days}} left', - }, -}); - -const dateFormatOptions = { - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', -} as const; - -const shortDateFormatOptions = { - month: 'short', - day: 'numeric', -} as const; - -const SECOND = 1000; -const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; - -const MAX_DELAY = 2147483647; - -const selectUnits = (delta: number) => { - const absDelta = Math.abs(delta); - - if (absDelta < MINUTE) { - return 'second'; - } else if (absDelta < HOUR) { - return 'minute'; - } else if (absDelta < DAY) { - return 'hour'; - } - - return 'day'; -}; - -const getUnitDelay = (units: string) => { - switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; - } -}; - -export const timeAgoString = ( - intl: { - formatDate: IntlShape['formatDate']; - formatMessage: ( - { id, defaultMessage }: MessageDescriptor, - values?: Record, - ) => string; - }, - date: Date, - now: number, - year: number, - timeGiven: boolean, - short?: boolean, -) => { - const delta = now - date.getTime(); - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage( - short ? messages.just_now : messages.just_now_full, - ); - } else if (delta < 7 * DAY) { - if (delta < MINUTE) { - relativeTime = intl.formatMessage( - short ? messages.seconds : messages.seconds_full, - { number: Math.floor(delta / SECOND) }, - ); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage( - short ? messages.minutes : messages.minutes_full, - { number: Math.floor(delta / MINUTE) }, - ); - } else if (delta < DAY) { - relativeTime = intl.formatMessage( - short ? messages.hours : messages.hours_full, - { number: Math.floor(delta / HOUR) }, - ); - } else { - relativeTime = intl.formatMessage( - short ? messages.days : messages.days_full, - { number: Math.floor(delta / DAY) }, - ); - } - } else if (date.getFullYear() === year) { - relativeTime = intl.formatDate(date, shortDateFormatOptions); - } else { - relativeTime = intl.formatDate(date, { - ...shortDateFormatOptions, - year: 'numeric', - }); - } - - return relativeTime; -}; - -const timeRemainingString = ( - intl: IntlShape, - date: Date, - now: number, - timeGiven = true, -) => { - const delta = date.getTime() - now; - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.moments_remaining); - } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { - number: Math.floor(delta / SECOND), - }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { - number: Math.floor(delta / MINUTE), - }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { - number: Math.floor(delta / HOUR), - }); - } else { - relativeTime = intl.formatMessage(messages.days_remaining, { - number: Math.floor(delta / DAY), - }); - } - - return relativeTime; -}; - -interface Props { - intl: IntlShape; - timestamp: string; - year?: number; - futureDate?: boolean; - short?: boolean; -} -interface States { - now: number; -} -class RelativeTimestamp extends Component { - state = { - now: Date.now(), - }; - - _timer: number | undefined; - - shouldComponentUpdate(nextProps: Props, nextState: States) { - // As of right now the locale doesn't change without a new page load, - // but we might as well check in case that ever changes. - return ( - this.props.timestamp !== nextProps.timestamp || - this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now - ); - } - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: Date.now() }); - } - } - - componentDidMount() { - this._scheduleNextUpdate(this.props, this.state); - } - - UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) { - this._scheduleNextUpdate(nextProps, nextState); - } - - componentWillUnmount() { - window.clearTimeout(this._timer); - } - - _scheduleNextUpdate(props: Props, state: States) { - window.clearTimeout(this._timer); - - const { timestamp } = props; - const delta = new Date(timestamp).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); - const updateInterval = 1000 * 10; - const delay = - delta < 0 - ? Math.max(updateInterval, unitDelay - unitRemainder) - : Math.max(updateInterval, unitRemainder); - - this._timer = window.setTimeout(() => { - this.setState({ now: Date.now() }); - }, delay); - } - - render() { - const { - timestamp, - intl, - futureDate, - year = new Date().getFullYear(), - short = true, - } = this.props; - - const timeGiven = timestamp.includes('T'); - const date = new Date(timestamp); - const relativeTime = futureDate - ? timeRemainingString(intl, date, this.state.now, timeGiven) - : timeAgoString(intl, date, this.state.now, year, timeGiven, short); - - return ( - - ); - } -} - -const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp); - -export { RelativeTimestampWithIntl as RelativeTimestamp }; diff --git a/app/javascript/mastodon/components/relative_timestamp/index.tsx b/app/javascript/mastodon/components/relative_timestamp/index.tsx new file mode 100644 index 00000000000000..493e535a71fa83 --- /dev/null +++ b/app/javascript/mastodon/components/relative_timestamp/index.tsx @@ -0,0 +1,83 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { FC } from 'react'; + +import { useIntl } from 'react-intl'; + +import { + formatTime, + MAX_TIMEOUT, + relativeTimeParts, + SECOND, + unitToTime, +} from '@/mastodon/utils/time'; + +const dateFormatOptions = { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +} as const; + +export const RelativeTimestamp: FC<{ + timestamp: string; + long?: boolean; + noTime?: boolean; + noFuture?: boolean; +}> = ({ timestamp, long = false, noTime = false, noFuture = false }) => { + const intl = useIntl(); + + const [now, setNow] = useState(() => Date.now()); + + const date = useMemo(() => { + const date = new Date(timestamp); + return noFuture ? new Date(Math.min(date.getTime(), now)) : date; + }, [noFuture, now, timestamp]); + const ts = date.getTime(); + + useEffect(() => { + let timeoutId = 0; + const scheduleNextUpdate = () => { + const { unit, delta } = relativeTimeParts(ts); + const unitDelay = unitToTime(unit); + const remainder = Math.abs(delta % unitDelay); + const minDelay = 10 * SECOND; + const delay = Math.min( + Math.max(delta < 0 ? unitDelay - remainder : remainder, minDelay), + MAX_TIMEOUT, + ); + + timeoutId = window.setTimeout(() => { + setNow(Date.now()); + scheduleNextUpdate(); + }, delay); + }; + + scheduleNextUpdate(); + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [ts]); + + const daysOnly = !timestamp.includes('T') || noTime; + const relativeTime = useMemo( + () => + formatTime({ + timestamp: ts, + intl, + short: !long, + noTime: daysOnly, + now, + }), + [ts, intl, long, daysOnly, now], + ); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/relative_timestamp/relative_timestamp.stories.tsx b/app/javascript/mastodon/components/relative_timestamp/relative_timestamp.stories.tsx new file mode 100644 index 00000000000000..978382515da50d --- /dev/null +++ b/app/javascript/mastodon/components/relative_timestamp/relative_timestamp.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { DAY } from '@/mastodon/utils/time'; + +import { RelativeTimestamp } from './index'; + +const meta = { + title: 'Components/RelativeTimestamp', + component: RelativeTimestamp, + args: { + timestamp: new Date(Date.now() - DAY * 3).toISOString(), + long: false, + noTime: false, + noFuture: false, + }, + argTypes: { + timestamp: { + control: 'date', + }, + }, + render(props) { + const { timestamp } = props; + const dateString = toDateString(timestamp); + + return ; + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Plain: Story = {}; + +export const Long: Story = { + args: { + long: true, + }, +}; + +export const DateOnly: Story = { + args: { + noTime: true, + }, +}; + +export const NoFuture: Story = { + args: { + timestamp: new Date(Date.now() + DAY * 3).toISOString(), + noFuture: true, + }, +}; + +// Storybook has a known bug with changing a date control from a string to number. +function toDateString(timestamp?: number | string) { + if (!timestamp) { + return new Date().toISOString(); + } + + if (typeof timestamp === 'number') { + return new Date(timestamp).toISOString(); + } + + return timestamp; +} diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx index 1dc1d45083dfd9..1c251d6abcda9c 100644 --- a/app/javascript/mastodon/components/router.tsx +++ b/app/javascript/mastodon/components/router.tsx @@ -17,6 +17,9 @@ import { isDevelopment } from 'mastodon/utils/environment'; interface MastodonLocationState { fromMastodon?: boolean; mastodonModalKey?: string; + // Prevent the rightmost column in advanced UI from scrolling + // into view on location changes + preventMultiColumnAutoScroll?: string; } export type LocationState = MastodonLocationState | null | undefined; diff --git a/app/javascript/mastodon/components/short_number.tsx b/app/javascript/mastodon/components/short_number.tsx index 5702d1a2e4fbc0..4b690c2f8d0d63 100644 --- a/app/javascript/mastodon/components/short_number.tsx +++ b/app/javascript/mastodon/components/short_number.tsx @@ -57,6 +57,7 @@ const ShortNumberCounter: React.FC = ({ value }) => { ); diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx index 65ff49569c6545..1d591d87ce32c7 100644 --- a/app/javascript/mastodon/components/status/boost_button.tsx +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -50,6 +50,7 @@ const StandaloneBoostButton: FC = ({ status, counters }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -124,6 +125,7 @@ const BoostOrQuoteMenu: FC = ({ status, counters }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index e4c04cf872a4ad..7a3533a0304c4e 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -135,7 +135,7 @@ class StatusActionBar extends ImmutablePureComponent { if (signedIn) { this.props.onReply(this.props.status); } else { - this.props.onInteractionModal(this.props.status); + this.props.onInteractionModal(this.props.status, 'reply'); } }; @@ -157,7 +157,7 @@ class StatusActionBar extends ImmutablePureComponent { if (signedIn) { this.props.onFavourite(this.props.status); } else { - this.props.onInteractionModal(this.props.status); + this.props.onInteractionModal(this.props.status, 'favourite'); } }; @@ -455,7 +455,7 @@ class StatusActionBar extends ImmutablePureComponent { const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark); const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite); const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); - + const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications'; return ( diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 14045d48f11005..fd5578fc9825ee 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -86,7 +86,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ onReblogForceModal (status) { dispatch(toggleReblog(status.get('id'), false, true)); }, - + onQuote (status) { dispatch(quoteComposeById(status.get('id'))); }, @@ -265,10 +265,11 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps})); }, - onInteractionModal (status) { + onInteractionModal (status, intent) { dispatch(openModal({ modalType: 'INTERACTION', modalProps: { + intent, accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/features/account_edit/components/column.tsx b/app/javascript/mastodon/features/account_edit/components/column.tsx index 5f0ad929a1b605..9fb83e444ce455 100644 --- a/app/javascript/mastodon/features/account_edit/components/column.tsx +++ b/app/javascript/mastodon/features/account_edit/components/column.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; +import { Helmet } from 'react-helmet'; import { Link } from 'react-router-dom'; import { Column } from '@/mastodon/components/column'; @@ -36,22 +37,27 @@ export const AccountEditColumn: FC<{ const { multiColumn } = useColumnsContext(); return ( - - - - - } - /> - - {children} - + <> + + + + + } + /> + + {children} + + + {title} + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/components/edit_button.tsx b/app/javascript/mastodon/features/account_edit/components/edit_button.tsx index f2fecf21d0c858..eaf6e291cd51b6 100644 --- a/app/javascript/mastodon/features/account_edit/components/edit_button.tsx +++ b/app/javascript/mastodon/features/account_edit/components/edit_button.tsx @@ -1,8 +1,5 @@ import type { FC, MouseEventHandler } from 'react'; -import type { MessageDescriptor } from 'react-intl'; -import { defineMessages, useIntl } from 'react-intl'; - import classNames from 'classnames'; import { Button } from '@/mastodon/components/button'; @@ -12,43 +9,19 @@ import EditIcon from '@/material-icons/400-24px/edit.svg?react'; import classes from '../styles.module.scss'; -const messages = defineMessages({ - add: { - id: 'account_edit.button.add', - defaultMessage: 'Add {item}', - }, - edit: { - id: 'account_edit.button.edit', - defaultMessage: 'Edit {item}', - }, - delete: { - id: 'account_edit.button.delete', - defaultMessage: 'Delete {item}', - }, -}); - export interface EditButtonProps { onClick: MouseEventHandler; - item: string | MessageDescriptor; - edit?: boolean; + label: string; icon?: boolean; disabled?: boolean; } export const EditButton: FC = ({ onClick, - item, - edit = false, - icon = edit, + label, + icon = false, disabled, }) => { - const intl = useIntl(); - - const itemText = typeof item === 'string' ? item : intl.formatMessage(item); - const label = intl.formatMessage(messages[edit ? 'edit' : 'add'], { - item: itemText, - }); - if (icon) { return ( @@ -83,18 +56,15 @@ export const EditIconButton: FC<{ export const DeleteIconButton: FC<{ onClick: MouseEventHandler; - item: string; + label: string; disabled?: boolean; -}> = ({ onClick, item, disabled }) => { - const intl = useIntl(); - return ( - - ); -}; +}> = ({ onClick, label, disabled }) => ( + +); diff --git a/app/javascript/mastodon/features/account_edit/components/field_actions.tsx b/app/javascript/mastodon/features/account_edit/components/field_actions.tsx index aed2fc3e0bdb3f..fecdcb8effc430 100644 --- a/app/javascript/mastodon/features/account_edit/components/field_actions.tsx +++ b/app/javascript/mastodon/features/account_edit/components/field_actions.tsx @@ -1,15 +1,25 @@ import type { FC } from 'react'; import { useCallback } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + import { openModal } from '@/mastodon/actions/modal'; import { useAppDispatch } from '@/mastodon/store'; import { EditButton, DeleteIconButton } from './edit_button'; -export const AccountFieldActions: FC<{ item: string; id: string }> = ({ - item, - id, -}) => { +const messages = defineMessages({ + edit: { + id: 'account_edit.field_actions.edit', + defaultMessage: 'Edit field', + }, + delete: { + id: 'account_edit.field_actions.delete', + defaultMessage: 'Delete field', + }, +}); + +export const AccountFieldActions: FC<{ id: string }> = ({ id }) => { const dispatch = useAppDispatch(); const handleEdit = useCallback(() => { dispatch( @@ -28,10 +38,19 @@ export const AccountFieldActions: FC<{ item: string; id: string }> = ({ ); }, [dispatch, id]); + const intl = useIntl(); + return ( <> - - + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/components/image_edit.tsx b/app/javascript/mastodon/features/account_edit/components/image_edit.tsx new file mode 100644 index 00000000000000..340b8156eb67fd --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/image_edit.tsx @@ -0,0 +1,131 @@ +import { useCallback, useMemo } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import type { OffsetValue } from 'react-overlays/esm/usePopper'; + +import type { ModalType } from '@/mastodon/actions/modal'; +import { openModal } from '@/mastodon/actions/modal'; +import { Dropdown } from '@/mastodon/components/dropdown_menu'; +import { IconButton } from '@/mastodon/components/icon_button'; +import type { MenuItem } from '@/mastodon/models/dropdown_menu'; +import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { selectImageInfo } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import AddIcon from '@/material-icons/400-24px/add.svg?react'; +import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; +import EditIcon from '@/material-icons/400-24px/edit.svg?react'; +import CameraIcon from '@/material-icons/400-24px/photo_camera.svg?react'; +import ReplaceImageIcon from '@/material-icons/400-24px/replace_image.svg?react'; + +import classes from '../styles.module.scss'; + +const messages = defineMessages({ + add: { + id: 'account_edit.image_edit.add_button', + defaultMessage: 'Add image', + }, + replace: { + id: 'account_edit.image_edit.replace_button', + defaultMessage: 'Replace image', + }, + altAdd: { + id: 'account_edit.image_edit.alt_add_button', + description: 'Alt is short for "alternative".', + defaultMessage: 'Add alt text', + }, + altEdit: { + id: 'account_edit.image_edit.alt_edit_button', + description: 'Alt is short for "alternative".', + defaultMessage: 'Edit alt text', + }, + remove: { + id: 'account_edit.image_edit.remove_button', + defaultMessage: 'Remove image', + }, +}); + +export const AccountImageEdit: FC<{ + className?: string; + location: ImageLocation; +}> = ({ className, location }) => { + const intl = useIntl(); + const { alt, src } = useAppSelector((state) => + selectImageInfo(state, location), + ); + const hasAlt = !!alt; + const dispatch = useAppDispatch(); + + const handleModal = useCallback( + (type: ModalType) => { + dispatch(openModal({ modalType: type, modalProps: { location } })); + }, + [dispatch, location], + ); + + const items = useMemo( + () => + [ + { + text: intl.formatMessage(messages.replace), + action: () => { + handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD'); + }, + icon: ReplaceImageIcon, + }, + { + text: intl.formatMessage(hasAlt ? messages.altEdit : messages.altAdd), + action: () => { + handleModal('ACCOUNT_EDIT_IMAGE_ALT'); + }, + icon: hasAlt ? EditIcon : AddIcon, + }, + null, + { + text: intl.formatMessage(messages.remove), + action: () => { + handleModal('ACCOUNT_EDIT_IMAGE_DELETE'); + }, + icon: DeleteIcon, + dangerous: true, + }, + ] satisfies MenuItem[], + [handleModal, hasAlt, intl], + ); + + const handleAddImage = useCallback(() => { + handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD'); + }, [handleModal]); + + const iconClassName = classNames(classes.imageButton, className); + + if (!src) { + return ( + + ); + } + + return ( + + ); +}; + +const popperOffset = [0, 6] as OffsetValue; diff --git a/app/javascript/mastodon/features/account_edit/components/item_list.tsx b/app/javascript/mastodon/features/account_edit/components/item_list.tsx index eb6cf590f5ebfe..2b5dc9ab9f7b9a 100644 --- a/app/javascript/mastodon/features/account_edit/components/item_list.tsx +++ b/app/javascript/mastodon/features/account_edit/components/item_list.tsx @@ -1,5 +1,7 @@ import { useCallback } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + import classes from '../styles.module.scss'; import { DeleteIconButton, EditButton } from './edit_button'; @@ -50,6 +52,17 @@ type AccountEditItemButtonsProps = Pick< 'onEdit' | 'onDelete' | 'disabled' > & { item: Item }; +const messages = defineMessages({ + edit: { + id: 'account_edit.item_list.edit', + defaultMessage: 'Edit {name}', + }, + delete: { + id: 'account_edit.item_list.delete', + defaultMessage: 'Delete {name}', + }, +}); + const AccountEditItemButtons = ({ item, onDelete, @@ -63,6 +76,8 @@ const AccountEditItemButtons = ({ onDelete?.(item); }, [item, onDelete]); + const intl = useIntl(); + if (!onEdit && !onDelete) { return null; } @@ -71,15 +86,15 @@ const AccountEditItemButtons = ({
{onEdit && ( )} {onDelete && ( diff --git a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx index f0bba5a7459ae1..3b423c0735613e 100644 --- a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx +++ b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx @@ -1,95 +1,80 @@ import type { ChangeEventHandler, FC } from 'react'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useId, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import type { ApiHashtagJSON } from '@/mastodon/api_types/tags'; import { Combobox } from '@/mastodon/components/form_fields'; -import { - addFeaturedTag, - clearSearch, - updateSearchQuery, -} from '@/mastodon/reducers/slices/profile_edit'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { useSearchTags } from '@/mastodon/hooks/useSearchTags'; +import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags'; +import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch } from '@/mastodon/store'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import classes from '../styles.module.scss'; -type SearchResult = Omit & { - label?: string; -}; - const messages = defineMessages({ placeholder: { id: 'account_edit_tags.search_placeholder', defaultMessage: 'Enter a hashtag…', }, - addTag: { - id: 'account_edit_tags.add_tag', - defaultMessage: 'Add #{tagName}', - }, }); export const AccountEditTagSearch: FC = () => { const intl = useIntl(); + const [query, setQuery] = useState(''); const { - query, + tags: suggestedTags, + searchTags, + resetSearch, isLoading, - results: rawResults, - } = useAppSelector((state) => state.profileEdit.search); - const results = useMemo(() => { - if (!rawResults) { - return []; - } - - const results: SearchResult[] = [...rawResults]; // Make array mutable - const trimmedQuery = query.trim(); - if ( - trimmedQuery.length > 0 && - results.every( - (result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(), - ) - ) { - results.push({ - id: 'new', - name: trimmedQuery, - label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }), - }); - } - return results; - }, [intl, query, rawResults]); + } = useSearchTags({ + query, + // Remove existing featured tags from suggestions + filterResults: (tag) => !tag.featuring, + }); - const dispatch = useAppDispatch(); const handleSearchChange: ChangeEventHandler = useCallback( (e) => { - void dispatch(updateSearchQuery(e.target.value)); + setQuery(e.target.value); + searchTags(e.target.value); }, - [dispatch], + [searchTags], ); + const dispatch = useAppDispatch(); const handleSelect = useCallback( - (item: SearchResult) => { - void dispatch(clearSearch()); - void dispatch(addFeaturedTag({ name: item.name })); + (item: TagSearchResult) => { + resetSearch(); + setQuery(''); + void dispatch(addFeaturedTags({ names: [item.name] })); }, - [dispatch], + [dispatch, resetSearch], ); + const inputId = useId(); + const inputLabel = intl.formatMessage(messages.placeholder); + return ( - + <> + + + ); }; -const renderItem = (item: SearchResult) => item.label ?? `#${item.name}`; +const renderItem = (item: TagSearchResult) => item.label ?? `#${item.name}`; diff --git a/app/javascript/mastodon/features/account_edit/featured_tags.tsx b/app/javascript/mastodon/features/account_edit/featured_tags.tsx index dbcdad6d625ab1..5fc602208dff56 100644 --- a/app/javascript/mastodon/features/account_edit/featured_tags.tsx +++ b/app/javascript/mastodon/features/account_edit/featured_tags.tsx @@ -3,13 +3,14 @@ import type { FC } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { Callout } from '@/mastodon/components/callout'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { Tag } from '@/mastodon/components/tags/tag'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; import type { TagData } from '@/mastodon/reducers/slices/profile_edit'; import { - addFeaturedTag, + addFeaturedTags, deleteFeaturedTag, fetchProfile, fetchSuggestedTags, @@ -28,17 +29,25 @@ import classes from './styles.module.scss'; const messages = defineMessages({ columnTitle: { id: 'account_edit_tags.column_title', - defaultMessage: 'Edit featured hashtags', + defaultMessage: 'Edit Tags', }, }); const selectTags = createAppSelector( - [(state) => state.profileEdit], - (profileEdit) => ({ + [ + (state) => state.profileEdit, + (state) => + state.server.getIn( + ['server', 'accounts', 'max_featured_tags'], + 10, + ) as number, + ], + (profileEdit, maxTags) => ({ tags: profileEdit.profile?.featuredTags ?? [], tagSuggestions: profileEdit.tagSuggestions ?? [], isLoading: !profileEdit.profile || !profileEdit.tagSuggestions, isPending: profileEdit.isPending, + maxTags, }), ); @@ -47,7 +56,7 @@ export const AccountEditFeaturedTags: FC = () => { const account = useAccount(accountId); const intl = useIntl(); - const { tags, tagSuggestions, isLoading, isPending } = + const { tags, tagSuggestions, isLoading, isPending, maxTags } = useAppSelector(selectTags); const dispatch = useAppDispatch(); @@ -67,6 +76,8 @@ export const AccountEditFeaturedTags: FC = () => { return ; } + const canAddMoreTags = tags.length < maxTags; + return ( { tagName='p' /> - + {canAddMoreTags && } - {tagSuggestions.length > 0 && ( + {tagSuggestions.length > 0 && canAddMoreTags && (
{
)} + {!canAddMoreTags && ( + + + + )} + {isLoading && } = ({ }) => { const dispatch = useAppDispatch(); const handleAddTag = useCallback(() => { - void dispatch(addFeaturedTag({ name })); + void dispatch(addFeaturedTags({ names: [name] })); }, [dispatch, name]); return ; }; diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index 43a13f612a1302..9161b2f7a553a8 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'; import type { ModalType } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal'; +import { AccountBio } from '@/mastodon/components/account_bio'; import { Avatar } from '@/mastodon/components/avatar'; import { Button } from '@/mastodon/components/button'; import { DismissibleCallout } from '@/mastodon/components/callout/dismissible'; @@ -23,6 +24,7 @@ import { AccountEditColumn, AccountEditEmptyColumn } from './components/column'; import { EditButton } from './components/edit_button'; import { AccountField } from './components/field'; import { AccountFieldActions } from './components/field_actions'; +import { AccountImageEdit } from './components/image_edit'; import { AccountEditSection } from './components/section'; import classes from './styles.module.scss'; @@ -40,6 +42,14 @@ export const messages = defineMessages({ defaultMessage: 'Your display name is how your name appears on your profile and in timelines.', }, + displayNameAddLabel: { + id: 'account_edit.display_name.add_label', + defaultMessage: 'Add display name', + }, + displayNameEditLabel: { + id: 'account_edit.display_name.edit_label', + defaultMessage: 'Edit display name', + }, bioTitle: { id: 'account_edit.bio.title', defaultMessage: 'Bio', @@ -48,6 +58,14 @@ export const messages = defineMessages({ id: 'account_edit.bio.placeholder', defaultMessage: 'Add a short introduction to help others identify you.', }, + bioAddLabel: { + id: 'account_edit.bio.label', + defaultMessage: 'Add bio', + }, + bioEditLabel: { + id: 'account_edit.bio.edit_label', + defaultMessage: 'Edit bio', + }, customFieldsTitle: { id: 'account_edit.custom_fields.title', defaultMessage: 'Custom fields', @@ -57,9 +75,13 @@ export const messages = defineMessages({ defaultMessage: 'Add your pronouns, external links, or anything else you’d like to share.', }, - customFieldsName: { - id: 'account_edit.custom_fields.name', - defaultMessage: 'field', + customFieldsAddLabel: { + id: 'account_edit.custom_fields.add_label', + defaultMessage: 'Add field', + }, + customFieldsEditLabel: { + id: 'account_edit.custom_fields.edit_label', + defaultMessage: 'Edit field', }, customFieldsTipTitle: { id: 'account_edit.custom_fields.tip_title', @@ -74,9 +96,9 @@ export const messages = defineMessages({ defaultMessage: 'Help others identify, and have quick access to, your favorite topics.', }, - featuredHashtagsItem: { - id: 'account_edit.featured_hashtags.item', - defaultMessage: 'hashtags', + featuredHashtagsEditLabel: { + id: 'account_edit.featured_hashtags.edit_label', + defaultMessage: 'Add hashtags', }, profileTabTitle: { id: 'account_edit.profile_tab.title', @@ -164,8 +186,12 @@ export const AccountEdit: FC = () => {
{headerSrc && } + +
+
+ +
-
@@ -176,8 +202,12 @@ export const AccountEdit: FC = () => { buttons={ } > @@ -191,12 +221,18 @@ export const AccountEdit: FC = () => { buttons={ } > - + { description={messages.customFieldsPlaceholder} showDescription={!hasFields} buttons={ - <> - {profile.fields.length > 1 && ( - - )} - {hasFields && ( - = maxFieldCount} +
+ + = maxFieldCount} + /> +
} > {hasFields && ( @@ -233,10 +266,7 @@ export const AccountEdit: FC = () => {
- + ))} @@ -271,8 +301,8 @@ export const AccountEdit: FC = () => { buttons={ } > diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx index c7b3b6ebc54fa6..41991da7d0f468 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx @@ -1,10 +1,17 @@ -import { useCallback, useMemo, useState } from 'react'; +import { + forwardRef, + useCallback, + useImperativeHandle, + useMemo, + useState, +} from 'react'; import type { FC } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import type { Map as ImmutableMap } from 'immutable'; +import { closeModal } from '@/mastodon/actions/modal'; import { Button } from '@/mastodon/components/button'; import { Callout } from '@/mastodon/components/callout'; import { EmojiTextInputField } from '@/mastodon/components/form_fields'; @@ -18,6 +25,7 @@ import { useAppDispatch, useAppSelector, } from '@/mastodon/store'; +import { isUrlWithoutProtocol } from '@/mastodon/utils/checks'; import { ConfirmationModal } from '../../ui/components/confirmation_modals'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; @@ -48,16 +56,21 @@ const messages = defineMessages({ }, editValueHint: { id: 'account_edit.field_edit_modal.value_hint', - defaultMessage: 'E.g. “example.me”', - }, - limitHeader: { - id: 'account_edit.field_edit_modal.limit_header', - defaultMessage: 'Recommended character limit exceeded', + defaultMessage: 'E.g. “https://example.me”', }, save: { id: 'account_edit.save', defaultMessage: 'Save', }, + discardMessage: { + id: 'account_edit.field_edit_modal.discard_message', + defaultMessage: + 'You have unsaved changes. Are you sure you want to discard them?', + }, + discardConfirm: { + id: 'account_edit.field_edit_modal.discard_confirm', + defaultMessage: 'Discard', + }, }); // We have two different values- the hard limit set by the server, @@ -82,19 +95,39 @@ const selectEmojiCodes = createAppSelector( (emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(), ); -export const EditFieldModal: FC = ({ - onClose, - fieldKey, -}) => { +interface ConfirmationMessage { + message: string; + confirm: string; + props: { fieldKey?: string; lastLabel: string; lastValue: string }; +} + +interface ModalRef { + getCloseConfirmationMessage: () => null | ConfirmationMessage; +} + +export const EditFieldModal = forwardRef< + ModalRef, + DialogModalProps & { + fieldKey?: string; + lastLabel?: string; + lastValue?: string; + } +>(({ onClose, fieldKey, lastLabel, lastValue }, ref) => { const intl = useIntl(); const field = useAppSelector((state) => selectFieldById(state, fieldKey)); - const [newLabel, setNewLabel] = useState(field?.name ?? ''); - const [newValue, setNewValue] = useState(field?.value ?? ''); + const oldLabel = lastLabel ?? field?.name; + const oldValue = lastValue ?? field?.value; + const [newLabel, setNewLabel] = useState(oldLabel ?? ''); + const [newValue, setNewValue] = useState(oldValue ?? ''); + const isDirty = newLabel !== oldLabel || newValue !== oldValue; const { nameLimit, valueLimit } = useAppSelector(selectFieldLimits); const isPending = useAppSelector((state) => state.profileEdit.isPending); const disabled = + !newLabel.trim() || + !newValue.trim() || + !isDirty || !nameLimit || !valueLimit || newLabel.length > nameLimit || @@ -109,6 +142,10 @@ export const EditFieldModal: FC = ({ ); return hasLink && hasEmoji; }, [customEmojiCodes, newLabel, newValue]); + const hasLinkWithoutProtocol = useMemo( + () => isUrlWithoutProtocol(newValue), + [newValue], + ); const dispatch = useAppDispatch(); const handleSave = useCallback(() => { @@ -117,11 +154,41 @@ export const EditFieldModal: FC = ({ } void dispatch( updateField({ id: fieldKey, name: newLabel, value: newValue }), - ).then(onClose); - }, [disabled, dispatch, fieldKey, isPending, newLabel, newValue, onClose]); + ).then(() => { + // Close without confirmation. + dispatch( + closeModal({ + modalType: 'ACCOUNT_EDIT_FIELD_EDIT', + ignoreFocus: false, + }), + ); + }); + }, [disabled, dispatch, fieldKey, isPending, newLabel, newValue]); + + useImperativeHandle( + ref, + () => ({ + getCloseConfirmationMessage: () => { + if (!newLabel || !newValue || !isDirty) { + return null; + } + return { + message: intl.formatMessage(messages.discardMessage), + confirm: intl.formatMessage(messages.discardConfirm), + props: { + fieldKey, + lastLabel: newLabel, + lastValue: newValue, + }, + }; + }, + }), + [fieldKey, intl, isDirty, newLabel, newValue], + ); return ( = ({ {(newLabel.length > RECOMMENDED_LIMIT || newValue.length > RECOMMENDED_LIMIT) && ( - + + + + )} + + {hasLinkWithoutProtocol && ( + https://, + }} /> )} ); -}; +}); +EditFieldModal.displayName = 'EditFieldModal'; export const DeleteFieldModal: FC = ({ onClose, diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx index 5eee431a27214d..8a94c99ac28a1d 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx @@ -212,11 +212,9 @@ export const ReorderFieldsModal: FC = ({ onClose }) => { return; } newFields.push({ name: field.name, value: field.value }); - - void dispatch(patchProfile({ fields_attributes: newFields })).then( - onClose, - ); } + + void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose); }, [dispatch, fieldKeys, fields, onClose]); const emojis = useAppSelector((state) => state.custom_emojis); diff --git a/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx b/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx new file mode 100644 index 00000000000000..973c74ff83e0f3 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx @@ -0,0 +1,144 @@ +import type { ChangeEventHandler, FC } from 'react'; +import { useCallback, useState } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Details } from '@/mastodon/components/details'; +import { TextAreaField } from '@/mastodon/components/form_fields'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { patchProfile } from '@/mastodon/reducers/slices/profile_edit'; +import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; + +import { ConfirmationModal } from '../../ui/components/confirmation_modals'; +import type { DialogModalProps } from '../../ui/components/dialog_modal'; + +import classes from './styles.module.scss'; + +export const ImageAltModal: FC< + DialogModalProps & { location: ImageLocation } +> = ({ onClose, location }) => { + const { profile, isPending } = useAppSelector((state) => state.profileEdit); + + const initialAlt = profile?.[`${location}Description`]; + const imageSrc = profile?.[`${location}Static`]; + + const [altText, setAltText] = useState(initialAlt ?? ''); + + const dispatch = useAppDispatch(); + const handleSave = useCallback(() => { + void dispatch( + patchProfile({ + [`${location}_description`]: altText, + }), + ).then(onClose); + }, [altText, dispatch, location, onClose]); + + if (!imageSrc) { + return ; + } + + return ( + + ) : ( + + ) + } + onClose={onClose} + onConfirm={handleSave} + confirm={ + + } + updating={isPending} + > +
+ +
+
+ ); +}; + +export const ImageAltTextField: FC<{ + imageSrc: string; + altText: string; + onChange: (altText: string) => void; + hideTip?: boolean; +}> = ({ imageSrc, altText, onChange, hideTip }) => { + const altLimit = useAppSelector( + (state) => + state.server.getIn( + ['server', 'configuration', 'media_attachments', 'description_limit'], + 150, + ) as number, + ); + + const handleChange: ChangeEventHandler = useCallback( + (event) => { + onChange(event.currentTarget.value); + }, + [onChange], + ); + + return ( + <> + + + + } + hint={ + + } + onChange={handleChange} + value={altText} + maxLength={altLimit} + /> + + {!hideTip && ( +
+ } + className={classes.altHint} + > +
    {chunks}
, + li: (chunks) =>
  • {chunks}
  • , + }} + tagName='div' + /> +
    + )} + + ); +}; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx new file mode 100644 index 00000000000000..50bcf3d8a1f6f6 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Button } from '@/mastodon/components/button'; +import { deleteImage } from '@/mastodon/reducers/slices/profile_edit'; +import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; + +import { DialogModal } from '../../ui/components/dialog_modal'; +import type { DialogModalProps } from '../../ui/components/dialog_modal'; + +export const ImageDeleteModal: FC< + DialogModalProps & { location: ImageLocation } +> = ({ onClose, location }) => { + const isPending = useAppSelector((state) => state.profileEdit.isPending); + const dispatch = useAppDispatch(); + const handleDelete = useCallback(() => { + void dispatch(deleteImage({ location })).then(onClose); + }, [dispatch, location, onClose]); + + return ( + + } + buttons={ + + } + > + + + ); +}; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx new file mode 100644 index 00000000000000..23636083de7ba1 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx @@ -0,0 +1,451 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { ChangeEventHandler, FC } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import type { Area } from 'react-easy-crop'; +import Cropper from 'react-easy-crop'; + +import { setDragUploadEnabled } from '@/mastodon/actions/compose_typed'; +import { Button } from '@/mastodon/components/button'; +import { RangeInputField } from '@/mastodon/components/form_fields/range_input_field'; +import { + selectImageInfo, + uploadImage, +} from '@/mastodon/reducers/slices/profile_edit'; +import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; + +import { DialogModal } from '../../ui/components/dialog_modal'; +import type { DialogModalProps } from '../../ui/components/dialog_modal'; + +import { ImageAltTextField } from './image_alt'; +import classes from './styles.module.scss'; + +import 'react-easy-crop/react-easy-crop.css'; + +const messages = defineMessages({ + avatarAdd: { + id: 'account_edit.upload_modal.title_add.avatar', + defaultMessage: 'Add profile photo', + }, + headerAdd: { + id: 'account_edit.upload_modal.title_add.header', + defaultMessage: 'Add cover photo', + }, + avatarReplace: { + id: 'account_edit.upload_modal.title_replace.avatar', + defaultMessage: 'Replace profile photo', + }, + headerReplace: { + id: 'account_edit.upload_modal.title_replace.header', + defaultMessage: 'Replace cover photo', + }, + zoomLabel: { + id: 'account_edit.upload_modal.step_crop.zoom', + defaultMessage: 'Zoom', + }, +}); + +export const ImageUploadModal: FC< + DialogModalProps & { location: ImageLocation } +> = ({ onClose, location }) => { + const { src: oldSrc } = useAppSelector((state) => + selectImageInfo(state, location), + ); + const intl = useIntl(); + const title = intl.formatMessage( + oldSrc ? messages[`${location}Replace`] : messages[`${location}Add`], + ); + + // State for individual steps. + const [step, setStep] = useState<'select' | 'crop' | 'alt'>('select'); + const [imageSrc, setImageSrc] = useState(null); + const [imageBlob, setImageBlob] = useState(null); + + const handleFile = useCallback((file: File) => { + const reader = new FileReader(); + reader.addEventListener('load', () => { + const result = reader.result; + if (typeof result === 'string' && result.length > 0) { + setImageSrc(result); + setStep('crop'); + } + }); + reader.readAsDataURL(file); + }, []); + + const handleCrop = useCallback( + (crop: Area) => { + if (!imageSrc) { + setStep('select'); + return; + } + void calculateCroppedImage(imageSrc, crop).then((blob) => { + setImageBlob(blob); + setStep('alt'); + }); + }, + [imageSrc], + ); + + const dispatch = useAppDispatch(); + const handleSave = useCallback( + (altText: string) => { + if (!imageBlob) { + setStep('crop'); + return; + } + void dispatch(uploadImage({ location, imageBlob, altText })).then( + onClose, + ); + }, + [dispatch, imageBlob, location, onClose], + ); + + const handleCancel = useCallback(() => { + switch (step) { + case 'crop': + setImageSrc(null); + setStep('select'); + break; + case 'alt': + setImageBlob(null); + setStep('crop'); + break; + default: + onClose(); + } + }, [onClose, step]); + + return ( + + {step === 'select' && ( + + )} + {step === 'crop' && imageSrc && ( + + )} + {step === 'alt' && imageBlob && ( + + )} + + ); +}; + +// Taken from app/models/concerns/account/header.rb and app/models/concerns/account/avatar.rb +const ALLOWED_MIME_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', +]; + +const StepUpload: FC<{ + location: ImageLocation; + onFile: (file: File) => void; +}> = ({ location, onFile }) => { + const inputRef = useRef(null); + const handleUploadClick = useCallback(() => { + inputRef.current?.click(); + }, []); + + const handleFileChange: ChangeEventHandler = useCallback( + (event) => { + const file = event.currentTarget.files?.[0]; + if (!file || !ALLOWED_MIME_TYPES.includes(file.type)) { + return; + } + onFile(file); + }, + [onFile], + ); + + // Handle drag and drop + const [isDragging, setDragging] = useState(false); + + const handleDragOver = useCallback((event: DragEvent) => { + event.preventDefault(); + if (!event.dataTransfer?.types.includes('Files')) { + return; + } + + const items = Array.from(event.dataTransfer.items); + if ( + !items.some( + (item) => + item.kind === 'file' && ALLOWED_MIME_TYPES.includes(item.type), + ) + ) { + return; + } + + setDragging(true); + }, []); + const handleDragDrop = useCallback( + (event: DragEvent) => { + event.preventDefault(); + setDragging(false); + + if (!event.dataTransfer?.files) { + return; + } + + const file = Array.from(event.dataTransfer.files).find((f) => + ALLOWED_MIME_TYPES.includes(f.type), + ); + if (!file) { + return; + } + + onFile(file); + }, + [onFile], + ); + const handleDragLeave = useCallback((event: DragEvent) => { + event.preventDefault(); + setDragging(false); + }, []); + + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(setDragUploadEnabled(false)); + document.addEventListener('dragover', handleDragOver); + document.addEventListener('drop', handleDragDrop); + document.addEventListener('dragleave', handleDragLeave); + + return () => { + document.removeEventListener('dragover', handleDragOver); + document.removeEventListener('drop', handleDragDrop); + document.removeEventListener('dragleave', handleDragLeave); + dispatch(setDragUploadEnabled(true)); + }; + }, [handleDragLeave, handleDragDrop, handleDragOver, dispatch]); + + if (isDragging) { + return ( +
    + +
    + ); + } + + return ( +
    + + , + limit: 8, + width: location === 'avatar' ? 400 : 1500, + height: location === 'avatar' ? 400 : 500, + }} + tagName='p' + /> + + + +
    + ); +}; + +const StepCrop: FC<{ + src: string; + location: ImageLocation; + onCancel: () => void; + onComplete: (crop: Area) => void; +}> = ({ src, location, onCancel, onComplete }) => { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [croppedArea, setCroppedArea] = useState(null); + const [zoom, setZoom] = useState(1); + const intl = useIntl(); + + const handleZoomChange: ChangeEventHandler = useCallback( + (event) => { + setZoom(event.currentTarget.valueAsNumber); + }, + [], + ); + const handleCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => { + setCroppedArea(croppedAreaPixels); + }, []); + + const handleNext = useCallback(() => { + if (croppedArea) { + onComplete(croppedArea); + } + }, [croppedArea, onComplete]); + + return ( + <> +
    + +
    + +
    + + + +
    + + ); +}; + +const StepAlt: FC<{ + imageBlob: Blob; + onCancel: () => void; + onComplete: (altText: string) => void; + location: ImageLocation; +}> = ({ imageBlob, onCancel, onComplete, location }) => { + const [altText, setAltText] = useState(''); + + const handleComplete = useCallback(() => { + onComplete(altText); + }, [altText, onComplete]); + + const imageSrc = useMemo(() => URL.createObjectURL(imageBlob), [imageBlob]); + + return ( + <> + + +
    + + + +
    + + ); +}; + +async function calculateCroppedImage( + imageSrc: string, + crop: Area, +): Promise { + const image = await dataUriToImage(imageSrc); + const canvas = new OffscreenCanvas(crop.width, crop.height); + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get canvas context'); + } + + ctx.imageSmoothingQuality = 'high'; + + // Draw the image + ctx.drawImage( + image, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height, + ); + + return canvas.convertToBlob({ + quality: 0.7, + type: 'image/jpeg', + }); +} + +function dataUriToImage(dataUri: string) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => { + resolve(image); + }); + image.addEventListener('error', (event) => { + if (event.error instanceof Error) { + reject(event.error); + } else { + reject(new Error('Failed to load image')); + } + }); + image.src = dataUri; + }); +} diff --git a/app/javascript/mastodon/features/account_edit/modals/index.ts b/app/javascript/mastodon/features/account_edit/modals/index.ts index 861e81f5977cd0..8f59a686883d7c 100644 --- a/app/javascript/mastodon/features/account_edit/modals/index.ts +++ b/app/javascript/mastodon/features/account_edit/modals/index.ts @@ -1,6 +1,9 @@ export * from './bio_modal'; export * from './fields_modals'; export * from './fields_reorder_modal'; +export { ImageAltModal } from './image_alt'; +export * from './image_delete'; +export * from './image_upload'; export * from './name_modal'; export * from './profile_display_modal'; export * from './verified_modal'; diff --git a/app/javascript/mastodon/features/account_edit/modals/profile_display_modal.tsx b/app/javascript/mastodon/features/account_edit/modals/profile_display_modal.tsx index adad4e6f8fb99c..dc2d961c69b199 100644 --- a/app/javascript/mastodon/features/account_edit/modals/profile_display_modal.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/profile_display_modal.tsx @@ -62,24 +62,26 @@ export const ProfileDisplayModal: FC = ({ onClose }) => { } /> - - } - hint={ - - } - /> + {profile.showMedia && ( + + } + hint={ + + } + /> + )} = ({ onClose }) => { } value={`Mastodon`} /> -
    - +
    - -
    + } + > <a> }} /> -
    +
  • = 500px) { height: 160px; @@ -16,12 +17,50 @@ width: 100%; height: 100%; } + + .imageButton { + top: 16px; + right: 24px; + } } .avatar { margin-top: -64px; margin-left: 18px; - border: 1px solid var(--color-border-primary); + position: relative; + width: 82px; + + > :global(.account__avatar) { + border: 1px solid var(--color-border-primary); + } + + .imageButton { + bottom: -8px; + right: -8px; + } +} + +.bio { + unicode-bidi: plaintext; + + p:not(:last-child) { + margin-bottom: 20px; + } + + a { + color: inherit; + text-decoration: underline; + } +} + +.fieldButtons { + display: flex; + gap: 8px; + align-items: end; + + @container (width < 500px) { + flex-direction: column; + } } .field { @@ -58,7 +97,8 @@ } .autoComplete, -.tagSuggestions { +.tagSuggestions, +.maxTagsWarning { margin: 12px 0; } @@ -153,6 +193,41 @@ font-size: 15px; } +// Image edit component + +.imageButton { + --default-bg-color: var(--color-bg-primary); + + &, + &:global(.active) { + // Overrides the transparent background added by default with .active + --hover-bg-color: var(--color-bg-brand-softer-solid); + } + + position: absolute; + width: 28px; + height: 28px; + border: 1px solid var(--color-border-primary); + border-radius: 9999px; + box-sizing: border-box; + padding: 4px; + transition: + color 0.2s ease-in-out, + background-color 0.2s ease-in-out; + + svg { + width: 18px; + height: 18px; + } +} + +.imageMenu { + svg { + width: 20px; + height: 20px; + } +} + // Item list component .itemList { diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index 1172ff3dc506e4..5cec2250ef6787 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -2,10 +2,12 @@ import { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router'; +import { useHistory } from 'react-router'; import { List as ImmutableList } from 'immutable'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { fetchEndorsedAccounts } from 'mastodon/actions/accounts'; import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { Account } from 'mastodon/components/account'; @@ -35,21 +37,27 @@ import { EmptyMessage } from './components/empty_message'; import { FeaturedTag } from './components/featured_tag'; import type { TagMap } from './components/featured_tag'; -interface Params { - acct?: string; - id?: string; -} - const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ multiColumn, }) => { const accountId = useAccountId(); + const account = useAccount(accountId); const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); const forceEmptyState = suspended || blockedBy || hidden; - const { acct = '' } = useParams(); const dispatch = useAppDispatch(); + const history = useHistory(); + useEffect(() => { + if ( + account && + !account.show_featured && + isServerFeatureEnabled('profile_redesign') + ) { + history.push(`/@${account.acct}`); + } + }, [account, history]); + useEffect(() => { if (accountId) { void dispatch(fetchFeaturedTags({ accountId })); @@ -103,7 +111,14 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ ); } - if (featuredTags.isEmpty() && featuredAccountIds.isEmpty()) { + const noTags = + featuredTags.isEmpty() || isServerFeatureEnabled('profile_redesign'); + + if ( + noTags && + featuredAccountIds.isEmpty() && + listedCollections.length === 0 + ) { return ( = ({ )} - {!featuredTags.isEmpty() && ( + {!noTags && ( <>

    = ({ aria-posinset={index + 1} aria-setsize={featuredTags.size} > - + ))} diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx index 594f71cb232498..52f30ac5057b44 100644 --- a/app/javascript/mastodon/features/account_gallery/index.tsx +++ b/app/javascript/mastodon/features/account_gallery/index.tsx @@ -2,10 +2,9 @@ import { useEffect, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { createSelector } from '@reduxjs/toolkit'; -import type { Map as ImmutableMap } from 'immutable'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, isList } from 'immutable'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { openModal } from 'mastodon/actions/modal'; import { expandAccountMediaTimeline } from 'mastodon/actions/timelines'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; @@ -18,38 +17,69 @@ import Column from 'mastodon/features/ui/components/column'; import { useAccountId } from 'mastodon/hooks/useAccountId'; import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import type { RootState } from 'mastodon/store'; -import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { + useAppSelector, + useAppDispatch, + createAppSelector, +} from 'mastodon/store'; import { MediaItem } from './components/media_item'; -const getAccountGallery = createSelector( +const emptyList = ImmutableList(); + +const redesignEnabled = isServerFeatureEnabled('profile_redesign'); + +const selectGalleryTimeline = createAppSelector( [ - (state: RootState, accountId: string) => - (state.timelines as ImmutableMap).getIn( - [`account:${accountId}:media`, 'items'], - ImmutableList(), - ) as ImmutableList, - (state: RootState) => state.statuses, + (_state, accountId?: string | null) => accountId, + (state) => state.timelines, + (state) => state.accounts, + (state) => state.statuses, ], - (statusIds, statuses) => { - let items = ImmutableList(); + (accountId, timelines, accounts, statuses) => { + if (!accountId) { + return null; + } + const account = accounts.get(accountId); + if (!account) { + return null; + } + + let items = emptyList; + const { show_media, show_media_replies } = account; + // If the account disabled showing media, don't display anything. + if (!show_media && redesignEnabled) { + return { + items, + hasMore: false, + isLoading: false, + showingReplies: false, + }; + } - statusIds.forEach((statusId) => { - const status = statuses.get(statusId) as - | ImmutableMap - | undefined; + const showingReplies = show_media_replies && redesignEnabled; + const timeline = timelines.get( + `account:${accountId}:media${showingReplies ? ':with_replies' : ''}`, + ); + const statusIds = timeline?.get('items'); - if (status) { + if (isList(statusIds)) { + for (const statusId of statusIds) { + const status = statuses.get(statusId); items = items.concat( ( - status.get('media_attachments') as ImmutableList + status?.get('media_attachments') as ImmutableList ).map((media) => media.set('status', status)), ); } - }); + } - return items; + return { + items, + hasMore: !!timeline?.get('hasMore'), + isLoading: !!timeline?.get('isLoading'), + showingReplies, + }; }, ); @@ -58,27 +88,12 @@ export const AccountGallery: React.FC<{ }> = ({ multiColumn }) => { const dispatch = useAppDispatch(); const accountId = useAccountId(); - const attachments = useAppSelector((state) => - accountId - ? getAccountGallery(state, accountId) - : ImmutableList(), - ); - const isLoading = useAppSelector((state) => - (state.timelines as ImmutableMap).getIn([ - `account:${accountId}:media`, - 'isLoading', - ]), - ); - const hasMore = useAppSelector((state) => - (state.timelines as ImmutableMap).getIn([ - `account:${accountId}:media`, - 'hasMore', - ]), - ); - const account = useAppSelector((state) => - accountId ? state.accounts.get(accountId) : undefined, - ); - const isAccount = !!account; + const { + isLoading = true, + hasMore = false, + items: attachments = emptyList, + showingReplies: withReplies = false, + } = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {}; const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); @@ -87,16 +102,18 @@ export const AccountGallery: React.FC<{ | undefined; useEffect(() => { - if (accountId && isAccount) { - void dispatch(expandAccountMediaTimeline(accountId)); + if (accountId) { + void dispatch(expandAccountMediaTimeline(accountId, { withReplies })); } - }, [dispatch, accountId, isAccount]); + }, [dispatch, accountId, withReplies]); const handleLoadMore = useCallback(() => { if (maxId) { - void dispatch(expandAccountMediaTimeline(accountId, { maxId })); + void dispatch( + expandAccountMediaTimeline(accountId, { maxId, withReplies }), + ); } - }, [dispatch, accountId, maxId]); + }, [maxId, dispatch, accountId, withReplies]); const handleOpenMedia = useCallback( (attachment: MediaAttachment) => { diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 9731f4cc3340c2..6a9d51f737c30f 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -211,6 +211,7 @@ export const AccountHeader: React.FC<{ ))} = ({ - accountId, -}) => { +const LegacyNumberFields: FC<{ accountId: string }> = ({ accountId }) => { const intl = useIntl(); const account = useAccount(accountId); @@ -29,24 +28,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({ } return ( -
    - {!isRedesignEnabled() && ( - - - - )} +
    + + + = ({ renderer={FollowersCounter} /> +
    + ); +}; - {isRedesignEnabled() && ( - - - - ), - }} - /> +const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const createdThisYear = useMemo( + () => account?.created_at.includes(new Date().getFullYear().toString()), + [account?.created_at], + ); + + if (!account) { + return null; + } + + return ( +
      + > +
    • + + + + +
    • + +
    • + + + + +
    • + +
    • + + + + +
    • + +
    • + + + {createdThisYear ? ( + + ) : ( + + )} + +
    • +
    ); }; + +export const AccountNumberFields = isRedesignEnabled() + ? RedesignNumberFields + : LegacyNumberFields; diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index 51a7962c762f2c..3b6a95d099c567 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -107,7 +107,7 @@ } $button-breakpoint: 420px; -$button-fallback-breakpoint: #{$button-breakpoint} + 55px; +$button-fallback-breakpoint: $button-breakpoint + 55px; .buttonsDesktop { @container (width < #{$button-breakpoint}) { @@ -132,7 +132,7 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px; } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } @@ -293,11 +293,7 @@ svg.badgeIcon { .fieldOverflowButton { --default-bg-color: var(--color-bg-secondary-solid); - --hover-bg-color: color-mix( - in oklab, - var(--color-bg-brand-base), - var(--default-bg-color) var(--overlay-strength-brand) - ); + --hover-bg-color: var(--color-bg-brand-softer-solid); position: absolute; right: 8px; @@ -312,16 +308,33 @@ svg.badgeIcon { } .fieldNumbersWrapper { + display: flex; font-size: 13px; padding: 0; + margin: 8px 0; + gap: 20px; - a { - font-weight: unset; + li { + @container (width < 420px) { + flex: 1 1 0px; + } } + a, strong { + display: block; font-weight: 600; color: var(--color-text-primary); + font-size: 15px; + } + + a { + padding: 0; + + &:hover, + &:focus { + text-decoration: underline; + } } } diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx index eeb48c1c532d98..5febb8eaf8c564 100644 --- a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -5,25 +5,16 @@ import { FormattedMessage } from 'react-intl'; import type { NavLinkProps } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useAccountId } from '@/mastodon/hooks/useAccountId'; + import { isRedesignEnabled } from '../common'; import classes from './redesign.module.scss'; export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { if (isRedesignEnabled()) { - return ( -
    - - - - - - - - - -
    - ); + return ; } return (
    @@ -49,3 +40,32 @@ export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { const isActive: Required['isActive'] = (match, location) => match?.url === location.pathname || (!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`)); + +const RedesignTabs: FC = () => { + const accountId = useAccountId(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + const { acct, show_featured, show_media } = account; + + return ( +
    + + + + {show_media && ( + + + + )} + {show_featured && ( + + + + )} +
    + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx index 45fe4d7105d59e..d108a14fd695a4 100644 --- a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx @@ -141,8 +141,11 @@ const InnerNodeModal: FC<{ onChange={handleChange} label={intl.formatMessage(messages.fieldLabel)} className={classes.noteInput} - hasError={state === 'error'} - hint={errorText} + status={ + state === 'error' + ? { variant: 'error', message: errorText } + : undefined + } // eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal. autoFocus /> diff --git a/app/javascript/mastodon/features/account_timeline/v2/index.tsx b/app/javascript/mastodon/features/account_timeline/v2/index.tsx index c88d2a12d97de1..693813bdbe9f6b 100644 --- a/app/javascript/mastodon/features/account_timeline/v2/index.tsx +++ b/app/javascript/mastodon/features/account_timeline/v2/index.tsx @@ -18,7 +18,10 @@ import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { RemoteHint } from '@/mastodon/components/remote_hint'; import StatusList from '@/mastodon/components/status_list'; import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; -import { useAccountId } from '@/mastodon/hooks/useAccountId'; +import { + useAccountId, + useCurrentAccountId, +} from '@/mastodon/hooks/useAccountId'; import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility'; import { selectTimelineByKey } from '@/mastodon/selectors/timelines'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; @@ -34,6 +37,7 @@ import { usePinnedStatusIds, } from './pinned_statuses'; import classes from './styles.module.scss'; +import { TagSuggestions } from './tags_suggestions'; const emptyList = ImmutableList(); @@ -135,6 +139,7 @@ const Prepend: FC<{ accountId: string; forceEmpty: boolean; }> = ({ forceEmpty, accountId }) => { + const me = useCurrentAccountId(); if (forceEmpty) { return ; } @@ -144,6 +149,7 @@ const Prepend: FC<{ + {me === accountId && } ); }; diff --git a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss index b39892beec88d1..1df19feb1d5fc5 100644 --- a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss +++ b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss @@ -49,8 +49,12 @@ } } -.tagsWrapper { +.tagsWrapper, +.tagSuggestions { margin: 0 24px 8px; +} + +.tagsWrapper { display: flex; flex-wrap: nowrap; justify-content: flex-start; diff --git a/app/javascript/mastodon/features/account_timeline/v2/tags_suggestions.tsx b/app/javascript/mastodon/features/account_timeline/v2/tags_suggestions.tsx new file mode 100644 index 00000000000000..93ac491f6caee2 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/v2/tags_suggestions.tsx @@ -0,0 +1,128 @@ +import type { FC } from 'react'; +import { useEffect, useCallback, useState } from 'react'; + +import { FormattedMessage, FormattedList } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { fetchFeaturedTags } from '@/mastodon/actions/featured_tags'; +import { Callout } from '@/mastodon/components/callout'; +import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; +import { useDismissible } from '@/mastodon/hooks/useDismissible'; +import { + fetchProfile, + fetchSuggestedTags, + addFeaturedTags, +} from '@/mastodon/reducers/slices/profile_edit'; +import { useAppSelector, useAppDispatch } from '@/mastodon/store'; + +import classes from './styles.module.scss'; + +const MAX_SUGGESTED_TAGS = 3; + +export const TagSuggestions: FC = () => { + const { dismiss, wasDismissed } = useDismissible( + 'profile/featured_tag_suggestions', + ); + + const suggestedTags = useAppSelector((state) => + state.profileEdit.tagSuggestions?.slice(0, MAX_SUGGESTED_TAGS), + ); + const existingTagCount = useAppSelector( + (state) => state.profileEdit.profile?.featuredTags.length, + ); + const dispatch = useAppDispatch(); + + const isLoading = !suggestedTags || existingTagCount === undefined; + + useEffect(() => { + if (isLoading) { + void dispatch(fetchProfile()); + void dispatch(fetchSuggestedTags()); + } + }, [dispatch, isLoading]); + + const me = useCurrentAccountId(); + const [showSuccessNotice, setSuccessNotice] = useState(false); + + const handleAdd = useCallback(() => { + if (!suggestedTags?.length || !me) { + return; + } + + const addTags = async () => { + await dispatch( + addFeaturedTags({ names: suggestedTags.map((tag) => tag.name) }), + ); + await dispatch(fetchFeaturedTags({ accountId: me })); + setSuccessNotice(true); + dismiss(); + }; + void addTags(); + }, [dismiss, dispatch, me, suggestedTags]); + + const handleDismissSuccessNotice = useCallback(() => { + setSuccessNotice(false); + }, []); + + if (showSuccessNotice) { + return ( + + {chunks}, + }} + /> + + ); + } + + if ( + isLoading || + !suggestedTags.length || + existingTagCount > 0 || + wasDismissed + ) { + return null; + } + + return ( + + } + onSecondary={dismiss} + secondaryLabel={ + + } + > + `#${name}`)} + /> + ), + }} + /> + + ); +}; diff --git a/app/javascript/mastodon/features/collections/detail/collection_list.tsx b/app/javascript/mastodon/features/collections/detail/accounts_list.tsx similarity index 77% rename from app/javascript/mastodon/features/collections/detail/collection_list.tsx rename to app/javascript/mastodon/features/collections/detail/accounts_list.tsx index f66fd855cfe6b9..e458dd27f06fcb 100644 --- a/app/javascript/mastodon/features/collections/detail/collection_list.tsx +++ b/app/javascript/mastodon/features/collections/detail/accounts_list.tsx @@ -2,17 +2,23 @@ import { Fragment, useCallback, useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { Button } from '@/mastodon/components/button'; -import { useRelationship } from '@/mastodon/hooks/useRelationship'; -import type { ApiCollectionJSON } from 'mastodon/api_types/collections'; +import { openModal } from 'mastodon/actions/modal'; +import type { + ApiCollectionJSON, + CollectionAccountItem, +} from 'mastodon/api_types/collections'; import { Account } from 'mastodon/components/account'; +import { Button } from 'mastodon/components/button'; import { DisplayName } from 'mastodon/components/display_name'; import { Article, ItemList, } from 'mastodon/components/scrollable_list/components'; import { useAccount } from 'mastodon/hooks/useAccount'; +import { useDismissible } from 'mastodon/hooks/useDismissible'; +import { useRelationship } from 'mastodon/hooks/useRelationship'; import { me } from 'mastodon/initial_state'; +import { useAppDispatch } from 'mastodon/store'; import classes from './styles.module.scss'; @@ -62,6 +68,52 @@ const AccountItem: React.FC<{ ); }; +const RevokeControls: React.FC<{ + collectionId: string; + collectionItem: CollectionAccountItem; +}> = ({ collectionId, collectionItem }) => { + const dispatch = useAppDispatch(); + + const confirmRevoke = useCallback(() => { + void dispatch( + openModal({ + modalType: 'REVOKE_COLLECTION_INCLUSION', + modalProps: { + collectionId, + collectionItemId: collectionItem.id, + }, + }), + ); + }, [collectionId, collectionItem.id, dispatch]); + + const { wasDismissed, dismiss } = useDismissible( + `collection-revoke-hint-${collectionItem.id}`, + ); + + if (wasDismissed) { + return null; + } + + return ( +
    + + +
    + ); +}; + const SensitiveScreen: React.FC<{ sensitive: boolean | undefined; focusTargetRef: React.RefObject; @@ -166,6 +218,10 @@ export const CollectionAccountsList: React.FC<{ accountId={currentUserInCollection.account_id} collectionOwnerId={collection.account_id} /> +

    - ), + date: , }} tagName='li' /> diff --git a/app/javascript/mastodon/features/collections/detail/collection_menu.tsx b/app/javascript/mastodon/features/collections/detail/collection_menu.tsx index b236c1cadb1234..90c6315cbbc7d3 100644 --- a/app/javascript/mastodon/features/collections/detail/collection_menu.tsx +++ b/app/javascript/mastodon/features/collections/detail/collection_menu.tsx @@ -33,6 +33,10 @@ const messages = defineMessages({ id: 'collections.report_collection', defaultMessage: 'Report this collection', }, + revoke: { + id: 'collections.revoke_collection_inclusion', + defaultMessage: 'Remove myself from this collection', + }, more: { id: 'status.more', defaultMessage: 'More' }, }); @@ -71,6 +75,22 @@ export const CollectionMenu: React.FC<{ ); }, [collection, dispatch]); + const currentAccountInCollection = collection.items.find( + (item) => item.account_id === me, + ); + + const openRevokeConfirmation = useCallback(() => { + void dispatch( + openModal({ + modalType: 'REVOKE_COLLECTION_INCLUSION', + modalProps: { + collectionId: collection.id, + collectionItemId: currentAccountInCollection?.id, + }, + }), + ); + }, [collection.id, currentAccountInCollection?.id, dispatch]); + const menu = useMemo(() => { if (isOwnCollection) { const commonItems: MenuItem[] = [ @@ -99,34 +119,43 @@ export const CollectionMenu: React.FC<{ } else { return commonItems; } - } else if (ownerAccount) { - const items: MenuItem[] = [ - { - text: intl.formatMessage(messages.report), - action: openReportModal, - }, - ]; - const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`; - // Don't show menu link to featured collections while on that very page - if ( - !matchPath(location.pathname, { - path: featuredCollectionsPath, - exact: true, - }) - ) { - items.unshift( - ...[ - { - text: intl.formatMessage(messages.viewOtherCollections), - to: featuredCollectionsPath, - }, - null, - ], - ); + } else { + const items: MenuItem[] = []; + + if (ownerAccount) { + const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`; + // Don't show menu link to featured collections while on that very page + if ( + !matchPath(location.pathname, { + path: featuredCollectionsPath, + exact: true, + }) + ) { + items.push( + ...[ + { + text: intl.formatMessage(messages.viewOtherCollections), + to: featuredCollectionsPath, + }, + null, + ], + ); + } } + + if (currentAccountInCollection) { + items.push({ + text: intl.formatMessage(messages.revoke), + action: openRevokeConfirmation, + }); + } + + items.push({ + text: intl.formatMessage(messages.report), + action: openReportModal, + }); + return items; - } else { - return []; } }, [ isOwnCollection, @@ -134,6 +163,8 @@ export const CollectionMenu: React.FC<{ id, openDeleteConfirmation, context, + currentAccountInCollection, + openRevokeConfirmation, ownerAccount, openReportModal, ]); diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx index 4f21e7d2672826..8db00e73d34813 100644 --- a/app/javascript/mastodon/features/collections/detail/index.tsx +++ b/app/javascript/mastodon/features/collections/detail/index.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { useLocation, useParams } from 'react-router'; +import { useHistory, useLocation, useParams } from 'react-router'; import { openModal } from '@/mastodon/actions/modal'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; @@ -24,7 +24,7 @@ import { me } from 'mastodon/initial_state'; import { fetchCollection } from 'mastodon/reducers/slices/collections'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { CollectionAccountsList } from './collection_list'; +import { CollectionAccountsList } from './accounts_list'; import { CollectionMetaData } from './collection_list_item'; import { CollectionMenu } from './collection_menu'; import classes from './styles.module.scss'; @@ -84,6 +84,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ const intl = useIntl(); const { name, description, tag, account_id } = collection; const dispatch = useAppDispatch(); + const history = useHistory(); const handleShare = useCallback(() => { dispatch( @@ -97,12 +98,14 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ }, [collection, dispatch]); const location = useLocation<{ newCollection?: boolean } | undefined>(); - const wasJustCreated = location.state?.newCollection; + const isNewCollection = location.state?.newCollection; useEffect(() => { - if (wasJustCreated) { + if (isNewCollection) { + // Replace with current pathname to clear `newCollection` state + history.replace(location.pathname); handleShare(); } - }, [handleShare, wasJustCreated]); + }, [history, handleShare, isNewCollection, location.pathname]); return (
    diff --git a/app/javascript/mastodon/features/collections/detail/revoke_collection_inclusion_modal.tsx b/app/javascript/mastodon/features/collections/detail/revoke_collection_inclusion_modal.tsx new file mode 100644 index 00000000000000..c2c2bafe9dd2a5 --- /dev/null +++ b/app/javascript/mastodon/features/collections/detail/revoke_collection_inclusion_modal.tsx @@ -0,0 +1,82 @@ +import { useCallback } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { showAlert } from 'mastodon/actions/alerts'; +import type { BaseConfirmationModalProps } from 'mastodon/features/ui/components/confirmation_modals/confirmation_modal'; +import { ConfirmationModal } from 'mastodon/features/ui/components/confirmation_modals/confirmation_modal'; +import { revokeCollectionInclusion } from 'mastodon/reducers/slices/collections'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + revokeCollectionInclusionTitle: { + id: 'confirmations.revoke_collection_inclusion.title', + defaultMessage: 'Remove yourself from this collection?', + }, + revokeCollectionInclusionMessage: { + id: 'confirmations.revoke_collection_inclusion.message', + defaultMessage: + "This action is permanent, and the curator won't be able to re-add you to the collection later on.", + }, + revokeCollectionInclusionConfirm: { + id: 'confirmations.revoke_collection_inclusion.confirm', + defaultMessage: 'Remove me', + }, +}); + +export const RevokeCollectionInclusionModal: React.FC< + { + collectionId: string; + collectionItemId: string; + } & BaseConfirmationModalProps +> = ({ collectionId, collectionItemId, onClose }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const collectionName = useAppSelector( + (state) => state.collections.collections[collectionId]?.name, + ); + + const onConfirm = useCallback(async () => { + try { + await dispatch( + revokeCollectionInclusion({ + collectionId, + itemId: collectionItemId, + }), + ).unwrap(); + + dispatch( + showAlert({ + message: intl.formatMessage( + { + id: 'collections.revoke_inclusion.confirmation', + defaultMessage: 'You\'ve been removed from "{collection}"', + }, + { + collection: collectionName, + }, + ), + }), + ); + } catch { + dispatch( + showAlert({ + message: intl.formatMessage({ + id: 'collections.revoke_inclusion.error', + defaultMessage: 'There was an error, please try again later.', + }), + }), + ); + } + }, [dispatch, collectionId, collectionName, collectionItemId, intl]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/collections/detail/share_modal.tsx b/app/javascript/mastodon/features/collections/detail/share_modal.tsx index 0f4681d07762a3..26bab6abe00c88 100644 --- a/app/javascript/mastodon/features/collections/detail/share_modal.tsx +++ b/app/javascript/mastodon/features/collections/detail/share_modal.tsx @@ -64,7 +64,7 @@ export const CollectionShareModal: React.FC<{ onClose(); dispatch(changeCompose(shareMessage)); dispatch(focusCompose()); - }, [collectionLink, dispatch, intl, isOwnCollection, onClose]); + }, [onClose, collectionLink, dispatch, intl, isOwnCollection]); return ( diff --git a/app/javascript/mastodon/features/collections/detail/styles.module.scss b/app/javascript/mastodon/features/collections/detail/styles.module.scss index ad084eaed6fff1..786c0e7000b98e 100644 --- a/app/javascript/mastodon/features/collections/detail/styles.module.scss +++ b/app/javascript/mastodon/features/collections/detail/styles.module.scss @@ -103,3 +103,24 @@ line-height: 1.5; cursor: default; } + +.revokeControlWrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: -10px; + padding-bottom: 16px; + padding-inline: calc(26px + var(--avatar-width)) 16px; + + :global(.button) { + min-width: 30%; + white-space: normal; + } + + --avatar-width: 46px; + + @container (width < 360px) { + --avatar-width: 35px; + } +} diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx index 47af9e211c0e62..a6942193d07643 100644 --- a/app/javascript/mastodon/features/collections/editor/accounts.tsx +++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx @@ -2,7 +2,7 @@ import { useCallback, useId, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import CancelIcon from '@/material-icons/400-24px/cancel.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; @@ -25,17 +25,17 @@ import { ItemList, Scrollable, } from 'mastodon/components/scrollable_list/components'; -import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts'; import { useAccount } from 'mastodon/hooks/useAccount'; +import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts'; import { me } from 'mastodon/initial_state'; import { addCollectionItem, + getCollectionItemIds, removeCollectionItem, + updateCollectionEditorField, } from 'mastodon/reducers/slices/collections'; import { store, useAppDispatch, useAppSelector } from 'mastodon/store'; -import type { TempCollectionState } from './state'; -import { getCollectionEditorState } from './state'; import classes from './styles.module.scss'; import { WizardStepHeader } from './wizard_step_header'; @@ -52,9 +52,8 @@ function isOlderThanAWeek(date?: string): boolean { const AddedAccountItem: React.FC<{ accountId: string; - isRemovable: boolean; onRemove: (id: string) => void; -}> = ({ accountId, isRemovable, onRemove }) => { +}> = ({ accountId, onRemove }) => { const intl = useIntl(); const account = useAccount(accountId); @@ -86,17 +85,15 @@ const AddedAccountItem: React.FC<{ id={accountId} extraAccountInfo={lastPostHint} > - {isRemovable && ( - - )} + ); }; @@ -139,28 +136,25 @@ export const CollectionAccounts: React.FC<{ const intl = useIntl(); const dispatch = useAppDispatch(); const history = useHistory(); - const location = useLocation(); - const { id, initialItemIds } = getCollectionEditorState( - collection, - location.state, - ); + + const { id, items } = collection ?? {}; const isEditMode = !!id; - const collectionItems = collection?.items; + const collectionItems = items; - const [searchValue, setSearchValue] = useState(''); - // This state is only used when creating a new collection. - // In edit mode, the collection will be updated instantly - const [addedAccountIds, setAccountIds] = useState(initialItemIds); + const addedAccountIds = useAppSelector( + (state) => state.collections.editor.accountIds, + ); + + // In edit mode, we're bypassing state and just return collection items directly, + // since they're edited "live", saving after each addition/deletion const accountIds = useMemo( () => - isEditMode - ? (collectionItems - ?.map((item) => item.account_id) - .filter((id): id is string => !!id) ?? []) - : addedAccountIds, + isEditMode ? getCollectionItemIds(collectionItems) : addedAccountIds, [isEditMode, collectionItems, addedAccountIds], ); + const [searchValue, setSearchValue] = useState(''); + const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT; const { @@ -233,28 +227,41 @@ export const CollectionAccounts: React.FC<{ [dispatch, relationships], ); - const removeAccountItem = useCallback((accountId: string) => { - setAccountIds((ids) => ids.filter((id) => id !== accountId)); - }, []); + const removeAccountItem = useCallback( + (accountId: string) => { + dispatch( + updateCollectionEditorField({ + field: 'accountIds', + value: accountIds.filter((id) => id !== accountId), + }), + ); + }, + [accountIds, dispatch], + ); const addAccountItem = useCallback( (accountId: string) => { confirmFollowStatus(accountId, () => { - setAccountIds((ids) => [...ids, accountId]); + dispatch( + updateCollectionEditorField({ + field: 'accountIds', + value: [...accountIds, accountId], + }), + ); }); }, - [confirmFollowStatus], + [accountIds, confirmFollowStatus, dispatch], ); const toggleAccountItem = useCallback( (item: SuggestionItem) => { - if (addedAccountIds.includes(item.id)) { + if (accountIds.includes(item.id)) { removeAccountItem(item.id); } else { addAccountItem(item.id); } }, - [addAccountItem, addedAccountIds, removeAccountItem], + [accountIds, addAccountItem, removeAccountItem], ); const instantRemoveAccountItem = useCallback( @@ -367,6 +374,7 @@ export const CollectionAccounts: React.FC<{ onSelectItem={ isEditMode ? instantToggleAccountItem : toggleAccountItem } + closeOnSelect={false} /> {hasMaxAccounts && ( diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx index 6234bca5142713..73847a2b20912e 100644 --- a/app/javascript/mastodon/features/collections/editor/details.tsx +++ b/app/javascript/mastodon/features/collections/editor/details.tsx @@ -1,92 +1,96 @@ -import { useCallback, useState } from 'react'; +import { Fragment, useCallback, useMemo, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { isFulfilled } from '@reduxjs/toolkit'; +import { languages } from '@/mastodon/initial_state'; +import { + hasSpecialCharacters, + inputToHashtag, + trimHashFromStart, +} from '@/mastodon/utils/hashtags'; import type { - ApiCollectionJSON, ApiCreateCollectionPayload, ApiUpdateCollectionPayload, } from 'mastodon/api_types/collections'; import { Button } from 'mastodon/components/button'; import { CheckboxField, + ComboboxField, Fieldset, FormStack, RadioButtonField, + SelectField, TextAreaField, } from 'mastodon/components/form_fields'; import { TextInputField } from 'mastodon/components/form_fields/text_input_field'; +import { useSearchTags } from 'mastodon/hooks/useSearchTags'; +import type { TagSearchResult } from 'mastodon/hooks/useSearchTags'; import { createCollection, updateCollection, + updateCollectionEditorField, } from 'mastodon/reducers/slices/collections'; -import { useAppDispatch } from 'mastodon/store'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import type { TempCollectionState } from './state'; -import { getCollectionEditorState } from './state'; import classes from './styles.module.scss'; import { WizardStepHeader } from './wizard_step_header'; -export const CollectionDetails: React.FC<{ - collection?: ApiCollectionJSON | null; -}> = ({ collection }) => { +export const CollectionDetails: React.FC = () => { const dispatch = useAppDispatch(); const history = useHistory(); - const location = useLocation(); - - const { - id, - initialName, - initialDescription, - initialTopic, - initialItemIds, - initialDiscoverable, - initialSensitive, - } = getCollectionEditorState(collection, location.state); - - const [name, setName] = useState(initialName); - const [description, setDescription] = useState(initialDescription); - const [topic, setTopic] = useState(initialTopic); - const [discoverable, setDiscoverable] = useState(initialDiscoverable); - const [sensitive, setSensitive] = useState(initialSensitive); + const { id, name, description, topic, discoverable, sensitive, accountIds } = + useAppSelector((state) => state.collections.editor); const handleNameChange = useCallback( (event: React.ChangeEvent) => { - setName(event.target.value); + dispatch( + updateCollectionEditorField({ + field: 'name', + value: event.target.value, + }), + ); }, - [], + [dispatch], ); const handleDescriptionChange = useCallback( (event: React.ChangeEvent) => { - setDescription(event.target.value); - }, - [], - ); - - const handleTopicChange = useCallback( - (event: React.ChangeEvent) => { - setTopic(event.target.value); + dispatch( + updateCollectionEditorField({ + field: 'description', + value: event.target.value, + }), + ); }, - [], + [dispatch], ); const handleDiscoverableChange = useCallback( (event: React.ChangeEvent) => { - setDiscoverable(event.target.value === 'public'); + dispatch( + updateCollectionEditorField({ + field: 'discoverable', + value: event.target.value === 'public', + }), + ); }, - [], + [dispatch], ); const handleSensitiveChange = useCallback( (event: React.ChangeEvent) => { - setSensitive(event.target.checked); + dispatch( + updateCollectionEditorField({ + field: 'sensitive', + value: event.target.checked, + }), + ); }, - [], + [dispatch], ); const handleSubmit = useCallback( @@ -112,7 +116,7 @@ export const CollectionDetails: React.FC<{ description, discoverable, sensitive, - account_ids: initialItemIds, + account_ids: accountIds, }; if (topic) { payload.tag_name = topic; @@ -124,9 +128,7 @@ export const CollectionDetails: React.FC<{ }), ).then((result) => { if (isFulfilled(result)) { - history.replace( - `/collections/${result.payload.collection.id}/edit/details`, - ); + history.replace(`/collections`); history.push(`/collections/${result.payload.collection.id}`, { newCollection: true, }); @@ -143,7 +145,7 @@ export const CollectionDetails: React.FC<{ sensitive, dispatch, history, - initialItemIds, + accountIds, ], ); @@ -199,24 +201,9 @@ export const CollectionDetails: React.FC<{ maxLength={100} /> - - } - hint={ - - } - value={topic} - onChange={handleTopicChange} - maxLength={40} - /> + + +
    ); }; + +const TopicField: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { id, topic } = useAppSelector((state) => state.collections.editor); + + const collection = useAppSelector((state) => + id ? state.collections.collections[id] : undefined, + ); + const [isInitialValue, setIsInitialValue] = useState( + () => trimHashFromStart(topic) === (collection?.tag?.name ?? ''), + ); + + const { tags, isLoading, searchTags } = useSearchTags({ + query: topic, + }); + + const handleTopicChange = useCallback( + (event: React.ChangeEvent) => { + setIsInitialValue(false); + dispatch( + updateCollectionEditorField({ + field: 'topic', + value: inputToHashtag(event.target.value), + }), + ); + searchTags(event.target.value); + }, + [dispatch, searchTags], + ); + + const handleSelectTopicSuggestion = useCallback( + (item: TagSearchResult) => { + dispatch( + updateCollectionEditorField({ + field: 'topic', + value: inputToHashtag(item.name), + }), + ); + }, + [dispatch], + ); + + const topicHasSpecialCharacters = useMemo( + () => hasSpecialCharacters(topic), + [topic], + ); + + return ( + + } + hint={ + + } + value={topic} + items={tags} + isLoading={isLoading} + renderItem={renderTagItem} + onSelectItem={handleSelectTopicSuggestion} + onChange={handleTopicChange} + autoCapitalize='off' + autoCorrect='off' + spellCheck='false' + maxLength={40} + status={ + topicHasSpecialCharacters + ? { + variant: 'warning', + message: intl.formatMessage({ + id: 'collections.topic_special_chars_hint', + defaultMessage: + 'Special characters will be removed when saving', + }), + } + : undefined + } + suppressMenu={isInitialValue} + /> + ); +}; + +const renderTagItem = (item: TagSearchResult) => item.label ?? `#${item.name}`; + +const LanguageField: React.FC = () => { + const dispatch = useAppDispatch(); + const initialLanguage = useAppSelector( + (state) => state.compose.get('default_language') as string, + ); + const { language } = useAppSelector((state) => state.collections.editor); + + const selectedLanguage = language ?? initialLanguage; + + const handleLanguageChange = useCallback( + (event: React.ChangeEvent) => { + dispatch( + updateCollectionEditorField({ + field: 'language', + value: event.target.value, + }), + ); + }, + [dispatch], + ); + + return ( + + } + value={selectedLanguage} + onChange={handleLanguageChange} + > + + {languages?.map(([code, name, localName]) => ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/collections/editor/index.tsx b/app/javascript/mastodon/features/collections/editor/index.tsx index 2200bccb1751b8..ff1549b9423db3 100644 --- a/app/javascript/mastodon/features/collections/editor/index.tsx +++ b/app/javascript/mastodon/features/collections/editor/index.tsx @@ -16,7 +16,10 @@ import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import { Column } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import { fetchCollection } from 'mastodon/reducers/slices/collections'; +import { + collectionEditorActions, + fetchCollection, +} from 'mastodon/reducers/slices/collections'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { CollectionAccounts } from './accounts'; @@ -68,6 +71,7 @@ export const CollectionEditorPage: React.FC<{ const collection = useAppSelector((state) => id ? state.collections.collections[id] : undefined, ); + const editorStateId = useAppSelector((state) => state.collections.editor.id); const isEditMode = !!id; const isLoading = isEditMode && !collection; @@ -77,6 +81,18 @@ export const CollectionEditorPage: React.FC<{ } }, [dispatch, id]); + useEffect(() => { + if (id !== editorStateId) { + void dispatch(collectionEditorActions.reset()); + } + }, [dispatch, editorStateId, id]); + + useEffect(() => { + if (collection) { + void dispatch(collectionEditorActions.init(collection)); + } + }, [dispatch, collection]); + const pageTitle = intl.formatMessage(usePageTitle(id)); return ( @@ -104,7 +120,7 @@ export const CollectionEditorPage: React.FC<{ exact path={`${path}/details`} // eslint-disable-next-line react/jsx-no-bind - render={() => } + render={() => } /> )} diff --git a/app/javascript/mastodon/features/collections/editor/state.ts b/app/javascript/mastodon/features/collections/editor/state.ts deleted file mode 100644 index abac0b94b54c90..00000000000000 --- a/app/javascript/mastodon/features/collections/editor/state.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { - ApiCollectionJSON, - ApiCreateCollectionPayload, -} from '@/mastodon/api_types/collections'; - -/** - * Temporary editor state across creation steps, - * kept in location state - */ -export type TempCollectionState = - | Partial - | undefined; - -/** - * Resolve initial editor state. Temporary location state - * trumps stored data, otherwise initial values are returned. - */ -export function getCollectionEditorState( - collection: ApiCollectionJSON | null | undefined, - locationState: TempCollectionState, -) { - const { - id, - name = '', - description = '', - tag, - language = '', - discoverable = true, - sensitive = false, - items, - } = collection ?? {}; - - const collectionItemIds = - items?.map((item) => item.account_id).filter(onlyExistingIds) ?? []; - - const initialItemIds = ( - locationState?.account_ids ?? collectionItemIds - ).filter(onlyExistingIds); - - return { - id, - initialItemIds, - initialName: locationState?.name ?? name, - initialDescription: locationState?.description ?? description, - initialTopic: locationState?.tag_name ?? tag?.name ?? '', - initialLanguage: locationState?.language ?? language, - initialDiscoverable: locationState?.discoverable ?? discoverable, - initialSensitive: locationState?.sensitive ?? sensitive, - }; -} - -const onlyExistingIds = (id?: string): id is string => !!id; diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts index 5602577dbe9ace..ce8eea09abd776 100644 --- a/app/javascript/mastodon/features/emoji/worker.ts +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -1,9 +1,4 @@ import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants'; -import { - importCustomEmojiData, - importEmojiData, - importLegacyShortcodes, -} from './loader'; addEventListener('message', handleMessage); self.postMessage('ready'); // After the worker is ready, notify the main thread @@ -16,6 +11,8 @@ function handleMessage(event: MessageEvent<{ locale: string }>) { } async function loadData(locale: string) { + const { importCustomEmojiData, importEmojiData, importLegacyShortcodes } = + await import('./loader'); let importCount: number | undefined; if (locale === EMOJI_TYPE_CUSTOM) { importCount = (await importCustomEmojiData())?.length; diff --git a/app/javascript/mastodon/features/followers/components/header.tsx b/app/javascript/mastodon/features/followers/components/header.tsx new file mode 100644 index 00000000000000..2733e1d0f4e196 --- /dev/null +++ b/app/javascript/mastodon/features/followers/components/header.tsx @@ -0,0 +1,36 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; +import type { MessageDescriptor } from 'react-intl'; + +import { DisplayNameSimple } from '@/mastodon/components/display_name/simple'; +import { useAccount } from '@/mastodon/hooks/useAccount'; + +import classes from '../styles.module.scss'; + +export const AccountListHeader: FC<{ + accountId: string; + total?: number; + titleText: MessageDescriptor; +}> = ({ accountId, total, titleText }) => { + const intl = useIntl(); + const account = useAccount(accountId); + return ( + <> +

    + {intl.formatMessage(titleText, { + name: , + })} +

    + {!!total && ( +

    + +

    + )} + + ); +}; diff --git a/app/javascript/mastodon/features/followers/components/list.tsx b/app/javascript/mastodon/features/followers/components/list.tsx index 00860b9d0713d3..0cb36d0e4c0cb8 100644 --- a/app/javascript/mastodon/features/followers/components/list.tsx +++ b/app/javascript/mastodon/features/followers/components/list.tsx @@ -12,8 +12,6 @@ import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility'; import { useLayout } from '@/mastodon/hooks/useLayout'; import { isHideItem, me } from 'mastodon/initial_state'; -import { AccountHeader } from '../../account_timeline/components/account_header'; - import { RemoteHint } from './remote'; export interface AccountList { @@ -26,6 +24,7 @@ interface AccountListProps { accountId?: string | null; append?: ReactNode; emptyMessage: ReactNode; + header?: ReactNode; footer?: ReactNode; list?: AccountList | null; loadMore: () => void; @@ -37,6 +36,7 @@ export const AccountList: FC = ({ accountId, append, emptyMessage, + header, footer, list, loadMore, @@ -93,7 +93,7 @@ export const AccountList: FC = ({ hasMore={!forceEmptyState && list?.hasMore} isLoading={list?.isLoading ?? true} onLoadMore={loadMore} - prepend={} + prepend={header} alwaysPrepend append={append ?? } emptyMessage={emptyMessage} diff --git a/app/javascript/mastodon/features/followers/index.tsx b/app/javascript/mastodon/features/followers/index.tsx index 15dcbb5a69e542..bba2f4cb083348 100644 --- a/app/javascript/mastodon/features/followers/index.tsx +++ b/app/javascript/mastodon/features/followers/index.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import type { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessage, FormattedMessage } from 'react-intl'; import { useDebouncedCallback } from 'use-debounce'; @@ -14,8 +14,14 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import type { EmptyMessageProps } from './components/empty'; import { BaseEmptyMessage } from './components/empty'; +import { AccountListHeader } from './components/header'; import { AccountList } from './components/list'; +const titleText = defineMessage({ + id: 'followers.title', + defaultMessage: 'Following {name}', +}); + const Followers: FC = () => { const accountId = useAccountId(); const account = useAccount(accountId); @@ -64,6 +70,15 @@ const Followers: FC = () => { return ( + ) + } footer={footer} emptyMessage={} list={followerList} diff --git a/app/javascript/mastodon/features/followers/styles.module.scss b/app/javascript/mastodon/features/followers/styles.module.scss new file mode 100644 index 00000000000000..f58b345becbe40 --- /dev/null +++ b/app/javascript/mastodon/features/followers/styles.module.scss @@ -0,0 +1,11 @@ +.title { + font-size: 20px; + font-weight: 600; + margin: 20px 16px 10px; +} + +.subtitle { + font-size: 14px; + color: var(--color-text-secondary); + margin: 10px 16px; +} diff --git a/app/javascript/mastodon/features/following/index.tsx b/app/javascript/mastodon/features/following/index.tsx index 85894c18af4f3a..6bc7abda69aaa6 100644 --- a/app/javascript/mastodon/features/following/index.tsx +++ b/app/javascript/mastodon/features/following/index.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import type { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessage, FormattedMessage } from 'react-intl'; import { useDebouncedCallback } from 'use-debounce'; @@ -14,10 +14,16 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import type { EmptyMessageProps } from '../followers/components/empty'; import { BaseEmptyMessage } from '../followers/components/empty'; +import { AccountListHeader } from '../followers/components/header'; import { AccountList } from '../followers/components/list'; import { RemoteHint } from './components/remote'; +const titleText = defineMessage({ + id: 'following.title', + defaultMessage: 'Followed by {name}', +}); + const Followers: FC = () => { const accountId = useAccountId(); const account = useAccount(accountId); @@ -69,6 +75,15 @@ const Followers: FC = () => { accountId={accountId} append={domain && } emptyMessage={} + header={ + accountId && ( + + ) + } footer={footer} list={followingList} loadMore={loadMore} diff --git a/app/javascript/mastodon/features/interaction_modal/index.tsx b/app/javascript/mastodon/features/interaction_modal/index.tsx index 03cfc2c484165e..624ecd5613f8c4 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.tsx +++ b/app/javascript/mastodon/features/interaction_modal/index.tsx @@ -25,6 +25,8 @@ const messages = defineMessages({ }, }); +type InteractionIntent = 'follow' | 'reblog' | 'favourite' | 'reply' | 'vote'; + interface LoginFormMessage { type: | 'fetchInteractionURL' @@ -32,6 +34,8 @@ interface LoginFormMessage { | 'fetchInteractionURL-success'; uri_or_domain: string; template?: string; + param?: string; + intent?: InteractionIntent; } const PERSISTENCE_KEY = 'mastodon_home'; @@ -110,7 +114,11 @@ const isValueValid = (value: string) => { } }; -const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { +const sendToFrame = ( + frame: HTMLIFrameElement | null, + value: string, + intent: string, +): void => { if (valueToDomain(value.trim()) === localDomain) { window.location.href = '/auth/sign_in'; return; @@ -120,6 +128,7 @@ const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { { type: 'fetchInteractionURL', uri_or_domain: value.trim(), + intent, }, window.origin, ); @@ -127,7 +136,8 @@ const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { const LoginForm: React.FC<{ resourceUrl: string; -}> = ({ resourceUrl }) => { + intent: string; +}> = ({ resourceUrl, intent }) => { const intl = useIntl(); const [value, setValue] = useState( localStorage.getItem(PERSISTENCE_KEY) ?? '', @@ -161,7 +171,7 @@ const LoginForm: React.FC<{ try { const url = new URL( event.data.template.replace( - '{uri}', + `{${event.data.param}}`, encodeURIComponent(resourceUrl), ), ); @@ -242,8 +252,8 @@ const LoginForm: React.FC<{ const handleSubmit = useCallback(() => { setIsSubmitting(true); - sendToFrame(iframeRef.current, value); - }, [setIsSubmitting, value]); + sendToFrame(iframeRef.current, value, intent); + }, [setIsSubmitting, value, intent]); const handleFocus = useCallback(() => { setExpanded(true); @@ -287,7 +297,7 @@ const LoginForm: React.FC<{ setError(false); setValue(selectedOptionValue); setIsSubmitting(true); - sendToFrame(iframeRef.current, selectedOptionValue); + sendToFrame(iframeRef.current, selectedOptionValue, intent); } break; @@ -300,6 +310,7 @@ const LoginForm: React.FC<{ setValue, selectedOption, options, + intent, ], ); @@ -318,9 +329,9 @@ const LoginForm: React.FC<{ setValue(option); setError(false); setIsSubmitting(true); - sendToFrame(iframeRef.current, option); + sendToFrame(iframeRef.current, option, intent); }, - [options, setSelectedOption, setValue, setError], + [options, setSelectedOption, setValue, setError, intent], ); const domain = (valueToDomain(value) ?? '').trim(); @@ -404,7 +415,8 @@ const LoginForm: React.FC<{ const InteractionModal: React.FC<{ accountId: string; url: string; -}> = ({ accountId, url }) => { + intent: string; +}> = ({ accountId, url, intent }) => { const dispatch = useAppDispatch(); const signupUrl = useAppSelector( (state) => @@ -479,7 +491,7 @@ const InteractionModal: React.FC<{

    - +

    (null); const firstStatusId = useAppSelector((state) => decodedUrl - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) + ? (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) : undefined, ); const story = useAppSelector((state) => diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index 1bff4dc128e630..d7db8eb14909e1 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -28,11 +28,10 @@ import { DisplayName } from 'mastodon/components/display_name'; import ScrollableList from 'mastodon/components/scrollable_list'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; +import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { useSearchAccounts } from './use_search_accounts'; - export const messages = defineMessages({ manageMembers: { id: 'column.list_members', diff --git a/app/javascript/mastodon/features/notifications/components/report.jsx b/app/javascript/mastodon/features/notifications/components/report.jsx index ed043ae7893bf2..bc3631c86eac4a 100644 --- a/app/javascript/mastodon/features/notifications/components/report.jsx +++ b/app/javascript/mastodon/features/notifications/components/report.jsx @@ -49,7 +49,7 @@ class Report extends ImmutablePureComponent {

    - · + ·
    {intl.formatMessage(messages[report.get('category')])}
    diff --git a/app/javascript/mastodon/features/onboarding/follows.tsx b/app/javascript/mastodon/features/onboarding/follows.tsx index d30834d0b6a87b..d53f1ea50ddaa2 100644 --- a/app/javascript/mastodon/features/onboarding/follows.tsx +++ b/app/javascript/mastodon/features/onboarding/follows.tsx @@ -121,7 +121,6 @@ export const Follows: React.FC<{ icon='person' iconComponent={PersonIcon} multiColumn={multiColumn} - showBackButton /> 0 &&
    }
    - +
    diff --git a/app/javascript/mastodon/features/onboarding/profile.tsx b/app/javascript/mastodon/features/onboarding/profile.tsx index 3f1168abe5c1f9..fe8efba695af9f 100644 --- a/app/javascript/mastodon/features/onboarding/profile.tsx +++ b/app/javascript/mastodon/features/onboarding/profile.tsx @@ -133,7 +133,7 @@ export const Profile: React.FC<{ }), ) .then(() => { - history.push('/start/follows'); + history.push('/home'); dispatch(closeOnboarding()); return ''; }) @@ -160,6 +160,7 @@ export const Profile: React.FC<{ icon='person' iconComponent={PersonIcon} multiColumn={multiColumn} + showBackButton />
    @@ -229,7 +230,7 @@ export const Profile: React.FC<{ } value={displayName} onChange={handleDisplayNameChange} - hasError={!!errors?.display_name} + status={errors?.display_name ? 'error' : undefined} id='display_name' />
    @@ -251,7 +252,7 @@ export const Profile: React.FC<{ } value={note} onChange={handleNoteChange} - hasError={!!errors?.note} + status={errors?.note ? 'error' : undefined} id='note' />
    @@ -297,8 +298,8 @@ export const Profile: React.FC<{ ) : ( )} diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx index e848e6929950a4..83e6820ac82192 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx @@ -86,6 +86,7 @@ export const Footer: React.FC<{ openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reply', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -106,6 +107,7 @@ export const Footer: React.FC<{ openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'favourite', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 772e747e714627..b96622911979fa 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -165,24 +165,11 @@ class Status extends ImmutablePureComponent { newRepliesIds: [], }; - UNSAFE_componentWillMount () { + componentDidMount() { this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true })); - } - - componentDidMount () { attachFullscreenListener(this.onFullScreenChange); } - UNSAFE_componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchStatus(nextProps.params.statusId, { forceFetch: true })); - } - - if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) { - this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') }); - } - } - handleToggleMediaVisibility = () => { this.setState({ showMedia: !this.state.showMedia }); }; @@ -197,6 +184,7 @@ class Status extends ImmutablePureComponent { dispatch(openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'favourite', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -249,6 +237,7 @@ class Status extends ImmutablePureComponent { dispatch(openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reply', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -266,6 +255,7 @@ class Status extends ImmutablePureComponent { dispatch(openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -311,7 +301,12 @@ class Status extends ImmutablePureComponent { const { dispatch, history } = this.props; const handleDeleteSuccess = () => { - history.push('/'); + history.push('/', { + // Preventing the default "scroll to right" on + // location change in advanced UI to avoid conflict + // with the composer being focused + preventMultiColumnAutoScroll: true + }); }; if (!deleteModal) { @@ -325,13 +320,13 @@ class Status extends ImmutablePureComponent { // Error handling - could show error message }); } else { - dispatch(openModal({ - modalType: 'CONFIRM_DELETE_STATUS', - modalProps: { - statusId: status.get('id'), + dispatch(openModal({ + modalType: 'CONFIRM_DELETE_STATUS', + modalProps: { + statusId: status.get('id'), withRedraft, onDeleteSuccess: handleDeleteSuccess - } + } })); } }; @@ -541,19 +536,27 @@ class Status extends ImmutablePureComponent { this.statusNode = c; }; - componentDidUpdate (prevProps) { - const { status, descendantsIds } = this.props; + componentDidUpdate(prevProps) { + const { status, descendantsIds, params } = this.props; const isSameStatus = status && (prevProps.status?.get('id') === status.get('id')); // Only highlight replies after the initial load if (prevProps.descendantsIds.length && isSameStatus) { const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); - + if (newRepliesIds.length) { this.setState({newRepliesIds}); } } + + if (params.statusId && prevProps.params.statusId !== params.statusId) { + this.props.dispatch(fetchStatus(params.statusId, { forceFetch: true })); + } + + if (status && status.get('id') !== this.state.loadedStatusId) { + this.setState({ showMedia: defaultMediaVisibility(this.props.status), loadedStatusId: status.get('id') }); + } } componentWillUnmount () { @@ -694,7 +697,7 @@ class Status extends ImmutablePureComponent { {descendants} - + ; + const formattedDate = ; const formattedName = ; const label = currentVersion.get('original') ? ( diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx index 19898fb57d0fbc..b0397f4d7be8f1 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx @@ -21,7 +21,7 @@ interface ConfirmationModalProps { cancel?: React.ReactNode; secondary?: React.ReactNode; onSecondary?: () => void; - onConfirm: () => void; + onConfirm: () => void | Promise; noCloseOnConfirm?: boolean; extraContent?: React.ReactNode; children?: React.ReactNode; @@ -56,7 +56,7 @@ export const ConfirmationModal: React.FC< onClose(); } - onConfirm(); + void onConfirm(); }, [onClose, onConfirm, noCloseOnConfirm]); const handleSecondary = useCallback(() => { diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 24c5cc89f8beae..e4d2bc4bd899fb 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -89,6 +89,7 @@ export const MODAL_COMPONENTS = { 'REPORT': ReportModal, 'REPORT_COLLECTION': ReportCollectionModal, 'SHARE_COLLECTION': () => import('@/mastodon/features/collections/detail/share_modal').then(module => ({ default: module.CollectionShareModal })), + 'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, 'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }), @@ -113,6 +114,9 @@ export const MODAL_COMPONENTS = { 'ACCOUNT_EDIT_FIELD_EDIT': accountEditModal('EditFieldModal'), 'ACCOUNT_EDIT_FIELD_DELETE': accountEditModal('DeleteFieldModal'), 'ACCOUNT_EDIT_FIELDS_REORDER': accountEditModal('ReorderFieldsModal'), + 'ACCOUNT_EDIT_IMAGE_ALT': accountEditModal('ImageAltModal'), + 'ACCOUNT_EDIT_IMAGE_DELETE': accountEditModal('ImageDeleteModal'), + 'ACCOUNT_EDIT_IMAGE_UPLOAD': accountEditModal('ImageUploadModal'), }; /** @arg {keyof import('@/mastodon/features/account_edit/modals')} type */ diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index fc1f35ee437400..6fcab6d06643bf 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -23,7 +23,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; -import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; @@ -121,7 +121,11 @@ const mapStateToProps = state => ({ layout: state.getIn(['meta', 'layout']), isComposing: state.getIn(['compose', 'is_composing']), hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null, - canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), + canUploadMore: + !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) + && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), + isUploadEnabled: + state.getIn(['compose', 'isDragDisabled']) !== true, firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0, username: state.getIn(['accounts', me, 'username']), @@ -154,7 +158,9 @@ class SwitchingColumnsArea extends PureComponent { } handleChildrenContentChange() { - if (!this.props.singleColumn) { + const {preventMultiColumnAutoScroll} = this.props.location.state ?? {}; + + if (!this.props.singleColumn && !preventMultiColumnAutoScroll) { const isRtlLayout = document.getElementsByTagName('body')[0] ?.classList.contains('rtl'); const modifier = isRtlLayout ? -1 : 1; @@ -174,28 +180,27 @@ class SwitchingColumnsArea extends PureComponent { const { signedIn } = this.props.identity; const pathName = this.props.location.pathname; - let redirect; - + let rootRedirect; if (signedIn) { if (forceOnboarding) { - redirect = ; + rootRedirect = '/start'; } else if (singleColumn) { - redirect = ; + rootRedirect = '/home'; } else { - redirect = ; + rootRedirect = '/deck/getting-started'; } } else if (singleUserMode && owner && initialState?.accounts[owner]) { - redirect = ; + rootRedirect = `/@${initialState.accounts[owner].username}`; } else if (trendsEnabled && landingPage === 'trends') { - redirect = ; + rootRedirect = '/explore'; } else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') { - redirect = ; + rootRedirect = '/public/local'; } else { - redirect = ; + rootRedirect = '/about'; } const profileRedesignRoutes = []; - if (isClientFeatureEnabled('profile_editing')) { + if (isServerFeatureEnabled('profile_redesign')) { profileRedesignRoutes.push( , @@ -212,7 +217,7 @@ class SwitchingColumnsArea extends PureComponent { - {redirect} + {singleColumn ? : null} {singleColumn && pathName.startsWith('/deck/') ? : null} @@ -264,8 +269,8 @@ class SwitchingColumnsArea extends PureComponent { - - + + @@ -366,6 +371,9 @@ class UI extends PureComponent { }; handleDragEnter = (e) => { + if (!this.props.isUploadEnabled) { + return; + } e.preventDefault(); if (!this.dragTargets) { @@ -382,6 +390,9 @@ class UI extends PureComponent { }; handleDragOver = (e) => { + if (!this.props.isUploadEnabled) { + return; + } if (this.dataTransferIsText(e.dataTransfer)) return false; e.preventDefault(); @@ -397,6 +408,9 @@ class UI extends PureComponent { }; handleDrop = (e) => { + if (!this.props.isUploadEnabled) { + return; + } if (this.dataTransferIsText(e.dataTransfer)) return; e.preventDefault(); @@ -471,7 +485,6 @@ class UI extends PureComponent { document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); document.addEventListener('dragleave', this.handleDragLeave, false); - document.addEventListener('dragend', this.handleDragEnd, false); if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); @@ -498,7 +511,6 @@ class UI extends PureComponent { document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); document.removeEventListener('dragleave', this.handleDragLeave); - document.removeEventListener('dragend', this.handleDragEnd); } setRef = c => { diff --git a/app/javascript/mastodon/features/lists/use_search_accounts.ts b/app/javascript/mastodon/hooks/useSearchAccounts.ts similarity index 100% rename from app/javascript/mastodon/features/lists/use_search_accounts.ts rename to app/javascript/mastodon/hooks/useSearchAccounts.ts diff --git a/app/javascript/mastodon/hooks/useSearchTags.ts b/app/javascript/mastodon/hooks/useSearchTags.ts new file mode 100644 index 00000000000000..2f029b07e836ca --- /dev/null +++ b/app/javascript/mastodon/hooks/useSearchTags.ts @@ -0,0 +1,121 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { useDebouncedCallback } from 'use-debounce'; + +import { apiGetSearch } from 'mastodon/api/search'; +import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; +import { trimHashFromStart } from 'mastodon/utils/hashtags'; + +export type TagSearchResult = Omit & { + label?: string; +}; + +const messages = defineMessages({ + addTag: { + id: 'account_edit_tags.add_tag', + defaultMessage: 'Add #{tagName}', + }, +}); + +const fetchSearchHashtags = ({ + q, + limit, + signal, +}: { + q: string; + limit: number; + signal: AbortSignal; +}) => apiGetSearch({ q, type: 'hashtags', limit }, { signal }); + +export function useSearchTags({ + query, + limit = 11, + filterResults, +}: { + query?: string; + limit?: number; + filterResults?: (account: ApiHashtagJSON) => boolean; +} = {}) { + const intl = useIntl(); + const [fetchedTags, setFetchedTags] = useState([]); + const [loadingState, setLoadingState] = useState< + 'idle' | 'loading' | 'error' + >('idle'); + + const searchRequestRef = useRef(null); + + const searchTags = useDebouncedCallback( + (value: string) => { + if (searchRequestRef.current) { + searchRequestRef.current.abort(); + } + + const trimmedQuery = trimHashFromStart(value.trim()); + + if (trimmedQuery.length === 0) { + setFetchedTags([]); + return; + } + + setLoadingState('loading'); + + searchRequestRef.current = new AbortController(); + + void fetchSearchHashtags({ + q: trimmedQuery, + limit, + signal: searchRequestRef.current.signal, + }) + .then(({ hashtags }) => { + const tags = filterResults + ? hashtags.filter(filterResults) + : hashtags; + setFetchedTags(tags); + setLoadingState('idle'); + }) + .catch(() => { + setLoadingState('error'); + }); + }, + 500, + { leading: true, trailing: true }, + ); + + const resetSearch = useCallback(() => { + setFetchedTags([]); + setLoadingState('idle'); + }, []); + + // Add dedicated item for adding the current query + const tags = useMemo(() => { + const trimmedQuery = query ? trimHashFromStart(query.trim()) : ''; + if (!trimmedQuery || !fetchedTags.length) { + return fetchedTags; + } + + const results: TagSearchResult[] = [...fetchedTags]; // Make array mutable + if ( + trimmedQuery.length > 0 && + results.every( + (result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(), + ) + ) { + results.push({ + id: 'new', + name: trimmedQuery, + label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }), + }); + } + return results; + }, [fetchedTags, query, intl]); + + return { + tags, + searchTags, + resetSearch, + isLoading: loadingState === 'loading', + isError: loadingState === 'error', + }; +} diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts index 7f00c064942daa..36da60d3db896b 100644 --- a/app/javascript/mastodon/initial_state.ts +++ b/app/javascript/mastodon/initial_state.ts @@ -192,7 +192,7 @@ export const languages = initialState?.languages.map((lang) => { lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1], lang[2], - ]; + ] as InitialStateLanguage; }); export function getAccessToken(): string | undefined { diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index e3c75bd21b5281..bfd0f8bd60a52b 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -717,7 +717,6 @@ "notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.", "notifications_permission_banner.title": "لا تفوت شيئاً أبداً", "onboarding.follows.back": "عودة", - "onboarding.follows.done": "تمّ", "onboarding.follows.empty": "نأسف، لا يمكن عرض نتائج في الوقت الحالي. جرب البحث أو انتقل لصفحة الاستكشاف لإيجاد أشخاص للمتابعة، أو حاول مرة أخرى.", "onboarding.follows.search": "بحث", "onboarding.follows.title": "للبدء قم بمتابعة أشخاص", @@ -727,7 +726,6 @@ "onboarding.profile.display_name_hint": "اسمك الكامل أو اسمك المرح…", "onboarding.profile.note": "نبذة عنك", "onboarding.profile.note_hint": "يمكنك @ذِكر أشخاص آخرين أو استعمال #الوسوم…", - "onboarding.profile.save_and_continue": "حفظ و إستمرار", "onboarding.profile.title": "إعداد الملف الشخصي", "onboarding.profile.upload_avatar": "تحميل صورة الملف الشخصي", "onboarding.profile.upload_header": "تحميل رأسية الملف الشخصي", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index 3ef11b39c99104..850971819ae53a 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -28,6 +28,7 @@ "account.featured_tags.last_status_never": "Nun hai nenguna publicación", "account.follow": "Siguir", "account.follow_back": "Siguir tamién", + "account.follow_request_short": "Solicitú", "account.followers": "Siguidores", "account.followers.empty": "Naide sigue a esti perfil.", "account.following": "Siguiendo", @@ -43,6 +44,7 @@ "account.mute": "Desactivar los avisos de @{name}", "account.mute_notifications_short": "Silenciar avisos", "account.mute_short": "Silenciar", + "account.name.help.header": "Un \"handle\" ye como una dirección de corréu", "account.no_bio": "Nun se fornió nenguna descripción.", "account.open_original_page": "Abrir la páxina orixinal", "account.posts": "Artículos", @@ -59,10 +61,17 @@ "account.unmute": "Activar los avisos de @{name}", "account.unmute_notifications_short": "Dexar de silenciar notificaciones", "account.unmute_short": "Activar los avisos", + "account_edit.custom_fields.placeholder": "Añade los tos pronombres, enllaces externos o cualquier otra cosa que quieras compartir.", + "account_edit.custom_fields.tip_title": "Conseyu: Añadir enllaces verificáos", + "account_edit.display_name.title": "Nome a mostrar", + "account_edit.field_edit_modal.name_hint": "Por exemplu: \"Web personal\"", + "account_edit.profile_tab.hint.title": "Les visualizaciones sigan variando", + "account_edit.verified_modal.details": "Añade credibilidá al to perfil de Mastodon verificando enllaces a webs personales. Asina ye como funciona:", "account_note.placeholder": "Calca equí p'amestar una nota", "admin.dashboard.retention.average": "Media", "admin.dashboard.retention.cohort": "Mes de rexistru", "admin.dashboard.retention.cohort_size": "Perfiles nuevos", + "admin.impact_report.instance_followers": "Seguidores que perderíen los nuestros usuarios", "alert.rate_limited.message": "Volvi tentalo dempués de la hora: {retry_time, time, medium}.", "alert.unexpected.message": "Prodúxose un error inesperáu.", "alert.unexpected.title": "¡Meca!", @@ -383,7 +392,6 @@ "notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.", "notifications.policy.accept": "Aceptar", "notifications.policy.accept_hint": "Amosar n'avisos", - "onboarding.follows.done": "Fecho", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…", "password_confirmation.exceeds_maxlength": "La contraseña de confirmación supera la llongura de caráuteres máxima", diff --git a/app/javascript/mastodon/locales/az.json b/app/javascript/mastodon/locales/az.json index e4b5758c44624b..e664b6bd0cb8cd 100644 --- a/app/javascript/mastodon/locales/az.json +++ b/app/javascript/mastodon/locales/az.json @@ -661,7 +661,6 @@ "notifications_permission_banner.enable": "Masaüstü bildirişləri fəallaşdır", "notifications_permission_banner.title": "Heç nəyi buraxmayın", "onboarding.follows.back": "Geri", - "onboarding.follows.done": "Hazırdır", "onboarding.follows.search": "Axtar", "onboarding.follows.title": "Başlamaq üçün insanları izləyin", "onboarding.profile.discoverable": "Profilimi kəşf edilə bilən et", @@ -670,7 +669,6 @@ "onboarding.profile.display_name_hint": "Tam adınız və ya ləqəbiniz…", "onboarding.profile.note": "Bioqrafiya", "onboarding.profile.note_hint": "Digər insanların @adını_çəkə və ya #mövzu_etiketləri istifadə edə bilərsiniz…", - "onboarding.profile.save_and_continue": "Saxla və davam et", "onboarding.profile.title": "Profili ayarla", "onboarding.profile.upload_avatar": "Profil şəkli yüklə", "onboarding.profile.upload_header": "Profil başlığı yüklə", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index b4a6c58bfd205c..aa510b681f25fa 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -39,7 +39,7 @@ "account.edit_profile_short": "Рэдагаваць", "account.enable_notifications": "Апавяшчаць мяне пра допісы @{name}", "account.endorse": "Паказваць у профілі", - "account.familiar_followers_many": "Мае сярод падпісчыкаў {name1}, {name2}, і {othersCount, plural, one {яшчэ # чалавека, знаёмага Вам} few {яшчэ # чалавекі, знаёмыя Вам} many {яшчэ # чалавек, знаёмых Вам} other {яшчэ # чалавекі, знаёмыя Вам}}", + "account.familiar_followers_many": "Мае сярод падпісчыкаў {name1}, {name2}, і {othersCount, plural, one {яшчэ #-го чалавека, знаёмага Вам} few {яшчэ #-х чалавек, знаёмых Вам} many {яшчэ # людзей, знаёмых Вам} other {яшчэ # чалавек, знаёмых Вам}}", "account.familiar_followers_one": "Мае сярод падпісчыкаў {name1}", "account.familiar_followers_two": "Мае сярод падпісчыкаў {name1} і {name2}", "account.featured": "Рэкамендаванае", @@ -73,7 +73,6 @@ "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Змяніць {item}", "account_edit.column_button": "Гатова", "account_edit.column_title": "Рэдагаваць профіль", + "account_edit.custom_fields.name": "поле", "account_edit.custom_fields.placeholder": "Дадайце свае займеннікі, знешнія спасылкі ці нешта іншае, чым Вы хацелі б падзяліцца.", - "account_edit.custom_fields.title": "Карыстальніцкія палі", + "account_edit.custom_fields.reorder_button": "Змяніць парадак палёў", + "account_edit.custom_fields.tip_content": "Вы можаце лёгка дадаць даверу да свайго ўліковага запісу Mastodon пацвярджэннем спасылак на любы з Вашых сайтаў.", + "account_edit.custom_fields.tip_title": "Падказка: Дадаванне пацверджаных спасылак", + "account_edit.custom_fields.title": "Адвольныя палі", + "account_edit.custom_fields.verified_hint": "Як мне дадаць пацверджаную спасылку?", "account_edit.display_name.placeholder": "Вашае бачнае імя — гэта імя, якое іншыя людзі бачаць у Вашым профілі і ў стужках.", "account_edit.display_name.title": "Бачнае імя", "account_edit.featured_hashtags.item": "хэштэгі", "account_edit.featured_hashtags.placeholder": "Дапамажыце іншым зразумець, якія тэмы Вас цікавяць, і атрымаць доступ да іх.", "account_edit.featured_hashtags.title": "Выбраныя хэштэгі", + "account_edit.field_delete_modal.confirm": "Вы ўпэўненыя, што хочаце выдаліць гэтае адвольнае поле? Гэтае дзеянне будзе незваротным.", + "account_edit.field_delete_modal.delete_button": "Выдаліць", + "account_edit.field_delete_modal.title": "Выдаліць адвольнае поле?", + "account_edit.field_edit_modal.add_title": "Дадаць адвольнае поле", + "account_edit.field_edit_modal.edit_title": "Рэдагаваць адвольнае поле", + "account_edit.field_edit_modal.limit_header": "Перавышаная рэкамендаваная колькасць сімвалаў", + "account_edit.field_edit_modal.limit_message": "Карыстальнікі мабільных прылад могуць не ўбачыць Вашае поле цалкам.", + "account_edit.field_edit_modal.link_emoji_warning": "Мы раім не ўжываць адвольныя эмодзі разам з url-спасылкамі. Адвольныя палі, якія ўтрымліваюць і тое, і другое, будуць адлюстраваныя выключна як тэкст, а не спасылкі, каб не блытаць карыстальнікаў.", + "account_edit.field_edit_modal.name_hint": "Напрыклад, \"Асабісты Сайт\"", + "account_edit.field_edit_modal.name_label": "Назва", + "account_edit.field_edit_modal.url_warning": "Каб дадаць спасылку, калі ласка, дадайце {protocol} у яе пачатку.", + "account_edit.field_edit_modal.value_hint": "Напрыклад, “https://example.me”", + "account_edit.field_edit_modal.value_label": "Значэнне", + "account_edit.field_reorder_modal.drag_cancel": "Перасоўванне адмененае. Поле {item} было павернутае на месца.", + "account_edit.field_reorder_modal.drag_end": "Поле {item} было павернутае на месца.", + "account_edit.field_reorder_modal.drag_instructions": "Каб змяніць парадак адвольных палёў, націсніце прабел або ўвод. Падчас перасоўвання карыстайцеся клавішамі са стрэлкамі, каб пасунуць поле ўверх ці ўніз. Націсніце прабел ці ўвод зноў, каб замацаваць поле на новым месцы, або націсніце Esc, каб скасаваць дзеянне.", + "account_edit.field_reorder_modal.drag_move": "Поле {item} было перасунутае.", + "account_edit.field_reorder_modal.drag_over": "Поле {item} было перасунутае над \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Абранае поле \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Перасунуць поле \"{item}\"", + "account_edit.field_reorder_modal.title": "Перасунуць палі", + "account_edit.image_alt_modal.add_title": "Дадаць альт. тэкст", + "account_edit.image_alt_modal.details_content": "ЯК РАБІЦЬ:
    • Апішыце, як Вы выглядаеце на відарысе
    • Апісвайце ад трэцяй асобы (напрыклад, “Алесь” замест \"я”)
    • Будзьце сціслымі — некалькі слоў звычайна дастаткова
    ЯК НЕ РАБІЦЬ:
    • Пачынаць з \"Фотаздымак...\" — гэта залішняе для чытачоў
    ПРЫКЛАД:
    • “Алесь у вышыванцы і акулярах”
    ", + "account_edit.image_alt_modal.details_title": "Падказка: Альтэрнатыўны тэкст для фота профілю", + "account_edit.image_alt_modal.edit_title": "Рэдагаваць альт. тэкст", + "account_edit.image_alt_modal.text_hint": "Альтэрнатыўны тэкст дапамагае чытачам Вашага кантэнту лепш яго разумець.", + "account_edit.image_alt_modal.text_label": "Альт. тэкст", + "account_edit.image_delete_modal.confirm": "Вы ўпэўненыя, што хочаце выдаліць гэты відарыс? Гэтае дзеянне будзе незваротным.", + "account_edit.image_delete_modal.delete_button": "Выдаліць", + "account_edit.image_delete_modal.title": "Выдаліць відарыс?", + "account_edit.image_edit.add_button": "Дадаць відарыс", + "account_edit.image_edit.alt_add_button": "Дадаць альт. тэкст", + "account_edit.image_edit.alt_edit_button": "Рэдагаваць альт. тэкст", + "account_edit.image_edit.remove_button": "Прыбраць відарыс", + "account_edit.image_edit.replace_button": "Замяніць відарыс", "account_edit.name_modal.add_title": "Дадаць бачнае імя", "account_edit.name_modal.edit_title": "Змяніць бачнае імя", "account_edit.profile_tab.button_label": "Змяніць", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Змяняйце на свой густ укладкі свайго профілю і тое, што яны паказваюць.", "account_edit.profile_tab.title": "Налады ўкладкі профілю", "account_edit.save": "Захаваць", + "account_edit.upload_modal.back": "Назад", + "account_edit.upload_modal.done": "Гатова", + "account_edit.upload_modal.next": "Далей", + "account_edit.upload_modal.step_crop.zoom": "Маштаб", + "account_edit.upload_modal.step_upload.button": "Агляд файлаў", + "account_edit.upload_modal.step_upload.dragging": "Перацягн. сюды, каб запамп.", + "account_edit.upload_modal.step_upload.header": "Выбраць відарыс", + "account_edit.upload_modal.step_upload.hint": "Фармату WEBP, PNG, GIF або JPG, да {limit} МБ.{br}Відарыс будзе сціснуты да памеру {width}x{height} пікселяў.", + "account_edit.upload_modal.title_add": "Дадаць фота профілю", + "account_edit.upload_modal.title_replace": "Замяніць фота профілю", + "account_edit.verified_modal.details": "Дадайце даверу да Вашага профілю Mastodon, пацвярджэннем спасылак на ўласныя сайты. Вось як гэта працуе:", + "account_edit.verified_modal.invisible_link.details": "Дадайце спасылку ў свой загаловак. Важнай часткай з'яўляецца rel=\"me\", яна прадухіляе выдачу сябе за іншую асобу на сайтах з карыстальніцкім кантэнтам. Вы нават можаце выкарыстоўваць тэг link у загалоўку старонкі замест {tag}, але HTML код павінен быць даступным без выканання JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Як мне зрабіць спасылку нябачнай?", + "account_edit.verified_modal.step1.header": "Скапіруйце HTML код знізу і ўстаўце яго ў загаловак свайго сайту", + "account_edit.verified_modal.step2.details": "Калі Вы ўжо дадалі свой сайт у якасці адвольнага поля, Вам спатрэбіцца яго выдаліць і зноў дадаць, каб запусціць верыфікацыю.", + "account_edit.verified_modal.step2.header": "Дадаць свой сайт як адвольнае поле", + "account_edit.verified_modal.title": "Як дадаць пацверджаную спасылку", "account_edit_tags.add_tag": "Дадаць #{tagName}", "account_edit_tags.column_title": "Змяніць выбраныя хэштэгі", "account_edit_tags.help_text": "Выбраныя хэштэгі дапамагаюць карыстальнікам знаходзіць Ваш профіль і ўзаемадзейнічаць з ім. Яны дзейнічаюць як фільтры пры праглядзе актыўнасці на Вашай старонцы.", @@ -293,6 +349,8 @@ "collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя", "collections.accounts.empty_title": "Гэтая калекцыя пустая", "collections.collection_description": "Апісанне", + "collections.collection_language": "Мова", + "collections.collection_language_none": "Няма", "collections.collection_name": "Назва", "collections.collection_topic": "Тэма", "collections.confirm_account_removal": "Упэўненыя, што хочаце прыбраць гэты ўліковы запіс з гэтай калекцыі?", @@ -306,10 +364,15 @@ "collections.create_collection": "Стварыць калекцыю", "collections.delete_collection": "Выдаліць калекцыю", "collections.description_length_hint": "Максімум 100 сімвалаў", + "collections.detail.accept_inclusion": "Добра", "collections.detail.accounts_heading": "Уліковыя запісы", + "collections.detail.author_added_you": "{author} дадаў(-ла) Вас у гэтую калекцыю", "collections.detail.curated_by_author": "Курыруе {author}", "collections.detail.curated_by_you": "Курыруеце Вы", "collections.detail.loading": "Загружаецца калекцыя…", + "collections.detail.other_accounts_in_collection": "Іншыя ў гэтай калекцыі:", + "collections.detail.revoke_inclusion": "Прыбраць сябе", + "collections.detail.sensitive_note": "У гэтай калекцыі прысутнічаюць уліковыя запісы і кантэнт, змесціва якіх можа падацца адчувальным для некаторых карыстальнікаў.", "collections.detail.share": "Падзяліцца гэтай калекцыяй", "collections.edit_details": "Рэдагаваць падрабязнасці", "collections.error_loading_collections": "Адбылася памылка падчас загрузкі Вашых калекцый.", @@ -324,10 +387,14 @@ "collections.old_last_post_note": "Апошні допіс быў больш за тыдзень таму", "collections.remove_account": "Прыбраць гэты ўліковы запіс", "collections.report_collection": "Паскардзіцца на гэту калекцыю", + "collections.revoke_collection_inclusion": "Прыбраць сябе з гэтай калекцыі", + "collections.revoke_inclusion.confirmation": "Вас прыбралі з \"{collection}\"", + "collections.revoke_inclusion.error": "Адбылася памылка, калі ласка, спрабуйце яшчэ раз пазней.", "collections.search_accounts_label": "Шукайце ўліковыя запісы, каб дадаць іх сюды…", "collections.search_accounts_max_reached": "Вы дадалі максімальную колькасць уліковых запісаў", "collections.sensitive": "Адчувальная", "collections.topic_hint": "Дадайце хэштэг, які дапаможа іншым зразумець галоўную тэму гэтай калекцыі.", + "collections.topic_special_chars_hint": "Спецыяльныя сімвалы будуць прыбраныя пры захаванні", "collections.view_collection": "Глядзець калекцыю", "collections.view_other_collections_by_user": "Паглядзець іншыя калекцыі гэтага карыстальніка", "collections.visibility_public": "Публічная", @@ -447,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Выдаліць падпісчыка", "confirmations.remove_from_followers.message": "{name} больш не будзе падпісаны(-ая) на Вас. Упэўненыя, што хочаце працягнуць?", "confirmations.remove_from_followers.title": "Выдаліць падпісчыка?", + "confirmations.revoke_collection_inclusion.confirm": "Прыбраць сябе", + "confirmations.revoke_collection_inclusion.message": "Гэтае дзеянне канчатковае, і куратар не зможа пасля зноў дадаць Вас у гэтую калекцыю.", + "confirmations.revoke_collection_inclusion.title": "Прыбраць Вас з гэтай калекцыі?", "confirmations.revoke_quote.confirm": "Выдаліць допіс", "confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.", "confirmations.revoke_quote.title": "Выдаліць допіс?", @@ -558,6 +628,10 @@ "featured_carousel.header": "{count, plural,one {Замацаваны допіс} other {Замацаваныя допісы}}", "featured_carousel.slide": "Допіс {current, number} з {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Апошнім часам Вы рабілі допісы пра {items}. Дадаць гэта ў рэкамендаваныя хэштэгі?", + "featured_tags.suggestions.add": "Дадаць", + "featured_tags.suggestions.added": "Кіруйце сваімі рэкамендаванымі хэштэгамі ў любы час праз Рэдагаваць профіль > Рэкамендаваныя хэштэгі.", + "featured_tags.suggestions.dismiss": "Не, дзякуй", "filter_modal.added.context_mismatch_explanation": "Гэтая катэгорыя фільтра не прымяняецца да кантэксту, у якім Вы адкрылі гэты допіс. Калі Вы хочаце, каб паведамленне таксама было адфільтраванае ў гэтым кантэксце, Вам трэба будзе адрэдагаваць фільтр.", "filter_modal.added.context_mismatch_title": "Неадпаведны кантэкст!", "filter_modal.added.expired_explanation": "Тэрмін дзеяння гэтай катэгорыі фільтраў скончыўся, вам трэба будзе змяніць дату заканчэння тэрміну дзеяння, каб яна прымянялася", @@ -769,6 +843,7 @@ "navigation_bar.automated_deletion": "Аўтаматычнае выдаленне допісаў", "navigation_bar.blocks": "Заблакіраваныя карыстальнікі", "navigation_bar.bookmarks": "Закладкі", + "navigation_bar.collections": "Калекцыі", "navigation_bar.direct": "Прыватныя згадванні", "navigation_bar.domain_blocks": "Заблакіраваныя дамены", "navigation_bar.favourites": "Упадабанае", @@ -915,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Каб атрымліваць апавяшчэнні, калі Mastodon не адкрыты, уключыце апавяшчэнні працоўнага стала. Вы зможаце дакладна кантраляваць, якія падзеі будуць ствараць апавяшчэнні з дапамогай {icon} кнопкі, як толькі яны будуць уключаны.", "notifications_permission_banner.title": "Не прапусціце нічога", "onboarding.follows.back": "Назад", - "onboarding.follows.done": "Гатова", "onboarding.follows.empty": "На жаль, зараз немагчыма паказаць вынікі. Вы можаце паспрабаваць выкарыстоўваць пошук і праглядзець старонку агляду, каб знайсці людзей, на якіх можна падпісацца, або паўтарыць спробу пазней.", + "onboarding.follows.next": "Далей: Наладзьце свой профіль", "onboarding.follows.search": "Пошук", "onboarding.follows.title": "Падпішыцеся на некага, каб пачаць", "onboarding.profile.discoverable": "Зрабіць мой профіль бачным", "onboarding.profile.discoverable_hint": "Калі Вы звяртаецеся да адкрытасці на Mastodon, Вашы допісы могуць з'яўляцца ў выніках пошуку і трэндах, а Ваш профіль можа быць прапанаваны людзям з такімі ж інтарэсамі.", "onboarding.profile.display_name": "Бачнае імя", "onboarding.profile.display_name_hint": "Ваша поўнае імя або ваш псеўданім…", + "onboarding.profile.finish": "Гатова", "onboarding.profile.note": "Біяграфія", "onboarding.profile.note_hint": "Вы можаце @згадваць іншых людзей або выкарыстоўваць #хэштэгі…", - "onboarding.profile.save_and_continue": "Захаваць і працягнуць", "onboarding.profile.title": "Налады профілю", "onboarding.profile.upload_avatar": "Загрузіць фота профілю", "onboarding.profile.upload_header": "Загрузіць шапку профілю", @@ -1133,7 +1208,7 @@ "status.quote_manual_review": "Аўтар зробіць агляд уручную", "status.quote_noun": "Цытаваць", "status.quote_policy_change": "Змяніць, хто можа цытаваць", - "status.quote_post_author": "Цытаваў допіс @{name}", + "status.quote_post_author": "Цытаваў(-ла) допіс @{name}", "status.quote_private": "Прыватныя допісы нельга цытаваць", "status.quotes.empty": "Яшчэ ніхто не цытаваў гэты допіс. Калі гэта адбудзецца, то Вы пабачыце гэта тут.", "status.quotes.local_other_disclaimer": "Цытаты, у якіх адмовіў аўтар, паказаныя не будуць.", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 3549c0ae2a3992..850f1175440ded 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -680,7 +680,6 @@ "notifications_permission_banner.how_to_control": "За да получавате известия, когато Mastodon не е отворен, включете известията на работния плот. Може да управлявате точно кои видове взаимодействия пораждат известия на работния плот чрез бутона {icon} по-горе, след като бъдат включени.", "notifications_permission_banner.title": "Никога не пропускайте нищо", "onboarding.follows.back": "Назад", - "onboarding.follows.done": "Готово", "onboarding.follows.empty": "За съжаление, в момента не могат да се показват резултати. Може да опитате посредством търсене или сърфиране да разгледате страницата, за да намерите хора за последване, или опитайте пак по-късно.", "onboarding.follows.search": "Търсене", "onboarding.follows.title": "Последвайте хора, за да започнете", @@ -690,7 +689,6 @@ "onboarding.profile.display_name_hint": "Вашето пълно име или псевдоним…", "onboarding.profile.note": "Биография", "onboarding.profile.note_hint": "Може да @споменавате други хора или #хаштагове…", - "onboarding.profile.save_and_continue": "Запазване и продължаване", "onboarding.profile.title": "Настройване на профила", "onboarding.profile.upload_avatar": "Качване на снимка на профила", "onboarding.profile.upload_header": "Качване на заглавка на профила", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 0e9e9d33e6ad1d..cb55e4a68a3da5 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -533,13 +533,11 @@ "notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.", "notifications_permission_banner.title": "Na vankit netra morse", "onboarding.follows.back": "Distreiñ", - "onboarding.follows.done": "Graet", "onboarding.follows.search": "Klask", "onboarding.profile.display_name": "Anv diskouezet", "onboarding.profile.display_name_hint": "Hoc'h anv klok pe hoc'h anv fentus…", "onboarding.profile.note": "Berr-ha-berr", "onboarding.profile.note_hint": "Gallout a rit @menegiñ tud all pe #gerioù-klik…", - "onboarding.profile.save_and_continue": "Enrollañ ha kenderc'hel", "onboarding.profile.title": "Kefluniañ ar profil", "onboarding.profile.upload_avatar": "Enporzhiañ ur skeudenn profil", "password_confirmation.mismatching": "Disheñvel eo an daou c'her-termen-se", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 496cb0d0c96006..08b644dd521959 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -70,7 +70,6 @@ "account.go_to_profile": "Vés al perfil", "account.hide_reblogs": "Amaga els impulsos de @{name}", "account.in_memoriam": "En Memòria.", - "account.joined_long": "Membre des de {date}", "account.joined_short": "S'hi va unir", "account.languages": "Canvia les llengües subscrites", "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}", @@ -786,7 +785,6 @@ "notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no és obert cal activar les notificacions d’escriptori. Pots controlar amb precisió quins tipus d’interaccions generen notificacions d’escriptori després d’activar el botó {icon} de dalt.", "notifications_permission_banner.title": "No et perdis mai res", "onboarding.follows.back": "Enrere", - "onboarding.follows.done": "Fet", "onboarding.follows.empty": "Malauradament, cap resultat pot ser mostrat ara mateix. Pots provar de fer servir la cerca o visitar la pàgina Explora per a trobar gent a qui seguir o provar-ho de nou més tard.", "onboarding.follows.search": "Cerca", "onboarding.follows.title": "Seguiu gent per a començar", @@ -796,7 +794,6 @@ "onboarding.profile.display_name_hint": "El teu nom complet o el teu malnom…", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Pots @mencionar altra gent o #etiquetes…", - "onboarding.profile.save_and_continue": "Desa i continua", "onboarding.profile.title": "Configuració del perfil", "onboarding.profile.upload_avatar": "Importa una foto de perfil", "onboarding.profile.upload_header": "Importa una capçalera de perfil", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index d79d67d4da5402..02fe2ebc196b45 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -71,7 +71,6 @@ "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}", @@ -841,7 +840,6 @@ "notifications_permission_banner.how_to_control": "Chcete-li dostávat oznámení, i když nemáte Mastodon otevřený, povolte oznámení na ploše. Můžete si zvolit, o kterých druzích interakcí chcete být oznámením na ploše informování pod tlačítkem {icon} výše.", "notifications_permission_banner.title": "Nenechte si nic uniknout", "onboarding.follows.back": "Zpět", - "onboarding.follows.done": "Hotovo", "onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit najít uživatele ke sledování za pomocí vyhledávání nebo na stránce „Objevit“, nebo to zkuste znovu později.", "onboarding.follows.search": "Hledat", "onboarding.follows.title": "Sledujte lidi a začněte", @@ -851,7 +849,6 @@ "onboarding.profile.display_name_hint": "Vaše celé jméno nebo přezdívka…", "onboarding.profile.note": "O vás", "onboarding.profile.note_hint": "Můžete @zmínit jiné osoby nebo #hashtagy…", - "onboarding.profile.save_and_continue": "Uložit a pokračovat", "onboarding.profile.title": "Nastavení profilu", "onboarding.profile.upload_avatar": "Nahrát profilový obrázek", "onboarding.profile.upload_header": "Nahrát hlavičku profilu", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index abd60569696c26..ccb9375d7f44a9 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Mynd i'r proffil", "account.hide_reblogs": "Cuddio hybiau gan @{name}", "account.in_memoriam": "Er Cof.", - "account.joined_long": "Ymunodd ar {date}", "account.joined_short": "Ymunodd", "account.languages": "Newid ieithoedd wedi tanysgrifio iddyn nhw", "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}", @@ -151,13 +150,52 @@ "account_edit.button.edit": "Golygu {item}", "account_edit.column_button": "Gorffen", "account_edit.column_title": "Golygu Proffil", + "account_edit.custom_fields.name": "maes", "account_edit.custom_fields.placeholder": "Ychwanegwch eich rhagenwau, dolenni allanol, neu unrhyw beth arall hoffech ei rannu.", + "account_edit.custom_fields.reorder_button": "Ail-drefnu meysydd", + "account_edit.custom_fields.tip_content": "Gallwch chi ychwanegu hygrededd at eich cyfrif Mastodon yn hawdd trwy wirio dolenni i unrhyw wefannau rydych chi'n berchen arnyn nhw.", + "account_edit.custom_fields.tip_title": "Awgrym: Ychwanegu dolenni wedi'u gwirio", "account_edit.custom_fields.title": "Meysydd cyfaddas", + "account_edit.custom_fields.verified_hint": "Sut ydw i'n ychwanegu dolen wedi'i gwirio?", "account_edit.display_name.placeholder": "Eich enw dangos yw sut mae'ch enw'n ymddangos ar eich proffil ac mewn llinellau amser.", "account_edit.display_name.title": "Enw dangos", "account_edit.featured_hashtags.item": "hashnodau", "account_edit.featured_hashtags.placeholder": "Helpwch eraill i adnabod, a chael mynediad cyflym at eich hoff bynciau.", "account_edit.featured_hashtags.title": "Hashnodau dan sylw", + "account_edit.field_delete_modal.confirm": "Ydych chi'n siŵr eich bod chi eisiau dileu'r maes cyfaddas hwn? Does dim modd dadwneud y weithred hon.", + "account_edit.field_delete_modal.delete_button": "Dileu", + "account_edit.field_delete_modal.title": "Dileu maes cyfaddas?", + "account_edit.field_edit_modal.add_title": "Ychwanegu maes cyfaddas", + "account_edit.field_edit_modal.edit_title": "Golygu maes cyfaddas", + "account_edit.field_edit_modal.limit_header": "Wedi mynd dros y terfyn nodau sy'n cael eu hargymell", + "account_edit.field_edit_modal.limit_message": "Efallai na fydd defnyddwyr symudol yn gweld eich maes yn llawn.", + "account_edit.field_edit_modal.link_emoji_warning": "Rydym yn argymell yn erbyn defnyddio emoji personol ar y cyd ag URLau. Bydd meysydd personol sy'n cynnwys y ddau yn cael eu harddangos fel testun yn unig yn hytrach nag fel dolen, er mwyn atal dryswch ymhlith defnyddwyr.", + "account_edit.field_edit_modal.name_hint": "e.e. “Gwefan bersonol”", + "account_edit.field_edit_modal.url_warning": "I ychwanegu dolen, cofiwch gynnwys {protocol} ar y dechrau.", + "account_edit.field_edit_modal.value_hint": "E.e. “https://example.me”", + "account_edit.field_edit_modal.value_label": "Gwerth", + "account_edit.field_reorder_modal.drag_cancel": "Cafodd y llusgo ei ddiddymu. Cafodd y maes \"{item}\" ei ollwng.", + "account_edit.field_reorder_modal.drag_end": "Cafodd y maes \"{item}\" ei ollwng.", + "account_edit.field_reorder_modal.drag_instructions": "I aildrefnu meysydd cyfaddas, pwyswch y bylchwr neu enter. Wrth lusgo, defnyddiwch y bysellau saeth i symud y maes i fyny neu i lawr. Pwyswch y bylchwr neu enter eto i ollwng y maes yn ei safle newydd, neu pwyswch escape i'w ddiddymu.", + "account_edit.field_reorder_modal.drag_move": "Symudwyd y maes \"{item}\".", + "account_edit.field_reorder_modal.drag_over": "Symudwyd y maes \"{item}\" dros \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Wedi codi maes \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Llusgo'r maes \"{item}\"", + "account_edit.field_reorder_modal.title": "Aildrefnu meysydd", + "account_edit.image_alt_modal.add_title": "Ychwanegu testun amgen", + "account_edit.image_alt_modal.details_content": "GWNEUD:
    • Disgrifio'ch hun fel yn eich llun
    • Defnyddio iaith trydydd person (e.e. “Siôn” yn lle “fi”)
    • Bod yn gryno – mae ychydig o eiriau’n aml yn ddigon
    PEIDIO:
    • Dechrau gyda “Llun o” – mae’n ddiangen ar gyfer darllenwyr sgrin
    ENGHRAIFFT:
    • “Dyma Siôn yn gwisgo crys gwyrdd a sbectol”
    ", + "account_edit.image_alt_modal.details_title": "Awgrymiadau: Testun amgen ar gyfer lluniau proffil", + "account_edit.image_alt_modal.edit_title": "Golygu testun amgen", + "account_edit.image_alt_modal.text_hint": "Mae testun amgen yn helpu defnyddwyr darllenwyr sgrin i ddeall eich cynnwys.", + "account_edit.image_alt_modal.text_label": "Testun amgen", + "account_edit.image_delete_modal.confirm": "Ydych chi'n siŵr eich bod chi eisiau dileu'r ddelwedd hon? Does dim modd dadwneud hynny.", + "account_edit.image_delete_modal.delete_button": "Dileu", + "account_edit.image_delete_modal.title": "Dileu delwedd?", + "account_edit.image_edit.add_button": "Ychwanegu Delwedd", + "account_edit.image_edit.alt_add_button": "Ychwanegu testun amgen", + "account_edit.image_edit.alt_edit_button": "Golygu testun amgen", + "account_edit.image_edit.remove_button": "Tynnu delwedd", + "account_edit.image_edit.replace_button": "Amnewid delwedd", "account_edit.name_modal.add_title": "Ychwanegu enw dangos", "account_edit.name_modal.edit_title": "Golygu enw dangos", "account_edit.profile_tab.button_label": "Cyfaddasu", @@ -172,6 +210,24 @@ "account_edit.profile_tab.subtitle": "Cyfaddaswch y tabiau ar eich proffil a'r hyn maen nhw'n ei ddangos.", "account_edit.profile_tab.title": "Gosodiadau tab proffil", "account_edit.save": "Cadw", + "account_edit.upload_modal.back": "Nôl", + "account_edit.upload_modal.done": "Gorffen", + "account_edit.upload_modal.next": "Nesaf", + "account_edit.upload_modal.step_crop.zoom": "Chwyddo", + "account_edit.upload_modal.step_upload.button": "Pori ffeiliau", + "account_edit.upload_modal.step_upload.dragging": "Gollwng i lwytho i fyny", + "account_edit.upload_modal.step_upload.header": "Dewiswch ddelwedd", + "account_edit.upload_modal.step_upload.hint": "Fformat WEBP, PNG, GIF neu JPG, hyd at {limit}MB.{br}Bydd y ddelwedd yn cael ei haddasu i {width}x{height}px.", + "account_edit.upload_modal.title_add": "Ychwanegu llun proffil", + "account_edit.upload_modal.title_replace": "Amnewid llun proffil", + "account_edit.verified_modal.details": "Ychwanegwch hygrededd at eich proffil Mastodon trwy wirio dolenni i wefannau personol. Dyma sut mae'n gweithio:", + "account_edit.verified_modal.invisible_link.details": "Ychwanegwch y ddolen at eich pennyn. Y rhan bwysig yw rel=\"me\" sy'n atal dynwared ar wefannau gyda chynnwys sy'n cael ei gynhyrchu gan ddefnyddwyr. Gallwch hyd yn oed ddefnyddio tag dolen ym mhennyn y dudalen yn lle {tag}, ond rhaid bod yr HTML yn hygyrch ac heb weithredu JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Sut ydw i'n gwneud y ddolen yn anweledig?", + "account_edit.verified_modal.step1.header": "Copïwch y cod HTML isod a'i gludo i bennyn eich gwefan", + "account_edit.verified_modal.step2.details": "Os ydych chi eisoes wedi ychwanegu eich gwefan fel maes cyfaddas, bydd angen i chi ei dileu a'i hail-ychwanegu i cychwyn dilysu.", + "account_edit.verified_modal.step2.header": "Ychwanegwch eich gwefan fel maes cyfaddas", + "account_edit.verified_modal.title": "Sut i ychwanegu dolen wedi'i gwirio", + "account_edit_tags.add_tag": "Ychwanegu #{tagName}", "account_edit_tags.column_title": "Golygu hashnodau dan sylw", "account_edit_tags.help_text": "Mae hashnodau dan sylw yn helpu defnyddwyr i ddarganfod a rhyngweithio â'ch proffil. Maen nhw'n ymddangos fel hidlwyr ar olwg Gweithgaredd eich tudalen Proffil.", "account_edit_tags.search_placeholder": "Rhowch hashnod…", @@ -274,6 +330,8 @@ "callout.dismiss": "Cau", "carousel.current": "Sleid {current, number} / {max, number}", "carousel.slide": "Sleid {current, number} o {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} nod sy'n cael eu hargymell", + "character_counter.required": "{currentLength}/{maxLength} nod", "closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.", "closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.", "closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall", @@ -303,10 +361,15 @@ "collections.create_collection": "Creu casgliad", "collections.delete_collection": "Dileu casgliad", "collections.description_length_hint": "Terfyn o 100 nod", + "collections.detail.accept_inclusion": "Iawn", "collections.detail.accounts_heading": "Cyfrifon", + "collections.detail.author_added_you": "Ychwanegodd {author} chi at y casgliad hwn", "collections.detail.curated_by_author": "Wedi'i guradu gan {author}", "collections.detail.curated_by_you": "Wedi'i guradu gennych chi", "collections.detail.loading": "Yn llwytho casgliad…", + "collections.detail.other_accounts_in_collection": "Eraill yn y casgliad hwn:", + "collections.detail.revoke_inclusion": "Tynnu fi", + "collections.detail.sensitive_note": "Mae'r casgliad hwn yn cynnwys cyfrifon a chynnwys a allai fod yn sensitif i rai defnyddwyr.", "collections.detail.share": "Rhannu'r casgliad hwn", "collections.edit_details": "Golygu manylion", "collections.error_loading_collections": "Bu gwall wrth geisio llwytho eich casgliadau.", @@ -321,10 +384,14 @@ "collections.old_last_post_note": "Postiwyd ddiwethaf dros wythnos yn ôl", "collections.remove_account": "Dileu'r cyfrif hwn", "collections.report_collection": "Adroddwch am y casgliad hwn", + "collections.revoke_collection_inclusion": "Tynnu fy hun o'r casgliad hwn", + "collections.revoke_inclusion.confirmation": "Rydych chi wedi cael eich tynnu o \"{collection}\"", + "collections.revoke_inclusion.error": "Bu gwall, ceisiwch eto yn nes ymlaen.", "collections.search_accounts_label": "Chwiliwch am gyfrifon i'w hychwanegu…", "collections.search_accounts_max_reached": "Rydych chi wedi ychwanegu'r nifer mwyaf o gyfrifon", "collections.sensitive": "Sensitif", "collections.topic_hint": "Ychwanegwch hashnod sy'n helpu eraill i ddeall prif bwnc y casgliad hwn.", + "collections.topic_special_chars_hint": "Bydd nodau arbennig yn cael eu tynnu wrth gadw", "collections.view_collection": "Gweld y casgliad", "collections.view_other_collections_by_user": "Edrychwch ar gasgliadau eraill gan y defnyddiwr hwn", "collections.visibility_public": "Cyhoeddus", @@ -444,6 +511,9 @@ "confirmations.remove_from_followers.confirm": "Dileu dilynwr", "confirmations.remove_from_followers.message": "Bydd {name} yn rhoi'r gorau i'ch dilyn. A ydych yn siŵr eich bod am fwrw ymlaen?", "confirmations.remove_from_followers.title": "Tynnu dilynwr?", + "confirmations.revoke_collection_inclusion.confirm": "Tynnu fi", + "confirmations.revoke_collection_inclusion.message": "Mae'r weithred hon yn barhaol, a fydd y curadur ddim yn gallu eich ail-ychwanegu at y casgliad yn ddiweddarach.", + "confirmations.revoke_collection_inclusion.title": "Tynnu eich hun o'r casgliad hwn?", "confirmations.revoke_quote.confirm": "Dileu'r postiad", "confirmations.revoke_quote.message": "Does dim modd dadwneud y weithred hon.", "confirmations.revoke_quote.title": "Dileu'r postiad?", @@ -766,6 +836,7 @@ "navigation_bar.automated_deletion": "Dileu postiadau'n awtomatig", "navigation_bar.blocks": "Defnyddwyr wedi'u rhwystro", "navigation_bar.bookmarks": "Nodau Tudalen", + "navigation_bar.collections": "Casgliadau", "navigation_bar.direct": "Crybwylliadau preifat", "navigation_bar.domain_blocks": "Parthau wedi'u rhwystro", "navigation_bar.favourites": "Ffefrynnau", @@ -912,17 +983,17 @@ "notifications_permission_banner.how_to_control": "I dderbyn hysbysiadau pan nad yw Mastodon ar agor, galluogwch hysbysiadau bwrdd gwaith. Gallwch reoli'n union pa fathau o ryngweithiadau sy'n cynhyrchu hysbysiadau bwrdd gwaith trwy'r botwm {icon} uchod unwaith y byddan nhw wedi'u galluogi.", "notifications_permission_banner.title": "Peidiwch â cholli dim", "onboarding.follows.back": "Nôl", - "onboarding.follows.done": "Wedi gorffen", "onboarding.follows.empty": "Yn anffodus, nid oes modd dangos unrhyw ganlyniadau ar hyn o bryd. Gallwch geisio defnyddio chwilio neu bori'r dudalen archwilio i ddod o hyd i bobl i'w dilyn, neu ceisio eto yn nes ymlaen.", + "onboarding.follows.next": "Nesaf: Gosodwch eich proffil", "onboarding.follows.search": "Chwilio", "onboarding.follows.title": "Dilynwch bobl i gychwyn arni", "onboarding.profile.discoverable": "Gwnewch fy mhroffil yn un y gellir ei ddarganfod", "onboarding.profile.discoverable_hint": "Pan fyddwch yn dewis ymuno â darganfod ar Mastodon, gall eich postiadau ymddangos mewn canlyniadau chwilio a threndiau, ac efallai y bydd eich proffil yn cael ei awgrymu i bobl sydd â diddordebau tebyg i chi.", "onboarding.profile.display_name": "Enw dangos", "onboarding.profile.display_name_hint": "Eich enw llawn neu'ch enw hwyl…", + "onboarding.profile.finish": "Gorffen", "onboarding.profile.note": "Bywgraffiad", "onboarding.profile.note_hint": "Gallwch @grybwyll pobl eraill neu #hashnodau…", - "onboarding.profile.save_and_continue": "Cadw a pharhau", "onboarding.profile.title": "Gosodiad proffil", "onboarding.profile.upload_avatar": "Llwytho llun proffil", "onboarding.profile.upload_header": "Llwytho pennyn proffil", @@ -1069,6 +1140,9 @@ "sign_in_banner.mastodon_is": "Mastodon yw'r ffordd orau o gadw i fyny â'r hyn sy'n digwydd.", "sign_in_banner.sign_in": "Mewngofnodi", "sign_in_banner.sso_redirect": "Mewngofnodi neu Gofrestru", + "skip_links.hotkey": "Allwedd boeth {hotkey}", + "skip_links.skip_to_content": "Symud i'r prif gynnwys", + "skip_links.skip_to_navigation": "Symud i'r prif lywio", "status.admin_account": "Agor rhyngwyneb cymedroli @{name}", "status.admin_domain": "Agor rhyngwyneb cymedroli {domain}", "status.admin_status": "Agor y postiad hwn yn y rhyngwyneb cymedroli", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 12f05b0324aad9..8eb992fd4ed262 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Rediger profil", "account_edit.custom_fields.name": "felt", "account_edit.custom_fields.placeholder": "Tilføj dine pronominer, eksterne links eller andet, du gerne vil dele.", + "account_edit.custom_fields.reorder_button": "Omsorter felter", "account_edit.custom_fields.tip_content": "Du kan nemt øge troværdigheden af din Mastodon-konto ved at verificere links til alle websteder, du ejer.", "account_edit.custom_fields.tip_title": "Tip: Tilføjelse af bekræftede links", "account_edit.custom_fields.title": "Brugerdefinerede felter", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Slet brugerdefineret felt?", "account_edit.field_edit_modal.add_title": "Tilføj brugerdefineret felt", "account_edit.field_edit_modal.edit_title": "Rediger brugerdefineret felt", + "account_edit.field_edit_modal.limit_header": "Anbefalet tegngrænse overskredet", + "account_edit.field_edit_modal.limit_message": "Mobilbrugere kan muligvis ikke se hele dit felt.", + "account_edit.field_edit_modal.link_emoji_warning": "Vi fraråder brug af brugerdefinerede emoji i kombination med url'er. Brugerdefinerede felter, der indeholder begge dele, vises kun som tekst i stedet for som et link for at undgå forvirring hos brugerne.", "account_edit.field_edit_modal.name_hint": "F.eks. “Personligt websted”", "account_edit.field_edit_modal.name_label": "Etiket", - "account_edit.field_edit_modal.value_hint": "F.eks. “eksempel.me”", + "account_edit.field_edit_modal.url_warning": "For at tilføje et link, skal du inkludere {protocol} i begyndelsen.", + "account_edit.field_edit_modal.value_hint": "F.eks. “https://example.me”", "account_edit.field_edit_modal.value_label": "Værdi", + "account_edit.field_reorder_modal.drag_cancel": "Trækningen blev annulleret. Feltet \"{item}\" blev sluppet.", + "account_edit.field_reorder_modal.drag_end": "Feltet \"{item}\" blev sluppet.", + "account_edit.field_reorder_modal.drag_instructions": "For at omarrangere brugerdefinerede felter skal du trykke på mellemrumstasten eller Enter. Mens du trækker, kan du bruge piletasterne til at flytte feltet op eller ned. Tryk på mellemrumstasten eller Enter igen for at placere feltet i sin nye position, eller tryk på Escape for at annullere.", + "account_edit.field_reorder_modal.drag_move": "Feltet \"{item}\" blev flyttet.", + "account_edit.field_reorder_modal.drag_over": "Feltet \"{item}\" blev flyttet over \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Samlede feltet \"{item}\" op.", + "account_edit.field_reorder_modal.handle_label": "Træk feltet \"{item}\"", + "account_edit.field_reorder_modal.title": "Omarrangér felter", + "account_edit.image_alt_modal.add_title": "Tilføj alt-tekst", + "account_edit.image_alt_modal.details_content": "GØR GERNE:
    • Beskriv dig selv som vist på billedet
    • Brug tredje person (f.eks. “Alex“ i stedet for “mig“)
    • Vær kortfattet – et par ord er ofte nok
    GØR IKKE:
    • Start ikke med “Foto af“ – det er overflødigt for skærmlæsere
    EKSEMPEL:
    • “Alex iført en grøn skjorte og briller”
    ", + "account_edit.image_alt_modal.details_title": "Tips: Alt-tekst til profilfotos", + "account_edit.image_alt_modal.edit_title": "Rediger alt-tekst", + "account_edit.image_alt_modal.text_hint": "Alt-tekst hjælper brugere af skærmlæsere med at forstå dit indhold.", + "account_edit.image_alt_modal.text_label": "Alt-tekst", + "account_edit.image_delete_modal.confirm": "Er du sikker på, at du vil slette dette billede? Denne handling kan ikke fortrydes.", + "account_edit.image_delete_modal.delete_button": "Slet", + "account_edit.image_delete_modal.title": "Slet billede?", + "account_edit.image_edit.add_button": "Tilføj billede", + "account_edit.image_edit.alt_add_button": "Tilføj alt-tekst", + "account_edit.image_edit.alt_edit_button": "Rediger alt-tekst", + "account_edit.image_edit.remove_button": "Fjern billede", + "account_edit.image_edit.replace_button": "Erstat billede", "account_edit.name_modal.add_title": "Tilføj visningsnavn", "account_edit.name_modal.edit_title": "Rediger visningsnavn", "account_edit.profile_tab.button_label": "Tilpas", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Tilpas fanerne på din profil og det, de viser.", "account_edit.profile_tab.title": "Indstillinger for profil-fane", "account_edit.save": "Gem", + "account_edit.upload_modal.back": "Tilbage", + "account_edit.upload_modal.done": "Færdig", + "account_edit.upload_modal.next": "Næste", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Gennemse filer", + "account_edit.upload_modal.step_upload.dragging": "Slip for at uploade", + "account_edit.upload_modal.step_upload.header": "Vælg et billede", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF eller JPG-format, op til {limit} MB.{br}Billede vil blive skaleret til {width}x{height} px.", + "account_edit.upload_modal.title_add": "Tilføj profilfoto", + "account_edit.upload_modal.title_replace": "Erstat profilfoto", "account_edit.verified_modal.details": "Øg troværdigheden af din Mastodon-profil ved at verificere links til personlige websteder. Sådan fungerer det:", "account_edit.verified_modal.invisible_link.details": "Føj linket til din header. Det vigtige er rel=\"me\", som forhindrer imitatorer på websteder med brugergenereret indhold. Du kan endda bruge et link-tag i sidens header i stedet for {tag}, men HTML-koden skal være tilgængelig uden at afvikle JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Hvordan gør jeg linket usynligt?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Tilføj op til {count} konti, du følger", "collections.accounts.empty_title": "Denne samling er tom", "collections.collection_description": "Beskrivelse", + "collections.collection_language": "Sprog", + "collections.collection_language_none": "Intet", "collections.collection_name": "Navn", "collections.collection_topic": "Emne", "collections.confirm_account_removal": "Er du sikker på, at du vil fjerne denne konto fra denne samling?", @@ -326,12 +364,14 @@ "collections.create_collection": "Opret samling", "collections.delete_collection": "Slet samling", "collections.description_length_hint": "Begrænset til 100 tegn", + "collections.detail.accept_inclusion": "Okay", "collections.detail.accounts_heading": "Konti", "collections.detail.author_added_you": "{author} tilføjede dig til denne samling", "collections.detail.curated_by_author": "Kurateret af {author}", "collections.detail.curated_by_you": "Kurateret af dig", "collections.detail.loading": "Indlæser samling…", "collections.detail.other_accounts_in_collection": "Andre i denne samling:", + "collections.detail.revoke_inclusion": "Fjern mig", "collections.detail.sensitive_note": "Denne samling indeholder konti og indhold, der kan være følsomt for nogle brugere.", "collections.detail.share": "Del denne samling", "collections.edit_details": "Rediger detaljer", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Seneste indlæg er fra over en uge siden", "collections.remove_account": "Fjern denne konto", "collections.report_collection": "Anmeld denne samling", + "collections.revoke_collection_inclusion": "Fjern mig selv fra denne samling", + "collections.revoke_inclusion.confirmation": "Du er blevet fjernet fra \"{collection}\"", + "collections.revoke_inclusion.error": "Der opstod en fejl, prøv igen senere.", "collections.search_accounts_label": "Søg efter konti for at tilføje…", "collections.search_accounts_max_reached": "Du har tilføjet det maksimale antal konti", "collections.sensitive": "Sensitivt", "collections.topic_hint": "Tilføj et hashtag, der hjælper andre med at forstå det overordnede emne for denne samling.", + "collections.topic_special_chars_hint": "Specialtegn fjernes, når der gemmes", "collections.view_collection": "Vis samling", "collections.view_other_collections_by_user": "Se andre samlinger af denne bruger", "collections.visibility_public": "Offentlig", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Fjern følger", "confirmations.remove_from_followers.message": "{name} vil ikke længere følge dig. Er du sikker på, at du vil fortsætte?", "confirmations.remove_from_followers.title": "Fjern følger?", + "confirmations.revoke_collection_inclusion.confirm": "Fjern mig", + "confirmations.revoke_collection_inclusion.message": "Denne handling er permanent, og kuratoren vil ikke kunne føje dig til samlingen igen senere.", + "confirmations.revoke_collection_inclusion.title": "Fjern dig selv fra denne samling?", "confirmations.revoke_quote.confirm": "Fjern indlæg", "confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.", "confirmations.revoke_quote.title": "Fjern indlæg?", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {fastgjort indlæg} other {fastgjorte indlæg}}", "featured_carousel.slide": "Indlæg {current, number} af {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Du har for nylig lagt indlæg op om {items}. Skal disse tilføjes som fremhævede hashtags?", + "featured_tags.suggestions.add": "Tilføj", + "featured_tags.suggestions.added": "Du kan til enhver tid administrere dine fremhævede hashtags under Rediger profil > Fremhævede hashtags.", + "featured_tags.suggestions.dismiss": "Nej tak", "filter_modal.added.context_mismatch_explanation": "Denne filterkategori omfatter ikke konteksten, hvorunder dette indlæg er tilgået. Redigér filteret, hvis indlægget også ønskes filtreret i denne kontekst.", "filter_modal.added.context_mismatch_title": "Kontekstmisforhold!", "filter_modal.added.expired_explanation": "Denne filterkategori er udløbet. Ændr dens udløbsdato, for at anvende den.", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Aktivér computernotifikationer for at få besked, når Mastodon ikke er åben. Når de er aktiveret, kan man via knappen {icon} ovenfor præcist styre, hvilke typer af interaktioner, som genererer computernotifikationer.", "notifications_permission_banner.title": "Gå aldrig glip af noget", "onboarding.follows.back": "Tilbage", - "onboarding.follows.done": "Færdig", "onboarding.follows.empty": "Ingen resultater tilgængelige pt. Prøv at bruge søgning eller gennemse siden for at finde personer at følge, eller forsøg igen senere.", + "onboarding.follows.next": "Næste: Opsætning af din profil", "onboarding.follows.search": "Søg", "onboarding.follows.title": "Følg folk for at komme i gang", "onboarding.profile.discoverable": "Gør min profil synlig", "onboarding.profile.discoverable_hint": "Når du vælger at være synlig på Mastodon, kan dine indlæg blive vist i søgeresultater og trender, og din profil kan blive foreslået til personer med samme interesser som dig.", "onboarding.profile.display_name": "Vist navn", "onboarding.profile.display_name_hint": "Dit fulde navn eller dit sjove navn…", + "onboarding.profile.finish": "Afslut", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "Du kan @omtale andre personer eller #hashtags…", - "onboarding.profile.save_and_continue": "Gem og fortsæt", "onboarding.profile.title": "Profilopsætning", "onboarding.profile.upload_avatar": "Upload profilbillede", "onboarding.profile.upload_header": "Upload profilbanner", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index b3426e51ccc4f2..a0f9b9f06b18b0 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -73,7 +73,6 @@ "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", @@ -107,7 +106,7 @@ "account.mutual": "Ihr folgt einander", "account.name.help.domain": "{domain} ist der Server, auf dem das Profil registriert ist und die Beiträge verwaltet werden.", "account.name.help.domain_self": "{domain} ist der Server, auf dem du registriert bist und deine Beiträge verwaltet werden.", - "account.name.help.footer": "So wie du E-Mails an andere trotz unterschiedlicher E-Mail-Clients senden kannst, so kannst du auch mit anderen Profilen auf unterschiedlichen Mastodon-Servern interagieren. Wenn andere soziale Apps die gleichen Kommunikationsregeln (das ActivityPub-Protokoll) wie Mastodon verwenden, dann funktioniert die Kommunikation auch dort.", + "account.name.help.footer": "So wie du E-Mails an andere trotz unterschiedlicher E-Mail-Provider senden kannst, so kannst du auch mit anderen Profilen auf unterschiedlichen Mastodon-Servern interagieren. Wenn andere soziale Apps die gleichen Kommunikationsregeln (das ActivityPub-Protokoll) wie Mastodon verwenden, dann funktioniert die Kommunikation auch dort.", "account.name.help.header": "Deine Adresse im Fediverse ist wie eine E-Mail-Adresse", "account.name.help.username": "{username} ist der Profilname auf deren Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.", "account.name.help.username_self": "{username} ist dein Profilname auf diesem Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.", @@ -153,6 +152,7 @@ "account_edit.column_title": "Profil bearbeiten", "account_edit.custom_fields.name": "Feld", "account_edit.custom_fields.placeholder": "Ergänze deine Pronomen, weiterführenden Links oder etwas anderes, das du teilen möchtest.", + "account_edit.custom_fields.reorder_button": "Felder neu anordnen", "account_edit.custom_fields.tip_content": "Du kannst deine Echtheit im Mastodon-Profil beweisen, wenn du verifizierte Links zu deinen Websites bereitstellst.", "account_edit.custom_fields.tip_title": "Tipp: Ergänze verifizierte Links", "account_edit.custom_fields.title": "Zusatzfelder", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Zusatzfeld löschen?", "account_edit.field_edit_modal.add_title": "Zusatzfeld hinzufügen", "account_edit.field_edit_modal.edit_title": "Zusatzfeld bearbeiten", + "account_edit.field_edit_modal.limit_header": "Empfohlenes Zeichenlimit überschritten", + "account_edit.field_edit_modal.limit_message": "Auf mobilen Endgeräten wird das Feld möglicherweise nicht vollständig angezeigt.", + "account_edit.field_edit_modal.link_emoji_warning": "Das Verwenden von Emojis wird bei URLs nicht empfohlen. Die Zusatzfelder werden bei dieser Kombination nur als Text und nicht als Link dargestellt.", "account_edit.field_edit_modal.name_hint": "z. B. „Meine Website“", "account_edit.field_edit_modal.name_label": "Beschriftung", - "account_edit.field_edit_modal.value_hint": "z. B. „example.me“", + "account_edit.field_edit_modal.url_warning": "Um einen Link hinzuzufügen, füge {protocol} an den Anfang ein.", + "account_edit.field_edit_modal.value_hint": "z. B. „https://beispiel.tld“", "account_edit.field_edit_modal.value_label": "Inhalt", + "account_edit.field_reorder_modal.drag_cancel": "Das Ziehen wurde abgebrochen und das Feld „{item}“ wurde abgelegt.", + "account_edit.field_reorder_modal.drag_end": "Das Feld „{item}“ wurde abgelegt.", + "account_edit.field_reorder_modal.drag_instructions": "Drücke zum Anordnen der Zusatzfelder die Eingabe- oder Leertaste. Verwende beim Ziehen die Pfeiltasten, um es hoch oder runter zu bewegen. Drücke erneut die Eingabe- oder Leertaste, um das Zusatzfeld an der gewünschten Position abzulegen. Mit der Escape-Taste kannst du den Vorgang abbrechen.", + "account_edit.field_reorder_modal.drag_move": "Das Feld „{item}“ wurde verschoben.", + "account_edit.field_reorder_modal.drag_over": "Das Feld „{item}“ wurde über „{over}“ verschoben.", + "account_edit.field_reorder_modal.drag_start": "Das Feld „{item}“ wurde ausgewählt.", + "account_edit.field_reorder_modal.handle_label": "Das Feld „{item}“ verschieben", + "account_edit.field_reorder_modal.title": "Felder neu anordnen", + "account_edit.image_alt_modal.add_title": "Bildbeschreibung hinzufügen", + "account_edit.image_alt_modal.details_content": "Richtig:
    • Beschreibe dich selbst wie abgebildet
    • Verwende beim Beschreiben die 3. Person (z. B. „Axel“ anstatt „ich“)
    • Formuliere kurz und knackig – ein paar Wörter sind ausreichend
    Falsch:
    • Mit „Bild von“ beginnen – bei Screenreadern überflüssig
    Korrektes Beispiel:
    • „Axel trägt ein grünes T-Shirt und eine Brille.“
    ", + "account_edit.image_alt_modal.details_title": "Hinweis: Bildbeschreibung für Profilbilder", + "account_edit.image_alt_modal.edit_title": "Bildbeschreibung bearbeiten", + "account_edit.image_alt_modal.text_hint": "Bildbeschreibungen können sehbehinderten oder blinden Menschen helfen, deine Medieninhalte mithilfe eines Screenreaders besser zu verstehen und einzuordnen.", + "account_edit.image_alt_modal.text_label": "Bildbeschreibung", + "account_edit.image_delete_modal.confirm": "Möchtest du dieses Bild wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "account_edit.image_delete_modal.delete_button": "Löschen", + "account_edit.image_delete_modal.title": "Bild löschen?", + "account_edit.image_edit.add_button": "Bild hinzufügen", + "account_edit.image_edit.alt_add_button": "Bildbeschreibung hinzufügen", + "account_edit.image_edit.alt_edit_button": "Bildbeschreibung bearbeiten", + "account_edit.image_edit.remove_button": "Bild entfernen", + "account_edit.image_edit.replace_button": "Bild ersetzen", "account_edit.name_modal.add_title": "Anzeigenamen hinzufügen", "account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten", "account_edit.profile_tab.button_label": "Anpassen", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Passe die Tabs deines Profils und deren Inhalte an.", "account_edit.profile_tab.title": "Profil-Tab-Einstellungen", "account_edit.save": "Speichern", + "account_edit.upload_modal.back": "Zurück", + "account_edit.upload_modal.done": "Fertig", + "account_edit.upload_modal.next": "Weiter", + "account_edit.upload_modal.step_crop.zoom": "Vergrößern", + "account_edit.upload_modal.step_upload.button": "Datei hochladen", + "account_edit.upload_modal.step_upload.dragging": "Zum Hochladen hier ablegen", + "account_edit.upload_modal.step_upload.header": "Wähle ein Bild", + "account_edit.upload_modal.step_upload.hint": "WebP, PNG, GIF oder JPG. Höchstens {limit} MB groß.{br}Das Bild wird auf {width}x{height} px skaliert.", + "account_edit.upload_modal.title_add": "Profilbild hinzufügen", + "account_edit.upload_modal.title_replace": "Profilbild ändern", "account_edit.verified_modal.details": "Beweise die Echtheit deines Mastodon-Profils, indem du verifizierte Links zu deinen persönlichen Websites ergänzt. So funktioniert’s:", "account_edit.verified_modal.invisible_link.details": "Füge den Link in den Header ein. Der wichtige Teil ist rel=\"me\". Du kannst auch den Tag link im Header (statt {tag}) verwenden, jedoch muss die Internetseite ohne JavaScript abrufbar sein.", "account_edit.verified_modal.invisible_link.summary": "Wie blende ich den Link aus?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Füge bis zu {count} Konten, denen du folgst, hinzu", "collections.accounts.empty_title": "Diese Sammlung ist leer", "collections.collection_description": "Beschreibung", + "collections.collection_language": "Sprache", + "collections.collection_language_none": "Nicht festgelegt", "collections.collection_name": "Titel", "collections.collection_topic": "Thema", "collections.confirm_account_removal": "Möchtest du dieses Konto wirklich aus der Sammlung entfernen?", @@ -326,12 +364,14 @@ "collections.create_collection": "Sammlung erstellen", "collections.delete_collection": "Sammlung löschen", "collections.description_length_hint": "Maximal 100 Zeichen", + "collections.detail.accept_inclusion": "Einverstanden", "collections.detail.accounts_heading": "Konten", "collections.detail.author_added_you": "{author} hat dich zur Sammlung hinzugefügt", "collections.detail.curated_by_author": "Kuratiert von {author}", "collections.detail.curated_by_you": "Kuratiert von dir", "collections.detail.loading": "Sammlung wird geladen …", "collections.detail.other_accounts_in_collection": "Weitere Profile in dieser Sammlung:", + "collections.detail.revoke_inclusion": "Mich entfernen", "collections.detail.sensitive_note": "Diese Sammlung enthält Profile und Inhalte, die manche als anstößig empfinden.", "collections.detail.share": "Sammlung teilen", "collections.edit_details": "Details bearbeiten", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Neuester Beitrag mehr als eine Woche alt", "collections.remove_account": "Dieses Konto entfernen", "collections.report_collection": "Sammlung melden", + "collections.revoke_collection_inclusion": "Mich aus dieser Sammlung entfernen", + "collections.revoke_inclusion.confirmation": "Du wurdest aus „{collection}“ entfernt", + "collections.revoke_inclusion.error": "Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.", "collections.search_accounts_label": "Suche nach Konten, um sie hinzuzufügen …", "collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt", "collections.sensitive": "Inhaltswarnung", "collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.", + "collections.topic_special_chars_hint": "Sonderzeichen werden beim Speichern entfernt", "collections.view_collection": "Sammlungen anzeigen", "collections.view_other_collections_by_user": "Andere Sammlungen dieses Kontos ansehen", "collections.visibility_public": "Öffentlich", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Follower entfernen", "confirmations.remove_from_followers.message": "{name} wird dir nicht länger folgen. Bist du dir sicher?", "confirmations.remove_from_followers.title": "Follower entfernen?", + "confirmations.revoke_collection_inclusion.confirm": "Mich entfernen", + "confirmations.revoke_collection_inclusion.message": "Diese Aktion kann nicht rückgängig gemacht werden. Es wird daher nicht möglich sein, dich zu dieser Sammlung erneut hinzuzufügen.", + "confirmations.revoke_collection_inclusion.title": "Möchtest du wirklich aus dieser Sammlung entfernt werden?", "confirmations.revoke_quote.confirm": "Zitat entfernen", "confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.", "confirmations.revoke_quote.title": "Mein Zitat aus diesem Beitrag entfernen?", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Angehefteter Beitrag} other {Angeheftete Beiträge}}", "featured_carousel.slide": "Beitrag {current, number} von {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "In deinen letzten Beiträgen hast du {items} verwendet. Möchtest du sie zu deinen vorgestellten Hashtags hinzufügen?", + "featured_tags.suggestions.add": "Hinzufügen", + "featured_tags.suggestions.added": "Du kannst deine vorgestellten Hashtags jederzeit in den Einstellungen Profil bearbeiten > Vorgestellte Hashtags anpassen.", + "featured_tags.suggestions.dismiss": "Nein danke", "filter_modal.added.context_mismatch_explanation": "Diese Filterkategorie gilt nicht für den Kontext, in welchem du auf diesen Beitrag zugegriffen hast. Wenn der Beitrag auch in diesem Kontext gefiltert werden soll, musst du den Filter bearbeiten.", "filter_modal.added.context_mismatch_title": "Kontext stimmt nicht überein!", "filter_modal.added.expired_explanation": "Diese Filterkategorie ist abgelaufen. Du musst das Ablaufdatum für diese Kategorie ändern.", @@ -936,20 +987,20 @@ "notifications.policy.filter_private_mentions_title": "unerwünschten privaten Erwähnungen", "notifications.policy.title": "Benachrichtigungen verwalten von …", "notifications_permission_banner.enable": "Aktiviere Desktop-Benachrichtigungen", - "notifications_permission_banner.how_to_control": "Um Benachrichtigungen zu erhalten, wenn Mastodon nicht geöffnet ist, aktiviere die Desktop-Benachrichtigungen. Du kannst genau bestimmen, welche Arten von Interaktionen Desktop-Benachrichtigungen über die {icon} -Taste erzeugen, sobald diese aktiviert sind.", + "notifications_permission_banner.how_to_control": "Aktiviere Desktop-Benachrichtigungen, um Mitteilungen zu erhalten, wenn Mastodon nicht geöffnet ist. Du kannst für jede Kategorie einstellen, ob du Desktop-Benachrichtigungen erhalten möchtest. Sobald sie aktiviert sind, klicke dafür auf das {icon} -Symbol.", "notifications_permission_banner.title": "Nichts verpassen", "onboarding.follows.back": "Zurück", - "onboarding.follows.done": "Fertig", "onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden – oder du versuchst es später erneut.", + "onboarding.follows.next": "Nächster Schritt: Profil einrichten", "onboarding.follows.search": "Suchen", "onboarding.follows.title": "Folge Profilen, um loszulegen", "onboarding.profile.discoverable": "Mein Profil darf entdeckt werden", "onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.", "onboarding.profile.display_name": "Anzeigename", "onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …", + "onboarding.profile.finish": "Fertig", "onboarding.profile.note": "Über mich", "onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …", - "onboarding.profile.save_and_continue": "Speichern und fortfahren", "onboarding.profile.title": "Profil einrichten", "onboarding.profile.upload_avatar": "Profilbild hochladen", "onboarding.profile.upload_header": "Titelbild hochladen", @@ -1153,7 +1204,7 @@ "status.quote_error.pending_approval": "Veröffentlichung ausstehend", "status.quote_error.pending_approval_popout.body": "Auf Mastodon kannst du selbst bestimmen, ob du von anderen zitiert werden darfst oder nicht – oder nur nach individueller Genehmigung. Wir warten in diesem Fall noch auf die Genehmigung des ursprünglichen Profils. Bis dahin steht die Veröffentlichung des Beitrags mit dem zitierten Post noch aus.", "status.quote_error.revoked": "Beitrag durch Autor*in entfernt", - "status.quote_followers_only": "Nur Follower können diesen Beitrag zitieren", + "status.quote_followers_only": "Nur Follower dürfen zitieren", "status.quote_manual_review": "Autor*in wird deine Anfrage manuell überprüfen", "status.quote_noun": "Zitat", "status.quote_policy_change": "Ändern, wer zitieren darf", @@ -1220,11 +1271,11 @@ "upload_error.limit": "Dateiupload-Limit überschritten.", "upload_error.poll": "Medien-Anhänge sind zusammen mit Umfragen nicht erlaubt.", "upload_error.quote": "Medien-Anhänge sind zusammen mit Zitaten nicht erlaubt.", - "upload_form.drag_and_drop.instructions": "Drücke zum Aufnehmen eines Medienanhangs die Eingabe- oder Leertaste. Verwende beim Ziehen die Pfeiltasten, um den Medienanhang zur gewünschten Position zu bewegen. Drücke erneut die Eingabe- oder Leertaste, um den Medienanhang an der gewünschten Position abzulegen. Mit der Escape-Taste kannst du den Vorgang abbrechen.", + "upload_form.drag_and_drop.instructions": "Drücke zum Auswählen eines Medienanhangs die Eingabe- oder Leertaste. Verwende beim Ziehen die Pfeiltasten, um den Medienanhang zur gewünschten Position zu bewegen. Drücke erneut die Eingabe- oder Leertaste, um den Medienanhang an der gewünschten Position abzulegen. Mit der Escape-Taste kannst du den Vorgang abbrechen.", "upload_form.drag_and_drop.on_drag_cancel": "Das Ziehen wurde abgebrochen und der Medienanhang {item} wurde abgelegt.", "upload_form.drag_and_drop.on_drag_end": "Der Medienanhang {item} wurde abgelegt.", "upload_form.drag_and_drop.on_drag_over": "Der Medienanhang {item} wurde bewegt.", - "upload_form.drag_and_drop.on_drag_start": "Der Medienanhang {item} wurde aufgenommen.", + "upload_form.drag_and_drop.on_drag_start": "Der Medienanhang {item} wurde ausgewählt.", "upload_form.edit": "Bearbeiten", "upload_progress.label": "Upload läuft …", "upload_progress.processing": "Wird verarbeitet …", @@ -1252,7 +1303,7 @@ "visibility_modal.helper.privacy_private_self_quote": "Beiträge mit privaten Erwähnungen können öffentlich nicht zitiert werden.", "visibility_modal.helper.private_quoting": "Beiträge, die nur für deine Follower bestimmt sind und auf Mastodon verfasst wurden, können nicht von anderen zitiert werden.", "visibility_modal.helper.unlisted_quoting": "Sollten dich andere zitieren, werden ihre zitierten Beiträge ebenfalls nicht in den Trends und öffentlichen Timelines angezeigt.", - "visibility_modal.instructions": "Lege fest, wer mit diesem Beitrag interagieren darf. Du hast auch die Möglichkeit, diese Einstellung auf alle zukünftigen Beiträge anzuwenden. Gehe zu: Einstellungen > Standardeinstellungen für Beiträge", + "visibility_modal.instructions": "Lege fest, wer mit diesem Beitrag interagieren darf. Du hast auch die Möglichkeit, diese Einstellung auf alle zukünftigen Beiträge anzuwenden. Navigiere zu: Einstellungen > Standardeinstellungen für Beiträge", "visibility_modal.privacy_label": "Sichtbarkeit", "visibility_modal.quote_followers": "Nur meine Follower dürfen mich zitieren", "visibility_modal.quote_label": "Wer darf mich zitieren?", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 49ba9480f38e1a..16b51552fd8612 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -10,7 +10,7 @@ "about.domain_blocks.suspended.explanation": "Τα δεδομένα αυτού του διακομιστή, δε θα επεξεργάζονται, δε θα αποθηκεύονται και δε θα ανταλλάσσονται, καθιστώντας οποιαδήποτε αλληλεπίδραση ή επικοινωνία με χρήστες από αυτόν το διακομιστή αδύνατη.", "about.domain_blocks.suspended.title": "Σε αναστολή", "about.language_label": "Γλώσσα", - "about.not_available": "Αυτές οι πληροφορίες δεν έχουν είναι διαθέσιμες σε αυτόν τον διακομιστή.", + "about.not_available": "Αυτές οι πληροφορίες δεν έχουν γίνει διαθέσιμες σε αυτόν τον διακομιστή.", "about.powered_by": "Αποκεντρωμένο μέσο κοινωνικής δικτύωσης που βασίζεται στο {mastodon}", "about.rules": "Κανόνες διακομιστή", "account.account_note_header": "Προσωπική σημείωση", @@ -67,13 +67,12 @@ "account.followers_counter": "{count, plural, one {{counter} ακόλουθος} other {{counter} ακόλουθοι}}", "account.followers_you_know_counter": "{counter} που ξέρεις", "account.following": "Ακολουθείτε", - "account.following_counter": "{count, plural, one {{counter} ακολουθεί} other {{counter} ακολουθούν}}", + "account.following_counter": "{count, plural, one {{counter} ακολουθεί} other {{counter} ακολουθεί}}", "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμη.", "account.follows_you": "Σε ακολουθεί", "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Επεξεργασία Προφίλ", "account_edit.custom_fields.name": "πεδίο", "account_edit.custom_fields.placeholder": "Προσθέστε τις αντωνυμίες σας, εξωτερικούς συνδέσμους ή οτιδήποτε άλλο θέλετε να μοιραστείτε.", + "account_edit.custom_fields.reorder_button": "Αναδιάταξη πεδίων", "account_edit.custom_fields.tip_content": "Μπορείς εύκολα να προσθέσεις αξιοπιστία στον Mastodon λογαριασμό σου επαληθεύοντας συνδέσμους σε οποιεσδήποτε ιστοσελίδες κατέχεις.", "account_edit.custom_fields.tip_title": "Συμβουλή: Προσθήκη επαληθευμένων συνδέσμων", "account_edit.custom_fields.title": "Προσαρμοσμένα πεδία", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Διαγραφή προσαρμοσμένου πεδίου;", "account_edit.field_edit_modal.add_title": "Προσθήκη προσαρμοσμένου πεδίου", "account_edit.field_edit_modal.edit_title": "Επεξεργασία προσαρμοσμένου πεδίου", + "account_edit.field_edit_modal.limit_header": "Ξεπεράστηκε το συνιστώμενο όριο χαρακτήρων", + "account_edit.field_edit_modal.limit_message": "Οι χρήστες κινητών ενδέχεται να μην βλέπουν πλήρως το πεδίο σας.", + "account_edit.field_edit_modal.link_emoji_warning": "Δεν συνιστούμε τη χρήση προσαρμοσμένων emoji σε συνδυασμό με URL. Τα προσαρμοσμένα πεδία που περιέχουν και τα δύο θα εμφανίζονται ως κείμενο μόνο αντί ως σύνδεσμος, προκειμένου να αποφευχθεί η σύγχυση του χρήστη.", "account_edit.field_edit_modal.name_hint": "Π.χ. “Προσωπική ιστοσελίδα”", "account_edit.field_edit_modal.name_label": "Ετικέτα", - "account_edit.field_edit_modal.value_hint": "Π.χ. “example.me”", + "account_edit.field_edit_modal.url_warning": "Για να προσθέσεις έναν σύνδεσμο, παρακαλούμε να συμπεριλάβεις το {protocol} στην αρχή.", + "account_edit.field_edit_modal.value_hint": "Π.χ. “https://example.me”", "account_edit.field_edit_modal.value_label": "Τιμή", + "account_edit.field_reorder_modal.drag_cancel": "Η μετακίνηση ακυρώθηκε. Το πεδίο \"{item}\" αφέθηκε.", + "account_edit.field_reorder_modal.drag_end": "Το πεδίο \"{item}\" αφέθηκε.", + "account_edit.field_reorder_modal.drag_instructions": "Για να αναδιατάξετε τα προσαρμοσμένα πεδία, πατήστε Space ή Enter. Κατά τη μετακίνηση, χρησιμοποιήστε τα πλήκτρα βέλους για να μετακινήσετε το πεδίο πάνω ή κάτω. Πατήστε Space ή Enter ξανά για να ρίξετε το πεδίο στη νέα του θέση ή πατήστε Escape για να ακυρώσετε.", + "account_edit.field_reorder_modal.drag_move": "Το πεδίο \"{item}\" μετακινήθηκε.", + "account_edit.field_reorder_modal.drag_over": "Το πεδίο \"{item}\" μετακινήθηκε στο \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Το πεδίο \"{item}\" σηκώθηκε.", + "account_edit.field_reorder_modal.handle_label": "Μετακίνηση πεδίου \"{item}\"", + "account_edit.field_reorder_modal.title": "Αναδιάταξη πεδίων", + "account_edit.image_alt_modal.add_title": "Προσθήκη εναλλακτικού κειμένου", + "account_edit.image_alt_modal.details_content": "ΚΑΝΕΤΕ:
    • Να περιγράφετε τον εαυτό σας όπως απεικονίζεται
    • Να χρησιμοποιείτε τρίτο πρόσωπο (Π.χ. “Άλεξ” αντί για “εγώ”)
    • Να είστε σύντομοι και περιεκτικοί – οι λίγες λέξεις μερικές φορές αρκούν
    ΜΗΝ ΚΑΝΕΤΕ:
    • Να ξεκινάτε με “Φωτογραφία του” – είναι περιττό για τους αναγνώστες οθόνης
    ΠΑΡΑΔΕΙΓΜΑ:
    • “Ο Άλεξ με μια πράσινη μπλούζα και γυαλιά”
    ", + "account_edit.image_alt_modal.details_title": "Συμβουλές: Εναλλακτικό κείμενο για εικόνες προφίλ", + "account_edit.image_alt_modal.edit_title": "Επεξεργασία εναλλακτικού κειμένου", + "account_edit.image_alt_modal.text_hint": "Το εναλλακτικό κείμενο βοηθά τους χρήστες με αναγνώστη οθόνης να κατανοήσουν το περιεχόμενό σας.", + "account_edit.image_alt_modal.text_label": "Εναλλακτικό κείμενο", + "account_edit.image_delete_modal.confirm": "Σίγουρα θέλετε να διαγράψετε αυτήν την εικόνα; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "account_edit.image_delete_modal.delete_button": "Διαγραφή", + "account_edit.image_delete_modal.title": "Διαγραφή εικόνας;", + "account_edit.image_edit.add_button": "Προσθήκη εικόνας", + "account_edit.image_edit.alt_add_button": "Προσθήκη εναλλακτικού κειμένου", + "account_edit.image_edit.alt_edit_button": "Επεξεργασία εναλλακτικού κειμένου", + "account_edit.image_edit.remove_button": "Αφαίρεση εικόνας", + "account_edit.image_edit.replace_button": "Αντικατάσταση εικόνας", "account_edit.name_modal.add_title": "Προσθήκη εμφανιζόμενου ονόματος", "account_edit.name_modal.edit_title": "Επεξεργασία εμφανιζόμενου ονόματος", "account_edit.profile_tab.button_label": "Προσαρμογή", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Προσαρμόστε τις καρτέλες στο προφίλ σας και τι εμφανίζουν.", "account_edit.profile_tab.title": "Ρυθμίσεις καρτέλας προφίλ", "account_edit.save": "Αποθήκευση", + "account_edit.upload_modal.back": "Πίσω", + "account_edit.upload_modal.done": "Έγινε", + "account_edit.upload_modal.next": "Επόμενο", + "account_edit.upload_modal.step_crop.zoom": "Μεγέθυνση", + "account_edit.upload_modal.step_upload.button": "Περιήγηση αρχείων", + "account_edit.upload_modal.step_upload.dragging": "Αποθέστε εδώ για ανέβασμα", + "account_edit.upload_modal.step_upload.header": "Επιλέξτε μια εικόνα", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF ή JPG μορφή, μέχρι {limit}MB.{br}Η ανάλυση της εικόνας θα προσαρμοστεί στα {width}x{height}px.", + "account_edit.upload_modal.title_add": "Προσθήκη εικόνας προφίλ", + "account_edit.upload_modal.title_replace": "Αντικατάσταση εικόνας προφίλ", "account_edit.verified_modal.details": "Πρόσθεσε αξιοπιστία στο Mastodon προφίλ σας επαληθεύοντας συνδέσμους σε προσωπικές ιστοσελίδες. Ορίστε πως δουλεύει:", "account_edit.verified_modal.invisible_link.details": "Πρόσθεσε τον σύνδεσμο στην κεφαλίδα σου. Το σημαντικό μέρος είναι το rel=\"me\" που αποτρέπει την μίμηση σε ιστοσελίδες με περιεχόμενο παραγόμενο από χρήστες. Μπορείς ακόμα να χρησιμοποιήσεις μια ετικέτα link στην κεφαλίδα της σελίδας αντί για {tag}, αλλά η HTML πρέπει να είναι προσβάσιμη χωρίς την εκτέλεση JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Πώς κάνω αυτόν τον σύνδεσμο αόρατο;", @@ -219,7 +255,7 @@ "alt_text_modal.change_thumbnail": "Αλλαγή μικρογραφίας", "alt_text_modal.describe_for_people_with_hearing_impairments": "Περιέγραψε αυτό για άτομα με προβλήματα ακοής…", "alt_text_modal.describe_for_people_with_visual_impairments": "Περιέγραψε αυτό για άτομα με προβλήματα όρασης…", - "alt_text_modal.done": "Ολοκληρώθηκε", + "alt_text_modal.done": "Τέλος", "announcement.announcement": "Ανακοίνωση", "annual_report.announcement.action_build": "Φτιάξε το Wrapstodon μου", "annual_report.announcement.action_dismiss": "Όχι, ευχαριστώ", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε", "collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή", "collections.collection_description": "Περιγραφή", + "collections.collection_language": "Γλώσσα", + "collections.collection_language_none": "Καμία", "collections.collection_name": "Όνομα", "collections.collection_topic": "Θέμα", "collections.confirm_account_removal": "Σίγουρα θέλετε να αφαιρέσετε αυτόν τον λογαριασμό από αυτή τη συλλογή;", @@ -326,12 +364,14 @@ "collections.create_collection": "Δημιουργία συλλογής", "collections.delete_collection": "Διαγραφή συλλογής", "collections.description_length_hint": "Όριο 100 χαρακτήρων", + "collections.detail.accept_inclusion": "Εντάξει", "collections.detail.accounts_heading": "Λογαριασμοί", "collections.detail.author_added_you": "Ο/Η {author} σας πρόσθεσε σε αυτήν τη συλλογή", "collections.detail.curated_by_author": "Επιμέλεια από {author}", "collections.detail.curated_by_you": "Επιμέλεια από εσάς", "collections.detail.loading": "Γίνεται φόρτωση της συλλογής…", "collections.detail.other_accounts_in_collection": "Άλλοι σε αυτήν τη συλλογή:", + "collections.detail.revoke_inclusion": "Αφαίρεσε με", "collections.detail.sensitive_note": "Αυτή η συλλογή περιέχει λογαριασμούς και περιεχόμενο που μπορεί να είναι ευαίσθητα σε ορισμένους χρήστες.", "collections.detail.share": "Κοινοποιήστε αυτήν τη συλλογή", "collections.edit_details": "Επεξεργασία λεπτομερειών", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Τελευταία ανάρτηση πριν από μια εβδομάδα", "collections.remove_account": "Αφαίρεση λογαριασμού", "collections.report_collection": "Αναφορά αυτής της συλλογής", + "collections.revoke_collection_inclusion": "Αφαίρεσε τον εαυτό μου από αυτήν τη συλλογή", + "collections.revoke_inclusion.confirmation": "Έχεις αφαιρεθεί από τη συλλογή \"{collection}\"", + "collections.revoke_inclusion.error": "Υπήρξε ένα σφάλμα, παρακαλούμε προσπαθήστε ξανά αργότερα.", "collections.search_accounts_label": "Αναζήτηση λογαριασμών για προσθήκη…", "collections.search_accounts_max_reached": "Έχετε προσθέσει τον μέγιστο αριθμό λογαριασμών", "collections.sensitive": "Ευαίσθητο", "collections.topic_hint": "Προσθέστε μια ετικέτα που βοηθά άλλους να κατανοήσουν το κύριο θέμα αυτής της συλλογής.", + "collections.topic_special_chars_hint": "Οι ειδικοί χαρακτήρες θα αφαιρεθούν κατά την αποθήκευση", "collections.view_collection": "Προβολή συλλογής", "collections.view_other_collections_by_user": "Δείτε άλλες συλλογές από αυτόν τον χρήστη", "collections.visibility_public": "Δημόσια", @@ -393,7 +437,7 @@ "combobox.no_results_found": "Κανένα αποτέλεσμα για αυτήν την αναζήτηση", "combobox.open_results": "Άνοιγμα αποτελεσμάτων", "combobox.results_available": "{count, plural, one {# πρόταση διαθέσιμη} other {# προτάσεις διαθέσιμες}}. Χρησιμοποιήστε τα βελάκια πάνω και κάτω για να πλοηγηθείτε. Πατήστε Enter για να επιλέξετε.", - "community.column_settings.local_only": "Τοπικά μόνο", + "community.column_settings.local_only": "Τοπική μόνο", "community.column_settings.media_only": "Μόνο πολυμέσα", "community.column_settings.remote_only": "Απομακρυσμένα μόνο", "compose.error.blank_post": "Η ανάρτηση δεν μπορεί να είναι κενή.", @@ -450,7 +494,7 @@ "confirmations.logout.confirm": "Αποσύνδεση", "confirmations.logout.message": "Σίγουρα θέλεις να αποσυνδεθείς;", "confirmations.logout.title": "Αποσύνδεση;", - "confirmations.missing_alt_text.confirm": "Προσθήκη εναλ κειμένου", + "confirmations.missing_alt_text.confirm": "Προσθήκη εναλλακτικού κειμένου", "confirmations.missing_alt_text.message": "Η ανάρτησή σου περιέχει πολυμέσα χωρίς εναλλακτικό κείμενο. Η προσθήκη περιγραφών βοηθά να γίνει το περιεχόμενό σου προσβάσιμο σε περισσότερους ανθρώπους.", "confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει", "confirmations.missing_alt_text.title": "Προσθήκη εναλλακτικού κειμένου;", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Αφαίρεση ακολούθου", "confirmations.remove_from_followers.message": "Ο χρήστης {name} θα σταματήσει να σε ακολουθεί. Σίγουρα θες να συνεχίσεις;", "confirmations.remove_from_followers.title": "Αφαίρεση ακολούθου;", + "confirmations.revoke_collection_inclusion.confirm": "Αφαίρεσε με", + "confirmations.revoke_collection_inclusion.message": "Αυτή η ενέργεια είναι μόνιμη, και ο επιμελητής δεν θα μπορεί να σε προσθέσει ξανά στη συλλογή αργότερα.", + "confirmations.revoke_collection_inclusion.title": "Αφαίρεσε τον εαυτό σου από αυτήν τη συλλογή;", "confirmations.revoke_quote.confirm": "Αφαίρεση ανάρτησης", "confirmations.revoke_quote.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", "confirmations.revoke_quote.title": "Αφαίρεση ανάρτησης;", @@ -529,7 +576,7 @@ "emoji_button.custom": "Προσαρμοσμένα", "emoji_button.flags": "Σημαίες", "emoji_button.food": "Φαγητά & Ποτά", - "emoji_button.label": "Εισάγετε emoji", + "emoji_button.label": "Εισαγωγή emoji", "emoji_button.nature": "Φύση", "emoji_button.not_found": "Δε βρέθηκε αντιστοίχιση εμότζι", "emoji_button.objects": "Αντικείμενα", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Καρφιτσωμένη Ανάρτηση} other {Καρφιτσωμένες Αναρτήσεις}}", "featured_carousel.slide": "Ανάρτηση {current, number} από {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Τον τελευταίο καιρό έχετε δημοσιεύσει για {items}. Προσθέστε αυτά ως αναδεδειγμένες ετικέτες;", + "featured_tags.suggestions.add": "Προσθήκη", + "featured_tags.suggestions.added": "Διαχειριστείτε τις αναδεδειγμένες ετικέτες σας οποιαδήποτε στιγμή κάτω από το Επεξεργασία προφίλ > Αναδεδειγμένες ετικέτες.", + "featured_tags.suggestions.dismiss": "Όχι, ευχαριστώ", "filter_modal.added.context_mismatch_explanation": "Αυτή η κατηγορία φίλτρων δεν ισχύει για το περιεχόμενο εντός του οποίου προσπελάσατε αυτή την ανάρτηση. Αν θέλετε να φιλτραριστεί η ανάρτηση και εντός αυτού του πλαισίου, θα πρέπει να τροποποιήσετε το φίλτρο.", "filter_modal.added.context_mismatch_title": "Ασυμφωνία περιεχομένου!", "filter_modal.added.expired_explanation": "Αυτή η κατηγορία φίλτρων έχει λήξει, πρέπει να αλλάξετε την ημερομηνία λήξης για να ισχύσει.", @@ -685,7 +736,7 @@ "ignore_notifications_modal.not_following_title": "Αγνόηση ειδοποιήσεων από άτομα που δεν ακολουθείς;", "ignore_notifications_modal.private_mentions_title": "Αγνόηση ειδοποιήσεων από μη ζητηθείσες ιδιωτικές επισημάνσεις;", "info_button.label": "Βοήθεια", - "info_button.what_is_alt_text": "Το εναλλακτικό κείμενο παρέχει περιγραφές εικόνας για άτομα με προβλήματα όρασης, διαδικτυακές συνδέσεις χαμηλής ταχύτητας ή για άτομα που αναζητούν επιπλέον περιεχόμενο.\\n\\nΜπορείς να βελτιώσεις την προσβασιμότητα και την κατανόηση για όλους, γράφοντας σαφές, συνοπτικό και αντικειμενικό εναλλακτικό κείμενο.\\n\\n
    • Κατέγραψε σημαντικά στοιχεία
    • \\n
    • Συνόψισε το κείμενο στις εικόνες
    • \\n
    • Χρησιμοποίησε δομή κανονικής πρότασης
    • \\n
    • Απέφυγε περιττές πληροφορίες
    • \\n
    • Εστίασε στις τάσεις και τα βασικά ευρήματα σε σύνθετα οπτικά στοιχεία (όπως διαγράμματα ή χάρτες)
    ", + "info_button.what_is_alt_text": "

    Τι είναι το εναλλακτικό κείμενο;

    Το εναλλακτικό κείμενο (alt text) παρέχει περιγραφές εικόνας για άτομα με προβλήματα όρασης, διαδικτυακές συνδέσεις χαμηλής ταχύτητας ή για άτομα που αναζητούν επιπλέον περιεχόμενο.

    Μπορείς να βελτιώσεις την προσβασιμότητα και την κατανόηση για όλους, γράφοντας σαφές, συνοπτικό και αντικειμενικό εναλλακτικό κείμενο.

    • Κατέγραψε σημαντικά στοιχεία
    • Συνόψισε το κείμενο στις εικόνες
    • Χρησιμοποίησε δομή κανονικής πρότασης
    • Απέφυγε περιττές πληροφορίες
    • Εστίασε στις τάσεις και τα βασικά ευρήματα σε σύνθετα οπτικά στοιχεία (όπως διαγράμματα ή χάρτες)
    ", "interaction_modal.action": "Για να αλληλεπιδράσετε με την ανάρτηση του/της {name}, πρέπει να συνδεθείτε στον λογαριασμό σας σε οποιονδήποτε διακομιστή Mastodon χρησιμοποιείτε.", "interaction_modal.go": "Πάμε", "interaction_modal.no_account_yet": "Δεν έχεις ακόμη λογαριασμό;", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Για να λαμβάνεις ειδοποιήσεις όταν το Mastodon δεν είναι ανοιχτό, ενεργοποίησε τις ειδοποιήσεις επιφάνειας εργασίας. Μπορείς να ελέγξεις με ακρίβεια ποιοι τύποι αλληλεπιδράσεων δημιουργούν ειδοποιήσεις επιφάνειας εργασίας μέσω του κουμπιού {icon} μόλις ενεργοποιηθούν.", "notifications_permission_banner.title": "Μη χάσεις στιγμή", "onboarding.follows.back": "Πίσω", - "onboarding.follows.done": "Έγινε", "onboarding.follows.empty": "Δυστυχώς, δεν μπορούν να εμφανιστούν αποτελέσματα αυτή τη στιγμή. Μπορείς να προσπαθήσεις να χρησιμοποιήσεις την αναζήτηση ή να περιηγηθείς στη σελίδα εξερεύνησης για να βρεις άτομα να ακολουθήσεις ή να δοκιμάσεις ξανά αργότερα.", + "onboarding.follows.next": "Επόμενο: Ρυθμίστε το προφίλ σας", "onboarding.follows.search": "Αναζήτηση", "onboarding.follows.title": "Ακολούθησε άτομα για να ξεκινήσεις", "onboarding.profile.discoverable": "Κάνε το προφίλ μου ανακαλύψιμο", "onboarding.profile.discoverable_hint": "Όταν επιλέγεις την δυνατότητα ανακάλυψης στο Mastodon, οι αναρτήσεις σου μπορεί να εμφανιστούν στα αποτελέσματα αναζήτησης και τις τάσεις, και το προφίλ σου μπορεί να προτείνεται σε άτομα με παρόμοια ενδιαφέροντα με εσένα.", "onboarding.profile.display_name": "Εμφανιζόμενο όνομα", "onboarding.profile.display_name_hint": "Το πλήρες ή το διασκεδαστικό σου όνομα…", + "onboarding.profile.finish": "Ολοκλήρωση", "onboarding.profile.note": "Βιογραφικό", "onboarding.profile.note_hint": "Μπορείς να @επισημάνεις άλλα άτομα ή #ετικέτες…", - "onboarding.profile.save_and_continue": "Αποθήκευση και συνέχεια", "onboarding.profile.title": "Ρύθμιση προφίλ", "onboarding.profile.upload_avatar": "Μεταφόρτωση εικόνας προφίλ", "onboarding.profile.upload_header": "Μεταφόρτωση κεφαλίδας προφίλ", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 0637704eb206d7..59bed6624b8a60 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Edit Profile", "account_edit.custom_fields.name": "field", "account_edit.custom_fields.placeholder": "Add your pronouns, external links, or anything else you’d like to share.", + "account_edit.custom_fields.reorder_button": "Reorder fields", "account_edit.custom_fields.tip_content": "You can easily add credibility to your Mastodon account by verifying links to any websites you own.", "account_edit.custom_fields.tip_title": "Tip: adding verified links", "account_edit.custom_fields.title": "Custom fields", @@ -167,10 +167,27 @@ "account_edit.field_delete_modal.title": "Delete custom field?", "account_edit.field_edit_modal.add_title": "Add custom field", "account_edit.field_edit_modal.edit_title": "Edit custom field", + "account_edit.field_edit_modal.limit_header": "Recommended character limit exceeded", + "account_edit.field_edit_modal.limit_message": "Mobile users might not see your field in full.", + "account_edit.field_edit_modal.link_emoji_warning": "We recommend against the use of custom emoji in combination with URLs. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.", "account_edit.field_edit_modal.name_hint": "Eg “Personal website”", "account_edit.field_edit_modal.name_label": "Label", - "account_edit.field_edit_modal.value_hint": "Eg “example.me”", + "account_edit.field_edit_modal.url_warning": "To add a link, please include {protocol} at the beginning.", + "account_edit.field_edit_modal.value_hint": "Eg “https://example.me”", "account_edit.field_edit_modal.value_label": "Value", + "account_edit.field_reorder_modal.drag_cancel": "Dragging was cancelled. Field \"{item}\" was dropped.", + "account_edit.field_reorder_modal.drag_end": "Field \"{item}\" was dropped.", + "account_edit.field_reorder_modal.drag_instructions": "To rearrange custom fields, press space or enter. While dragging, use the arrow keys to move the field up or down. Press space or enter again to drop the field in its new position, or press escape to cancel.", + "account_edit.field_reorder_modal.drag_move": "Field \"{item}\" was moved.", + "account_edit.field_reorder_modal.drag_over": "Field \"{item}\" was moved over \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"", + "account_edit.field_reorder_modal.title": "Rearrange fields", + "account_edit.image_edit.add_button": "Add image", + "account_edit.image_edit.alt_add_button": "Add alt text", + "account_edit.image_edit.alt_edit_button": "Edit alt text", + "account_edit.image_edit.remove_button": "Remove image", + "account_edit.image_edit.replace_button": "Replace image", "account_edit.name_modal.add_title": "Add display name", "account_edit.name_modal.edit_title": "Edit display name", "account_edit.profile_tab.button_label": "Customise", @@ -193,7 +210,7 @@ "account_edit.verified_modal.step2.header": "Add your website as a custom field", "account_edit.verified_modal.title": "How to add a verified link", "account_edit_tags.add_tag": "Add #{tagName}", - "account_edit_tags.column_title": "Edit featured hashtags", + "account_edit_tags.column_title": "Edit Tags", "account_edit_tags.help_text": "Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile page’s Activity view.", "account_edit_tags.search_placeholder": "Enter a hashtag…", "account_edit_tags.suggestions": "Suggestions:", @@ -326,12 +343,14 @@ "collections.create_collection": "Create collection", "collections.delete_collection": "Delete collection", "collections.description_length_hint": "100 characters limit", + "collections.detail.accept_inclusion": "OK", "collections.detail.accounts_heading": "Accounts", "collections.detail.author_added_you": "{author} added you to this collection", "collections.detail.curated_by_author": "Curated by {author}", "collections.detail.curated_by_you": "Curated by you", "collections.detail.loading": "Loading collection…", "collections.detail.other_accounts_in_collection": "Others in this collection:", + "collections.detail.revoke_inclusion": "Remove me", "collections.detail.sensitive_note": "This collection contains accounts and content that may be sensitive to some users.", "collections.detail.share": "Share this collection", "collections.edit_details": "Edit details", @@ -347,10 +366,14 @@ "collections.old_last_post_note": "Last posted over a week ago", "collections.remove_account": "Remove this account", "collections.report_collection": "Report this collection", + "collections.revoke_collection_inclusion": "Remove myself from this collection", + "collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"", + "collections.revoke_inclusion.error": "There was an error, please try again later.", "collections.search_accounts_label": "Search for accounts to add…", "collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.sensitive": "Sensitive", "collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", + "collections.topic_special_chars_hint": "Special characters will be removed when saving", "collections.view_collection": "View collection", "collections.view_other_collections_by_user": "View other collections by this user", "collections.visibility_public": "Public", @@ -470,6 +493,9 @@ "confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.title": "Remove follower?", + "confirmations.revoke_collection_inclusion.confirm": "Remove me", + "confirmations.revoke_collection_inclusion.message": "This action is permanent, and the curator won't be able to re-add you to the collection later on.", + "confirmations.revoke_collection_inclusion.title": "Remove yourself from this collection?", "confirmations.revoke_quote.confirm": "Remove post", "confirmations.revoke_quote.message": "This action cannot be undone.", "confirmations.revoke_quote.title": "Remove post?", @@ -939,17 +965,17 @@ "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.title": "Never miss a thing", "onboarding.follows.back": "Back", - "onboarding.follows.done": "Done", "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.", + "onboarding.follows.next": "Next: set up your profile", "onboarding.follows.search": "Search", "onboarding.follows.title": "Follow people to get started", "onboarding.profile.discoverable": "Make my profile discoverable", "onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.", "onboarding.profile.display_name": "Display name", "onboarding.profile.display_name_hint": "Your full name or your fun name…", + "onboarding.profile.finish": "Finish", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "You can @mention other people or #hashtags…", - "onboarding.profile.save_and_continue": "Save and continue", "onboarding.profile.title": "Profile setup", "onboarding.profile.upload_avatar": "Upload profile picture", "onboarding.profile.upload_header": "Upload profile header", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 482e9b0c3b9e4b..b5023a3349bf08 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -86,7 +86,6 @@ "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}", @@ -155,38 +154,44 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_edit.bio.edit_label": "Edit bio", + "account_edit.bio.label": "bio", "account_edit.bio.placeholder": "Add a short introduction to help others identify you.", "account_edit.bio.title": "Bio", "account_edit.bio_modal.add_title": "Add bio", "account_edit.bio_modal.edit_title": "Edit bio", - "account_edit.button.add": "Add {item}", - "account_edit.button.delete": "Delete {item}", - "account_edit.button.edit": "Edit {item}", "account_edit.column_button": "Done", "account_edit.column_title": "Edit Profile", - "account_edit.custom_fields.name": "field", + "account_edit.custom_fields.add_label": "Add field", + "account_edit.custom_fields.edit_label": "Edit field", "account_edit.custom_fields.placeholder": "Add your pronouns, external links, or anything else you’d like to share.", "account_edit.custom_fields.reorder_button": "Reorder fields", "account_edit.custom_fields.tip_content": "You can easily add credibility to your Mastodon account by verifying links to any websites you own.", "account_edit.custom_fields.tip_title": "Tip: Adding verified links", "account_edit.custom_fields.title": "Custom fields", "account_edit.custom_fields.verified_hint": "How do I add a verified link?", + "account_edit.display_name.add_label": "Add display name", + "account_edit.display_name.edit_label": "Edit display name", "account_edit.display_name.placeholder": "Your display name is how your name appears on your profile and in timelines.", "account_edit.display_name.title": "Display name", - "account_edit.featured_hashtags.item": "hashtags", + "account_edit.featured_hashtags.edit_label": "Add hashtags", "account_edit.featured_hashtags.placeholder": "Help others identify, and have quick access to, your favorite topics.", "account_edit.featured_hashtags.title": "Featured hashtags", + "account_edit.field_actions.delete": "Delete field", + "account_edit.field_actions.edit": "Edit field", "account_edit.field_delete_modal.confirm": "Are you sure you want to delete this custom field? This action can’t be undone.", "account_edit.field_delete_modal.delete_button": "Delete", "account_edit.field_delete_modal.title": "Delete custom field?", "account_edit.field_edit_modal.add_title": "Add custom field", + "account_edit.field_edit_modal.discard_confirm": "Discard", + "account_edit.field_edit_modal.discard_message": "You have unsaved changes. Are you sure you want to discard them?", "account_edit.field_edit_modal.edit_title": "Edit custom field", - "account_edit.field_edit_modal.limit_header": "Recommended character limit exceeded", - "account_edit.field_edit_modal.limit_message": "Mobile users might not see your field in full.", + "account_edit.field_edit_modal.limit_warning": "Recommended character limit exceeded. Mobile users might not see your field in full.", "account_edit.field_edit_modal.link_emoji_warning": "We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.", "account_edit.field_edit_modal.name_hint": "E.g. “Personal website”", "account_edit.field_edit_modal.name_label": "Label", - "account_edit.field_edit_modal.value_hint": "E.g. “example.me”", + "account_edit.field_edit_modal.url_warning": "To add a link, please include {protocol} at the beginning.", + "account_edit.field_edit_modal.value_hint": "E.g. “https://example.me”", "account_edit.field_edit_modal.value_label": "Value", "account_edit.field_reorder_modal.drag_cancel": "Dragging was cancelled. Field \"{item}\" was dropped.", "account_edit.field_reorder_modal.drag_end": "Field \"{item}\" was dropped.", @@ -196,6 +201,22 @@ "account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"", "account_edit.field_reorder_modal.title": "Rearrange fields", + "account_edit.image_alt_modal.add_title": "Add alt text", + "account_edit.image_alt_modal.details_content": "DO:
    • Describe yourself as pictured
    • Use third person language (e.g. “Alex” instead of “me”)
    • Be succinct – a few words is often enough
    DON’T:
    • Start with “Photo of” – it’s redundant for screen readers
    EXAMPLE:
    • “Alex wearing a green shirt and glasses”
    ", + "account_edit.image_alt_modal.details_title": "Tips: Alt text for profile photos", + "account_edit.image_alt_modal.edit_title": "Edit alt text", + "account_edit.image_alt_modal.text_hint": "Alt text helps screen reader users to understand your content.", + "account_edit.image_alt_modal.text_label": "Alt text", + "account_edit.image_delete_modal.confirm": "Are you sure you want to delete this image? This action can’t be undone.", + "account_edit.image_delete_modal.delete_button": "Delete", + "account_edit.image_delete_modal.title": "Delete image?", + "account_edit.image_edit.add_button": "Add image", + "account_edit.image_edit.alt_add_button": "Add alt text", + "account_edit.image_edit.alt_edit_button": "Edit alt text", + "account_edit.image_edit.remove_button": "Remove image", + "account_edit.image_edit.replace_button": "Replace image", + "account_edit.item_list.delete": "Delete {name}", + "account_edit.item_list.edit": "Edit {name}", "account_edit.name_modal.add_title": "Add display name", "account_edit.name_modal.edit_title": "Edit display name", "account_edit.profile_tab.button_label": "Customize", @@ -210,6 +231,18 @@ "account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.", "account_edit.profile_tab.title": "Profile tab settings", "account_edit.save": "Save", + "account_edit.upload_modal.back": "Back", + "account_edit.upload_modal.done": "Done", + "account_edit.upload_modal.next": "Next", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Browse files", + "account_edit.upload_modal.step_upload.dragging": "Drop to upload", + "account_edit.upload_modal.step_upload.header": "Choose an image", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF or JPG format, up to {limit}MB.{br}Image will be scaled to {width}x{height}px.", + "account_edit.upload_modal.title_add.avatar": "Add profile photo", + "account_edit.upload_modal.title_add.header": "Add cover photo", + "account_edit.upload_modal.title_replace.avatar": "Replace profile photo", + "account_edit.upload_modal.title_replace.header": "Replace cover photo", "account_edit.verified_modal.details": "Add credibility to your Mastodon profile by verifying links to personal websites. Here’s how it works:", "account_edit.verified_modal.invisible_link.details": "Add the link to your header. The important part is rel=\"me\" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.", "account_edit.verified_modal.invisible_link.summary": "How do I make the link invisible?", @@ -218,11 +251,13 @@ "account_edit.verified_modal.step2.header": "Add your website as a custom field", "account_edit.verified_modal.title": "How to add a verified link", "account_edit_tags.add_tag": "Add #{tagName}", - "account_edit_tags.column_title": "Edit featured hashtags", + "account_edit_tags.column_title": "Edit Tags", "account_edit_tags.help_text": "Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile page’s Activity view.", + "account_edit_tags.max_tags_reached": "You have reached the maximum number of featured hashtags.", "account_edit_tags.search_placeholder": "Enter a hashtag…", "account_edit_tags.suggestions": "Suggestions:", "account_edit_tags.tag_status_count": "{count, plural, one {# post} other {# posts}}", + "account_list.total": "{total, plural, one {# account} other {# accounts}}", "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", @@ -436,6 +471,8 @@ "collections.accounts.empty_description": "Add up to {count} accounts you follow", "collections.accounts.empty_title": "This collection is empty", "collections.collection_description": "Description", + "collections.collection_language": "Language", + "collections.collection_language_none": "None", "collections.collection_name": "Name", "collections.collection_topic": "Topic", "collections.confirm_account_removal": "Are you sure you want to remove this account from this collection?", @@ -449,12 +486,14 @@ "collections.create_collection": "Create collection", "collections.delete_collection": "Delete collection", "collections.description_length_hint": "100 characters limit", + "collections.detail.accept_inclusion": "Okay", "collections.detail.accounts_heading": "Accounts", "collections.detail.author_added_you": "{author} added you to this collection", "collections.detail.curated_by_author": "Curated by {author}", "collections.detail.curated_by_you": "Curated by you", "collections.detail.loading": "Loading collection…", "collections.detail.other_accounts_in_collection": "Others in this collection:", + "collections.detail.revoke_inclusion": "Remove me", "collections.detail.sensitive_note": "This collection contains accounts and content that may be sensitive to some users.", "collections.detail.share": "Share this collection", "collections.edit_details": "Edit details", @@ -470,10 +509,14 @@ "collections.old_last_post_note": "Last posted over a week ago", "collections.remove_account": "Remove this account", "collections.report_collection": "Report this collection", + "collections.revoke_collection_inclusion": "Remove myself from this collection", + "collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"", + "collections.revoke_inclusion.error": "There was an error, please try again later.", "collections.search_accounts_label": "Search for accounts to add…", "collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.sensitive": "Sensitive", "collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", + "collections.topic_special_chars_hint": "Special characters will be removed when saving", "collections.view_collection": "View collection", "collections.view_other_collections_by_user": "View other collections by this user", "collections.visibility_public": "Public", @@ -622,6 +665,9 @@ "confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.title": "Remove follower?", + "confirmations.revoke_collection_inclusion.confirm": "Remove me", + "confirmations.revoke_collection_inclusion.message": "This action is permanent, and the curator won't be able to re-add you to the collection later on.", + "confirmations.revoke_collection_inclusion.title": "Remove yourself from this collection?", "confirmations.revoke_quote.confirm": "Remove post", "confirmations.revoke_quote.message": "This action cannot be undone.", "confirmations.revoke_quote.title": "Remove post?", @@ -740,6 +786,10 @@ "featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}", "featured_carousel.slide": "Post {current, number} of {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Lately you’ve posted about {items}. Add these as featured hashtags?", + "featured_tags.suggestions.add": "Add", + "featured_tags.suggestions.added": "Manage your featured hashtags at any time under Edit Profile > Featured hashtags.", + "featured_tags.suggestions.dismiss": "No thanks", "filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.", "filter_modal.added.context_mismatch_title": "Context mismatch!", "filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.", @@ -782,7 +832,9 @@ "follow_suggestions.who_to_follow": "Who to follow", "followed_tags": "Followed hashtags", "followers.hide_other_followers": "This user has chosen to not make their other followers visible", + "followers.title": "Following {name}", "following.hide_other_following": "This user has chosen to not make the rest of who they follow visible", + "following.title": "Followed by {name}", "footer.about": "About", "footer.about_mastodon": "About Mastodon", "footer.about_server": "About {domain}", @@ -1119,17 +1171,17 @@ "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.title": "Never miss a thing", "onboarding.follows.back": "Back", - "onboarding.follows.done": "Done", "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.", + "onboarding.follows.next": "Next: Setup your profile", "onboarding.follows.search": "Search", "onboarding.follows.title": "Follow people to get started", "onboarding.profile.discoverable": "Make my profile discoverable", "onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.", "onboarding.profile.display_name": "Display name", "onboarding.profile.display_name_hint": "Your full name or your fun name…", + "onboarding.profile.finish": "Finish", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "You can @mention other people or #hashtags…", - "onboarding.profile.save_and_continue": "Save and continue", "onboarding.profile.title": "Profile setup", "onboarding.profile.upload_avatar": "Upload profile picture", "onboarding.profile.upload_header": "Upload profile header", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 29be4fbf40bcfc..67de7b360b1e87 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -14,6 +14,7 @@ "about.powered_by": "Malcentrigita socia retejo pere de {mastodon}", "about.rules": "Reguloj de la servilo", "account.account_note_header": "Personaj notoj", + "account.add_note": "Aldoni personan noton", "account.add_or_remove_from_list": "Aldoni al aŭ forigi el listoj", "account.badges.bot": "Aŭtomata", "account.badges.group": "Grupo", @@ -33,7 +34,7 @@ "account.endorse": "Montri en profilo", "account.familiar_followers_one": "Sekvita de {name1}", "account.familiar_followers_two": "Sekvita de {name1} kaj {name2}", - "account.featured": "Montrita", + "account.featured": "Elstarigitaj", "account.featured.accounts": "Profiloj", "account.featured.hashtags": "Kradvortoj", "account.featured_tags.last_status_at": "Lasta afîŝo je {date}", @@ -59,6 +60,9 @@ "account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.", "account.media": "Aŭdovidaĵoj", "account.mention": "Mencii @{name}", + "account.menu.add_to_list": "Aldoni al listo…", + "account.menu.block": "Bloki konton", + "account.menu.block_domain": "Bloki {domain}", "account.moved_to": "{name} indikis, ke ria nova konto estas nun:", "account.mute": "Silentigi @{name}", "account.mute_notifications_short": "Silentigu sciigojn", @@ -67,6 +71,7 @@ "account.muting": "Silentas", "account.mutual": "Vi sekvas unu la alian", "account.no_bio": "Neniu priskribo estas provizita.", + "account.node_modal.field_label": "Persona noto", "account.open_original_page": "Malfermi la originalan paĝon", "account.posts": "Afiŝoj", "account.posts_with_replies": "Afiŝoj kaj respondoj", @@ -86,6 +91,10 @@ "account.unmute": "Malsilentigi @{name}", "account.unmute_notifications_short": "Malsilentigu sciigojn", "account.unmute_short": "Ne plu silentigi", + "account_edit.featured_hashtags.item": "kradvortoj", + "account_edit.featured_hashtags.title": "Elstarigitaj kradvortoj", + "account_edit.save": "Konservi", + "account_edit_tags.add_tag": "Aldoni #{tagName}", "account_note.placeholder": "Alklaku por aldoni noton", "admin.dashboard.daily_retention": "Uzantoretenprocento laŭ tag post registro", "admin.dashboard.monthly_retention": "Uzantoretenprocento laŭ monato post registro", @@ -309,8 +318,8 @@ "emoji_button.search_results": "Serĉaj rezultoj", "emoji_button.symbols": "Simboloj", "emoji_button.travel": "Vojaĝoj kaj lokoj", - "empty_column.account_featured.me": "Vi ankoraŭ nenion prezentis. Ĉu vi sciis, ke vi povas prezenti viajn plej ofte uzatajn kradvortojn, kaj eĉ la kontojn de viaj amikoj sur via profilo?", - "empty_column.account_featured_other.unknown": "Ĉi tiu konto ankoraŭ ne montris ion ajn.", + "empty_column.account_featured.me": "Vi ankoraŭ elstarigis nenion. Ĉu vi sciis, ke vi povas elstarigi viajn plej ofte uzatajn kradvortojn, kaj eĉ la kontojn de viaj amikoj sur via profilo?", + "empty_column.account_featured_other.unknown": "Ĉi tiu konto ankoraŭ ne elstarigis ion ajn.", "empty_column.account_hides_collections": "Ĉi tiu uzanto elektis ne disponebligi ĉi tiu informon", "empty_column.account_suspended": "Konto suspendita", "empty_column.account_timeline": "Neniuj afiŝoj ĉi tie!", @@ -345,6 +354,7 @@ "explore.trending_tags": "Kradvortoj", "featured_carousel.header": "{count, plural, one {Alpinglita afiŝo} other {Alpinglitaj afiŝoj}}", "featured_carousel.slide": "Afiŝo {current, number} de {max, number}", + "featured_tags.more_items": "+{count}", "filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.", "filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!", "filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.", @@ -688,7 +698,6 @@ "notifications_permission_banner.how_to_control": "Por ricevi sciigojn kiam Mastodon ne estas malfermita, ebligu labortablajn sciigojn. Vi povas regi precize kiuj specoj de interagoj generas labortablajn sciigojn per la supra butono {icon} post kiam ili estas ebligitaj.", "notifications_permission_banner.title": "Neniam preterlasas iun ajn", "onboarding.follows.back": "Reen", - "onboarding.follows.done": "Farita", "onboarding.follows.empty": "Bedaŭrinde, neniu rezulto estas montrebla nuntempe. Vi povas provi serĉi aŭ foliumi la esploran paĝon por trovi kontojn por sekvi, aŭ retrovi baldaŭ.", "onboarding.follows.search": "Serĉi", "onboarding.follows.title": "Sekvi homojn por komenci", @@ -698,7 +707,6 @@ "onboarding.profile.display_name_hint": "Via plena nomo aŭ via kromnomo…", "onboarding.profile.note": "Sinprezento", "onboarding.profile.note_hint": "Vi povas @mencii aliajn homojn aŭ #kradvortojn…", - "onboarding.profile.save_and_continue": "Konservi kaj daŭrigi", "onboarding.profile.title": "Profila fikso", "onboarding.profile.upload_avatar": "Alŝuti profilbildon", "onboarding.profile.upload_header": "Alŝuti profilkapbildon", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index b1d21b738224df..67106dc618f975 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Editar perfil", "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Agregá tus pronombres personales, enlaces externos o cualquier otra cosa que quisieras compartir.", + "account_edit.custom_fields.reorder_button": "Reordenar campos", "account_edit.custom_fields.tip_content": "Podés agregar fácilmente credibilidad a tu cuenta de Mastodon verificando enlaces a cualquier sitio web que tengas.", "account_edit.custom_fields.tip_title": "Consejo: Agregá enlaces verificados", "account_edit.custom_fields.title": "Campos personalizados", @@ -162,15 +162,41 @@ "account_edit.featured_hashtags.item": "etiquetas", "account_edit.featured_hashtags.placeholder": "Ayudá a otras personas a identificarte y a tener un rápido acceso a tus temas favoritos.", "account_edit.featured_hashtags.title": "Etiquetas destacadas", - "account_edit.field_delete_modal.confirm": "¿Estás seguro de que querés eliminar este campo personalizado? Esta acción no se puede deshacer.", + "account_edit.field_delete_modal.confirm": "¿De verdad querés eliminar este campo personalizado? Esta acción no se puede deshacer.", "account_edit.field_delete_modal.delete_button": "Eliminar", "account_edit.field_delete_modal.title": "¿Eliminar campo personalizado?", "account_edit.field_edit_modal.add_title": "Agregar campo personalizado", "account_edit.field_edit_modal.edit_title": "Editar campo personalizado", + "account_edit.field_edit_modal.limit_header": "Se excedió el límite de caracteres recomendado", + "account_edit.field_edit_modal.limit_message": "Es posible que los usuarios en dispositivos móviles no vean tu campo completamente.", + "account_edit.field_edit_modal.link_emoji_warning": "No recomendamos el uso de emojis personalizados en combinación con direcciones web. Los campos personalizados que contengan ambos solo se mostrarán como texto en lugar de como enlace, con el fin de evitar la confusión del usuario.", "account_edit.field_edit_modal.name_hint": "Por ejemplo: «Sitio web personal»", "account_edit.field_edit_modal.name_label": "Etiqueta", - "account_edit.field_edit_modal.value_hint": "Por ejemplo: «ejemplo.com.ar»", + "account_edit.field_edit_modal.url_warning": "Para agregar un enlace, por favor, incluí {protocol} al comienzo.", + "account_edit.field_edit_modal.value_hint": "Por ejemplo: «https://ejemplo.com»", "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "El arrastre fue cancelado. Se soltó el campo «{item}».", + "account_edit.field_reorder_modal.drag_end": "Se soltó el campo «{item}».", + "account_edit.field_reorder_modal.drag_instructions": "Para reordenar campos personalizados, pulsá la tecla «Espacio» o «Intro ⏎». Mientras arrastrás, usá las teclas de flecha para mover el campo hacia arriba o hacia abajo. Presioná la tecla «Espacio» o «Intro ⏎» de nuevo para soltar el campo en su nueva posición, o presioná la tecla «Escape» para cancelar.", + "account_edit.field_reorder_modal.drag_move": "El campo «{item}» fue movido.", + "account_edit.field_reorder_modal.drag_over": "El campo «{item}» fue movido sobre «{over}».", + "account_edit.field_reorder_modal.drag_start": "Campo elegido «{item}».", + "account_edit.field_reorder_modal.handle_label": "Arrastrá el campo «{item}»", + "account_edit.field_reorder_modal.title": "Reordená los campos", + "account_edit.image_alt_modal.add_title": "Agregar texto alternativo", + "account_edit.image_alt_modal.details_content": "LO QUE VA:
    • Describite como se te ve en la imagen
    • Usá un idioma en tercera persona (por ejemplo: decí «Oliver», en lugar de, simplemente, «yo»)
    • Sé breve: unas pocas palabras son suficientes
    EVITÁ:
    • Comenzar con «Foto de…»: es redundante para lectores de pantalla
    EJEMPLO DE UNA BUENA DESCRIPCIÓN:
    • «Oliver abrazando a su gato Dulce»
    ", + "account_edit.image_alt_modal.details_title": "Consejos: Texto alternativo para imágenes de perfil", + "account_edit.image_alt_modal.edit_title": "Editar texto alternativo", + "account_edit.image_alt_modal.text_hint": "El texto alternativo ayuda a los usuarios de lectores de pantalla a entender tu contenido.", + "account_edit.image_alt_modal.text_label": "Texto alternativo", + "account_edit.image_delete_modal.confirm": "¿De verdad querés eliminar esta imagen? Esta acción no se puede deshacer.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "¿Eliminar imagen?", + "account_edit.image_edit.add_button": "Agregar imagen", + "account_edit.image_edit.alt_add_button": "Agregar texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Quitar imagen", + "account_edit.image_edit.replace_button": "Reemplazar imagen", "account_edit.name_modal.add_title": "Agregar nombre a mostrar", "account_edit.name_modal.edit_title": "Editar nombre a mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Personalizá las pestañas en tu perfil y qué van a mostrar.", "account_edit.profile_tab.title": "Configuración de pestaña de perfil", "account_edit.save": "Guardar", + "account_edit.upload_modal.back": "Volver", + "account_edit.upload_modal.done": "Listo", + "account_edit.upload_modal.next": "Siguiente", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Explorar archivos", + "account_edit.upload_modal.step_upload.dragging": "Arrastrá para subir", + "account_edit.upload_modal.step_upload.header": "Elegí una imagen", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF o JPG, hasta {limit} MB.{br}La imagen será escalada a {width}x{height} píxeles.", + "account_edit.upload_modal.title_add": "Agregar imagen de perfil", + "account_edit.upload_modal.title_replace": "Reemplazar imagen de perfil", "account_edit.verified_modal.details": "Agregá credibilidad a tu perfil de Mastodon verificando enlaces a sitios web personales. Así es cómo funciona:", "account_edit.verified_modal.invisible_link.details": "Agregá el enlace a tu encabezado. La parte importante es rel=\"yo\" que evita la suplantación en sitios web con contenido generado por el usuario. Incluso podés usar una etiqueta de enlace en el encabezado de la página en lugar de {tag}, pero el código HTML debe ser accesible sin ejecutar JavaScript.", "account_edit.verified_modal.invisible_link.summary": "¿Cómo hago el enlace invisible?", @@ -287,7 +323,7 @@ "bundle_column_error.network.title": "Error de red", "bundle_column_error.retry": "Intentá de nuevo", "bundle_column_error.return": "Volver al inicio", - "bundle_column_error.routing.body": "No se pudo encontrar la página solicitada. ¿Estás seguro que la dirección web es correcta?", + "bundle_column_error.routing.body": "No se pudo encontrar la página solicitada. ¿La dirección web es correcta?", "bundle_column_error.routing.title": "404", "bundle_modal_error.close": "Cerrar", "bundle_modal_error.message": "Algo salió mal al cargar esta pantalla.", @@ -313,9 +349,11 @@ "collections.accounts.empty_description": "Agregá hasta {count} cuentas que seguís", "collections.accounts.empty_title": "Esta colección está vacía", "collections.collection_description": "Descripción", + "collections.collection_language": "Idioma", + "collections.collection_language_none": "Ninguno", "collections.collection_name": "Nombre", "collections.collection_topic": "Tema", - "collections.confirm_account_removal": "¿Estás seguro de que querés eliminar esta cuenta de esta colección?", + "collections.confirm_account_removal": "¿De verdad querés eliminar esta cuenta de esta colección?", "collections.content_warning": "Advertencia de contenido", "collections.continue": "Continuar", "collections.create.accounts_subtitle": "Solo las cuentas que seguís —las cuales optaron por ser descubiertas— pueden ser agregadas.", @@ -326,12 +364,14 @@ "collections.create_collection": "Crear colección", "collections.delete_collection": "Eliminar colección", "collections.description_length_hint": "Límite de 100 caracteres", + "collections.detail.accept_inclusion": "Aceptar", "collections.detail.accounts_heading": "Cuentas", "collections.detail.author_added_you": "{author} te agregó a esta colección", "collections.detail.curated_by_author": "Curado por {author}", "collections.detail.curated_by_you": "Curado por vos", "collections.detail.loading": "Cargando colección…", "collections.detail.other_accounts_in_collection": "Otras cuentas en esta colección:", + "collections.detail.revoke_inclusion": "Quitarme", "collections.detail.sensitive_note": "Esta colección contiene cuentas y contenido que pueden ser sensibles a algunos usuarios.", "collections.detail.share": "Compartir esta colección", "collections.edit_details": "Editar detalles", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Último mensaje hace más de una semana", "collections.remove_account": "Eliminar esta cuenta", "collections.report_collection": "Denunciar esta colección", + "collections.revoke_collection_inclusion": "Quitarme de esta colección", + "collections.revoke_inclusion.confirmation": "Saliste de «{collection}»", + "collections.revoke_inclusion.error": "Hubo un error; por favor, intentalo de nuevo más tarde.", "collections.search_accounts_label": "Buscar cuentas para agregar…", "collections.search_accounts_max_reached": "Agregaste el número máximo de cuentas", "collections.sensitive": "Sensible", "collections.topic_hint": "Agregá una etiqueta que ayude a otros usuarios a entender el tema principal de esta colección.", + "collections.topic_special_chars_hint": "Los símbolos se eliminarán al guardar", "collections.view_collection": "Abrir colección", "collections.view_other_collections_by_user": "Ver otras colecciones de este usuario", "collections.visibility_public": "Pública", @@ -393,9 +437,9 @@ "combobox.no_results_found": "No hay resultados para esta búsqueda", "combobox.open_results": "Abrir resultados", "combobox.results_available": "{count, plural, one {# sugerencia} other {# sugerencias}} disponible. Usá las teclas de flecha arriba y flecha abajo para navegar. Toca la tecla «Intro ⏎» para seleccionar.", - "community.column_settings.local_only": "Sólo local", - "community.column_settings.media_only": "Sólo medios", - "community.column_settings.remote_only": "Sólo remoto", + "community.column_settings.local_only": "Solo local", + "community.column_settings.media_only": "Solo medios", + "community.column_settings.remote_only": "Solo remoto", "compose.error.blank_post": "El mensaje no puede estar en blanco.", "compose.language.change": "Cambiar idioma", "compose.language.search": "Buscar idiomas…", @@ -404,7 +448,7 @@ "compose.saved.body": "Mensaje guardado.", "compose_form.direct_message_warning_learn_more": "Aprendé más", "compose_form.encryption_warning": "Los mensajes en Mastodon no están cifrados de extremo a extremo. No compartas ninguna información sensible al usar Mastodon.", - "compose_form.hashtag_warning": "Este mensaje no se mostrará bajo ninguna etiqueta porque no es público. Sólo los mensajes públicos se pueden buscar por etiquetas.", + "compose_form.hashtag_warning": "Este mensaje no se mostrará bajo ninguna etiqueta porque no es público. Solo los mensajes públicos se pueden buscar por etiquetas.", "compose_form.lock_disclaimer": "Tu cuenta no es {locked}. Todos pueden seguirte para ver tus mensajes marcados como \"Sólo para seguidores\".", "compose_form.lock_disclaimer.lock": "privada", "compose_form.placeholder": "¿Qué onda?", @@ -424,13 +468,13 @@ "confirmation_modal.cancel": "Cancelar", "confirmations.block.confirm": "Bloquear", "confirmations.delete.confirm": "Eliminar", - "confirmations.delete.message": "¿Estás seguro que querés eliminar este mensaje?", + "confirmations.delete.message": "¿De verdad querés eliminar este mensaje?", "confirmations.delete.title": "¿Eliminar mensaje?", "confirmations.delete_collection.confirm": "Eliminar", "confirmations.delete_collection.message": "Esta acción no se puede deshacer.", "confirmations.delete_collection.title": "¿Eliminar «{name}»?", "confirmations.delete_list.confirm": "Eliminar", - "confirmations.delete_list.message": "¿Estás seguro que querés eliminar permanentemente esta lista?", + "confirmations.delete_list.message": "¿De verdad querés eliminar permanentemente esta lista?", "confirmations.delete_list.title": "¿Eliminar lista?", "confirmations.discard_draft.confirm": "Descartar y continuar", "confirmations.discard_draft.edit.cancel": "Reanudar edición", @@ -448,7 +492,7 @@ "confirmations.follow_to_list.message": "Necesitás seguir a {name} para agregarle a una lista.", "confirmations.follow_to_list.title": "¿Querés seguirle?", "confirmations.logout.confirm": "Cerrar sesión", - "confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?", + "confirmations.logout.message": "¿De verdad querés cerrar la sesión?", "confirmations.logout.title": "¿Cerrar sesión?", "confirmations.missing_alt_text.confirm": "Agregar texto alternativo", "confirmations.missing_alt_text.message": "Tu mensaje contiene medios sin texto alternativo. Agregar descripciones ayuda a que tu contenido sea accesible para más personas.", @@ -465,11 +509,14 @@ "confirmations.quiet_post_quote_info.message": "Al citar un mensaje público pero silencioso, tu mensaje se ocultará de las líneas temporales de tendencias.", "confirmations.quiet_post_quote_info.title": "Citado de mensajes públicos pero silenciosos", "confirmations.redraft.confirm": "Eliminar mensaje original y editarlo", - "confirmations.redraft.message": "¿Estás seguro que querés eliminar este mensaje y volver a editarlo? Se perderán las veces marcadas como favorito y sus adhesiones, y las respuestas al mensaje original quedarán huérfanas.", + "confirmations.redraft.message": "¿De verdad querés eliminar este mensaje y volver a editarlo? Se perderán las veces marcadas como favorito y sus adhesiones, y las respuestas al mensaje original quedarán huérfanas.", "confirmations.redraft.title": "¿Eliminar y volver a redactar mensaje?", "confirmations.remove_from_followers.confirm": "Quitar seguidor", - "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que querés continuar?", + "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿De verdad querés continuar?", "confirmations.remove_from_followers.title": "¿Quitar seguidor?", + "confirmations.revoke_collection_inclusion.confirm": "Quitarme", + "confirmations.revoke_collection_inclusion.message": "Esta acción es permanente, y el autor no podrá volver a agregarte a la colección más adelante.", + "confirmations.revoke_collection_inclusion.title": "¿Querés quitarte de esta colección?", "confirmations.revoke_quote.confirm": "Eliminar mensaje", "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", "confirmations.revoke_quote.title": "¿Eliminar mensaje?", @@ -491,7 +538,7 @@ "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", "directory.federated": "Desde fediverso conocido", - "directory.local": "Sólo de {domain}", + "directory.local": "Solo de {domain}", "directory.new_arrivals": "Recién llegados", "directory.recently_active": "Recientemente activos", "disabled_account_banner.account_settings": "Config. de la cuenta", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Mensaje fijado} other {Mensajes fijados}}", "featured_carousel.slide": "Mensaje {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Recientemente, publicaste sobre {items}. ¿Querés agregarlas como etiquetas destacadas?", + "featured_tags.suggestions.add": "Agregar", + "featured_tags.suggestions.added": "Administrá tus etiquetas destacadas cuando quieras en Editar perfil > Etiquetas destacadas.", + "featured_tags.suggestions.dismiss": "No, gracias", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que accediste a este mensaje. Si querés que el mensaje sea filtrado también en este contexto, vas a tener que editar el filtro.", "filter_modal.added.context_mismatch_title": "¡El contexto no coincide!", "filter_modal.added.expired_explanation": "Esta categoría de filtro caducó; vas a necesitar cambiar la fecha de caducidad para que se aplique.", @@ -866,10 +917,10 @@ "notification_requests.accept": "Aceptar", "notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}", "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitud} other {Aceptar solicitudes}}", - "notification_requests.confirm_accept_multiple.message": "Estás a punto de aceptar {count, plural, one {una solicitud} other {# solicitudes}}. ¿Estás seguro de que querés continuar?", + "notification_requests.confirm_accept_multiple.message": "Estás a punto de aceptar {count, plural, one {una solicitud} other {# solicitudes}}. ¿De verdad querés continuar?", "notification_requests.confirm_accept_multiple.title": "¿Aceptar solicitudes de notificación?", "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Descartar solicitud} other {Descartar solicitudes}}", - "notification_requests.confirm_dismiss_multiple.message": "Estás a punto de descartar {count, plural, one {una solicitud} other {# solicitudes}}. No vas a poder acceder fácilmente a {count, plural, one {ella} other {ellas}} de nuevo. ¿Estás seguro de que querés continuar?", + "notification_requests.confirm_dismiss_multiple.message": "Estás a punto de descartar {count, plural, one {una solicitud} other {# solicitudes}}. No vas a poder acceder fácilmente a {count, plural, one {ella} other {ellas}} de nuevo. ¿De verdad querés continuar?", "notification_requests.confirm_dismiss_multiple.title": "¿Descartar solicitudes de notificación?", "notification_requests.dismiss": "Descartar", "notification_requests.dismiss_multiple": "{count, plural, one {Descartar # solicitud…} other {Descartar # solicitudes…}}", @@ -883,7 +934,7 @@ "notification_requests.title": "Notificaciones filtradas", "notification_requests.view": "Ver notificaciones", "notifications.clear": "Limpiar notificaciones", - "notifications.clear_confirmation": "¿Estás seguro que querés limpiar todas tus notificaciones permanentemente?", + "notifications.clear_confirmation": "¿De verdad querés limpiar todas tus notificaciones permanentemente?", "notifications.clear_title": "¿Limpiar notificaciones?", "notifications.column_settings.admin.report": "Nuevas denuncias:", "notifications.column_settings.admin.sign_up": "Nuevos registros:", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Para recibir notificaciones cuando Mastodon no está abierto, habilitá las notificaciones de escritorio. Podés controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón {icon} de arriba, una vez que estén habilitadas.", "notifications_permission_banner.title": "No te pierdas nada", "onboarding.follows.back": "Volver", - "onboarding.follows.done": "Listo", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Podés intentar usar la búsqueda o navegar por la página de exploración para encontrar cuentas a las que seguir, o intentarlo de nuevo más tarde.", + "onboarding.follows.next": "Siguiente: Configurá tu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Para comenzar, empezá a seguir cuentas", "onboarding.profile.discoverable": "Hacer que mi perfil sea detectable", "onboarding.profile.discoverable_hint": "Cuando optás por ser detectable en Mastodon, tus mensajes pueden aparecer en los resultados de búsqueda y de tendencia, y tu perfil puede ser sugerido a personas con intereses similares a los tuyos.", "onboarding.profile.display_name": "Nombre para mostrar", "onboarding.profile.display_name_hint": "Tu nombre completo o tu pseudónimo…", + "onboarding.profile.finish": "Finalizar", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Podés @mencionar otras cuentas o usar #etiquetas…", - "onboarding.profile.save_and_continue": "Guardar y continuar", "onboarding.profile.title": "Configuración del perfil", "onboarding.profile.upload_avatar": "Subir avatar", "onboarding.profile.upload_header": "Subir cabecera", @@ -1192,7 +1243,7 @@ "status.uncached_media_warning": "Previsualización no disponible", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", - "subscribed_languages.lead": "Después del cambio, sólo los mensajes en los idiomas seleccionados aparecerán en tu línea temporal Principal y en las líneas de tiempo de lista. No seleccionés ningún idioma para poder recibir mensajes en todos los idiomas.", + "subscribed_languages.lead": "Después del cambio, solo los mensajes en los idiomas seleccionados aparecerán en tu línea temporal Principal y en las líneas de tiempo de lista. No seleccionés ningún idioma para poder recibir mensajes en todos los idiomas.", "subscribed_languages.save": "Guardar cambios", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "tabs_bar.home": "Principal", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index bb9aa90281d5b4..6057084391c5a9 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "En memoria.", - "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "Se verificó la propiedad de este enlace el {date}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Editar {item}", "account_edit.column_button": "Hecho", "account_edit.column_title": "Editar perfil", + "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Añade tus pronombres, enlaces externos o cualquier otra información que quieras compartir.", + "account_edit.custom_fields.reorder_button": "Reordenar campos", + "account_edit.custom_fields.tip_content": "Puedes añadir fácilmente credibilidad a tu cuenta de Mastodon verificando los enlaces a cualquier sitio web que poseas.", + "account_edit.custom_fields.tip_title": "Consejo: Añadir enlaces verificados", "account_edit.custom_fields.title": "Campos personalizados", + "account_edit.custom_fields.verified_hint": "¿Cómo agrego un enlace verificado?", "account_edit.display_name.placeholder": "Tu nombre de usuario es el nombre que aparece en tu perfil y en las líneas de tiempo.", "account_edit.display_name.title": "Nombre para mostrar", "account_edit.featured_hashtags.item": "etiquetas", "account_edit.featured_hashtags.placeholder": "Ayuda a otros a identificar tus temas favoritos y a acceder rápidamente a ellos.", "account_edit.featured_hashtags.title": "Etiquetas destacadas", + "account_edit.field_delete_modal.confirm": "¿Estás seguro de que deseas eliminar este campo personalizado? Esta acción no se puede deshacer.", + "account_edit.field_delete_modal.delete_button": "Eliminar", + "account_edit.field_delete_modal.title": "¿Eliminar campo personalizado?", + "account_edit.field_edit_modal.add_title": "Agregar campo personalizado", + "account_edit.field_edit_modal.edit_title": "Editar campo personalizado", + "account_edit.field_edit_modal.limit_header": "Se ha superado el límite de caracteres recomendado", + "account_edit.field_edit_modal.limit_message": "Es posible que los usuarios de dispositivos móviles no vean tu campo completo.", + "account_edit.field_edit_modal.link_emoji_warning": "No recomendamos el uso de emojis personalizados en combinación con direcciones URL. Los campos personalizados que contengan ambos se mostrarán solo como texto en lugar de como un enlace, con el fin de evitar confusiones al usuario.", + "account_edit.field_edit_modal.name_hint": "Por ejemplo, «sitio web personal»", + "account_edit.field_edit_modal.name_label": "Etiqueta", + "account_edit.field_edit_modal.url_warning": "Para agregar un enlace, incluye {protocol} al principio.", + "account_edit.field_edit_modal.value_hint": "Por ejemplo: “https://ejemplo.me”", + "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "Se canceló el arrastre. Se eliminó el campo «{item}».", + "account_edit.field_reorder_modal.drag_end": "Se ha eliminado el campo «{item}».", + "account_edit.field_reorder_modal.drag_instructions": "Para reorganizar los campos personalizados, presiona la barra espaciadora o Intro. Mientras arrastras, utiliza las teclas de flecha para mover el campo hacia arriba o hacia abajo. Presiona la barra espaciadora o Intro de nuevo para soltar el campo en su nueva posición, o presiona Esc para cancelar.", + "account_edit.field_reorder_modal.drag_move": "Se ha movido el campo «{item}».", + "account_edit.field_reorder_modal.drag_over": "El campo «{item}» se ha movido a «{over}».", + "account_edit.field_reorder_modal.drag_start": "Recogido el campo «{item}».", + "account_edit.field_reorder_modal.handle_label": "Arrastra el campo «{item}»", + "account_edit.field_reorder_modal.title": "Reorganizar campos", + "account_edit.image_alt_modal.add_title": "Añadir texto alternativo", + "account_edit.image_alt_modal.details_content": "QUÉ HACER:
    • Descríbete tal y como apareces en la imagen
    • Utiliza la tercera persona (p. ej., «Alex» en lugar de «yo»)
    • Sé conciso: unas pocas palabras suelen bastar
    QUÉ NO HACER:
    • Empezar con «Foto de»: es redundante para los lectores de pantalla
    EJEMPLO:
    • «Alex usando una camiseta verde y anteojos»
    ", + "account_edit.image_alt_modal.details_title": "Consejo: Texto alternativo para las fotos de perfil", + "account_edit.image_alt_modal.edit_title": "Editar texto alternativo", + "account_edit.image_alt_modal.text_hint": "El texto alternativo ayuda a los usuarios de lectores de pantalla a entender tu contenido.", + "account_edit.image_alt_modal.text_label": "Texto alternativo", + "account_edit.image_delete_modal.confirm": "¿Estás seguro de que quieres eliminar esta imagen? Esta acción no se puede deshacer.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "¿Eliminar imagen?", + "account_edit.image_edit.add_button": "Añadir imagen", + "account_edit.image_edit.alt_add_button": "Añadir texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Eliminar imagen", + "account_edit.image_edit.replace_button": "Reemplazar imagen", "account_edit.name_modal.add_title": "Añadir nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Personaliza las pestañas de tu perfil y lo que muestran.", "account_edit.profile_tab.title": "Configuración de la pestaña de perfil", "account_edit.save": "Guardar", + "account_edit.upload_modal.back": "Volver", + "account_edit.upload_modal.done": "Hecho", + "account_edit.upload_modal.next": "Siguiente", + "account_edit.upload_modal.step_crop.zoom": "Acercar", + "account_edit.upload_modal.step_upload.button": "Explorar archivos", + "account_edit.upload_modal.step_upload.dragging": "Arrastrar para subir", + "account_edit.upload_modal.step_upload.header": "Elegir una imagen", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF o JPG, con un tamaño máximo de {limit} MB.{br}La imagen se redimensionará a {width} x {height} píxeles.", + "account_edit.upload_modal.title_add": "Agregar foto de perfil", + "account_edit.upload_modal.title_replace": "Reemplazar foto de perfil", + "account_edit.verified_modal.details": "Agrega credibilidad a tu perfil de Mastodon verificando los enlaces a sitios web personales. Así es como funciona:", + "account_edit.verified_modal.invisible_link.details": "Agrega el enlace a tu encabezado. La parte importante es rel=\"me\", que evita la suplantación de identidad en sitios web con contenido generado por los usuarios. Incluso puedes usar una etiqueta de enlace en el encabezado de la página en lugar de {tag}, pero el HTML debe ser accesible sin ejecutar JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "¿Cómo hago para que el enlace sea invisible?", + "account_edit.verified_modal.step1.header": "Copia el código HTML que aparece a continuación y pégalo en el encabezado de tu sitio web", + "account_edit.verified_modal.step2.details": "Si ya has agregado tu sitio web como campo personalizado, deberás eliminarlo y volver a agregarlo para activar la verificación.", + "account_edit.verified_modal.step2.header": "Agrega tu sitio web como campo personalizado", + "account_edit.verified_modal.title": "Cómo agregar un enlace verificado", "account_edit_tags.add_tag": "Añadir #{tagName}", "account_edit_tags.column_title": "Editar etiquetas destacadas", "account_edit_tags.help_text": "Las etiquetas destacadas ayudan a los usuarios a descubrir tu perfil e interactuar con él. Aparecen como filtros en la vista Actividad de tu página de perfil.", @@ -275,6 +331,8 @@ "callout.dismiss": "Descartar", "carousel.current": "Diapositiva {current, number} / {max, number}", "carousel.slide": "Diapositiva {current, number} de {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} caracteres recomendados", + "character_counter.required": "{currentLength}/{maxLength} caracteres", "closed_registrations.other_server_instructions": "Como Mastodon es descentralizado, puedes crear una cuenta en otro servidor y seguir interactuando con este.", "closed_registrations_modal.description": "La creación de una cuenta en {domain} no es posible actualmente, pero ten en cuenta que no necesitas una cuenta específicamente en {domain} para usar Mastodon.", "closed_registrations_modal.find_another_server": "Buscar otro servidor", @@ -291,6 +349,8 @@ "collections.accounts.empty_description": "Añade hasta {count} cuentas que sigues", "collections.accounts.empty_title": "Esta colección está vacía", "collections.collection_description": "Descripción", + "collections.collection_language": "Idioma", + "collections.collection_language_none": "Ninguno", "collections.collection_name": "Nombre", "collections.collection_topic": "Tema", "collections.confirm_account_removal": "¿Estás seguro/a de que quieres eliminar esta cuenta de esta colección?", @@ -304,10 +364,15 @@ "collections.create_collection": "Crear colección", "collections.delete_collection": "Eliminar colección", "collections.description_length_hint": "Limitado a 100 caracteres", + "collections.detail.accept_inclusion": "Aceptar", "collections.detail.accounts_heading": "Cuentas", + "collections.detail.author_added_you": "{author} te ha añadido a esta colección", "collections.detail.curated_by_author": "Seleccionado por {author}", "collections.detail.curated_by_you": "Seleccionado por ti", "collections.detail.loading": "Cargando colección…", + "collections.detail.other_accounts_in_collection": "Otros en esta colección:", + "collections.detail.revoke_inclusion": "Excluirme", + "collections.detail.sensitive_note": "Esta colección contiene cuentas y contenido que pueden resultar sensibles para algunos usuarios.", "collections.detail.share": "Compartir esta colección", "collections.edit_details": "Editar detalles", "collections.error_loading_collections": "Se produjo un error al intentar cargar tus colecciones.", @@ -322,10 +387,14 @@ "collections.old_last_post_note": "Última publicación hace más de una semana", "collections.remove_account": "Eliminar esta cuenta", "collections.report_collection": "Reportar esta colección", + "collections.revoke_collection_inclusion": "Excluirme de esta colección", + "collections.revoke_inclusion.confirmation": "Has sido excluido de \"{collection}\"", + "collections.revoke_inclusion.error": "Se ha producido un error, por favor, inténtalo de nuevo más tarde.", "collections.search_accounts_label": "Buscar cuentas para añadir…", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", "collections.sensitive": "Sensible", "collections.topic_hint": "Añade una etiqueta que ayude a los demás a comprender el tema principal de esta colección.", + "collections.topic_special_chars_hint": "Los caracteres especiales se eliminarán al guardar", "collections.view_collection": "Ver colección", "collections.view_other_collections_by_user": "Ver otras colecciones de este usuario", "collections.visibility_public": "Pública", @@ -399,7 +468,7 @@ "confirmation_modal.cancel": "Cancelar", "confirmations.block.confirm": "Bloquear", "confirmations.delete.confirm": "Eliminar", - "confirmations.delete.message": "¿Estás seguro de que quieres borrar esta publicación?", + "confirmations.delete.message": "¿Estás seguro de que quieres eliminar esta publicación?", "confirmations.delete.title": "¿Deseas eliminar la publicación?", "confirmations.delete_collection.confirm": "Eliminar", "confirmations.delete_collection.message": "Esta acción no se puede deshacer.", @@ -439,12 +508,15 @@ "confirmations.quiet_post_quote_info.got_it": "Entendido", "confirmations.quiet_post_quote_info.message": "Al citar una publicación pública discreta, tu publicación se ocultará de las cronologías de tendencias.", "confirmations.quiet_post_quote_info.title": "Citar publicaciones públicas discretas", - "confirmations.redraft.confirm": "Borrar y volver a borrador", - "confirmations.redraft.message": "¿Estás seguro de que quieres borrar esta publicación y editarla? Los favoritos e impulsos se perderán, y las respuestas a la publicación original quedarán separadas.", - "confirmations.redraft.title": "¿Deseas borrar y volver a redactar la publicación?", + "confirmations.redraft.confirm": "Eliminar y volver a redactar", + "confirmations.redraft.message": "¿Estás seguro de que quieres eliminar esta publicación y volver a redactarla? Se perderán tanto los «Me gusta» como los impulsos, y las respuestas a la publicación original quedarán sin referencia.", + "confirmations.redraft.title": "¿Deseas eliminar y volver a redactar la publicación?", "confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?", + "confirmations.revoke_collection_inclusion.confirm": "Excluirme", + "confirmations.revoke_collection_inclusion.message": "Esta acción es permanente y el autor no podrá volver a agregarte a la colección más adelante.", + "confirmations.revoke_collection_inclusion.title": "¿Deseas excluirte de esta colección?", "confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", "confirmations.revoke_quote.title": "¿Deseas eliminar la publicación?", @@ -457,7 +529,7 @@ "content_warning.hide": "Ocultar publicación", "content_warning.show": "Mostrar de todos modos", "content_warning.show_more": "Mostrar más", - "conversation.delete": "Borrar conversación", + "conversation.delete": "Eliminar conversación", "conversation.mark_as_read": "Marcar como leído", "conversation.open": "Ver conversación", "conversation.with": "Con {names}", @@ -500,7 +572,7 @@ "embed.instructions": "Añade esta publicación a tu sitio web con el siguiente código.", "embed.preview": "Así es como se verá:", "emoji_button.activity": "Actividad", - "emoji_button.clear": "Borrar", + "emoji_button.clear": "Eliminar", "emoji_button.custom": "Personalizado", "emoji_button.flags": "Marcas", "emoji_button.food": "Comida y bebida", @@ -556,6 +628,10 @@ "featured_carousel.header": "{count, plural,one {Publicación fijada}other {Publicaciones fijadas}}", "featured_carousel.slide": "Publicación {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Últimamente has publicado sobre {items}. ¿Deseas añadirlo como etiqueta destacada?", + "featured_tags.suggestions.add": "Añadir", + "featured_tags.suggestions.added": "Controla tus etiquetas destacadas en cualquier momento en Editar perfil > Etiquetas destacadas.", + "featured_tags.suggestions.dismiss": "No, gracias", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publicación. Si deseas que la publicación también se filtre en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_title": "¡El contexto no coincide!", "filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado; deberás cambiar la fecha de caducidad para que se aplique.", @@ -728,7 +804,7 @@ "lists.create": "Crear", "lists.create_a_list_to_organize": "Crea una nueva lista para organizar tu página de inicio", "lists.create_list": "Crear lista", - "lists.delete": "Borrar lista", + "lists.delete": "Eliminar lista", "lists.done": "Hecho", "lists.edit": "Editar lista", "lists.exclusive": "Ocultar miembros en Inicio", @@ -767,6 +843,7 @@ "navigation_bar.automated_deletion": "Eliminación automática de publicaciones", "navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.bookmarks": "Marcadores", + "navigation_bar.collections": "Colecciones", "navigation_bar.direct": "Menciones privadas", "navigation_bar.domain_blocks": "Dominios ocultos", "navigation_bar.favourites": "Favoritos", @@ -857,7 +934,7 @@ "notification_requests.title": "Notificaciones filtradas", "notification_requests.view": "Ver notificaciones", "notifications.clear": "Limpiar notificaciones", - "notifications.clear_confirmation": "¿Seguro de querer borrar permanentemente todas tus notificaciones?", + "notifications.clear_confirmation": "¿Estás seguro de que quieres eliminar definitivamente todas tus notificaciones?", "notifications.clear_title": "¿Limpiar notificaciones?", "notifications.column_settings.admin.report": "Nuevas denuncias:", "notifications.column_settings.admin.sign_up": "Registros nuevos:", @@ -913,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Para recibir notificaciones cuando Mastodon no esté abierto, habilite las notificaciones de escritorio. Puedes controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón {icon} de arriba una vez que estén habilitadas.", "notifications_permission_banner.title": "Nunca te pierdas nada", "onboarding.follows.back": "Volver", - "onboarding.follows.done": "Hecho", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar gente a la que seguir, o inténtalo de nuevo más tarde.", + "onboarding.follows.next": "Siguiente: Configura tu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Sigue personas para comenzar", "onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas", "onboarding.profile.discoverable_hint": "Cuando permites que tu perfil aparezca en búsquedas en Mastodon, tus publicaciones pueden aparecer en los resultados de búsqueda y en las tendencias, y tu perfil puede ser sugerido a personas con intereses similares a los tuyos.", "onboarding.profile.display_name": "Nombre para mostrar", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", + "onboarding.profile.finish": "Fiinalizar", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…", - "onboarding.profile.save_and_continue": "Guardar y continuar", "onboarding.profile.title": "Configuración del perfil", "onboarding.profile.upload_avatar": "Subir foto de perfil", "onboarding.profile.upload_header": "Subir encabezado de perfil", @@ -1091,7 +1168,7 @@ "status.context.show": "Mostrar", "status.continued_thread": "Hilo continuado", "status.copy": "Copiar enlace al estado", - "status.delete": "Borrar", + "status.delete": "Eliminar", "status.delete.success": "Publicación eliminada", "status.detailed_status": "Vista de conversación detallada", "status.direct": "Mención privada @{name}", @@ -1144,7 +1221,7 @@ "status.reblogged_by": "Impulsado por {name}", "status.reblogs.empty": "Nadie impulsó esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.", "status.reblogs_count": "{count, plural,one {{counter} impulso} other {{counter} impulsos}}", - "status.redraft": "Borrar y volver a borrador", + "status.redraft": "Eliminar y volver a redactar", "status.remove_bookmark": "Eliminar marcador", "status.remove_favourite": "Eliminar de favoritos", "status.remove_quote": "Eliminar", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index e7c3d8a22a1e3f..c04df9cfd1ade8 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -73,7 +73,6 @@ "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Editar {item}", "account_edit.column_button": "Hecho", "account_edit.column_title": "Editar perfil", + "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Añade tus pronombres, enlaces externos o cualquier otra cosa que quieras compartir.", + "account_edit.custom_fields.reorder_button": "Reordenar campos", + "account_edit.custom_fields.tip_content": "Puedes añadir credibilidad fácilmente a tu cuenta de Mastodon verificando los enlaces a tus propias webs.", + "account_edit.custom_fields.tip_title": "Consejo: Añade enlaces verificados", "account_edit.custom_fields.title": "Campos personalizados", + "account_edit.custom_fields.verified_hint": "¿Cómo añado un enlace verificado?", "account_edit.display_name.placeholder": "Tu nombre de usuario es el nombre que aparece en tu perfil y en las cronologías.", "account_edit.display_name.title": "Nombre para mostrar", "account_edit.featured_hashtags.item": "etiquetas", "account_edit.featured_hashtags.placeholder": "Ayuda a otros a identificar tus temas favoritos y a acceder rápidamente a ellos.", "account_edit.featured_hashtags.title": "Etiquetas destacadas", + "account_edit.field_delete_modal.confirm": "¿Estás seguro de que quieres borrar este campo personalizado? La acción no se puede deshacer.", + "account_edit.field_delete_modal.delete_button": "Borrar", + "account_edit.field_delete_modal.title": "¿Borrar campo personalizado?", + "account_edit.field_edit_modal.add_title": "Añadir campo personalizado", + "account_edit.field_edit_modal.edit_title": "Editar campo personalizado", + "account_edit.field_edit_modal.limit_header": "Se ha sobrepasado el límite de caracteres recomendado", + "account_edit.field_edit_modal.limit_message": "Los usuarios de móviles no verán tu campo entero.", + "account_edit.field_edit_modal.link_emoji_warning": "Recomendamos no usar emojis personalizados combinados con enlaces. Los campos personalizados que contengan ambos solo se mostrarán como texto en vez de un enlace, para evitar confusiones.", + "account_edit.field_edit_modal.name_hint": "Ej. \"Web personal\"", + "account_edit.field_edit_modal.name_label": "Etiqueta", + "account_edit.field_edit_modal.url_warning": "Para añadir un enlace, incluye {protocol} al principio.", + "account_edit.field_edit_modal.value_hint": "Ejemplo: “https://example.me”", + "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "El arrastre se ha cancelado. El campo \"{item}\" se ha soltado.", + "account_edit.field_reorder_modal.drag_end": "El campo \"{item}\" se ha soltado.", + "account_edit.field_reorder_modal.drag_instructions": "Para reorganizar los campos personalizados, pulsa espacio o enter. Mientras arrastras, usa las flechas del teclado para mover un campo arriba o abajo. Pulsa espacio o enter de nuevo para situar el campo en su nueva posición, o pulsa escape para cancelar.", + "account_edit.field_reorder_modal.drag_move": "El campo \"{item}\" se ha movido.", + "account_edit.field_reorder_modal.drag_over": "El campo \"{item}\" se ha movido sobre \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" seleccionado.", + "account_edit.field_reorder_modal.handle_label": "Arrastra el campo \"{item}\"", + "account_edit.field_reorder_modal.title": "Reorganizar campos", + "account_edit.image_alt_modal.add_title": "Añadir texto alternativo", + "account_edit.image_alt_modal.details_content": "QUE HACER:
    • Descríbete tal y como apareces en la imagen
    • Exprésate en tercera persona (p. ej. “Alex” en lugar de “yo”)
    • Sé breve, unas pocas palabras son a menudo suficientes
    QUE NO HACER:
    • Comenzar con “Foto de” – es redundante para lectores de pantalla
    EJEMPLO:
    • “Alex visitiendo una camisa verde y gafas”
    ", + "account_edit.image_alt_modal.details_title": "Consejo: Texto alternativo para fotos de perfil", + "account_edit.image_alt_modal.edit_title": "Editar texto alternativo", + "account_edit.image_alt_modal.text_hint": "El texto alternativo ayuda a los usuarios de lectores de pantalla a entender tu contenido.", + "account_edit.image_alt_modal.text_label": "Texto alternativo", + "account_edit.image_delete_modal.confirm": "¿Estás seguro de que deseas eliminar esta imagen? Esta acción no se puede deshacer.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "¿Eliminar imagen?", + "account_edit.image_edit.add_button": "Añadir imagen", + "account_edit.image_edit.alt_add_button": "Añadir texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Quitar imagen", + "account_edit.image_edit.replace_button": "Sustituir imagen", "account_edit.name_modal.add_title": "Añadir nombre para mostrar", "account_edit.name_modal.edit_title": "Editar nombre para mostrar", "account_edit.profile_tab.button_label": "Personalizar", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Personaliza las pestañas de tu perfil y lo que muestran.", "account_edit.profile_tab.title": "Configuración de la pestaña de perfil", "account_edit.save": "Guardar", + "account_edit.upload_modal.back": "Atrás", + "account_edit.upload_modal.done": "Hecho", + "account_edit.upload_modal.next": "Siguiente", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Explorar archivos", + "account_edit.upload_modal.step_upload.dragging": "Suelta para subir", + "account_edit.upload_modal.step_upload.header": "Elige una imagen", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF o JPG, hasta {limit}MB.{br}La imagen será escalada a {width}x{height}px.", + "account_edit.upload_modal.title_add": "Añadir foto de perfil", + "account_edit.upload_modal.title_replace": "Reemplazar foto de perfil", + "account_edit.verified_modal.details": "Añade credibilidad a tu perfil de Mastodon verificando enlaces a tus webs personales. Así es como funciona:", + "account_edit.verified_modal.invisible_link.details": "Añade el enlace en el encabezado. La parte importante es rel=\"me\", que evita la suplantación de identidad en webs con contenido generado por usuarios. Incluso puedes utilizar un enlace con etiqueta en el encabezado de la página en vez de {tag}, pero el HTML debe ser accesible sin ejecutar JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "¿Cómo puedo hacer el enlace invisible?", + "account_edit.verified_modal.step1.header": "Copia el código HTML que hay debajo y pégalo en el encabezado de tu web", + "account_edit.verified_modal.step2.details": "Si ya lo has añadido a tu web como un campo personalizado, deberás borrarlo y añadirlo de nuevo para activar la verificación.", + "account_edit.verified_modal.step2.header": "Añade tu web como un campo personalizado", + "account_edit.verified_modal.title": "Cómo añadir un enlace verificado", "account_edit_tags.add_tag": "Agregar #{tagName}", "account_edit_tags.column_title": "Editar etiquetas destacadas", "account_edit_tags.help_text": "Las etiquetas destacadas ayudan a los usuarios a descubrir e interactuar con tu perfil. Aparecen como filtros en la vista de actividad de tu página de perfil.", @@ -275,6 +331,8 @@ "callout.dismiss": "Descartar", "carousel.current": "Diapositiva {current, number} / {max, number}", "carousel.slide": "Diapositiva {current, number} de {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} caracteres recomendados", + "character_counter.required": "{currentLength}/{maxLength} caracteres", "closed_registrations.other_server_instructions": "Como Mastodon es descentralizado, puedes crear una cuenta en otro servidor y seguir interactuando con este.", "closed_registrations_modal.description": "La creación de una cuenta en {domain} no es posible actualmente, pero ten en cuenta que no necesitas una cuenta específicamente en {domain} para usar Mastodon.", "closed_registrations_modal.find_another_server": "Buscar otro servidor", @@ -304,10 +362,15 @@ "collections.create_collection": "Crear colección", "collections.delete_collection": "Eliminar colección", "collections.description_length_hint": "Limitado a 100 caracteres", + "collections.detail.accept_inclusion": "De acuerdo", "collections.detail.accounts_heading": "Cuentas", + "collections.detail.author_added_you": "{author} te añadió a esta colección", "collections.detail.curated_by_author": "Seleccionado por {author}", "collections.detail.curated_by_you": "Seleccionado por ti", "collections.detail.loading": "Cargando colección…", + "collections.detail.other_accounts_in_collection": "Otros en esta colección:", + "collections.detail.revoke_inclusion": "Sácame de aquí", + "collections.detail.sensitive_note": "Esta colección contiene cuentas y contenido que puede ser sensible para algunos usuarios.", "collections.detail.share": "Compartir esta colección", "collections.edit_details": "Editar detalles", "collections.error_loading_collections": "Se ha producido un error al intentar cargar tus colecciones.", @@ -322,10 +385,14 @@ "collections.old_last_post_note": "Última publicación hace más de una semana", "collections.remove_account": "Quitar esta cuenta", "collections.report_collection": "Informar de esta colección", + "collections.revoke_collection_inclusion": "Sácame de esta colección", + "collections.revoke_inclusion.confirmation": "Has salido de \"{collection}\"", + "collections.revoke_inclusion.error": "Se ha producido un error, inténtalo de nuevo más tarde.", "collections.search_accounts_label": "Buscar cuentas para añadir…", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", "collections.sensitive": "Sensible", "collections.topic_hint": "Añadir una etiqueta que ayude a otros a entender el tema principal de esta colección.", + "collections.topic_special_chars_hint": "Los caracteres especiales se eliminarán al guardar", "collections.view_collection": "Ver colección", "collections.view_other_collections_by_user": "Ver otras colecciones de este usuario", "collections.visibility_public": "Pública", @@ -445,6 +512,9 @@ "confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?", + "confirmations.revoke_collection_inclusion.confirm": "Sácame", + "confirmations.revoke_collection_inclusion.message": "Esta acción es permanente, y el curador no podrá volver a añadirle a la colección más adelante.", + "confirmations.revoke_collection_inclusion.title": "¿Salirse de esta colección?", "confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.", "confirmations.revoke_quote.title": "¿Eliminar la publicación?", @@ -556,6 +626,10 @@ "featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijadas}}", "featured_carousel.slide": "Publicación {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Últimamente has publicado sobre {items}. ¿Quieres añadirlo como etiqueta destacada?", + "featured_tags.suggestions.add": "Añadir", + "featured_tags.suggestions.added": "Controla tus etiquetas destacadas cuando quieras en Editar perfil > Etiquetas destacadas.", + "featured_tags.suggestions.dismiss": "No, gracias", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_title": "¡El contexto no coincide!", "filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, tendrás que cambiar la fecha de caducidad para que se aplique.", @@ -767,6 +841,7 @@ "navigation_bar.automated_deletion": "Eliminación automática de publicaciones", "navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.bookmarks": "Marcadores", + "navigation_bar.collections": "Colecciones", "navigation_bar.direct": "Menciones privadas", "navigation_bar.domain_blocks": "Dominios ocultos", "navigation_bar.favourites": "Favoritos", @@ -913,17 +988,17 @@ "notifications_permission_banner.how_to_control": "Para recibir notificaciones cuando Mastodon no esté abierto, habilite las notificaciones de escritorio. Puedes controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón {icon} de arriba una vez que estén habilitadas.", "notifications_permission_banner.title": "Nunca te pierdas nada", "onboarding.follows.back": "Atrás", - "onboarding.follows.done": "Hecho", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar personas a las que seguir, o inténtalo de nuevo más tarde.", + "onboarding.follows.next": "Siguiente: Configura tu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Sigue personas para comenzar", "onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas", "onboarding.profile.discoverable_hint": "Cuando permites que tu perfil aparezca en búsquedas en Mastodon, tus publicaciones podrán aparecer en los resultados de búsqueda y en tendencias, y tu perfil podrá recomendarse a gente con intereses similares a los tuyos.", "onboarding.profile.display_name": "Nombre para mostrar", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", + "onboarding.profile.finish": "Terminar", "onboarding.profile.note": "Biografía", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…", - "onboarding.profile.save_and_continue": "Guardar y continuar", "onboarding.profile.title": "Configuración del perfil", "onboarding.profile.upload_avatar": "Subir foto de perfil", "onboarding.profile.upload_header": "Subir encabezado de perfil", @@ -1071,7 +1146,7 @@ "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Iniciar sesión o Registrarse", "skip_links.hotkey": "Atajo {hotkey}", - "skip_links.skip_to_content": "Abrir Saltar al contenido principal", + "skip_links.skip_to_content": "Saltar al contenido principal", "skip_links.skip_to_navigation": "Saltar a la navegación principal", "status.admin_account": "Abrir interfaz de moderación para @{name}", "status.admin_domain": "Abrir interfaz de moderación para {domain}", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 51a9abb35c3202..13ba8bd303fa6a 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -44,9 +44,11 @@ "account.familiar_followers_two": "Jälgijateks {name1} ja {name2}", "account.featured": "Esiletõstetud", "account.featured.accounts": "Profiilid", + "account.featured.collections": "Kogud", "account.featured.hashtags": "Teemaviited", "account.featured_tags.last_status_at": "Viimane postitus: {date}", "account.featured_tags.last_status_never": "Postitusi pole", + "account.field_overflow": "Näita kogu sisu", "account.filters.all": "Kõik tegevused", "account.filters.boosts_toggle": "Näita hooandmisi", "account.filters.posts_boosts": "Postitused ja hooandmised", @@ -71,7 +73,6 @@ "account.go_to_profile": "Vaata profiili", "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}", @@ -845,7 +846,6 @@ "notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.", "notifications_permission_banner.title": "Ära jää millestki ilma", "onboarding.follows.back": "Tagasi", - "onboarding.follows.done": "Valmis", "onboarding.follows.empty": "Kahjuks ei saa hetkel tulemusi näidata. Proovi kasutada otsingut või lehitse uurimise lehte, et leida inimesi, keda jälgida, või proovi hiljem uuesti.", "onboarding.follows.search": "Otsi", "onboarding.follows.title": "Jälgi inimesi, et alustada", @@ -855,7 +855,6 @@ "onboarding.profile.display_name_hint": "Su täisnimi või naljanimi…", "onboarding.profile.note": "Elulugu", "onboarding.profile.note_hint": "Saad @mainida teisi kasutajaid või lisada #teemaviidet…", - "onboarding.profile.save_and_continue": "Salvesta ja jätka", "onboarding.profile.title": "Profiili seadistamine", "onboarding.profile.upload_avatar": "Laadi üles profiilipilt", "onboarding.profile.upload_header": "Laadi üles profiili päis", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index fb50eff98b0fc7..778207fc85a578 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -751,7 +751,6 @@ "notifications_permission_banner.how_to_control": "Mastodon irekita ez dagoenean jakinarazpenak jasotzeko, gaitu mahaigaineko jakinarazpenak. Mahaigaineko jakinarazpenak ze elkarrekintzak eragingo dituzten zehazki kontrolatu dezakezu goiko {icon} botoia erabiliz, gaituta daudenean.", "notifications_permission_banner.title": "Ez galdu ezer inoiz", "onboarding.follows.back": "Atzera", - "onboarding.follows.done": "Egina", "onboarding.follows.empty": "Zoritxarrez, ezin da emaitzik erakutsi orain. Bilaketa erabil dezakezu edo Arakatu orrian jendea bilatu jarraitzeko, edo saiatu geroago.", "onboarding.follows.search": "Bilatu", "onboarding.follows.title": "Jarraitu jendea hasteko", @@ -761,7 +760,6 @@ "onboarding.profile.display_name_hint": "Zure izena edo ezizena…", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Beste pertsona batzuk @aipa ditzakezu edo #traolak erabili…", - "onboarding.profile.save_and_continue": "Gorde eta jarraitu", "onboarding.profile.title": "Profilaren konfigurazioa", "onboarding.profile.upload_avatar": "Igo profilaren irudia", "onboarding.profile.upload_header": "Igo profilaren goiburua", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 9501be8b939e71..3cd7bfaf801d87 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -751,7 +751,6 @@ "notifications_permission_banner.how_to_control": "برای دریافت آگاهی‌ها هنگام باز نبودن ماستودون، آگاهی‌های میزکار را به کار بیندازید. پس از به کار افتادنشان می‌توانید گونه‌های دقیق برهم‌کنش‌هایی که آگاهی‌های میزکار تولید می‌کنند را از {icon} بالا واپایید.", "notifications_permission_banner.title": "هرگز چیزی را از دست ندهید", "onboarding.follows.back": "بازگشت", - "onboarding.follows.done": "انجام شد", "onboarding.follows.empty": "متأسفانه هم‌اکنون نتیجه‌ای قابل نمایش نیست. می‌توانید استفاده از جست‌وجو یا مرور صفحهٔ کاوش را برای یافتن افرادی برای پی‌گیری آزموده یا دوباره تلاش کنید.", "onboarding.follows.search": "جست‌وجو", "onboarding.follows.title": "پی گرفتن افرادی برای آغاز", @@ -761,7 +760,6 @@ "onboarding.profile.display_name_hint": "نام کامل یا نام باحالتان…", "onboarding.profile.note": "درباره شما", "onboarding.profile.note_hint": "می‌توانید افراد دیگر را @نام‌بردن یا #برچسب بزنید…", - "onboarding.profile.save_and_continue": "ذخیره کن و ادامه بده", "onboarding.profile.title": "تنظیم نمایه", "onboarding.profile.upload_avatar": "بازگذاری تصویر نمایه", "onboarding.profile.upload_header": "بارگذاری تصویر سردر نمایه", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index f6a91c724e8c45..ffec8d59e851bb 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -73,7 +73,6 @@ "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}", @@ -153,19 +152,51 @@ "account_edit.column_title": "Muokkaa profiilia", "account_edit.custom_fields.name": "kenttä", "account_edit.custom_fields.placeholder": "Lisää pronominisi, ulkoisia linkkejä tai mitä tahansa muuta, jonka haluat jakaa.", + "account_edit.custom_fields.reorder_button": "Järjestele kenttiä", + "account_edit.custom_fields.tip_content": "Voit helposti lisätä Mastodon-tilisi uskottavuutta vahvistamalla mihin tahansa omistamaasi verkkosivustoon ohjaavat linkit.", + "account_edit.custom_fields.tip_title": "Vinkki: Vahvistettujen linkkien lisääminen", "account_edit.custom_fields.title": "Mukautetut kentät", + "account_edit.custom_fields.verified_hint": "Miten lisään vahvistetun linkin?", "account_edit.display_name.placeholder": "Näyttönimesi on nimi, joka näkyy profiilissasi ja aikajanoilla.", "account_edit.display_name.title": "Näyttönimi", "account_edit.featured_hashtags.item": "aihetunnisteet", "account_edit.featured_hashtags.placeholder": "Auta muita tunnistamaan suosikkiaiheesi ja saamaan nopea pääsy niihin.", "account_edit.featured_hashtags.title": "Esiteltävät aihetunnisteet", + "account_edit.field_delete_modal.confirm": "Haluatko varmasti poistaa tämän mukautetun kentän? Tätä toimea ei voi peruuttaa.", "account_edit.field_delete_modal.delete_button": "Poista", "account_edit.field_delete_modal.title": "Poistetaanko mukautettu kenttä?", "account_edit.field_edit_modal.add_title": "Lisää mukautettu kenttä", "account_edit.field_edit_modal.edit_title": "Muokkaa mukautettua kenttää", + "account_edit.field_edit_modal.limit_header": "Suositeltu merkkiraja ylitetty", + "account_edit.field_edit_modal.limit_message": "Mobiilikäyttäjät eivät välttämättä näe kenttää kokonaan.", + "account_edit.field_edit_modal.link_emoji_warning": "Emme suosittele käyttämään mukautettuja emojeita URL-osoitteiden kanssa. Molempia sisältävät mukautetut kentät näkyvät vain tekstinä linkin sijaan, jotta estetään käyttäjien sekaannus.", "account_edit.field_edit_modal.name_hint": "Esim. ”Henkilökohtainen verkkosivusto”", "account_edit.field_edit_modal.name_label": "Nimike", + "account_edit.field_edit_modal.url_warning": "Lisää linkki sisällyttämällä alkuun {protocol}.", + "account_edit.field_edit_modal.value_hint": "Esim. ”https://example.me”", "account_edit.field_edit_modal.value_label": "Arvo", + "account_edit.field_reorder_modal.drag_cancel": "Veto peruttu. Kenttää ”{item}” ei siirretty.", + "account_edit.field_reorder_modal.drag_end": "Kenttä ”{item}” pudotettu.", + "account_edit.field_reorder_modal.drag_instructions": "Siirrä mukautettuja kenttiä painamalla välilyöntiä tai Enter-näppäintä. Raahatessa siirrä kenttää ylös tai alas nuolinäppäimillä. Pudota kenttä uuteen kohtaansa painamalla uudelleen välilyöntiä tai Enter-näppäintä, tai peru painamalla Escape-näppäintä.", + "account_edit.field_reorder_modal.drag_move": "Kenttä ”{item}” siirretty.", + "account_edit.field_reorder_modal.drag_over": "Kenttä ”{item}” siirretty kentän ”{over}” päälle.", + "account_edit.field_reorder_modal.drag_start": "Valittu kenttä ”{item}”.", + "account_edit.field_reorder_modal.handle_label": "Siirrä kenttää ”{item}”", + "account_edit.field_reorder_modal.title": "Järjestele kenttiä", + "account_edit.image_alt_modal.add_title": "Lisää tekstivastine", + "account_edit.image_alt_modal.details_content": "TEE NÄIN:
    • Kuvaile itseäsi kuvan mukaisesti
    • Käytä kolmannen persoonan muotoja (esim. ”Aleksi” eikä ”minä”)
    • Ole ytimekäs – usein muutama sana riittää
    ÄLÄ TEE NÄIN:
    • Aloita sanalla ”Kuva” – sen on tarpeetonta näytönlukuohjelmille
    ESIMERKKI:
    • ”Aleksi yllään vihreä paita ja lasit”
    ", + "account_edit.image_alt_modal.details_title": "Vinkkejä: profiilikuvien tekstivastineet", + "account_edit.image_alt_modal.edit_title": "Muokkaa tekstivastinetta", + "account_edit.image_alt_modal.text_hint": "Tekstivastine auttaa näytönlukuohjelmien käyttäjiä ymmärtämään sisältöä.", + "account_edit.image_alt_modal.text_label": "Tekstivastine", + "account_edit.image_delete_modal.confirm": "Haluatko varmasti poistaa tämän kuvan? Tätä toimea ei voi kumota.", + "account_edit.image_delete_modal.delete_button": "Poista", + "account_edit.image_delete_modal.title": "Poistetaanko kuva?", + "account_edit.image_edit.add_button": "Lisää kuva", + "account_edit.image_edit.alt_add_button": "Lisää tekstivastine", + "account_edit.image_edit.alt_edit_button": "Muokkaa tekstivastinetta", + "account_edit.image_edit.remove_button": "Poista kuva", + "account_edit.image_edit.replace_button": "Korvaa kuva", "account_edit.name_modal.add_title": "Lisää näyttönimi", "account_edit.name_modal.edit_title": "Muokkaa näyttönimeä", "account_edit.profile_tab.button_label": "Mukauta", @@ -180,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Mukauta profiilisi välilehtiä ja sitä, mitä niissä näkyy.", "account_edit.profile_tab.title": "Profiilin välilehtien asetukset", "account_edit.save": "Tallenna", + "account_edit.upload_modal.back": "Takaisin", + "account_edit.upload_modal.done": "Valmis", + "account_edit.upload_modal.next": "Seuraava", + "account_edit.upload_modal.step_crop.zoom": "Lähennä/loitonna", + "account_edit.upload_modal.step_upload.button": "Selaa tiedostoja", + "account_edit.upload_modal.step_upload.dragging": "Lähetä pudottamalla", + "account_edit.upload_modal.step_upload.header": "Valitse kuva", + "account_edit.upload_modal.step_upload.hint": "WEBP-, PNG-, GIF- tai JPG-muotoinen, enintään {limit} Mt.{br}Kuva skaalautuu kokoon {width}×{height} px.", + "account_edit.upload_modal.title_add": "Lisää profiilikuva", + "account_edit.upload_modal.title_replace": "Korvaa profiilikuva", + "account_edit.verified_modal.details": "Lisää Mastodon-profiiliisi uskottavuutta vahvistamalla linkit henkilökohtaisiin verkkosivustoihin. Näin se toimii:", + "account_edit.verified_modal.invisible_link.details": "Lisää linkki HTML:n head-osaan. Tärkeä kohta on rel=\"me\", joka estää toiseksi tekeytymisen sivustoilla, joilla on käyttäjien luomaa sisältöä. Voit jopa käyttää link-tunnistetta sivun head-osassa {tag}-tunnisteen sijaan, mutta HTML:n tulee olla saatavilla suorittamatta JavaScriptia.", + "account_edit.verified_modal.invisible_link.summary": "Miten teen linkistä näkymättömän?", + "account_edit.verified_modal.step1.header": "Kopioi alla oleva HTML-koodi ja liitä se verkkosivustosi head-osaan", + "account_edit.verified_modal.step2.details": "Jos olet jo lisännyt verkkosivustosi mukautettuna kenttänä, sinun tulee poistaa ja lisätä se uudelleen, jotta voit käynnistää vahvistuksen.", + "account_edit.verified_modal.step2.header": "Lisää verkkosivustosi mukautettuna kenttänä", + "account_edit.verified_modal.title": "Miten lisätä vahvistettu linkki", "account_edit_tags.add_tag": "Lisää #{tagName}", "account_edit_tags.column_title": "Muokkaa esiteltäviä aihetunnisteita", "account_edit_tags.help_text": "Esiteltävät aihetunnisteet auttavat käyttäjiä löytämään profiilisi ja olemaan vuorovaikutuksessa sen kanssa. Ne näkyvät suodattimina profiilisivusi Toiminta-näkymässä.", @@ -200,8 +248,8 @@ "alert.rate_limited.title": "Pyyntömäärää rajoitettu", "alert.unexpected.message": "Tapahtui odottamaton virhe.", "alert.unexpected.title": "Hups!", - "alt_text_badge.title": "Vaihtoehtoinen teksti", - "alt_text_modal.add_alt_text": "Lisää vaihtoehtoinen teksti", + "alt_text_badge.title": "Tekstivastine", + "alt_text_modal.add_alt_text": "Lisää tekstivastine", "alt_text_modal.add_text_from_image": "Lisää teksti kuvasta", "alt_text_modal.cancel": "Peruuta", "alt_text_modal.change_thumbnail": "Vaihda pikkukuva", @@ -301,6 +349,8 @@ "collections.accounts.empty_description": "Lisää enintään {count} seuraamaasi tiliä", "collections.accounts.empty_title": "Tämä kokoelma on tyhjä", "collections.collection_description": "Kuvaus", + "collections.collection_language": "Kieli", + "collections.collection_language_none": "Ei mikään", "collections.collection_name": "Nimi", "collections.collection_topic": "Aihe", "collections.confirm_account_removal": "Haluatko varmasti poistaa tämän tilin tästä kokoelmasta?", @@ -314,10 +364,15 @@ "collections.create_collection": "Luo kokoelma", "collections.delete_collection": "Poista kokoelma", "collections.description_length_hint": "100 merkin rajoitus", + "collections.detail.accept_inclusion": "Selvä", "collections.detail.accounts_heading": "Tilit", + "collections.detail.author_added_you": "{author} lisäsi sinut tähän kokoelmaan", "collections.detail.curated_by_author": "Koonnut {author}", "collections.detail.curated_by_you": "Itse kokoamasi", "collections.detail.loading": "Ladataan kokoelmaa…", + "collections.detail.other_accounts_in_collection": "Muut tässä kokoelmassa:", + "collections.detail.revoke_inclusion": "Poista minut", + "collections.detail.sensitive_note": "Tämä kokoelma sisältää tilejä ja sisältöä, jotka saattavat olla arkaluonteisia joillekin käyttäjille.", "collections.detail.share": "Jaa tämä kokoelma", "collections.edit_details": "Muokkaa tietoja", "collections.error_loading_collections": "Kokoelmien latauksessa tapahtui virhe.", @@ -332,10 +387,14 @@ "collections.old_last_post_note": "Julkaissut viimeksi yli viikko sitten", "collections.remove_account": "Poista tämä tili", "collections.report_collection": "Raportoi tämä kokoelma", + "collections.revoke_collection_inclusion": "Poista itseni tästä kokoelmasta", + "collections.revoke_inclusion.confirmation": "Sinut on poistettu kokoelmasta ”{collection}”", + "collections.revoke_inclusion.error": "Tapahtui virhe – yritä myöhemmin uudelleen.", "collections.search_accounts_label": "Hae lisättäviä tilejä…", "collections.search_accounts_max_reached": "Olet lisännyt enimmäismäärän tilejä", "collections.sensitive": "Arkaluonteinen", "collections.topic_hint": "Lisää aihetunniste, joka auttaa muita ymmärtämään tämän kokoelman pääaiheen.", + "collections.topic_special_chars_hint": "Erikoismerkit poistetaan tallennettaessa", "collections.view_collection": "Näytä kokoelma", "collections.view_other_collections_by_user": "Näytä muut tämän käyttäjän kokoelmat", "collections.visibility_public": "Julkinen", @@ -435,10 +494,10 @@ "confirmations.logout.confirm": "Kirjaudu ulos", "confirmations.logout.message": "Haluatko varmasti kirjautua ulos?", "confirmations.logout.title": "Kirjaudutaanko ulos?", - "confirmations.missing_alt_text.confirm": "Lisää vaihtoehtoinen teksti", - "confirmations.missing_alt_text.message": "Julkaisussasi on mediaa ilman vaihtoehtoista tekstiä. Kuvausten lisääminen auttaa tekemään sisällöstäsi saavutettavamman useammille ihmisille.", + "confirmations.missing_alt_text.confirm": "Lisää tekstivastine", + "confirmations.missing_alt_text.message": "Julkaisussasi on mediaa ilman tekstivastinetta. Kuvausten lisääminen auttaa tekemään sisällöstäsi saavutettavamman useammille ihmisille.", "confirmations.missing_alt_text.secondary": "Julkaise silti", - "confirmations.missing_alt_text.title": "Lisätäänkö vaihtoehtoinen teksti?", + "confirmations.missing_alt_text.title": "Lisätäänkö tekstivastine?", "confirmations.mute.confirm": "Mykistä", "confirmations.private_quote_notify.cancel": "Takaisin muokkaukseen", "confirmations.private_quote_notify.confirm": "Julkaise", @@ -455,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Poista seuraaja", "confirmations.remove_from_followers.message": "{name} lakkaa seuraamasta sinua. Haluatko varmasti jatkaa?", "confirmations.remove_from_followers.title": "Poistetaanko seuraaja?", + "confirmations.revoke_collection_inclusion.confirm": "Poista minut", + "confirmations.revoke_collection_inclusion.message": "Tämä toimi on pysyvä, eikä kokoaja pysty lisäämään sinua kokoelmaan myöhemmin uudelleen.", + "confirmations.revoke_collection_inclusion.title": "Poistetaanko itsesi tästä kokoelmasta?", "confirmations.revoke_quote.confirm": "Poista julkaisu", "confirmations.revoke_quote.message": "Tätä toimea ei voi peruuttaa.", "confirmations.revoke_quote.title": "Poistetaanko julkaisu?", @@ -566,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Kiinnitetty julkaisu} other {Kiinnitetyt julkaisut}}", "featured_carousel.slide": "Julkaisu {current, number} / {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Olet viime aikoina julkaissut aiheista {items}. Lisätäänkö nämä esiteltäviksi aihetunnisteiksi?", + "featured_tags.suggestions.add": "Lisää", + "featured_tags.suggestions.added": "Hallitse esiteltäviä aihetunnisteitasi milloin tahansa kohdasta Muokkaa profiilia > Esiteltävät aihetunnisteet.", + "featured_tags.suggestions.dismiss": "Ei kiitos", "filter_modal.added.context_mismatch_explanation": "Tämä suodatinluokka ei koske asiayhteyttä, jossa olet tarkastellut tätä julkaisua. Jos haluat julkaisun suodatettavan myös tässä asiayhteydessä, muokkaa suodatinta.", "filter_modal.added.context_mismatch_title": "Asiayhteys ei täsmää!", "filter_modal.added.expired_explanation": "Tämä suodatinluokka on vanhentunut, joten sinun on muutettava viimeistä voimassaolopäivää, jotta suodatusta käytettäisiin.", @@ -670,7 +736,7 @@ "ignore_notifications_modal.not_following_title": "Sivuutetaanko ilmoitukset käyttäjiltä, joita et seuraa?", "ignore_notifications_modal.private_mentions_title": "Sivuutetaanko ilmoitukset pyytämättömistä yksityismaininnoista?", "info_button.label": "Ohje", - "info_button.what_is_alt_text": "

    Mikä vaihtoehtoinen teksti on?

    Vaihtoehtoinen teksti tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.

    Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen vaihtoehtoisen tekstin.

    • Ota mukaan tärkeät elementit
    • Tiivistä kuvissa oleva teksti
    • Käytä tavallisia lauserakenteita
    • Vältä turhaa tietoa
    • Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)
    ", + "info_button.what_is_alt_text": "

    Mikä tekstivastine on?

    Tekstivastine tarjoaa kuvauksen kuvista ihmisille, joilla on näkövamma tai matalan kaistanleveyden yhteys tai jotka kaipaavat lisäkontekstia.

    Voit parantaa saavutettavuutta ja ymmärrettävyyttä kaikkien näkökulmasta kirjoittamalla selkeän, tiiviin ja objektiivisen tekstivastineen.

    • Ota mukaan tärkeät elementit
    • Tiivistä kuvissa oleva teksti
    • Käytä tavallisia lauserakenteita
    • Vältä turhaa tietoa
    • Keskity trendeihin ja keskeisiin tuloksiin monimutkaisissa visuaalisissa esityksissä (kuten kaavioissa tai kartoissa)
    ", "interaction_modal.action": "Jotta voit olla vuorovaikutuksessa käyttäjän {name} julkaisun kanssa, sinun on kirjauduttava sisään tilillesi käyttämälläsi Mastodon-palvelimella.", "interaction_modal.go": "Siirry", "interaction_modal.no_account_yet": "Eikö sinulla ole vielä tiliä?", @@ -924,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Saadaksesi ilmoituksia, kun Mastodon ei ole auki, ota työpöytäilmoitukset käyttöön. Voit hallita tarkasti, mistä saat työpöytäilmoituksia kun ilmoitukset on otettu käyttöön yllä olevan {icon}-painikkeen kautta.", "notifications_permission_banner.title": "Älä anna minkään mennä ohi", "onboarding.follows.back": "Takaisin", - "onboarding.follows.done": "Valmis", "onboarding.follows.empty": "Valitettavasti tuloksia ei voida näyttää juuri nyt. Voit kokeilla hakua tai selata tutustumissivua löytääksesi seurattavaa tai yrittää myöhemmin uudelleen.", + "onboarding.follows.next": "Seuraavaksi: määrittele profiilisi", "onboarding.follows.search": "Haku", "onboarding.follows.title": "Aloita seuraamalla käyttäjiä", "onboarding.profile.discoverable": "Aseta profiilini löydettäväksi", "onboarding.profile.discoverable_hint": "Kun olet määrittänyt itsesi löydettäväksi Mastodonista, julkaisusi voivat näkyä hakutuloksissa ja suosituissa kohteissa. Lisäksi profiiliasi voidaan ehdottaa käyttäjille, jotka ovat kiinnostuneita kanssasi samoista aiheista.", "onboarding.profile.display_name": "Näyttönimi", "onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…", + "onboarding.profile.finish": "Valmis", "onboarding.profile.note": "Elämäkerta", "onboarding.profile.note_hint": "Voit @mainita muita käyttäjiä tai #aihetunnisteita…", - "onboarding.profile.save_and_continue": "Tallenna ja jatka", "onboarding.profile.title": "Profiilin määritys", "onboarding.profile.upload_avatar": "Lähetä profiilikuva", "onboarding.profile.upload_header": "Lähetä profiilin otsakekuva", diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json index 82c1616ddf2f7f..c31f8c7681ea35 100644 --- a/app/javascript/mastodon/locales/fil.json +++ b/app/javascript/mastodon/locales/fil.json @@ -313,7 +313,6 @@ "notifications.policy.filter_not_followers_title": "Mga taong hindi ka susundan", "notifications.policy.filter_not_following_title": "Mga taong hindi mo sinusundan", "onboarding.profile.note_hint": "Maaari mong @bangitin ang ibang mga tao o mga #hashtag…", - "onboarding.profile.save_and_continue": "Iimbak at magpatuloy", "picture_in_picture.restore": "Ilagay ito pabalik", "poll.closed": "Sarado", "poll.reveal": "Ipakita ang mga resulta", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 799b0a4bc4f129..110cdd1e7f9eb2 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -73,7 +73,6 @@ "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}", @@ -915,7 +914,6 @@ "notifications_permission_banner.how_to_control": "Ger skriviborðsfráboðanir virknar fyri at móttaka fráboðanir, tá Mastodon ikki er opið. Tá tær eru gjørdar virknar, kanst tú stýra, hvørji sløg av samvirkni geva skriviborðsfráboðanir. Hetta umvegis {icon} knøttin omanfyri.", "notifications_permission_banner.title": "Miss einki", "onboarding.follows.back": "Aftur", - "onboarding.follows.done": "Liðugt", "onboarding.follows.empty": "Tíverri kunnu eingi úrslit vísast beint nú. Tú kanst royna at brúka leiting ella at kaga gjøgnum Rannsaka síðuna fyri at finna fólk at fylgja - ella royna aftur seinni.", "onboarding.follows.search": "Leita", "onboarding.follows.title": "Fylg fólki fyri at koma í gongd", @@ -925,7 +923,6 @@ "onboarding.profile.display_name_hint": "Títt fulla navn ella títt stuttliga navn…", "onboarding.profile.note": "Ævilýsing", "onboarding.profile.note_hint": "Tú kanst @umrøða onnur fólk ella #frámerki…", - "onboarding.profile.save_and_continue": "Goym og halt fram", "onboarding.profile.title": "Vangauppsetan", "onboarding.profile.upload_avatar": "Legg vangamynd upp", "onboarding.profile.upload_header": "Legg vangahøvd upp", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 2733ef316b12d3..23ab79e5961e06 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -18,12 +18,12 @@ "account.add_note": "Ajouter une note personnelle", "account.add_or_remove_from_list": "Ajouter ou enlever de listes", "account.badges.admin": "Admin", - "account.badges.blocked": "Bloqué", + "account.badges.blocked": "Bloqué·e", "account.badges.bot": "Bot", "account.badges.domain_blocked": "Domaine bloqué", "account.badges.group": "Groupe", - "account.badges.muted": "Masqué", - "account.badges.muted_until": "Masqué jusqu’au {until}", + "account.badges.muted": "Masqué·e", + "account.badges.muted_until": "Masqué·e jusqu’au {until}", "account.block": "Bloquer @{name}", "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", @@ -39,7 +39,7 @@ "account.edit_profile_short": "Modifier", "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_many": "Suivi·e par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", "account.familiar_followers_one": "Suivi·e par {name1}", "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", @@ -65,7 +65,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·e}, other {{counter} suivi·e·s}}", + "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.", @@ -73,7 +73,6 @@ "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Modifier {item}", "account_edit.column_button": "Terminé", "account_edit.column_title": "Modifier le profil", + "account_edit.custom_fields.name": "champ", "account_edit.custom_fields.placeholder": "Ajouter vos pronoms, vos sites, ou tout ce que vous voulez partager.", + "account_edit.custom_fields.reorder_button": "Réorganiser les champs", + "account_edit.custom_fields.tip_content": "Vous pouvez facilement ajouter de la crédibilité à votre compte Mastodon en vérifiant les liens vers tous les sites Web que vous possédez.", + "account_edit.custom_fields.tip_title": "Astuce : ajout de liens vérifiés", "account_edit.custom_fields.title": "Champs personnalisés", + "account_edit.custom_fields.verified_hint": "Comment ajouter un lien vérifié ?", "account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.", "account_edit.display_name.title": "Nom public", "account_edit.featured_hashtags.item": "hashtags", "account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.", "account_edit.featured_hashtags.title": "Hashtags mis en avant", + "account_edit.field_delete_modal.confirm": "Voulez-vous vraiment supprimer ce champ personnalisé ? Cette action ne peut pas être annulée.", + "account_edit.field_delete_modal.delete_button": "Supprimer", + "account_edit.field_delete_modal.title": "Supprimer le champ personnalisé ?", + "account_edit.field_edit_modal.add_title": "Ajouter un champ personnalisé", + "account_edit.field_edit_modal.edit_title": "Modifier un champ personnalisé", + "account_edit.field_edit_modal.limit_header": "Limite de caractères recommandée dépassée", + "account_edit.field_edit_modal.limit_message": "L'affichage du champ peut être tronqué sur les téléphones.", + "account_edit.field_edit_modal.link_emoji_warning": "Nous déconseillons l'usage d'émoji personnalisé avec les URL. Les champs personnalisés contenant les deux seront affichés comme du texte et non un lien, afin d'éviter toute confusion.", + "account_edit.field_edit_modal.name_hint": "Par exemple « Site Web personnel »", + "account_edit.field_edit_modal.name_label": "Libellé", + "account_edit.field_edit_modal.url_warning": "Pour ajouter un lien, veuillez inclure {protocol} au début.", + "account_edit.field_edit_modal.value_hint": "Par exemple « https://exemple.me »", + "account_edit.field_edit_modal.value_label": "Valeur", + "account_edit.field_reorder_modal.drag_cancel": "Déplacement annulé. Le champ « {item} » a été redéposé.", + "account_edit.field_reorder_modal.drag_end": "Le champ « {item} » a été déposé.", + "account_edit.field_reorder_modal.drag_instructions": "Pour réorganiser les champs personnalisés, appuyer sur espace ou entrée. Utiliser les touches fléchées pour déplacer le champ vers le haut ou vers le bas. Appuyer à nouveau sur espace ou entrée pour déposer le champ dans sa nouvelle position, ou appuyer sur échap pour annuler.", + "account_edit.field_reorder_modal.drag_move": "Le champ « {item} » a été déplacé.", + "account_edit.field_reorder_modal.drag_over": "Le champ « {item} » a été déplacé au-dessus de « {over} ».", + "account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", + "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", + "account_edit.field_reorder_modal.title": "Réorganiser les champs", + "account_edit.image_alt_modal.add_title": "Ajouter un texte alternatif", + "account_edit.image_alt_modal.details_content": "À faire :
    • Se décrire comme vous apparaissez sur la photo
    • Utiliser la troisième personne (par exemple « Alex » au lieu de « moi »)
    • Être succinct·e – quelques mot suffisent souvent
    À éviter :
    • Commencer par « Une photo de » – c'est redondant pour les lecteurs d'écran
    Example :
    • « Alex portant une chemise vert et des lunettes »
    ", + "account_edit.image_alt_modal.details_title": "Astuces : texte alternatif pour les photos de profil", + "account_edit.image_alt_modal.edit_title": "Modifier le texte alternatif", + "account_edit.image_alt_modal.text_hint": "Le texte alternatif aide les personnes utilisant un lecteur d'écran à comprendre votre contenu.", + "account_edit.image_alt_modal.text_label": "Texte alternatif", + "account_edit.image_delete_modal.confirm": "Voulez-vous vraiment supprimer cette image ? Cette action est irréversible.", + "account_edit.image_delete_modal.delete_button": "Supprimer", + "account_edit.image_delete_modal.title": "Supprimer l'image ?", + "account_edit.image_edit.add_button": "Ajouter une image", + "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", + "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", + "account_edit.image_edit.remove_button": "Supprimer l’image", + "account_edit.image_edit.replace_button": "Remplacer l'image", "account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.profile_tab.button_label": "Personnaliser", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.", "account_edit.profile_tab.title": "Paramètres de l'onglet du profil", "account_edit.save": "Enregistrer", + "account_edit.upload_modal.back": "Retour", + "account_edit.upload_modal.done": "Terminé", + "account_edit.upload_modal.next": "Suivant", + "account_edit.upload_modal.step_crop.zoom": "Agrandir", + "account_edit.upload_modal.step_upload.button": "Parcourir les fichiers", + "account_edit.upload_modal.step_upload.dragging": "Déposer pour téléverser", + "account_edit.upload_modal.step_upload.header": "Choisir une image", + "account_edit.upload_modal.step_upload.hint": "Format WebP, PNG, GIF ou JPEG, jusqu'à {limit} Mo.{br}L'image sera redimensionnée à {width} × {height} px.", + "account_edit.upload_modal.title_add": "Ajouter une photo de profil", + "account_edit.upload_modal.title_replace": "Remplacer la photo de profil", + "account_edit.verified_modal.details": "Ajouter de la crédibilité à votre profil Mastodon en vérifiant les liens vers vos sites Web personnels. Voici comment cela fonctionne :", + "account_edit.verified_modal.invisible_link.details": "Ajouter le lien dans votre en-tête. La partie importante est « rel=\"me\" » qui empêche l'usurpation d'identité sur des sites Web ayant du contenu généré par d'autres utilisateur·rice·s. Vous pouvez aussi utiliser une balise link dans l'en-tête de la page au lieu de {tag}, mais le code HTML doit être accessible sans avoir besoin d'exécuter du JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Comment rendre le lien invisible ?", + "account_edit.verified_modal.step1.header": "Copier-coller le code HTML ci-dessous dans l'en-tête de votre site web", + "account_edit.verified_modal.step2.details": "Si vous avez déjà ajouté votre site Web en tant que champ personnalisé, vous devrez le supprimer et le rajouter pour déclencher la vérification.", + "account_edit.verified_modal.step2.header": "Ajouter votre site Web en tant que champ personnalisé", + "account_edit.verified_modal.title": "Comment ajouter un lien vérifié ?", "account_edit_tags.add_tag": "Ajouter #{tagName}", "account_edit_tags.column_title": "Modifier les hashtags mis en avant", "account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.", @@ -293,6 +349,8 @@ "collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez", "collections.accounts.empty_title": "Cette collection est vide", "collections.collection_description": "Description", + "collections.collection_language": "Langue", + "collections.collection_language_none": "Aucune", "collections.collection_name": "Nom", "collections.collection_topic": "Sujet", "collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection ?", @@ -306,10 +364,15 @@ "collections.create_collection": "Créer une collection", "collections.delete_collection": "Supprimer la collection", "collections.description_length_hint": "Maximum 100 caractères", + "collections.detail.accept_inclusion": "D'accord", "collections.detail.accounts_heading": "Comptes", + "collections.detail.author_added_you": "{author} vous a ajouté·e à cette collection", "collections.detail.curated_by_author": "Organisée par {author}", "collections.detail.curated_by_you": "Organisée par vous", "collections.detail.loading": "Chargement de la collection…", + "collections.detail.other_accounts_in_collection": "Autres comptes dans cette collection :", + "collections.detail.revoke_inclusion": "Me retirer", + "collections.detail.sensitive_note": "Cette collection contient des comptes et du contenu qui peut être sensibles.", "collections.detail.share": "Partager la collection", "collections.edit_details": "Modifier les détails", "collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.", @@ -324,10 +387,14 @@ "collections.old_last_post_note": "Dernière publication il y a plus d'une semaine", "collections.remove_account": "Supprimer ce compte", "collections.report_collection": "Signaler cette collection", + "collections.revoke_collection_inclusion": "Me retirer de cette collection", + "collections.revoke_inclusion.confirmation": "Vous avez été retiré·e de « {collection} »", + "collections.revoke_inclusion.error": "Une erreur s'est produite, veuillez réessayer plus tard.", "collections.search_accounts_label": "Chercher des comptes à ajouter…", "collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes", "collections.sensitive": "Sensible", "collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.", + "collections.topic_special_chars_hint": "Les caractères spéciaux seront supprimés lors de l'enregistrement", "collections.view_collection": "Voir la collection", "collections.view_other_collections_by_user": "Voir les autres collections par ce compte", "collections.visibility_public": "Publique", @@ -447,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Supprimer l'abonné·e", "confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Voulez-vous vraiment continuer ?", "confirmations.remove_from_followers.title": "Supprimer l'abonné·e ?", + "confirmations.revoke_collection_inclusion.confirm": "Me retirer", + "confirmations.revoke_collection_inclusion.message": "Cette action est permanente, la personne qui gère la collection ne pourra plus vous y rajouter plus tard.", + "confirmations.revoke_collection_inclusion.title": "Vous retirer de cette collection ?", "confirmations.revoke_quote.confirm": "Retirer le message", "confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.", "confirmations.revoke_quote.title": "Retirer le message ?", @@ -558,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}", "featured_carousel.slide": "Message {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Récemment, vous avez posté à propos de {items}. Voulez-vous mettre ces hashtags en avant ?", + "featured_tags.suggestions.add": "Ajouter", + "featured_tags.suggestions.added": "Gérer vos hashtags mis en avant à tout moment depuis Modifier le profil > Hashtags mis en avant.", + "featured_tags.suggestions.dismiss": "Non merci", "filter_modal.added.context_mismatch_explanation": "Cette catégorie de filtre ne s'applique pas au contexte dans lequel vous avez accédé à cette publication. Si vous voulez que la publication soit filtrée dans ce contexte également, vous devrez modifier le filtre.", "filter_modal.added.context_mismatch_title": "Incompatibilité du contexte!", "filter_modal.added.expired_explanation": "Cette catégorie de filtre a expiré, vous devrez modifier la date d'expiration pour qu'elle soit appliquée.", @@ -916,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Pour recevoir des notifications lorsque Mastodon n’est pas ouvert, activez les notifications de bureau. Vous pouvez contrôler précisément quels types d’interactions génèrent des notifications de bureau via le bouton {icon} ci-dessus une fois qu’elles sont activées.", "notifications_permission_banner.title": "Ne rien rater", "onboarding.follows.back": "Retour", - "onboarding.follows.done": "Terminé", "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer de rechercher ou de parcourir la page \"Explorer\" pour trouver des personnes à suivre, ou réessayer plus tard.", + "onboarding.follows.next": "Suivant : configurer votre profil", "onboarding.follows.search": "Recherche", "onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.profile.discoverable": "Permettre de découvrir mon profil", "onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.", "onboarding.profile.display_name": "Nom affiché", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", + "onboarding.profile.finish": "Terminer", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", - "onboarding.profile.save_and_continue": "Enregistrer et continuer", "onboarding.profile.title": "Configuration du profil", "onboarding.profile.upload_avatar": "Importer une photo de profil", "onboarding.profile.upload_header": "Importer un entête de profil", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 8d34fc935d93b8..9f0c6e78bc1324 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -18,12 +18,12 @@ "account.add_note": "Ajouter une note personnelle", "account.add_or_remove_from_list": "Ajouter ou retirer des listes", "account.badges.admin": "Admin", - "account.badges.blocked": "Bloqué", + "account.badges.blocked": "Bloqué·e", "account.badges.bot": "Bot", "account.badges.domain_blocked": "Domaine bloqué", "account.badges.group": "Groupe", - "account.badges.muted": "Masqué", - "account.badges.muted_until": "Masqué jusqu’au {until}", + "account.badges.muted": "Masqué·e", + "account.badges.muted_until": "Masqué·e jusqu’au {until}", "account.block": "Bloquer @{name}", "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", @@ -39,7 +39,7 @@ "account.edit_profile_short": "Modifier", "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_many": "Suivi·e par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", "account.familiar_followers_one": "Suivi·e par {name1}", "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", @@ -65,15 +65,14 @@ "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·e}, other {{counter} suivi·e·s}}", + "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.", "account.follows_you": "Vous suit", "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.in_memoriam": "En mémoire.", "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Modifier {item}", "account_edit.column_button": "Terminé", "account_edit.column_title": "Modifier le profil", + "account_edit.custom_fields.name": "champ", "account_edit.custom_fields.placeholder": "Ajouter vos pronoms, vos sites, ou tout ce que vous voulez partager.", + "account_edit.custom_fields.reorder_button": "Réorganiser les champs", + "account_edit.custom_fields.tip_content": "Vous pouvez facilement ajouter de la crédibilité à votre compte Mastodon en vérifiant les liens vers tous les sites Web que vous possédez.", + "account_edit.custom_fields.tip_title": "Astuce : ajout de liens vérifiés", "account_edit.custom_fields.title": "Champs personnalisés", + "account_edit.custom_fields.verified_hint": "Comment ajouter un lien vérifié ?", "account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.", "account_edit.display_name.title": "Nom public", "account_edit.featured_hashtags.item": "hashtags", "account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.", "account_edit.featured_hashtags.title": "Hashtags mis en avant", + "account_edit.field_delete_modal.confirm": "Voulez-vous vraiment supprimer ce champ personnalisé ? Cette action ne peut pas être annulée.", + "account_edit.field_delete_modal.delete_button": "Supprimer", + "account_edit.field_delete_modal.title": "Supprimer le champ personnalisé ?", + "account_edit.field_edit_modal.add_title": "Ajouter un champ personnalisé", + "account_edit.field_edit_modal.edit_title": "Modifier un champ personnalisé", + "account_edit.field_edit_modal.limit_header": "Limite de caractères recommandée dépassée", + "account_edit.field_edit_modal.limit_message": "L'affichage du champ peut être tronqué sur les téléphones.", + "account_edit.field_edit_modal.link_emoji_warning": "Nous déconseillons l'usage d'émoji personnalisé avec les URL. Les champs personnalisés contenant les deux seront affichés comme du texte et non un lien, afin d'éviter toute confusion.", + "account_edit.field_edit_modal.name_hint": "Par exemple « Site Web personnel »", + "account_edit.field_edit_modal.name_label": "Libellé", + "account_edit.field_edit_modal.url_warning": "Pour ajouter un lien, veuillez inclure {protocol} au début.", + "account_edit.field_edit_modal.value_hint": "Par exemple « https://exemple.me »", + "account_edit.field_edit_modal.value_label": "Valeur", + "account_edit.field_reorder_modal.drag_cancel": "Déplacement annulé. Le champ « {item} » a été redéposé.", + "account_edit.field_reorder_modal.drag_end": "Le champ « {item} » a été déposé.", + "account_edit.field_reorder_modal.drag_instructions": "Pour réorganiser les champs personnalisés, appuyer sur espace ou entrée. Utiliser les touches fléchées pour déplacer le champ vers le haut ou vers le bas. Appuyer à nouveau sur espace ou entrée pour déposer le champ dans sa nouvelle position, ou appuyer sur échap pour annuler.", + "account_edit.field_reorder_modal.drag_move": "Le champ « {item} » a été déplacé.", + "account_edit.field_reorder_modal.drag_over": "Le champ « {item} » a été déplacé au-dessus de « {over} ».", + "account_edit.field_reorder_modal.drag_start": "Champ « {item} » sélectionné.", + "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", + "account_edit.field_reorder_modal.title": "Réorganiser les champs", + "account_edit.image_alt_modal.add_title": "Ajouter un texte alternatif", + "account_edit.image_alt_modal.details_content": "À faire :
    • Se décrire comme vous apparaissez sur la photo
    • Utiliser la troisième personne (par exemple « Alex » au lieu de « moi »)
    • Être succinct·e – quelques mot suffisent souvent
    À éviter :
    • Commencer par « Une photo de » – c'est redondant pour les lecteurs d'écran
    Example :
    • « Alex portant une chemise vert et des lunettes »
    ", + "account_edit.image_alt_modal.details_title": "Astuces : texte alternatif pour les photos de profil", + "account_edit.image_alt_modal.edit_title": "Modifier le texte alternatif", + "account_edit.image_alt_modal.text_hint": "Le texte alternatif aide les personnes utilisant un lecteur d'écran à comprendre votre contenu.", + "account_edit.image_alt_modal.text_label": "Texte alternatif", + "account_edit.image_delete_modal.confirm": "Voulez-vous vraiment supprimer cette image ? Cette action est irréversible.", + "account_edit.image_delete_modal.delete_button": "Supprimer", + "account_edit.image_delete_modal.title": "Supprimer l'image ?", + "account_edit.image_edit.add_button": "Ajouter une image", + "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", + "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", + "account_edit.image_edit.remove_button": "Supprimer l’image", + "account_edit.image_edit.replace_button": "Remplacer l'image", "account_edit.name_modal.add_title": "Ajouter un nom public", "account_edit.name_modal.edit_title": "Modifier le nom public", "account_edit.profile_tab.button_label": "Personnaliser", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.", "account_edit.profile_tab.title": "Paramètres de l'onglet du profil", "account_edit.save": "Enregistrer", + "account_edit.upload_modal.back": "Retour", + "account_edit.upload_modal.done": "Terminé", + "account_edit.upload_modal.next": "Suivant", + "account_edit.upload_modal.step_crop.zoom": "Agrandir", + "account_edit.upload_modal.step_upload.button": "Parcourir les fichiers", + "account_edit.upload_modal.step_upload.dragging": "Déposer pour téléverser", + "account_edit.upload_modal.step_upload.header": "Choisir une image", + "account_edit.upload_modal.step_upload.hint": "Format WebP, PNG, GIF ou JPEG, jusqu'à {limit} Mo.{br}L'image sera redimensionnée à {width} × {height} px.", + "account_edit.upload_modal.title_add": "Ajouter une photo de profil", + "account_edit.upload_modal.title_replace": "Remplacer la photo de profil", + "account_edit.verified_modal.details": "Ajouter de la crédibilité à votre profil Mastodon en vérifiant les liens vers vos sites Web personnels. Voici comment cela fonctionne :", + "account_edit.verified_modal.invisible_link.details": "Ajouter le lien dans votre en-tête. La partie importante est « rel=\"me\" » qui empêche l'usurpation d'identité sur des sites Web ayant du contenu généré par d'autres utilisateur·rice·s. Vous pouvez aussi utiliser une balise link dans l'en-tête de la page au lieu de {tag}, mais le code HTML doit être accessible sans avoir besoin d'exécuter du JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Comment rendre le lien invisible ?", + "account_edit.verified_modal.step1.header": "Copier-coller le code HTML ci-dessous dans l'en-tête de votre site web", + "account_edit.verified_modal.step2.details": "Si vous avez déjà ajouté votre site Web en tant que champ personnalisé, vous devrez le supprimer et le rajouter pour déclencher la vérification.", + "account_edit.verified_modal.step2.header": "Ajouter votre site Web en tant que champ personnalisé", + "account_edit.verified_modal.title": "Comment ajouter un lien vérifié ?", "account_edit_tags.add_tag": "Ajouter #{tagName}", "account_edit_tags.column_title": "Modifier les hashtags mis en avant", "account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.", @@ -293,6 +349,8 @@ "collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez", "collections.accounts.empty_title": "Cette collection est vide", "collections.collection_description": "Description", + "collections.collection_language": "Langue", + "collections.collection_language_none": "Aucune", "collections.collection_name": "Nom", "collections.collection_topic": "Sujet", "collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection ?", @@ -306,10 +364,15 @@ "collections.create_collection": "Créer une collection", "collections.delete_collection": "Supprimer la collection", "collections.description_length_hint": "Maximum 100 caractères", + "collections.detail.accept_inclusion": "D'accord", "collections.detail.accounts_heading": "Comptes", + "collections.detail.author_added_you": "{author} vous a ajouté·e à cette collection", "collections.detail.curated_by_author": "Organisée par {author}", "collections.detail.curated_by_you": "Organisée par vous", "collections.detail.loading": "Chargement de la collection…", + "collections.detail.other_accounts_in_collection": "Autres comptes dans cette collection :", + "collections.detail.revoke_inclusion": "Me retirer", + "collections.detail.sensitive_note": "Cette collection contient des comptes et du contenu qui peut être sensibles.", "collections.detail.share": "Partager la collection", "collections.edit_details": "Modifier les détails", "collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.", @@ -324,10 +387,14 @@ "collections.old_last_post_note": "Dernière publication il y a plus d'une semaine", "collections.remove_account": "Supprimer ce compte", "collections.report_collection": "Signaler cette collection", + "collections.revoke_collection_inclusion": "Me retirer de cette collection", + "collections.revoke_inclusion.confirmation": "Vous avez été retiré·e de « {collection} »", + "collections.revoke_inclusion.error": "Une erreur s'est produite, veuillez réessayer plus tard.", "collections.search_accounts_label": "Chercher des comptes à ajouter…", "collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes", "collections.sensitive": "Sensible", "collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.", + "collections.topic_special_chars_hint": "Les caractères spéciaux seront supprimés lors de l'enregistrement", "collections.view_collection": "Voir la collection", "collections.view_other_collections_by_user": "Voir les autres collections par ce compte", "collections.visibility_public": "Publique", @@ -447,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Supprimer l'abonné·e", "confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Voulez-vous vraiment continuer ?", "confirmations.remove_from_followers.title": "Supprimer l'abonné·e ?", + "confirmations.revoke_collection_inclusion.confirm": "Me retirer", + "confirmations.revoke_collection_inclusion.message": "Cette action est permanente, la personne qui gère la collection ne pourra plus vous y rajouter plus tard.", + "confirmations.revoke_collection_inclusion.title": "Vous retirer de cette collection ?", "confirmations.revoke_quote.confirm": "Retirer le message", "confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.", "confirmations.revoke_quote.title": "Retirer le message ?", @@ -558,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}", "featured_carousel.slide": "Message {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Récemment, vous avez posté à propos de {items}. Voulez-vous mettre ces hashtags en avant ?", + "featured_tags.suggestions.add": "Ajouter", + "featured_tags.suggestions.added": "Gérer vos hashtags mis en avant à tout moment depuis Modifier le profil > Hashtags mis en avant.", + "featured_tags.suggestions.dismiss": "Non merci", "filter_modal.added.context_mismatch_explanation": "Cette catégorie de filtre ne s'applique pas au contexte dans lequel vous avez accédé à ce message. Si vous voulez que le message soit filtré dans ce contexte également, vous devrez modifier le filtre.", "filter_modal.added.context_mismatch_title": "Incompatibilité du contexte !", "filter_modal.added.expired_explanation": "Cette catégorie de filtre a expiré, vous devrez modifier la date d'expiration pour qu'elle soit appliquée.", @@ -916,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Pour recevoir des notifications lorsque Mastodon n’est pas ouvert, activez les notifications du bureau. Vous pouvez contrôler précisément quels types d’interactions génèrent des notifications de bureau via le bouton {icon} ci-dessus une fois qu’elles sont activées.", "notifications_permission_banner.title": "Toujours au courant", "onboarding.follows.back": "Retour", - "onboarding.follows.done": "Terminé", "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer d'utiliser la recherche ou parcourir la page de découverte pour trouver des personnes à suivre, ou réessayez plus tard.", + "onboarding.follows.next": "Suivant : configurer votre profil", "onboarding.follows.search": "Recherche", "onboarding.follows.title": "Suivre des personnes pour commencer", "onboarding.profile.discoverable": "Permettre de découvrir mon profil", "onboarding.profile.discoverable_hint": "Lorsque vous acceptez d'être découvert sur Mastodon, vos messages peuvent apparaître dans les résultats de recherche et les tendances, et votre profil peut être suggéré à des personnes ayant des intérêts similaires aux vôtres.", "onboarding.profile.display_name": "Nom affiché", "onboarding.profile.display_name_hint": "Votre nom complet ou votre nom rigolo…", + "onboarding.profile.finish": "Terminer", "onboarding.profile.note": "Biographie", "onboarding.profile.note_hint": "Vous pouvez @mentionner d'autres personnes ou #hashtags…", - "onboarding.profile.save_and_continue": "Enregistrer et continuer", "onboarding.profile.title": "Configuration du profil", "onboarding.profile.upload_avatar": "Importer une photo de profil", "onboarding.profile.upload_header": "Importer un entête de profil", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 230dae1d97a595..9592886fa6c182 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -664,7 +664,6 @@ "notifications_permission_banner.how_to_control": "Om meldingen te ûntfangen wannear’t Mastodon net iepen stiet. Jo kinne krekt bepale hokker soarte fan ynteraksjes wol of gjin desktopmeldingen jouwe fia de boppesteande {icon} knop.", "notifications_permission_banner.title": "Mis neat", "onboarding.follows.back": "Tebek", - "onboarding.follows.done": "Klear", "onboarding.follows.empty": "Spitigernôch kinne op dit stuit gjin resultaten toand wurde. Jo kinne probearje te sykjen of te blêdzjen troch de ferkenningsside om minsken te finen dy’t jo folgje kinne, of probearje it letter opnij.", "onboarding.follows.search": "Sykje", "onboarding.follows.title": "Folgje minsken om te begjinnen", @@ -674,7 +673,6 @@ "onboarding.profile.display_name_hint": "Jo folsleine namme of in aardige bynamme…", "onboarding.profile.note": "Biografy", "onboarding.profile.note_hint": "Jo kinne oare minsken @fermelde of #hashtags brûke…", - "onboarding.profile.save_and_continue": "Bewarje en trochgean", "onboarding.profile.title": "Profyl ynstelle", "onboarding.profile.upload_avatar": "Profylfoto oplade", "onboarding.profile.upload_header": "Omslachfoto foar profyl oplade", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index fae885d1bde03f..0ce04407a714bd 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Cuir Próifíl in Eagar", "account_edit.custom_fields.name": "réimse", "account_edit.custom_fields.placeholder": "Cuir do fhorainmneacha, naisc sheachtracha, nó aon rud eile ar mhaith leat a roinnt leis.", + "account_edit.custom_fields.reorder_button": "Athordaigh réimsí", "account_edit.custom_fields.tip_content": "Is féidir leat creidiúnacht a chur le do chuntas Mastodon go héasca trí naisc chuig aon suíomhanna Gréasáin ar leatsa iad a fhíorú.", "account_edit.custom_fields.tip_title": "Leid: Ag cur naisc fhíoraithe leis", "account_edit.custom_fields.title": "Réimsí saincheaptha", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "An bhfuil fonn ort an réimse saincheaptha a scriosadh?", "account_edit.field_edit_modal.add_title": "Cuir réimse saincheaptha leis", "account_edit.field_edit_modal.edit_title": "Cuir réimse saincheaptha in eagar", + "account_edit.field_edit_modal.limit_header": "Sáraíodh an teorainn carachtar molta", + "account_edit.field_edit_modal.limit_message": "B’fhéidir nach bhfeicfidh úsáideoirí soghluaiste do réimse ina iomláine.", + "account_edit.field_edit_modal.link_emoji_warning": "Molaimid gan emoji saincheaptha a úsáid i gcomhar le Urlanna. Taispeánfar réimsí saincheaptha ina bhfuil an dá cheann mar théacs amháin seachas mar nasc, chun mearbhall úsáideoirí a sheachaint.", "account_edit.field_edit_modal.name_hint": "M.sh. “Suíomh Gréasáin pearsanta”", "account_edit.field_edit_modal.name_label": "Lipéad", - "account_edit.field_edit_modal.value_hint": "M.sh. “shampla.me”", + "account_edit.field_edit_modal.url_warning": "Chun nasc a chur leis, cuir {protocol} ag an tús le do thoil.", + "account_edit.field_edit_modal.value_hint": "M.sh. “https://example.me”", "account_edit.field_edit_modal.value_label": "Luach", + "account_edit.field_reorder_modal.drag_cancel": "Cuireadh an tarraingt ar ceal. Baineadh an réimse \"{item}\".", + "account_edit.field_reorder_modal.drag_end": "Baineadh an réimse \"{item}\".", + "account_edit.field_reorder_modal.drag_instructions": "Chun réimsí saincheaptha a athshocrú, brúigh spás nó enter. Agus tú ag tarraingt, bain úsáid as na heochracha saigheada chun an réimse a bhogadh suas nó síos. Brúigh spás nó enter arís chun an réimse a scaoileadh ina shuíomh nua, nó brúigh escape chun cealú.", + "account_edit.field_reorder_modal.drag_move": "Bogadh an réimse \"{item}\".", + "account_edit.field_reorder_modal.drag_over": "Bogadh réimse \"{item}\" thar \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Réimse \"{item}\" bailithe.", + "account_edit.field_reorder_modal.handle_label": "Tarraing réimse \"{item}\"", + "account_edit.field_reorder_modal.title": "Athshocraigh réimsí", + "account_edit.image_alt_modal.add_title": "Cuir téacs malartach leis", + "account_edit.image_alt_modal.details_content": "DÉAN:
    • Déan cur síos ort féin mar atá sa phictiúr
    • Úsáid teanga an tríú pearsa (m.sh. “Alex” in ionad “mise”)
    • Bí gonta – is minic a bhíonn cúpla focal leordhóthanach
    NÁ DÉAN:
    • Tosaigh le “Grianghraf de” – tá sé iomarcach do léitheoirí scáileáin
    SAMPLA:
    • “Alex ag caitheamh léine ghlas agus spéaclaí”
    ", + "account_edit.image_alt_modal.details_title": "Leideanna: Téacs malartach do ghrianghraif phróifíle", + "account_edit.image_alt_modal.edit_title": "Cuir téacs alt in eagar", + "account_edit.image_alt_modal.text_hint": "Cuidíonn téacs malartach le húsáideoirí léitheoirí scáileáin d’ábhar a thuiscint.", + "account_edit.image_alt_modal.text_label": "Téacs malartach", + "account_edit.image_delete_modal.confirm": "An bhfuil tú cinnte gur mian leat an íomhá seo a scriosadh? Ní féidir an gníomh seo a chealú.", + "account_edit.image_delete_modal.delete_button": "Scrios", + "account_edit.image_delete_modal.title": "Íomhá a scriosadh?", + "account_edit.image_edit.add_button": "Cuir íomhá leis", + "account_edit.image_edit.alt_add_button": "Cuir téacs alt leis", + "account_edit.image_edit.alt_edit_button": "Cuir téacs alt in eagar", + "account_edit.image_edit.remove_button": "Bain íomhá", + "account_edit.image_edit.replace_button": "Athsholáthair íomhá", "account_edit.name_modal.add_title": "Cuir ainm taispeána leis", "account_edit.name_modal.edit_title": "Cuir ainm taispeána in eagar", "account_edit.profile_tab.button_label": "Saincheap", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Saincheap na cluaisíní ar do phróifíl agus a bhfuil á thaispeáint iontu.", "account_edit.profile_tab.title": "Socruithe an chluaisín próifíle", "account_edit.save": "Sábháil", + "account_edit.upload_modal.back": "Ar ais", + "account_edit.upload_modal.done": "Déanta", + "account_edit.upload_modal.next": "Ar Aghaidh", + "account_edit.upload_modal.step_crop.zoom": "Zúmáil", + "account_edit.upload_modal.step_upload.button": "Brabhsáil comhaid", + "account_edit.upload_modal.step_upload.dragging": "Scaoil le huaslódáil", + "account_edit.upload_modal.step_upload.header": "Roghnaigh íomhá", + "account_edit.upload_modal.step_upload.hint": "Formáid WEBP, PNG, GIF nó JPG, suas le {limit}MB.{br}Scálfar an íomhá go {width}x{height}px.", + "account_edit.upload_modal.title_add": "Cuir grianghraf próifíle leis", + "account_edit.upload_modal.title_replace": "Athsholáthar grianghraf próifíle", "account_edit.verified_modal.details": "Cuir creidiúnacht le do phróifíl Mastodon trí naisc chuig láithreáin ghréasáin phearsanta a fhíorú. Seo mar a oibríonn sé:", "account_edit.verified_modal.invisible_link.details": "Cuir an nasc le do cheanntásc. Is í an chuid thábhachtach ná rel=\"me\" a chuireann cosc ​​ar phearsanú ar shuíomhanna gréasáin a bhfuil inneachar a ghintear ag úsáideoirí. Is féidir leat clib nasc a úsáid fiú i gceanntásc an leathanaigh in ionad {tag}, ach caithfidh an HTML a bheith inrochtana gan JavaScript a chur i gcrích.", "account_edit.verified_modal.invisible_link.summary": "Conas a dhéanaim an nasc dofheicthe?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Cuir suas le {count} cuntas leis a leanann tú", "collections.accounts.empty_title": "Tá an bailiúchán seo folamh", "collections.collection_description": "Cur síos", + "collections.collection_language": "Teanga", + "collections.collection_language_none": "Dada", "collections.collection_name": "Ainm", "collections.collection_topic": "Topaic", "collections.confirm_account_removal": "An bhfuil tú cinnte gur mian leat an cuntas seo a bhaint den bhailiúchán seo?", @@ -326,10 +364,15 @@ "collections.create_collection": "Cruthaigh bailiúchán", "collections.delete_collection": "Scrios bailiúchán", "collections.description_length_hint": "Teorainn 100 carachtar", + "collections.detail.accept_inclusion": "Ceart go leor", "collections.detail.accounts_heading": "Cuntais", + "collections.detail.author_added_you": "Chuir {author} leis an mbailiúchán seo thú", "collections.detail.curated_by_author": "Curtha i dtoll a chéile ag {author}", "collections.detail.curated_by_you": "Curtha i dtoll a chéile agatsa", "collections.detail.loading": "Ag lódáil an bhailiúcháin…", + "collections.detail.other_accounts_in_collection": "Daoine eile sa bhailiúchán seo:", + "collections.detail.revoke_inclusion": "Bain mé", + "collections.detail.sensitive_note": "Tá cuntais agus ábhar sa bhailiúchán seo a d'fhéadfadh a bheith íogair do roinnt úsáideoirí.", "collections.detail.share": "Comhroinn an bailiúchán seo", "collections.edit_details": "Cuir sonraí in eagar", "collections.error_loading_collections": "Tharla earráid agus iarracht á déanamh do bhailiúcháin a luchtú.", @@ -344,10 +387,14 @@ "collections.old_last_post_note": "Postáilte go deireanach breis agus seachtain ó shin", "collections.remove_account": "Bain an cuntas seo", "collections.report_collection": "Tuairiscigh an bailiúchán seo", + "collections.revoke_collection_inclusion": "Bain mé féin as an mbailiúchán seo", + "collections.revoke_inclusion.confirmation": "Baineadh as \"{collection}\" thú", + "collections.revoke_inclusion.error": "Tharla earráid, déan iarracht arís ar ball.", "collections.search_accounts_label": "Cuardaigh cuntais le cur leis…", "collections.search_accounts_max_reached": "Tá an líon uasta cuntas curtha leis agat", "collections.sensitive": "Íogair", "collections.topic_hint": "Cuir haischlib leis a chabhraíonn le daoine eile príomhábhar an bhailiúcháin seo a thuiscint.", + "collections.topic_special_chars_hint": "Bainfear carachtair speisialta agus tú ag sábháil", "collections.view_collection": "Féach ar bhailiúchán", "collections.view_other_collections_by_user": "Féach ar bhailiúcháin eile ón úsáideoir seo", "collections.visibility_public": "Poiblí", @@ -467,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Bain leantóir", "confirmations.remove_from_followers.message": "Scoirfidh {name} de bheith ag leanúint leat. An bhfuil tú cinnte gur mian leat leanúint ar aghaidh?", "confirmations.remove_from_followers.title": "Bain an leantóir?", + "confirmations.revoke_collection_inclusion.confirm": "Bain mé", + "confirmations.revoke_collection_inclusion.message": "Is gníomh buan é seo, agus ní bheidh an coimeádaí in ann tú a chur leis an mbailiúchán arís níos déanaí.", + "confirmations.revoke_collection_inclusion.title": "Bain tú féin den bhailiúchán seo?", "confirmations.revoke_quote.confirm": "Bain postáil", "confirmations.revoke_quote.message": "Ní féidir an gníomh seo a chealú.", "confirmations.revoke_quote.title": "Bain postáil?", @@ -578,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Postáil phinnáilte} two {Poist Phionáilte} few {Poist Phionáilte} many {Poist Phionáilte} other {Poist Phionáilte}}", "featured_carousel.slide": "Post {current, number} of {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Le déanaí, tá postáil déanta agat faoi {items}. An gcuirfeá iad seo leis mar hashtags le feiceáil?", + "featured_tags.suggestions.add": "Cuir leis", + "featured_tags.suggestions.added": "Bainistigh do hashtags le feiceáil ag am ar bith faoi Cuir Próifíl in Eagar > Hashtags le feiceáil.", + "featured_tags.suggestions.dismiss": "Ní raibh maith agat", "filter_modal.added.context_mismatch_explanation": "Ní bhaineann an chatagóir scagaire seo leis an gcomhthéacs ina bhfuair tú rochtain ar an bpostáil seo. Más mian leat an postáil a scagadh sa chomhthéacs seo freisin, beidh ort an scagaire a chur in eagar.", "filter_modal.added.context_mismatch_title": "Neamhréir comhthéacs!", "filter_modal.added.expired_explanation": "Tá an chatagóir scagaire seo imithe in éag, beidh ort an dáta éaga a athrú chun é a chur i bhfeidhm.", @@ -936,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Chun fógraí a fháil nuair nach bhfuil Mastodon oscailte, cumasaigh fógraí deisce. Is féidir leat a rialú go beacht cé na cineálacha idirghníomhaíochtaí a ghineann fógraí deisce tríd an gcnaipe {icon} thuas nuair a bhíonn siad cumasaithe.", "notifications_permission_banner.title": "Ná caill aon rud go deo", "onboarding.follows.back": "Ar ais", - "onboarding.follows.done": "Déanta", "onboarding.follows.empty": "Ar an drochuair, ní féidir aon torthaí a thaispeáint faoi láthair. Is féidir leat triail a bhaint as cuardach nó brabhsáil ar an leathanach taiscéalaíochta chun teacht ar dhaoine le leanúint, nó bain triail eile as níos déanaí.", + "onboarding.follows.next": "Ar Aghaidh: Socraigh do phróifíl", "onboarding.follows.search": "Cuardach", "onboarding.follows.title": "Lean daoine le tosú", "onboarding.profile.discoverable": "Déan mo phróifíl a fháil amach", "onboarding.profile.discoverable_hint": "Nuair a roghnaíonn tú infhionnachtana ar Mastodon, d’fhéadfadh do phoist a bheith le feiceáil i dtorthaí cuardaigh agus treochtaí, agus d’fhéadfaí do phróifíl a mholadh do dhaoine a bhfuil na leasanna céanna acu leat.", "onboarding.profile.display_name": "Ainm taispeána", "onboarding.profile.display_name_hint": "D’ainm iomlán nó d’ainm spraíúil…", + "onboarding.profile.finish": "Críochnaigh", "onboarding.profile.note": "Bith", "onboarding.profile.note_hint": "Is féidir leat @ daoine eile a lua nó #hashtags…", - "onboarding.profile.save_and_continue": "Sábháil agus lean ar aghaidh", "onboarding.profile.title": "Socrú próifíle", "onboarding.profile.upload_avatar": "Íosluchtaigh pictiúr próifíl", "onboarding.profile.upload_header": "Íoslódáil an ceanntásca próifíl", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 521dcbd200245b..0ca7f12e3ed18f 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -752,7 +752,6 @@ "notifications_permission_banner.how_to_control": "Airson brathan fhaighinn nuair nach eil Mastodon fosgailte, cuir na brathan deasga an comas. Tha an smachd agad fhèin air dè na seòrsaichean de chonaltradh a ghineas brathan deasga leis a’ phutan {icon} gu h-àrd nuair a bhios iad air an cur an comas.", "notifications_permission_banner.title": "Na caill dad gu bràth tuilleadh", "onboarding.follows.back": "Air ais", - "onboarding.follows.done": "Deiseil", "onboarding.follows.empty": "Gu mì-fhortanach, chan urrainn dhuinn toradh a shealltainn an-dràsta. Feuch gleus an luirg no duilleag an rùrachaidh airson daoine ri leantainn a lorg no feuch ris a-rithist an ceann tamaill.", "onboarding.follows.search": "Lorg", "onboarding.follows.title": "Lean daoine airson tòiseachadh", @@ -762,7 +761,6 @@ "onboarding.profile.display_name_hint": "D’ ainm slàn no spòrsail…", "onboarding.profile.note": "Cunntas-beatha", "onboarding.profile.note_hint": "’S urrainn dhut @iomradh a thoirt air càch no air #tagaicheanHais…", - "onboarding.profile.save_and_continue": "Sàbhail ’s lean air adhart", "onboarding.profile.title": "Suidheachadh na pròifile", "onboarding.profile.upload_avatar": "Luchdaich suas dealbh na pròifil", "onboarding.profile.upload_header": "Luchdaich suas bann-cinn na pròifil", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 93b3e0ea011c5c..af372edd2b73bd 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Editar perfil", "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Engade os teus pronomes, ligazóns externas, ou o que queiras compartir.", + "account_edit.custom_fields.reorder_button": "Reordenar os campos", "account_edit.custom_fields.tip_content": "Podes darlle maior credibilidade á túa conta Mastodon se verificas as ligazóns a sitios web da túa propiedade.", "account_edit.custom_fields.tip_title": "Consello: Engadir ligazóns verificadas", "account_edit.custom_fields.title": "Campos personalizados", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Eliminar campo persoal?", "account_edit.field_edit_modal.add_title": "Engadir campo persoal", "account_edit.field_edit_modal.edit_title": "Editar campo persoal", + "account_edit.field_edit_modal.limit_header": "Superouse o límite de caracteres recomendado", + "account_edit.field_edit_modal.limit_message": "Nos dispositivos móbiles podería non verse o campo completo.", + "account_edit.field_edit_modal.link_emoji_warning": "Non recomendamos o uso de emojis persoais combinados con URLs. Os campos persoais que conteñen ambos móstranse só como texto e non como unha ligazón, para evitar a confusión de quen os lea.", "account_edit.field_edit_modal.name_hint": "Ex. \"Páxina web persoal\"", "account_edit.field_edit_modal.name_label": "Etiqueta", - "account_edit.field_edit_modal.value_hint": "Ex. \"exemplo.gal\"", + "account_edit.field_edit_modal.url_warning": "Para engadir unha ligazón, inclúe {protocol} diante.", + "account_edit.field_edit_modal.value_hint": "Ex. “https://exemplo.me”", "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "Cancelouse o arrastrado. Soltouse o campo \"{item}\".", + "account_edit.field_reorder_modal.drag_end": "Soltouse o campo \"{item}\".", + "account_edit.field_reorder_modal.drag_instructions": "Para ordear os campos persoais, preme espazo ou enter. Ao arrastrar, usa as teclas de frecha para mover o campo arriba ou abaixo. Preme espazo ou enter outra vez para soltar o campo na nova posición, ou preme escape para cancelar.", + "account_edit.field_reorder_modal.drag_move": "Moveuse o campo \"{item}\".", + "account_edit.field_reorder_modal.drag_over": "Moveuse o campo \"{item}\" enriba de \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Seleccionado o campo \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Arrastra o campo \"{item}\"", + "account_edit.field_reorder_modal.title": "Ordear campos", + "account_edit.image_alt_modal.add_title": "Engadir texto descritivo", + "account_edit.image_alt_modal.details_content": "COMO:
    • Descríbete como apareces na imaxe
    • Usa a terceira persoa (ex. “Alex”, e non “eu”)
    • Sé breve – abondan poucas palabras
    NON fagas:
    • Comezar con “Foto de” – é redundante nos lectores de pantalla
    EXEMPLO:
    • “Alex vestindo camisa verde e cos lentes postos”
    ", + "account_edit.image_alt_modal.details_title": "Consellos: Texto descritivo para fotos de perfil", + "account_edit.image_alt_modal.edit_title": "Editar descrición", + "account_edit.image_alt_modal.text_hint": "A descrición axuda ás persoas que usan lectores de pantalla a comprender o que publicas.", + "account_edit.image_alt_modal.text_label": "Texto descritivo", + "account_edit.image_delete_modal.confirm": "Tes certeza de querer eliminar esta imaxe? Non se poderá desfacer a acción.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "Eliminar imaxe?", + "account_edit.image_edit.add_button": "Engadir imaxe", + "account_edit.image_edit.alt_add_button": "Engadir descrición", + "account_edit.image_edit.alt_edit_button": "Editar descrición", + "account_edit.image_edit.remove_button": "Retirar a imaxe", + "account_edit.image_edit.replace_button": "Substituír a imaxe", "account_edit.name_modal.add_title": "Engadir nome público", "account_edit.name_modal.edit_title": "Editar o nome público", "account_edit.profile_tab.button_label": "Personalizar", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Personaliza as pestanas e o seu contido no teu perfil.", "account_edit.profile_tab.title": "Perfil e axustes das pestanas", "account_edit.save": "Gardar", + "account_edit.upload_modal.back": "Volver", + "account_edit.upload_modal.done": "Feito", + "account_edit.upload_modal.next": "Seguinte", + "account_edit.upload_modal.step_crop.zoom": "Achegamento", + "account_edit.upload_modal.step_upload.button": "Explorar ficheiros", + "account_edit.upload_modal.step_upload.dragging": "Solta aquí para subir", + "account_edit.upload_modal.step_upload.header": "Escoller unha imaxe", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF ou JPG, ata {limit}MB.{br}A imaxe será comprimida a {width}x{height}px.", + "account_edit.upload_modal.title_add": "Engadir foto do perfil", + "account_edit.upload_modal.title_replace": "Substituír foto do perfil", "account_edit.verified_modal.details": "Engade maior credibilidade ao teu perfil Mastodon verificando as ligazóns ás túas páxinas web persoais. Funciona así:", "account_edit.verified_modal.invisible_link.details": "Engade a ligazón ao «header» da páxina web. A parte importante é rel=\"me\", que evita a suplantación en sitios web con contido creado polas usuarias. Tamén podes usar a etiqueta «link» na cabeceira da páxina no lugar de {tag}, pero o HTML ten que ser accesible sen usar JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Como fago visible a ligazón?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Engade ata {count} contas que segues", "collections.accounts.empty_title": "A colección está baleira", "collections.collection_description": "Descrición", + "collections.collection_language": "Idioma", + "collections.collection_language_none": "Ningún", "collections.collection_name": "Nome", "collections.collection_topic": "Temática", "collections.confirm_account_removal": "Tes certeza de querer retirar esta conta desta colección?", @@ -326,12 +364,14 @@ "collections.create_collection": "Crear colección", "collections.delete_collection": "Eliminar colección", "collections.description_length_hint": "Límite de 100 caracteres", + "collections.detail.accept_inclusion": "Vale", "collections.detail.accounts_heading": "Contas", "collections.detail.author_added_you": "{author} engadíute a esta colección", "collections.detail.curated_by_author": "Seleccionadas por {author}", "collections.detail.curated_by_you": "Seleccionadas por ti", "collections.detail.loading": "Cargando colección…", "collections.detail.other_accounts_in_collection": "Outras contas na colección:", + "collections.detail.revoke_inclusion": "Non quero", "collections.detail.sensitive_note": "Esta colección presenta contas e contido que poderían ser sensibles para algunhas persoas.", "collections.detail.share": "Compartir esta colección", "collections.edit_details": "Editar detalles", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Hai máis dunha semana da última publicación", "collections.remove_account": "Retirar esta conta", "collections.report_collection": "Denunciar esta colección", + "collections.revoke_collection_inclusion": "Sácame desta colección", + "collections.revoke_inclusion.confirmation": "Quitámoste da colección \"{collection}\"", + "collections.revoke_inclusion.error": "Algo fallou, inténtao outra vez máis tarde.", "collections.search_accounts_label": "Buscar contas para engadir…", "collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas", "collections.sensitive": "Sensible", "collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.", + "collections.topic_special_chars_hint": "Vanse eliminar os caracteres especiais ao gardar", "collections.view_collection": "Ver colección", "collections.view_other_collections_by_user": "Ver outras coleccións desta usuaria", "collections.visibility_public": "Pública", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Quitar seguidora", "confirmations.remove_from_followers.message": "{name} vai deixar de seguirte. É isto o que queres?", "confirmations.remove_from_followers.title": "Quitar seguidora?", + "confirmations.revoke_collection_inclusion.confirm": "Quítame", + "confirmations.revoke_collection_inclusion.message": "A acción é definitiva, a creadora da colección non poderá volver a engadirte máis adiante.", + "confirmations.revoke_collection_inclusion.title": "Queres que non te inclúan nesta colección?", "confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.message": "Esta acción non se pode desfacer.", "confirmations.revoke_quote.title": "Eliminar publicación?", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Publicación fixada} other {Publicacións fixadas}}", "featured_carousel.slide": "Publicación {current, number} de {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Ultimamente publicaches sobre {items}. Engadimos isto como cancelos destacados?", + "featured_tags.suggestions.add": "Engadir", + "featured_tags.suggestions.added": "Xestiona os teus cancelos destacados cando queiras en Editar perfil > Cancelos destacados.", + "featured_tags.suggestions.dismiss": "Non, grazas", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro non se aplica ao contexto no que accedeches a esta publicación. Se queres que a publicación se filtre nese contexto tamén, terás que editar o filtro.", "filter_modal.added.context_mismatch_title": "Non concorda o contexto!", "filter_modal.added.expired_explanation": "Esta categoría de filtro caducou, terás que cambiar a data de caducidade para que se aplique.", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Activa as notificacións de escritorio para recibir notificacións mentras Mastodon non está aberto. Podes controlar de xeito preciso o tipo de interaccións que crean as notificacións de escritorio a través da {icon} superior unha vez están activadas.", "notifications_permission_banner.title": "Non perder nada", "onboarding.follows.back": "Volver", - "onboarding.follows.done": "Feito", "onboarding.follows.empty": "Desgraciadamente agora mesmo non hai nada que mostrar. Podes intentalo coa busca ou na páxina descubrir para atopar persoas ás que seguir, ou intentalo máis tarde.", + "onboarding.follows.next": "Seguinte: Configura o teu perfil", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Comeza seguindo algunhas persoas", "onboarding.profile.discoverable": "Que o meu perfil se poida atopar", "onboarding.profile.discoverable_hint": "Cando elixes que poidan atoparte en Mastodon as túas publicacións aparecerán nos resultados das buscas e nos temas en voga, e o teu perfil podería ser suxerido para seguimento a persoas con intereses semellantes aos teus.", "onboarding.profile.display_name": "Nome público", "onboarding.profile.display_name_hint": "O teu nome completo ou un nome divertido…", + "onboarding.profile.finish": "Finalizar", "onboarding.profile.note": "Acerca de ti", "onboarding.profile.note_hint": "Podes @mencionar a outras persoas ou usar #cancelos…", - "onboarding.profile.save_and_continue": "Gardar e continuar", "onboarding.profile.title": "Configuración do perfil", "onboarding.profile.upload_avatar": "Subir imaxe do perfil", "onboarding.profile.upload_header": "Subir cabeceira para o perfil", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 54af1720afca2a..4034ce3e308db3 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -73,7 +73,6 @@ "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "עריכת {item}", "account_edit.column_button": "סיום", "account_edit.column_title": "עריכת הפרופיל", + "account_edit.custom_fields.name": "שדה", "account_edit.custom_fields.placeholder": "הוסיפו צורת פניה, קישורים חיצוניים וכל דבר שתרצו לשתף.", + "account_edit.custom_fields.reorder_button": "הגדרת סדר השדות", + "account_edit.custom_fields.tip_content": "ניתן להוסיף אמינות לחשבון המסטודון שלך על ידי וידוא קישורים לאתרים שבבעלותך.", + "account_edit.custom_fields.tip_title": "טיפ: הוספת קישורים מוודאים", "account_edit.custom_fields.title": "שדות בהתאמה אישית", + "account_edit.custom_fields.verified_hint": "כיצד תוסיפו קישורים מוודאים?", "account_edit.display_name.placeholder": "שם התצוגה שלכן הוא איך שהשם יופיע בפרופיל ובצירי הזמנים.", "account_edit.display_name.title": "שם תצוגה", "account_edit.featured_hashtags.item": "תגיות", "account_edit.featured_hashtags.placeholder": "עזרו לאחרים לזהות ולגשת בקלות לנושאים החביבים עליכם.", "account_edit.featured_hashtags.title": "תגיות נבחרות", + "account_edit.field_delete_modal.confirm": "האם אתם בטוחיםות שברצונכן למחוק את השדה המיוחד הזה? פעולה זו לא ניתנת לביטול.", + "account_edit.field_delete_modal.delete_button": "מחק", + "account_edit.field_delete_modal.title": "מחיקת שדה מתואם אישית?", + "account_edit.field_edit_modal.add_title": "הוסף שדה מותאם אישית", + "account_edit.field_edit_modal.edit_title": "עריכת שדה מותאם אישית", + "account_edit.field_edit_modal.limit_header": "עברת את מגבלת התווים המומלצת", + "account_edit.field_edit_modal.limit_message": "משתמשים מטלפון חכם עלולים לא לראות את השדה במלואו.", + "account_edit.field_edit_modal.link_emoji_warning": "אנו ממליצים נגד שימוש באמוג'י ייחודיים ביחד עם URL. שדות מיוחדים שמכילים את שניהם יופיעו כמלל בלבד ולא כקישור, כדי למנוע בלבול משתמשים.", + "account_edit.field_edit_modal.name_hint": "למשל \"אתר אישי\"", + "account_edit.field_edit_modal.name_label": "תווית", + "account_edit.field_edit_modal.url_warning": "כדי להוסיף קישור, אנא הכלילו {protocol} בהתחלה.", + "account_edit.field_edit_modal.value_hint": "למשל “https://example.me”", + "account_edit.field_edit_modal.value_label": "ערך", + "account_edit.field_reorder_modal.drag_cancel": "הגרירה בוטלה. השדה \"{item}\" נעזב.", + "account_edit.field_reorder_modal.drag_end": "השדה \"{item}\" נעזב.", + "account_edit.field_reorder_modal.drag_instructions": "כדי לארגן מחדש את השדות המיוחדים, לחצו רווח או אנטר. כאשר גוררים, השתמשו במקשי החיצים כדי להזיז שדה מעלה או מטה. לחצו רווח או אנטר בשנית כדי לעזוב את השדה במיקומו החדש, או ESC לביטול.", + "account_edit.field_reorder_modal.drag_move": "השדה \"{item}\" הוזז.", + "account_edit.field_reorder_modal.drag_over": "השדה \"{item}\" הוזז על \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "השדה \"{item}\" נבחר.", + "account_edit.field_reorder_modal.handle_label": "הזזת השדה \"{item}\"", + "account_edit.field_reorder_modal.title": "סידור שדות", + "account_edit.image_alt_modal.add_title": "הוספת מלל חלופי", + "account_edit.image_alt_modal.details_content": "עשו:
    • תארו עצמכם כפי שאתם בצילום
    • השתמשו בשפה בגוף שלישי (למשל \"רונית\" במקום \"אני\")
    • היו תמציתיים - מילים ספורות יספיקו לרוב
    אל תעשו:
    • להתחיל תיאור עם \"תמונה של...\" - זה חזרתי מבחינת קוראי מסך
    דוגמא:
    • \"רונית לבושה בחולצה ירוקה ומרכיבה משקפים\"
    ", + "account_edit.image_alt_modal.details_title": "עצות: מלל חלופי לתמונות פרופיל", + "account_edit.image_alt_modal.edit_title": "עריכת מלל חלופי", + "account_edit.image_alt_modal.text_hint": "מלל חלופי מסייע למשתמשי קוראי מסך להבין את התוכן שלך.", + "account_edit.image_alt_modal.text_label": "מלל חלופי", + "account_edit.image_delete_modal.confirm": "האם למחוק את התמונה? לא ניתן לבטל פעולה זו.", + "account_edit.image_delete_modal.delete_button": "מחיקה", + "account_edit.image_delete_modal.title": "למחוק תמונה?", + "account_edit.image_edit.add_button": "הוספת תמונה", + "account_edit.image_edit.alt_add_button": "הוספת מלל חלופי", + "account_edit.image_edit.alt_edit_button": "עריכת מלל חלופי", + "account_edit.image_edit.remove_button": "הסרת תמונה", + "account_edit.image_edit.replace_button": "החלפת תמונה", "account_edit.name_modal.add_title": "הוספת שם תצוגה", "account_edit.name_modal.edit_title": "עריכת שם תצוגה", "account_edit.profile_tab.button_label": "התאמה אישית", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "התאימו את הטאבים בפרופיל שלכם ומה שהם יציגו.", "account_edit.profile_tab.title": "הגדרות טאבים לפרופיל", "account_edit.save": "שמירה", + "account_edit.upload_modal.back": "חזרה", + "account_edit.upload_modal.done": "בוצע", + "account_edit.upload_modal.next": "הבא", + "account_edit.upload_modal.step_crop.zoom": "הגדלה", + "account_edit.upload_modal.step_upload.button": "עיון בקבצים", + "account_edit.upload_modal.step_upload.dragging": "גרור להעלאה", + "account_edit.upload_modal.step_upload.header": "בחר/י תמונה", + "account_edit.upload_modal.step_upload.hint": "תכנים בתקן WEBP, PNG, GIF או JPG, עד לגודל {limit} מ\"ב.{br}התמונה תתוקן לגודל {width} על {height} פיקסלים.", + "account_edit.upload_modal.title_add": "הוספת תמונת פרופיל", + "account_edit.upload_modal.title_replace": "החלפת תמונת פרופיל", + "account_edit.verified_modal.details": "הוספת אמינות לחשבון המסטודון על ידי הוספת קישורים מוודאים לאתרים אישיים. כך זה עובד:", + "account_edit.verified_modal.invisible_link.details": "הוסיפו את הקישור בכותרת. החלק החשוב הוא rel=\"me\" שמונע התחזות על אתרים עם תוכן משתמשים. ניתן גם ליצור תגית link בכותרת העמוד במקום קישור {tag} אבל קוד ה־HTML חייב להופיע שם ללא הרצה של ג'אווהסקריפט.", + "account_edit.verified_modal.invisible_link.summary": "כיצד לגרום לקישור להיות בלתי נראה?", + "account_edit.verified_modal.step1.header": "העתיקו את קוד HTML שלמטה והדביקו אותו לכותרת האתר שלכם", + "account_edit.verified_modal.step2.details": "אם כבר הוספתן את אתרכן בשדה המיוחד, תצטרכו למחוק וליצור אותו מחדש כדי לגרום לתהליך הווידוא.", + "account_edit.verified_modal.step2.header": "הוסיפו את אתרכן בשדה המיוחד", + "account_edit.verified_modal.title": "כיצד תוסיפו קישורים מוודאים", "account_edit_tags.add_tag": "הוספת #{tagName}", "account_edit_tags.column_title": "עריכת תגיות נבחרות", "account_edit_tags.help_text": "תגיות נבחרות עוזרות למשתמשים לגלות ולהשתמש בפרופיל שלך. הן יופיעו כסננים במבט הפעילויות על עמוד הפרופיל שלך.", @@ -293,6 +349,8 @@ "collections.accounts.empty_description": "להוסיף עד ל־{count} חשבונות שאתם עוקבים אחריהם", "collections.accounts.empty_title": "האוסף הזה ריק", "collections.collection_description": "תיאור", + "collections.collection_language": "שפה", + "collections.collection_language_none": "לא מצוין", "collections.collection_name": "כינוי", "collections.collection_topic": "נושא", "collections.confirm_account_removal": "בוודאות להסיר חשבון זה מהאוסף?", @@ -306,10 +364,15 @@ "collections.create_collection": "יצירת אוסף", "collections.delete_collection": "מחיקת האוסף", "collections.description_length_hint": "מגבלה של 100 תווים", + "collections.detail.accept_inclusion": "אישור", "collections.detail.accounts_heading": "חשבונות", + "collections.detail.author_added_you": "{author} הוסיפו אותך לאוסף", "collections.detail.curated_by_author": "נאצר על ידי {author}", "collections.detail.curated_by_you": "נאצר על ידיך", "collections.detail.loading": "טוען אוסף…", + "collections.detail.other_accounts_in_collection": "אחרים באוסף:", + "collections.detail.revoke_inclusion": "הסירוני", + "collections.detail.sensitive_note": "האוסף מכיל חשבונות ותכנים שאולי יחשבו רגישים לחלק מהמשתמשים.", "collections.detail.share": "שיתוף אוסף", "collections.edit_details": "עריכת פרטים", "collections.error_loading_collections": "חלה שגיאה בנסיון לטעון את אוספיך.", @@ -324,10 +387,14 @@ "collections.old_last_post_note": "פרסמו לאחרונה לפני יותר משבוע", "collections.remove_account": "הסר חשבון זה", "collections.report_collection": "דיווח על אוסף זה", + "collections.revoke_collection_inclusion": "הסירוני מאוסף זה", + "collections.revoke_inclusion.confirmation": "הוסרת מֿ\"{collection}\"", + "collections.revoke_inclusion.error": "הייתה שגיאה. נסו שוב מאוחר יותר.", "collections.search_accounts_label": "לחפש חשבונות להוספה…", "collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי", "collections.sensitive": "רגיש", "collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.", + "collections.topic_special_chars_hint": "תווים מיוחדים יוסרו בעת השמירה", "collections.view_collection": "צפיה באוסף", "collections.view_other_collections_by_user": "צפייה באוספים אחרים של משתמש.ת אלו", "collections.visibility_public": "פומבי", @@ -447,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "הסרת עוקב", "confirmations.remove_from_followers.message": "{name} יוסר/תוסר ממעקב אחריך. האם להמשיך?", "confirmations.remove_from_followers.title": "להסיר עוקב/עוקבת?", + "confirmations.revoke_collection_inclusion.confirm": "הסירוני", + "confirmations.revoke_collection_inclusion.message": "פעולה זו היא סופית, והאוצרים לא יוכלו להוסיף אותך יותר לאוסף בעתיד.", + "confirmations.revoke_collection_inclusion.title": "להסירך מאוסף זה?", "confirmations.revoke_quote.confirm": "הסרת הודעה", "confirmations.revoke_quote.message": "פעולה זו אינה הפיכה.", "confirmations.revoke_quote.title": "הסרת הודעה?", @@ -558,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {הודעה אחת נעוצה} two {הודעותיים נעוצות} many {הודעות נעוצות} other {הודעות נעוצות}}", "featured_carousel.slide": "הודעה {current, number} מתוך {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "לאחרונה פרסמת על {items}. להוסיף את הנושאים לתגיות הנבחרות?", + "featured_tags.suggestions.add": "הוספה", + "featured_tags.suggestions.added": "ניהול התגיות הנבחרות בכל עת תחת עריכת פרופיל > תגיות נבחרות.", + "featured_tags.suggestions.dismiss": "לא תודה", "filter_modal.added.context_mismatch_explanation": "קטגוריית הסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.", "filter_modal.added.context_mismatch_title": "אין התאמה להקשר!", "filter_modal.added.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.", @@ -916,17 +990,17 @@ "notifications_permission_banner.how_to_control": "כדי לקבל התראות גם כאשר מסטודון סגור יש לאפשר התראות מסך. ניתן לשלוט בדיוק איזה סוג של אינטראקציות יביא להתראות מסך דרך כפתור ה- {icon} מרגע שהן מאופשרות.", "notifications_permission_banner.title": "לעולם אל תחמיץ דבר", "onboarding.follows.back": "בחזרה", - "onboarding.follows.done": "בוצע", "onboarding.follows.empty": "למצער, תוצאות לחיפושך אינן בנמצא. ניתן להשתמש בחיפוש או בדף החקירות לשם מציאת אנשים ולעקבם. אפשר גם לנסות שוב אחר כך.", + "onboarding.follows.next": "להמשיך ליצירת הפרופיל שלך", "onboarding.follows.search": "חיפוש", "onboarding.follows.title": "כדי להתחיל, יש לעקוב אחרי אנשים", "onboarding.profile.discoverable": "כלול את הפרופיל שלי בעמודת התגליות", "onboarding.profile.discoverable_hint": "כשתבחרו להכלל ב\"תגליות\" על מסטודון, ההודעות שלכם עשויות להופיע בתוצאות חיפוש ועמודות \"נושאים חמים\", והפרופיל יוצע לאחרים עם תחומי עניין משותפים לכם.", "onboarding.profile.display_name": "שם להצגה", "onboarding.profile.display_name_hint": "שמך המלא או כינוי הכיף שלך…", + "onboarding.profile.finish": "סיום", "onboarding.profile.note": "אודות", "onboarding.profile.note_hint": "ניתן @לאזכר משתמשים אחרים או #תגיות…", - "onboarding.profile.save_and_continue": "לשמור ולהמשיך", "onboarding.profile.title": "הגדרת פרופיל", "onboarding.profile.upload_avatar": "העלאת תמונת פרופיל", "onboarding.profile.upload_header": "העלאת כותרת פרופיל", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 05df7cc4021f2c..a97a7240c10c33 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -73,7 +73,6 @@ "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}", @@ -153,20 +152,51 @@ "account_edit.column_title": "Profil szerkesztése", "account_edit.custom_fields.name": "mező", "account_edit.custom_fields.placeholder": "Add meg a névmásaidat, külső hivatkozásaidat vagy bármi mást, amelyet megosztanál.", + "account_edit.custom_fields.reorder_button": "Mezők átrendezése", + "account_edit.custom_fields.tip_content": "Könnyedén nagyobb hitelességet adhatsz a Mastodon-fiókodnak a saját weboldalaidra mutató hivatkozások megerősítésével.", + "account_edit.custom_fields.tip_title": "Tipp: Ellenőrzött hivatkozások hozzáadása", "account_edit.custom_fields.title": "Egyéni mezők", + "account_edit.custom_fields.verified_hint": "Hogyan kell ellenőrzött hivatkozást hozzáadni?", "account_edit.display_name.placeholder": "A megjelenítendő név az, ahogy a neved megjelenik a profilodon és az idővonalakon.", "account_edit.display_name.title": "Megjelenítendő név", "account_edit.featured_hashtags.item": "hashtagek", "account_edit.featured_hashtags.placeholder": "Segíts másoknak, hogy azonosíthassák a kedvenc témáid, és gyorsan elérjék azokat.", "account_edit.featured_hashtags.title": "Kiemelt hashtagek", + "account_edit.field_delete_modal.confirm": "Biztos, hogy törlöd ezt az egyéni mezőt? Ez a művelet nem vonható vissza.", "account_edit.field_delete_modal.delete_button": "Törlés", "account_edit.field_delete_modal.title": "Egyéni mező törlése?", "account_edit.field_edit_modal.add_title": "Egyéni mező hozzáadása", "account_edit.field_edit_modal.edit_title": "Egyéni mező szerkesztése", + "account_edit.field_edit_modal.limit_header": "Az ajánlott karakterkorlát túllépve", + "account_edit.field_edit_modal.limit_message": "A mobilos felhasználók lehet, hogy nem fogják a teljes mezőt látni.", + "account_edit.field_edit_modal.link_emoji_warning": "Nem javasoljuk az egyéni emodzsik és webcímek együttes használatát. A mindkettőt tartalmazó egyéni mezők a felhasználók megzavarásának elkerülése érdekében csak szövegként jelennek meg, nem hivatkozásként.", "account_edit.field_edit_modal.name_hint": "Például „Személyes webhely”", "account_edit.field_edit_modal.name_label": "Címke", - "account_edit.field_edit_modal.value_hint": "Például „example.me”", + "account_edit.field_edit_modal.url_warning": "Hivatkozás hozzáadásakor add meg a {protocol} protokollt az elején.", + "account_edit.field_edit_modal.value_hint": "Például „https://example.me”", "account_edit.field_edit_modal.value_label": "Érték", + "account_edit.field_reorder_modal.drag_cancel": "Az áthúzás megszakítva. A(z) „{item}” mező el lett dobva.", + "account_edit.field_reorder_modal.drag_end": "A(z) „{item}” mező el lett dobva.", + "account_edit.field_reorder_modal.drag_instructions": "Az egyéni mezők átrendezéséhez nyomj Szóközt vagy Entert. Húzás közben használd a nyílgombokat a mező felfelé vagy lefelé mozgatásához. A mező új pozícióba helyezéséhez nyomd meg a Szóközt vagy az Entert, vagy a megszakításhoz nyomd meg az Esc gombot.", + "account_edit.field_reorder_modal.drag_move": "A(z) „{item}” mező át lett helyezve.", + "account_edit.field_reorder_modal.drag_over": "A(z) „{item}” mező át lett helyezve ennek a helyére: „{over}”.", + "account_edit.field_reorder_modal.drag_start": "A(z) „{item}” mező áthelyezéshez felvéve.", + "account_edit.field_reorder_modal.handle_label": "A(z) „{item}” mező húzása", + "account_edit.field_reorder_modal.title": "Mezők átrendezése", + "account_edit.image_alt_modal.add_title": "Helyettesítő szöveg hozzáadása", + "account_edit.image_alt_modal.details_content": "TEDD:
    • Írd le a képedet
    • Használj egyes szám harmadik személyt (például „én” helyett „Alex”)
    • Légy tömör – sokszor néhány szó is elég
    NE TEDD:
    • Ne kezdd azzal, hogy „X fényképe” – a képernyőolvasók számára felesleges
    Példa:
    • „Alex zöld inget és szemüveget viselve”
    ", + "account_edit.image_alt_modal.details_title": "Tippek: helyettesítő szöveg a profilképekhez", + "account_edit.image_alt_modal.edit_title": "Helyettesítő szöveg szerkesztése", + "account_edit.image_alt_modal.text_hint": "A helyettesítő szövegek segítenek a képernyőolvasót használóknak abban, hogy megértsék a tartalmat.", + "account_edit.image_alt_modal.text_label": "Helyettesítő szöveg", + "account_edit.image_delete_modal.confirm": "Biztos, hogy törlöd ezt a képet? Ez a művelet nem vonható vissza.", + "account_edit.image_delete_modal.delete_button": "Törlés", + "account_edit.image_delete_modal.title": "Törlöd a képet?", + "account_edit.image_edit.add_button": "Kép hozzáadása", + "account_edit.image_edit.alt_add_button": "Helyettesítő szöveg hozzáadása", + "account_edit.image_edit.alt_edit_button": "Helyettesítő szöveg szerkesztése", + "account_edit.image_edit.remove_button": "Kép eltávolítása", + "account_edit.image_edit.replace_button": "Kép cseréje", "account_edit.name_modal.add_title": "Megjelenítendő név hozzáadása", "account_edit.name_modal.edit_title": "Megjelenítendő név szerkesztése", "account_edit.profile_tab.button_label": "Testreszabás", @@ -181,6 +211,20 @@ "account_edit.profile_tab.subtitle": "Szabd testre a profilodon látható lapokat, és a megjelenített tartalmukat.", "account_edit.profile_tab.title": "Profil lap beállításai", "account_edit.save": "Mentés", + "account_edit.upload_modal.back": "Vissza", + "account_edit.upload_modal.done": "Kész", + "account_edit.upload_modal.next": "Következő", + "account_edit.upload_modal.step_crop.zoom": "Nagyítás", + "account_edit.upload_modal.step_upload.button": "Fájlok tallózása", + "account_edit.upload_modal.step_upload.dragging": "Ejtsd ide a feltöltéshez", + "account_edit.upload_modal.step_upload.header": "Válassz egy képet", + "account_edit.upload_modal.title_add": "Profilkép hozzáadása", + "account_edit.upload_modal.title_replace": "Profilkép cseréje", + "account_edit.verified_modal.details": "Növeld a Mastodon-profilod hitelességét a személyes webhelyekre mutató hivatkozások ellenőrzésével. Így működik:", + "account_edit.verified_modal.invisible_link.details": "A hivatkozás hozzáadása a fejlécedhez. A fontos rész a rel=\"me\", mely megakadályozza, hogy mások a nevedben lépjenek fel olyan oldalakon, ahol van felhasználók által előállított tartalom. A(z) {tag} helyett a „link” címkét is használhatod az oldal fejlécében, de a HTML-nek elérhetőnek kell lennie JavaScript futtatása nélkül is.", + "account_edit.verified_modal.invisible_link.summary": "Hogyan lehet egy hivatkozás láthatatlanná tenni?", + "account_edit.verified_modal.step1.header": "Másold a lenti HTML-kódot és illeszd be a webhelyed fejlécébe", + "account_edit.verified_modal.step2.details": "Ha már egyéni mezőként hozzáadtad a webhelyedet, akkor törölnöd kell, újból hozzá kell adnod, hogy újra ellenőrizve legyen.", "account_edit.verified_modal.step2.header": "Saját webhely hozzáadása egyéni mezőként", "account_edit.verified_modal.title": "Hogyan kell ellenőrzött hivatkozást hozzáadni", "account_edit_tags.add_tag": "#{tagName} hozzáadása", @@ -304,6 +348,8 @@ "collections.accounts.empty_description": "Adj hozzá legfeljebb {count} követett fiókot", "collections.accounts.empty_title": "Ez a gyűjtemény üres", "collections.collection_description": "Leírás", + "collections.collection_language": "Nyelv", + "collections.collection_language_none": "Egyik sem", "collections.collection_name": "Név", "collections.collection_topic": "Téma", "collections.confirm_account_removal": "Biztos, hogy eltávolítod ezt a fiókot ebből a gyűjteményből?", @@ -317,12 +363,14 @@ "collections.create_collection": "Gyűjtemény létrehozása", "collections.delete_collection": "Gyűjtemény törlése", "collections.description_length_hint": "100 karakteres korlát", + "collections.detail.accept_inclusion": "Rendben", "collections.detail.accounts_heading": "Fiókok", "collections.detail.author_added_you": "{author} hozzáadott ehhez a gyűjteményhez", "collections.detail.curated_by_author": "Válogatta: {author}", "collections.detail.curated_by_you": "Te válogattad", "collections.detail.loading": "Gyűjtemény betöltése…", "collections.detail.other_accounts_in_collection": "Mások ebben a gyűjteményben:", + "collections.detail.revoke_inclusion": "Saját magam eltávolítása", "collections.detail.sensitive_note": "Ebben a gyűjteményben egyesek számára érzékeny fiókok és tartalmak vannak.", "collections.detail.share": "Gyűjtemény megosztása", "collections.edit_details": "Részletek szerkesztése", @@ -338,10 +386,14 @@ "collections.old_last_post_note": "Egy hete osztott meg legutóbb", "collections.remove_account": "Fiók eltávolítása", "collections.report_collection": "Gyűjtemény jelentése", + "collections.revoke_collection_inclusion": "Saját magam eltávolítása ebből a gyűjteményből", + "collections.revoke_inclusion.confirmation": "El lettél távolítva innen: „{collection}”", + "collections.revoke_inclusion.error": "Hiba történt, próbáld újra később.", "collections.search_accounts_label": "Hozzáadandó fiókok keresése…", "collections.search_accounts_max_reached": "Elérte a hozzáadott fiókok maximális számát", "collections.sensitive": "Érzékeny", "collections.topic_hint": "Egy hashtag hozzáadása segít másoknak abban, hogy megértsék a gyűjtemény fő témáját.", + "collections.topic_special_chars_hint": "A különleges karakterek mentéskor el lesznek távolítva", "collections.view_collection": "Gyűjtemény megtekintése", "collections.view_other_collections_by_user": "Felhasználó más gyűjteményeinek megtekintése", "collections.visibility_public": "Nyilvános", @@ -461,6 +513,9 @@ "confirmations.remove_from_followers.confirm": "Követő eltávolítása", "confirmations.remove_from_followers.message": "{name} követ téged. Biztos, hogy folytatod?", "confirmations.remove_from_followers.title": "Követő eltávolítása?", + "confirmations.revoke_collection_inclusion.confirm": "Saját magam eltávolítása", + "confirmations.revoke_collection_inclusion.message": "Ez a művelet végleges, és a kurátor nem fog tudni újra hozzáadni ehhez a gyűjteményhez.", + "confirmations.revoke_collection_inclusion.title": "Eltávolítod magadat ebből a gyűjteményből?", "confirmations.revoke_quote.confirm": "Bejegyzés eltávolítása", "confirmations.revoke_quote.message": "Ez a művelet nem vonható vissza.", "confirmations.revoke_quote.title": "Bejegyzés eltávolítása?", @@ -930,17 +985,17 @@ "notifications_permission_banner.how_to_control": "Ahhoz, hogy értesítéseket kapj akkor, amikor a Mastodon nincs megnyitva, engedélyezd az asztali értesítéseket. Pontosan be tudod állítani, hogy milyen interakciókról értesülj a fenti {icon} gombon keresztül, ha egyszer már engedélyezted őket.", "notifications_permission_banner.title": "Soha ne mulassz el semmit", "onboarding.follows.back": "Vissza", - "onboarding.follows.done": "Kész", "onboarding.follows.empty": "Sajnos jelenleg nem jeleníthető meg eredmény. Kipróbálhatod a keresést vagy böngészheted a felfedező oldalon a követni kívánt személyeket, vagy próbáld meg később.", + "onboarding.follows.next": "Következik: A profil beállítása", "onboarding.follows.search": "Keresés", "onboarding.follows.title": "A kezdéshez kezdj el embereket követni", "onboarding.profile.discoverable": "Saját profil beállítása felfedezhetőként", "onboarding.profile.discoverable_hint": "A Mastodonon a felfedezhetőség választása esetén a saját bejegyzéseid megjelenhetnek a keresési eredmények és a felkapott tartalmak között, valamint a profilod a hozzád hasonló érdeklődési körrel rendelkező embereknél is ajánlásra kerülhet.", "onboarding.profile.display_name": "Megjelenített név", "onboarding.profile.display_name_hint": "Teljes neved vagy vicces neved…", + "onboarding.profile.finish": "Befejezés", "onboarding.profile.note": "Bemutatkozás", "onboarding.profile.note_hint": "Megemlíthetsz @másokat vagy #hashtag-eket…", - "onboarding.profile.save_and_continue": "Mentés és folytatás", "onboarding.profile.title": "Profilbeállítás", "onboarding.profile.upload_avatar": "Profilkép feltöltése", "onboarding.profile.upload_header": "Profil fejléc feltöltése", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index d9a2f5e8447147..3f65776b91f016 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -707,7 +707,6 @@ "notifications_permission_banner.how_to_control": "Pro reciper notificationes quando Mastodon non es aperte, activa le notificationes de scriptorio. Post lor activation, es possibile controlar precisemente qual typos de interaction genera notificationes de scriptorio per medio del button {icon} hic supra.", "notifications_permission_banner.title": "Non mancar jammais a un cosa", "onboarding.follows.back": "Retro", - "onboarding.follows.done": "Facite", "onboarding.follows.empty": "Regrettabilemente, non es possibile monstrar resultatos al momento. Tu pote tentar usar le recerca o percurrer le pagina de exploration pro cercar personas a sequer, o tentar lo de novo plus tarde.", "onboarding.follows.search": "Cercar", "onboarding.follows.title": "Seque personas pro comenciar", @@ -717,7 +716,6 @@ "onboarding.profile.display_name_hint": "Tu nomine complete o tu supernomine…", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "Tu pote @mentionar altere personas o #hashtags…", - "onboarding.profile.save_and_continue": "Salvar e continuar", "onboarding.profile.title": "Configuration del profilo", "onboarding.profile.upload_avatar": "Incargar imagine de profilo", "onboarding.profile.upload_header": "Actualisar capite de profilo", diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json index 1160973a63ddaf..77097b05c6482d 100644 --- a/app/javascript/mastodon/locales/ie.json +++ b/app/javascript/mastodon/locales/ie.json @@ -478,7 +478,6 @@ "onboarding.profile.display_name_hint": "Tui complet nómine o tui amusant nómine…", "onboarding.profile.note": "Biografie", "onboarding.profile.note_hint": "Tu posse @mentionar altri persones o #hashtags…", - "onboarding.profile.save_and_continue": "Conservar e avansar", "onboarding.profile.title": "Popular tu profil", "onboarding.profile.upload_avatar": "Cargar profil-portrete", "onboarding.profile.upload_header": "Cargar cap-image", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 13b38f40fe8977..9254164374f6fd 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -614,7 +614,6 @@ "notifications_permission_banner.how_to_control": "Por ganar savigi kande Mastodon ne es desklozita, ebligez komputilsavigi.", "notifications_permission_banner.title": "Irga kozo ne pasas vu", "onboarding.follows.back": "Retro", - "onboarding.follows.done": "Finis", "onboarding.follows.empty": "Regretinde, nula rezultajo povas montresar nune. Vu povas esforcar serchar, o irar al explorala pagino por trovar personi sequinda, o esforcar itere pose.", "onboarding.follows.search": "Serchar", "onboarding.follows.title": "Sequez personi por komencar", @@ -624,7 +623,6 @@ "onboarding.profile.display_name_hint": "Vua tota nomo o vua gaya nomo…", "onboarding.profile.note": "Biografio", "onboarding.profile.note_hint": "Vu povas @mencionar altra personi o #hashtagi…", - "onboarding.profile.save_and_continue": "Preservez e avancez", "onboarding.profile.title": "Kompletigez la profilo", "onboarding.profile.upload_avatar": "Kargez profiloportreto", "onboarding.profile.upload_header": "Kargez profilokapimajo", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 43b095e011c815..6606bef9b31bd1 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Breyta notandasniði", "account_edit.custom_fields.name": "reitur", "account_edit.custom_fields.placeholder": "Settu inn fornöfn sem þú vilt nota, ytri tengla eða hvaðeina sem þú vilt deila með öðrum.", + "account_edit.custom_fields.reorder_button": "Endurraða reitum", "account_edit.custom_fields.tip_content": "Þú getur á einfaldan hátt aukið trúverðugleika Mastodon-aðgangsins þíns með því að bæta við staðfestingartenglum sem vísa á vefsvæði sem þú átt.", "account_edit.custom_fields.tip_title": "Ábending: Bæta við staðfestingartenglum", "account_edit.custom_fields.title": "Sérsniðnir reitir", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Eyða sérsniðnum reit?", "account_edit.field_edit_modal.add_title": "Bæta við sérsniðnum reit", "account_edit.field_edit_modal.edit_title": "Breyta sérsniðnum reit", + "account_edit.field_edit_modal.limit_header": "Fór yfir takmörk á fjölda stafa", + "account_edit.field_edit_modal.limit_message": "Notendur á símtækjum gætu lent því að sjá ekki allan reitinn.", + "account_edit.field_edit_modal.link_emoji_warning": "Við mælum gegn því að nota sérsniðin tjáningartákn saman með vefslóðum. Sérsniðnir reitir sem innihalda hvort tveggja munu birtast sem einungis texti í stað þess að vera tenglar, til að koma í veg fyrir að notendur taki annað í misgripum fyrir hitt.", "account_edit.field_edit_modal.name_hint": "T.d. \"Eigið vefsvæði\"", "account_edit.field_edit_modal.name_label": "Skýring", - "account_edit.field_edit_modal.value_hint": "T.d. \"minnvefur.is\"", + "account_edit.field_edit_modal.url_warning": "Til að bæta við tengli skaltu hafa {protocol} á undan.", + "account_edit.field_edit_modal.value_hint": "T.d. “https://vefur.is”", "account_edit.field_edit_modal.value_label": "Gildi", + "account_edit.field_reorder_modal.drag_cancel": "Hætt var við að draga. Reitnum \"{item}\" var sleppt.", + "account_edit.field_reorder_modal.drag_end": "Reitnum \"{item}\" var sleppt.", + "account_edit.field_reorder_modal.drag_instructions": "Til að endurraða sérsniðnum reitum skaltu ýta á bilslá eða Enter. Á meðan verið er að draga geturðu notað örvalyklana til að færa reitinn upp eða niður. Ýttu aftur á bilslá eða Enter til að to sleppa reitnum á nýja staðinn sinn, eða ýtt á Escape til að hætta við.", + "account_edit.field_reorder_modal.drag_move": "Reiturinn \"{item}\" var færður.", + "account_edit.field_reorder_modal.drag_over": "Reiturinn \"{item}\" var færður yfir \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Náði reitnum \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Dragðu reitinn \"{item}\"", + "account_edit.field_reorder_modal.title": "Endurraða gagnasviðum", + "account_edit.image_alt_modal.add_title": "Bæta við hjálpartexta", + "account_edit.image_alt_modal.details_content": "GERÐU ÞETTA:
    • Lýstu þér eins og þú ert á myndinni
    • Notaðu þriðjupersónu (t.d. “Nonni” í staðinn fyrir “ég”)
    • Vertu stuttorð/ur – fá orð eru oft nóg
    EKKI GERA ÞETTA:
    • Byrja á “Mynd af” – það er óþarft fyrir skjálesara
    DÆMI:
    • “Nonni í grænni skyrtu og með gleraugu”
    ", + "account_edit.image_alt_modal.details_title": "Ábending: hjálpartexti fyrir auðkennismyndir", + "account_edit.image_alt_modal.edit_title": "Breyta hjálpartexta", + "account_edit.image_alt_modal.text_hint": "Hjálpartexti hjálpar fólki sem notar skjálesara að skilja efnið frá þér.", + "account_edit.image_alt_modal.text_label": "Hjálpartexti mynda", + "account_edit.image_delete_modal.confirm": "Ertu viss um að þú viljir eyða þessari mynd? Þessa aðgerð er ekki hægt að afturkalla.", + "account_edit.image_delete_modal.delete_button": "Eyða", + "account_edit.image_delete_modal.title": "Eyða mynd?", + "account_edit.image_edit.add_button": "Bæta við mynd", + "account_edit.image_edit.alt_add_button": "Bæta við hjálpartexta", + "account_edit.image_edit.alt_edit_button": "Breyta hjálpartexta", + "account_edit.image_edit.remove_button": "Fjarlægja mynd", + "account_edit.image_edit.replace_button": "Skipta um mynd", "account_edit.name_modal.add_title": "Bættu við birtingarnafni", "account_edit.name_modal.edit_title": "Breyta birtingarnafni", "account_edit.profile_tab.button_label": "Sérsníða", @@ -185,8 +211,22 @@ "account_edit.profile_tab.subtitle": "Sérsníddu flipana á notandasniðinu þínu og hvað þeir birta.", "account_edit.profile_tab.title": "Stillingar notandasniðsflipa", "account_edit.save": "Vista", + "account_edit.upload_modal.back": "Til baka", + "account_edit.upload_modal.done": "Lokið", + "account_edit.upload_modal.next": "Næsta", + "account_edit.upload_modal.step_crop.zoom": "Aðdráttur", + "account_edit.upload_modal.step_upload.button": "Skoða skrár", + "account_edit.upload_modal.step_upload.dragging": "Slepptu til að senda inn", + "account_edit.upload_modal.step_upload.header": "Veldu mynd", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF eða JPG-snið, allt að {limit}MB.{br}Mynd verður kvörðuð í {width}x{height}px.", + "account_edit.upload_modal.title_add": "Bæta við auðkennismynd", + "account_edit.upload_modal.title_replace": "Skipta um auðkennismynd", "account_edit.verified_modal.details": "Auktu trúverðugleika Mastodon-aðgangsins þíns með því að bæta við staðfestingartenglum sem vísa á vefsvæðin þín. Hérna sérðu hvernig það virkar:", + "account_edit.verified_modal.invisible_link.details": "Bættu tenglinum í hausinn hjá þér. Mikilvægi hlutinn er rel=\"me\" sem kemur í veg fyrir blekkingu verðandi persónuauðkenni á vefsvæðum með notandaframleiddu efni. Þú getur jafnvel notað tengimerkið í haus síðunnar í staðinn fyrir {tag}, en HTML-kóðinn verður samt að vera aðgengilegur án þess að keyra þurfi JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Hvernig geri ég tengilinn ósýnilegan?", + "account_edit.verified_modal.step1.header": "Afritaðu HTML-kóðann hér fyrir neðan og límdu hann inn í haus vefsvæðisins þíns", + "account_edit.verified_modal.step2.details": "Ef þú hefur þegar bættu vefsvæðinu þínu inn sem sérsniðnum reit, þá muntu þurfa að eyða honum og bæta við aftur til að gangsetja sannvottun.", + "account_edit.verified_modal.step2.header": "Bættu vefsvæðinu þínu inn sem sérsniðinn reit", "account_edit.verified_modal.title": "Hvernig er hægt að bæta við staðfestingartengli", "account_edit_tags.add_tag": "Bæta við #{tagName}", "account_edit_tags.column_title": "Breyta myllumerkjum með aukið vægi", @@ -309,6 +349,8 @@ "collections.accounts.empty_description": "Bættu við allt að {count} aðgöngum sem þú fylgist með", "collections.accounts.empty_title": "Þetta safn er tómt", "collections.collection_description": "Lýsing", + "collections.collection_language": "Tungumál", + "collections.collection_language_none": "Ekkert", "collections.collection_name": "Nafn", "collections.collection_topic": "Umfjöllunarefni", "collections.confirm_account_removal": "Ertu viss um að þú viljir fjarlægja þennan aðgang úr þessu safni?", @@ -322,10 +364,15 @@ "collections.create_collection": "Búa til safn", "collections.delete_collection": "Eyða safni", "collections.description_length_hint": "100 stafa takmörk", + "collections.detail.accept_inclusion": "Í lagi", "collections.detail.accounts_heading": "Aðgangar", + "collections.detail.author_added_you": "{author} bætti þér í þetta safn", "collections.detail.curated_by_author": "Safnað saman af {author}", "collections.detail.curated_by_you": "Safnað saman af þér", "collections.detail.loading": "Hleð inn safni…", + "collections.detail.other_accounts_in_collection": "Aðrir í þessu safni:", + "collections.detail.revoke_inclusion": "Fjarlægja mig", + "collections.detail.sensitive_note": "Þetta safn inniheldur aðganga og efni sem sumir notendur gætu verið viðkvæmir fyrir.", "collections.detail.share": "Deila þessu safni", "collections.edit_details": "Breyta ítarupplýsingum", "collections.error_loading_collections": "Villa kom upp þegar reynt var að hlaða inn söfnunum þínum.", @@ -340,10 +387,14 @@ "collections.old_last_post_note": "Birti síðast fyrir meira en viku síðan", "collections.remove_account": "Fjarlægja þennan aðgang", "collections.report_collection": "Kæra þetta safn", + "collections.revoke_collection_inclusion": "Fjarlægja mig úr þessu safni", + "collections.revoke_inclusion.confirmation": "Þú varst fjarlægð/ur úr \"{collection}\"", + "collections.revoke_inclusion.error": "Upp kom villa, reyndu aftur síðar.", "collections.search_accounts_label": "Leita að aðgöngum til að bæta við…", "collections.search_accounts_max_reached": "Þú hefur þegar bætt við leyfilegum hámarksfjölda aðganga", "collections.sensitive": "Viðkvæmt", "collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.", + "collections.topic_special_chars_hint": "Sérstafir verða fjarlægðir við vistun", "collections.view_collection": "Skoða safn", "collections.view_other_collections_by_user": "Skoða önnur söfn frá þessum notanda", "collections.visibility_public": "Opinbert", @@ -463,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Fjarlægja fylgjanda", "confirmations.remove_from_followers.message": "{name} mun hætta að fylgjast með þér. Ertu viss um að þú viljir halda áfram?", "confirmations.remove_from_followers.title": "Fjarlægja fylgjanda?", + "confirmations.revoke_collection_inclusion.confirm": "Fjarlægja mig", + "confirmations.revoke_collection_inclusion.message": "Þessi aðgerð er varanleg og umsjónaraðili safnsins mun ekki geta bætt þér aftur við síðar.", + "confirmations.revoke_collection_inclusion.title": "Á að fjarlægja þig úr þessu safni?", "confirmations.revoke_quote.confirm": "Fjarlægja færslu", "confirmations.revoke_quote.message": "Þessa aðgerð er ekki hægt að afturkalla.", "confirmations.revoke_quote.title": "Fjarlægja færslu?", @@ -574,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Fest færsla} other {Festar færslur}}", "featured_carousel.slide": "Færsla {current, number} af {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Að undanförnu hefurðu skrifað um {items}. Ætti að bæta þessu við sem myllumerkjum með aukið vægi?", + "featured_tags.suggestions.add": "Bæta við", + "featured_tags.suggestions.added": "Sýslaðu með þau myllumerki þín sem eru með með aukið vægi með því að fara í Breyta notandasniði > Myllumerki með aukið vægi.", + "featured_tags.suggestions.dismiss": "Nei takk", "filter_modal.added.context_mismatch_explanation": "Þessi síuflokkur á ekki við í því samhengi sem aðgangur þinn að þessari færslu felur í sér. Ef þú vilt að færslan sé einnig síuð í þessu samhengi, þá þarftu að breyta síunni.", "filter_modal.added.context_mismatch_title": "Misræmi í samhengi!", "filter_modal.added.expired_explanation": "Þessi síuflokkur er útrunninn, þú þarft að breyta gidistímanum svo hann geti átt við.", @@ -932,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Til að taka á móti tilkynningum þegar Mastodon er ekki opið, skaltu virkja tilkynningar á skjáborði. Þegar þær eru orðnar virkar geturðu stýrt nákvæmlega hverskonar atvik framleiða tilkynningar með því að nota {icon}-hnappinn hér fyrir ofan.", "notifications_permission_banner.title": "Aldrei missa af neinu", "onboarding.follows.back": "Til baka", - "onboarding.follows.done": "Lokið", "onboarding.follows.empty": "Því miður er ekki hægt að birta neinar niðurstöður í augnablikinu. Þú getur reynt að nota leitina eða skoðað könnunarsíðuna til að finna fólk til að fylgjast með, nú eða prófað aftur síðar.", + "onboarding.follows.next": "Næsta: Settu upp notandasniðið þitt", "onboarding.follows.search": "Leita", "onboarding.follows.title": "Þú ættir að fylgjast með fólki til að komast í gang", "onboarding.profile.discoverable": "Gera notandasniðið mitt uppgötvanlegt", "onboarding.profile.discoverable_hint": "Þegar þú velur að hægt sé að uppgötva þig á Mastodon, munu færslurnar þínar birtast í leitarniðurstöðum og vinsældalistum, auk þess sem stungið verður upp á notandasniðinu þínu við fólk sem er með svipuð áhugamál og þú.", "onboarding.profile.display_name": "Birtingarnafn", "onboarding.profile.display_name_hint": "Fullt nafn þitt eða eitthvað til gamans…", + "onboarding.profile.finish": "Ljúka", "onboarding.profile.note": "Æviágrip", "onboarding.profile.note_hint": "Þú getur @minnst á annað fólk eða #myllumerki…", - "onboarding.profile.save_and_continue": "Vista og halda áfram", "onboarding.profile.title": "Uppsetning notandasniðs", "onboarding.profile.upload_avatar": "Sendu inn auðkennismynd", "onboarding.profile.upload_header": "Sendu inn bakgrunnsmynd í haus notandasniðs", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index e6e1e21435e205..10c44a7b497c53 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Modifica il profilo", "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Aggiungi i tuoi pronomi, collegamenti esterni o qualsiasi altra cosa desideri condividere.", + "account_edit.custom_fields.reorder_button": "Riordina i campi", "account_edit.custom_fields.tip_content": "Puoi facilmente aggiungere credibilità al tuo account Mastodon, verificando i collegamenti a qualsiasi sito web di tua proprietà.", "account_edit.custom_fields.tip_title": "Suggerimento: aggiunta di collegamenti verificati", "account_edit.custom_fields.title": "Campi personalizzati", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Eliminare il campo personalizzato?", "account_edit.field_edit_modal.add_title": "Aggiungi campo personalizzato", "account_edit.field_edit_modal.edit_title": "Modifica campo personalizzato", - "account_edit.field_edit_modal.name_hint": "Ad esempio: “Sito web personale”", + "account_edit.field_edit_modal.limit_header": "Superato il limite di caratteri consigliato", + "account_edit.field_edit_modal.limit_message": "Gli utenti dai dispositivi mobili potrebbero non visualizzare completamente il tuo campo.", + "account_edit.field_edit_modal.link_emoji_warning": "Sconsigliamo l'uso di emoji personalizzate in combinazione con gli URL. I campi personalizzati che contengono entrambi verranno visualizzati solo come testo anziché come link, in modo da evitare confusione nell'utente.", + "account_edit.field_edit_modal.name_hint": "Per esempio: “Sito web personale”", "account_edit.field_edit_modal.name_label": "Etichetta", - "account_edit.field_edit_modal.value_hint": "Ad esempio: “example.me”", + "account_edit.field_edit_modal.url_warning": "Per aggiungere un collegamento, si prega di includere {protocol} all’inizio.", + "account_edit.field_edit_modal.value_hint": "Per esempio: “https://example.me”", "account_edit.field_edit_modal.value_label": "Valore", + "account_edit.field_reorder_modal.drag_cancel": "Il trascinamento è stato annullato. Il campo \"{item}\" è stato eliminato.", + "account_edit.field_reorder_modal.drag_end": "Il campo \"{item}\" è stato eliminato.", + "account_edit.field_reorder_modal.drag_instructions": "Per riorganizzare i campi personalizzati, premi la barra spaziatrice o Invio. Durante il trascinamento, usa i tasti freccia per spostare il campo verso l'alto o verso il basso. Premi di nuovo la barra spaziatrice o Invio per rilasciare il campo nella sua nuova posizione, oppure premi Esc per annullare.", + "account_edit.field_reorder_modal.drag_move": "Il campo \"{item}\" è stato spostato.", + "account_edit.field_reorder_modal.drag_over": "Il campo \"{item}\" è stato spostato su \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" selezionato.", + "account_edit.field_reorder_modal.handle_label": "Trascina il campo \"{item}\"", + "account_edit.field_reorder_modal.title": "Riorganizza i campi", + "account_edit.image_alt_modal.add_title": "Aggiungi il testo alternativo", + "account_edit.image_alt_modal.details_content": "COSA FARE:
    • Descriviti come nella foto
    • Usa la terza persona (es. “Alex” invece di “io”)
    • Sii conciso/a – poche parole spesso bastano
    COSA NON FARE:
    • Iniziare con “Foto di” – è ridondante per i lettori di schermo
    ESEMPIO:
    • “Alex sta indossando una camicia verde e gli occhiali”
    ", + "account_edit.image_alt_modal.details_title": "Suggerimenti: testo alternativo per le foto del profilo", + "account_edit.image_alt_modal.edit_title": "Modifica il testo alternativo", + "account_edit.image_alt_modal.text_hint": "Il testo alternativo aiuta gli utenti che utilizzano i lettori di schermo a comprendere i tuoi contenuti.", + "account_edit.image_alt_modal.text_label": "Testo alternativo", + "account_edit.image_delete_modal.confirm": "Si è sicuri di voler eliminare questa immagine? Questa azione non può essere annullata.", + "account_edit.image_delete_modal.delete_button": "Elimina", + "account_edit.image_delete_modal.title": "Eliminare l'immagine?", + "account_edit.image_edit.add_button": "Aggiungi un'immagine", + "account_edit.image_edit.alt_add_button": "Aggiungi il testo alternativo", + "account_edit.image_edit.alt_edit_button": "Modifica il testo alternativo", + "account_edit.image_edit.remove_button": "Rimuovi l'immagine", + "account_edit.image_edit.replace_button": "Sostituisci l'immagine", "account_edit.name_modal.add_title": "Aggiungi il nome mostrato", "account_edit.name_modal.edit_title": "Modifica il nome mostrato", "account_edit.profile_tab.button_label": "Personalizza", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Personalizza le schede del tuo profilo e ciò che mostrano.", "account_edit.profile_tab.title": "Impostazioni della scheda del profilo", "account_edit.save": "Salva", + "account_edit.upload_modal.back": "Indietro", + "account_edit.upload_modal.done": "Fatto", + "account_edit.upload_modal.next": "Avanti", + "account_edit.upload_modal.step_crop.zoom": "Ingrandimento", + "account_edit.upload_modal.step_upload.button": "Sfoglia i file", + "account_edit.upload_modal.step_upload.dragging": "Trascina per caricare", + "account_edit.upload_modal.step_upload.header": "Scegli un'immagine", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF o JPG, fino a {limit}MB.{br}L'immagine verrà ridimensionata a {width}x{height}px.", + "account_edit.upload_modal.title_add": "Aggiungi la foto del profilo", + "account_edit.upload_modal.title_replace": "Sostituisci la foto del profilo", "account_edit.verified_modal.details": "Aggiungi credibilità al tuo profilo Mastodon verificando i collegamenti ai siti web personali. Ecco come funziona:", "account_edit.verified_modal.invisible_link.details": "Aggiungi il collegamento alla tua intestazione. La parte importante è rel=\"me\" che impedisce l'impersonificazione sui siti web con contenuti generati dagli utenti. Puoi anche utilizzare un link tag nell'intestazione della pagina al posto di {tag}, ma il codice HTML deve essere accessibile senza eseguire JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Come faccio a rendere il collegamento invisibile?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Aggiungi fino a {count} account che segui", "collections.accounts.empty_title": "Questa collezione è vuota", "collections.collection_description": "Descrizione", + "collections.collection_language": "Lingua", + "collections.collection_language_none": "Nessuna", "collections.collection_name": "Nome", "collections.collection_topic": "Argomento", "collections.confirm_account_removal": "Si è sicuri di voler rimuovere questo account da questa collezione?", @@ -326,10 +364,15 @@ "collections.create_collection": "Crea la collezione", "collections.delete_collection": "Cancella la collezione", "collections.description_length_hint": "Limite di 100 caratteri", + "collections.detail.accept_inclusion": "Va bene", "collections.detail.accounts_heading": "Account", + "collections.detail.author_added_you": "{author} ti ha aggiunto a questa collezione", "collections.detail.curated_by_author": "Curata da {author}", "collections.detail.curated_by_you": "Curata da te", "collections.detail.loading": "Caricamento della collezione…", + "collections.detail.other_accounts_in_collection": "Altri in questa collezione:", + "collections.detail.revoke_inclusion": "Rimuovimi", + "collections.detail.sensitive_note": "Questa collezione contiene account e contenuto che potrebbero essere sensibili ad alcuni utenti.", "collections.detail.share": "Condividi questa collezione", "collections.edit_details": "Modifica i dettagli", "collections.error_loading_collections": "Si è verificato un errore durante il tentativo di caricare le tue collezioni.", @@ -344,10 +387,14 @@ "collections.old_last_post_note": "Ultimo post più di una settimana fa", "collections.remove_account": "Rimuovi questo account", "collections.report_collection": "Segnala questa collezione", + "collections.revoke_collection_inclusion": "Rimuovimi da questa collezione", + "collections.revoke_inclusion.confirmation": "Sei stato/a rimosso/a da \"{collection}\"", + "collections.revoke_inclusion.error": "Si è verificato un errore, si prega di riprovare più tardi.", "collections.search_accounts_label": "Cerca account da aggiungere…", "collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account", "collections.sensitive": "Sensibile", "collections.topic_hint": "Aggiungi un hashtag che aiuti gli altri a comprendere l'argomento principale di questa collezione.", + "collections.topic_special_chars_hint": "I caratteri speciali verranno rimossi durante il salvataggio", "collections.view_collection": "Visualizza la collezione", "collections.view_other_collections_by_user": "Visualizza altre collezioni da questo utente", "collections.visibility_public": "Pubblica", @@ -467,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Rimuovi il follower", "confirmations.remove_from_followers.message": "{name} smetterà di seguirti. Si è sicuri di voler procedere?", "confirmations.remove_from_followers.title": "Rimuovere il follower?", + "confirmations.revoke_collection_inclusion.confirm": "Rimuovimi", + "confirmations.revoke_collection_inclusion.message": "Questa azione è permanente e l'utente responsabile della collezione non sarà in grado di aggiungerti nuovamente ad essa in seguito.", + "confirmations.revoke_collection_inclusion.title": "Rimuovere te stesso/a da questa collezione?", "confirmations.revoke_quote.confirm": "Elimina il post", "confirmations.revoke_quote.message": "Questa azione non può essere annullata.", "confirmations.revoke_quote.title": "Rimuovere il post?", @@ -578,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {Post appuntato} other {Post appuntati}}", "featured_carousel.slide": "Post {current, number} di {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Ultimamente hai pubblicato contenuti relativi a {items}. Vuoi aggiungerli come hashtag in evidenza?", + "featured_tags.suggestions.add": "Aggiungi", + "featured_tags.suggestions.added": "Gestisci i tuoi hashtag in evidenza in qualsiasi momento in Modifica profilo > Hashtag in evidenza.", + "featured_tags.suggestions.dismiss": "No, grazie", "filter_modal.added.context_mismatch_explanation": "La categoria di questo filtro non si applica al contesto in cui hai acceduto a questo post. Se desideri che il post sia filtrato anche in questo contesto, dovrai modificare il filtro.", "filter_modal.added.context_mismatch_title": "Contesto non corrispondente!", "filter_modal.added.expired_explanation": "La categoria di questo filtro è scaduta, dovrvai modificarne la data di scadenza per applicarlo.", @@ -936,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Per ricevere le notifiche quando Mastodon non è aperto, abilita le notifiche desktop. Puoi controllare precisamente quali tipi di interazioni generano le notifiche destkop, tramite il pulsante {icon} sopra, una volta abilitate.", "notifications_permission_banner.title": "Non perderti mai nulla", "onboarding.follows.back": "Indietro", - "onboarding.follows.done": "Fatto", "onboarding.follows.empty": "Sfortunatamente, nessun risultato può essere mostrato in questo momento. Puoi provare a utilizzare la ricerca o sfogliare la pagina di esplorazione per trovare persone da seguire, oppure riprova più tardi.", + "onboarding.follows.next": "Successivo: imposta il tuo profilo", "onboarding.follows.search": "Cerca", "onboarding.follows.title": "Segui le persone per iniziare", "onboarding.profile.discoverable": "Rendi il mio profilo rilevabile", "onboarding.profile.discoverable_hint": "Quando attivi la rilevabilità su Mastodon, i tuoi post potrebbero apparire nei risultati di ricerca e nelle tendenze e il tuo profilo potrebbe essere suggerito a persone con interessi simili ai tuoi.", "onboarding.profile.display_name": "Nome da visualizzare", "onboarding.profile.display_name_hint": "Il tuo nome completo o il tuo nome divertente…", + "onboarding.profile.finish": "Fine", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Puoi @menzionare altre persone o #hashtags…", - "onboarding.profile.save_and_continue": "Salva e continua", "onboarding.profile.title": "Configurazione del profilo", "onboarding.profile.upload_avatar": "Carica l'immagine del profilo", "onboarding.profile.upload_header": "Carica l'intestazione del profilo", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 8c70f9af397f61..78fa8de0f0e373 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -847,7 +847,6 @@ "notifications_permission_banner.how_to_control": "Mastodonを閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。", "notifications_permission_banner.title": "お見逃しなく", "onboarding.follows.back": "戻る", - "onboarding.follows.done": "完了", "onboarding.follows.empty": "表示できる結果はありません。検索やエクスプローラーを使ったり、ほかのアカウントをフォローしたり、後でもう一度試しください。", "onboarding.follows.search": "検索", "onboarding.follows.title": "最初にフォローする人を選ぶ", @@ -857,7 +856,6 @@ "onboarding.profile.display_name_hint": "フルネーム、あるいは面白い名前など", "onboarding.profile.note": "自己紹介", "onboarding.profile.note_hint": "ほかのユーザーへのメンション (@mention) や、 #ハッシュタグ が使用できます", - "onboarding.profile.save_and_continue": "保存して続ける", "onboarding.profile.title": "プロフィールの設定", "onboarding.profile.upload_avatar": "プロフィール画像をアップロード", "onboarding.profile.upload_header": "プロフィールのヘッダー画像をアップロード", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 978e28bb59055d..a3f09b73201814 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -58,7 +58,6 @@ "account.follows_you": "Yeṭṭafaṛ-ik·em-id", "account.go_to_profile": "Ddu ɣer umaɣnu", "account.hide_reblogs": "Ffer ayen i ibeṭṭu @{name}", - "account.joined_long": "Yerna-d ass n {date}", "account.joined_short": "Izeddi da seg ass n", "account.languages": "Beddel tutlayin yettwajerden", "account.link_verified_on": "Taɣara n useɣwen-a tettwasenqed ass n {date}", @@ -592,14 +591,12 @@ "notifications_permission_banner.enable": "Rmed ilɣa n tnarit", "notifications_permission_banner.title": "Ur zeggel acemma", "onboarding.follows.back": "Uɣal", - "onboarding.follows.done": "Immed", "onboarding.follows.search": "Nadi", "onboarding.follows.title": "Ḍfeṛ walbɛaḍ i wakken ad ttebdud", "onboarding.profile.display_name": "Isem ara d-yettwaskanen", "onboarding.profile.display_name_hint": "Isem-ik·im ummid neɣ isem-ik·im n uqeṣṣer…", "onboarding.profile.note": "Tameddurt", "onboarding.profile.note_hint": "Tzemreḍ ad d-@tbedreḍ imdanen niḍen neɣ #ihacṭagen …", - "onboarding.profile.save_and_continue": "Sekles, tkemmleḍ", "onboarding.profile.title": "Asbadu n umaɣnu", "onboarding.profile.upload_avatar": "Sali tugna n umaɣnu", "onboarding.profile.upload_header": "Sali tacacit n umaɣnu", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index af65322b112ff2..953fa1fb9196cd 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -72,7 +72,6 @@ "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}에 이 링크의 소유권이 확인 됨", @@ -85,6 +84,7 @@ "account.menu.copied": "계정 링크를 복사했습니다", "account.menu.copy": "링크 복사하기", "account.menu.mention": "멘션", + "account.menu.mute": "계정 뮤트", "account.menu.note.description": "나에게만 보입니다", "account.menu.open_original_page": "{domain}에서 보기", "account.menu.remove_follower": "팔로워 제거", @@ -796,7 +796,6 @@ "notifications_permission_banner.how_to_control": "마스토돈이 열려 있지 않을 때에도 알림을 받으려면, 데스크탑 알림을 활성화 하세요. 당신은 어떤 종류의 반응이 데스크탑 알림을 발생할 지를 {icon} 버튼을 통해 세세하게 설정할 수 있습니다.", "notifications_permission_banner.title": "아무것도 놓치지 마세요", "onboarding.follows.back": "뒤로가기", - "onboarding.follows.done": "완료", "onboarding.follows.empty": "안타깝지만 아직은 아무 것도 보여드릴 수 없습니다. 검색을 이용하거나 둘러보기 페이지에서 팔로우 할 사람을 찾을 수 있습니다. 아니면 잠시 후에 다시 시도하세요.", "onboarding.follows.search": "검색", "onboarding.follows.title": "사람들을 팔로우하기", @@ -806,7 +805,6 @@ "onboarding.profile.display_name_hint": "진짜 이름 또는 재미난 이름…", "onboarding.profile.note": "자기소개", "onboarding.profile.note_hint": "남을 @mention 하거나 #hashtag 태그를 달 수 있습니다…", - "onboarding.profile.save_and_continue": "저장 및 계속", "onboarding.profile.title": "프로필 설정", "onboarding.profile.upload_avatar": "프로필 사진 업로드", "onboarding.profile.upload_header": "프로필 헤더 업로드", @@ -830,7 +828,8 @@ "privacy.private.short": "팔로워", "privacy.public.long": "마스토돈 내외 모두", "privacy.public.short": "공개", - "privacy.quote.disabled": "{visibility}, 인용 비활성화", + "privacy.quote.anyone": "{visibility}, 인용 허용", + "privacy.quote.disabled": "{visibility}, 인용 비활성", "privacy.quote.limited": "{visibility}, 제한된 인용", "privacy.unlisted.additional": "공개와 똑같지만 게시물이 실시간 피드나 해시태그, 둘러보기, (계정 설정에서 허용했더라도) 마스토돈 검색에서 제외됩니다.", "privacy.unlisted.long": "마스토돈 검색결과, 유행, 공개 타임라인에서 숨기기", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index b765dbbb51b2aa..f53292c751e37f 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -594,7 +594,6 @@ "notifications_permission_banner.how_to_control": "Para risivir avizos kuando Mastodon no esta avierto, kapasita avizos de ensimameza. Puedes kontrolar presizamente kualos tipos de enteraksiones djeneren avizos de ensimameza kon el boton {icon} arriva kuando esten kapasitadas.", "notifications_permission_banner.title": "Nunkua te piedres niente", "onboarding.follows.back": "Atras", - "onboarding.follows.done": "Fecho", "onboarding.follows.empty": "Malorozamente, no se pueden amostrar rezultados en este momento. Puedes aprovar uzar la bushkeda o navigar por la pajina de eksplorasyon para topar personas a las que segir, o aprovarlo de muevo mas tadre.", "onboarding.follows.search": "Bushka", "onboarding.follows.title": "Sige personas para ampezar", @@ -604,7 +603,6 @@ "onboarding.profile.display_name_hint": "Tu nombre para amostrar.", "onboarding.profile.note": "Tu deskripsyon", "onboarding.profile.note_hint": "Puedes @enmentar a otra djente o #etiketas…", - "onboarding.profile.save_and_continue": "Guadra i kontinua", "onboarding.profile.title": "Konfigurasyon de profil", "onboarding.profile.upload_avatar": "Karga imaje de profil", "onboarding.profile.upload_header": "Karga kavesera de profil", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index ef6fa617020c94..82a7eb9858e481 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -54,7 +54,7 @@ "account.follow": "Sekti", "account.follow_back": "Sekti atgal", "account.follow_back_short": "Sekti atgal", - "account.follow_request": "Prašyti sekti", + "account.follow_request": "Prašymas sekti", "account.follow_request_cancel": "Atšaukti prašymą", "account.follow_request_cancel_short": "Atšaukti", "account.follow_request_short": "Prašymas", @@ -69,7 +69,6 @@ "account.go_to_profile": "Eiti į profilį", "account.hide_reblogs": "Slėpti pasidalinimus iš @{name}", "account.in_memoriam": "Atminimui.", - "account.joined_long": "Prisijungė {date}", "account.joined_short": "Prisijungė", "account.languages": "Keisti prenumeruojamas kalbas", "account.link_verified_on": "Šios nuorodos nuosavybė buvo patikrinta {date}", @@ -745,7 +744,6 @@ "notifications_permission_banner.how_to_control": "Jei nori gauti pranešimus, kai Mastodon nėra atidarytas, įjunk darbalaukio pranešimus. Įjungęs (-usi) darbalaukio pranešimus, gali tiksliai valdyti, kokių tipų sąveikos generuoja darbalaukio pranešimus, naudojant pirmiau esančiu mygtuku {icon}.", "notifications_permission_banner.title": "Niekada nieko nepraleisk", "onboarding.follows.back": "Atgal", - "onboarding.follows.done": "Atlikta", "onboarding.follows.empty": "Deja, šiuo metu jokių rezultatų parodyti negalima. Gali pabandyti naudoti paiešką arba naršyti atradimo puslapį, kad surastum žmonių, kuriuos nori sekti, arba bandyti vėliau.", "onboarding.follows.search": "Ieškoti", "onboarding.follows.title": "Sekite asmenis, kad pradėtumėte", @@ -755,7 +753,6 @@ "onboarding.profile.display_name_hint": "Tavo pilnas vardas arba linksmas vardas…", "onboarding.profile.note": "Biografija", "onboarding.profile.note_hint": "Gali @paminėti kitus žmones arba #saitažodžius…", - "onboarding.profile.save_and_continue": "Išsaugoti ir tęsti", "onboarding.profile.title": "Profilio sąranka", "onboarding.profile.upload_avatar": "Įkelti profilio nuotrauką", "onboarding.profile.upload_header": "Įkelti profilio antraštę", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index d73177da90712c..83e436c75638d7 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -610,7 +610,6 @@ "onboarding.profile.display_name_hint": "Tavs pilnais vārds vai Tavs joku vārds…", "onboarding.profile.note": "Apraksts", "onboarding.profile.note_hint": "Tu vari @pieminēt citus cilvēkus vai #tēmturus…", - "onboarding.profile.save_and_continue": "Saglabāt un turpināt", "onboarding.profile.title": "Profila iestatīšana", "onboarding.profile.upload_avatar": "Augšupielādēt profila attēlu", "onboarding.profile.upload_header": "Augšupielādēt profila galveni", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 92b12958fe3ed2..49a54815d8ab14 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -481,7 +481,6 @@ "onboarding.profile.display_name": "Nama paparan", "onboarding.profile.display_name_hint": "Nama penuh anda atau nama anda yang menyeronokkan…", "onboarding.profile.note_hint": "Anda boleh @menyebut orang lain atau #hashtags…", - "onboarding.profile.save_and_continue": "Simpan dan teruskan", "onboarding.profile.upload_avatar": "Muat naik gambar profil", "password_confirmation.exceeds_maxlength": "Pengesahan kata laluan melebihi panjang kata laluan maksimum", "password_confirmation.mismatching": "Pengesahan kata laluan tidak sepadan", diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json index ecfe89311174a1..ba1e90e5b9369e 100644 --- a/app/javascript/mastodon/locales/my.json +++ b/app/javascript/mastodon/locales/my.json @@ -385,7 +385,6 @@ "onboarding.profile.display_name": "ဖော်ပြမည့်အမည်", "onboarding.profile.display_name_hint": "သင့်အမည်အပြည့်အစုံ သို့မဟုတ် သင့်အမည်ပြောင်။", "onboarding.profile.note": "ကိုယ်ရေးအကျဉ်း", - "onboarding.profile.save_and_continue": "သိမ်းပြီး ဆက်လုပ်ပါ", "onboarding.profile.title": "ပရိုဖိုင်စနစ် ထည့်သွင်းခြင်း", "onboarding.profile.upload_avatar": "ပရိုဖိုင်ပုံ အပ်လုဒ်လုပ်ပါ", "password_confirmation.exceeds_maxlength": "စကားဝှက်အတည်ပြုခြင်းတွင် အများဆုံးစကားဝှက်အရှည်ထက် ကျော်လွန်နေပါသည်", diff --git a/app/javascript/mastodon/locales/nan-TW.json b/app/javascript/mastodon/locales/nan-TW.json index 0c863ab16ea29f..f26df41d4f5e9d 100644 --- a/app/javascript/mastodon/locales/nan-TW.json +++ b/app/javascript/mastodon/locales/nan-TW.json @@ -73,7 +73,6 @@ "account.go_to_profile": "行kàu個人資料", "account.hide_reblogs": "Tshàng tuì @{name} 來ê轉PO", "account.in_memoriam": "佇tsia追悼。", - "account.joined_long": "佇 {date} 加入", "account.joined_short": "加入ê時", "account.languages": "變更訂閱的語言", "account.link_verified_on": "Tsit ê連結ê所有權佇 {date} 受檢查", @@ -151,13 +150,53 @@ "account_edit.button.edit": "編 {item}", "account_edit.column_button": "做好ah", "account_edit.column_title": "編輯個人資料", + "account_edit.custom_fields.name": "框á", "account_edit.custom_fields.placeholder": "加lí ê代名詞、外部連結,á是其他lí beh分享ê。", + "account_edit.custom_fields.reorder_button": "重排框á", + "account_edit.custom_fields.tip_content": "Lí通用驗證連kàu lí 所有ê網站ê連結,來增加lí ê Mastodon口座ê通信ê程度。", + "account_edit.custom_fields.tip_title": "撇步:加驗證過ê連結", "account_edit.custom_fields.title": "自訂欄", + "account_edit.custom_fields.verified_hint": "我beh án-tsuánn加驗證過ê連結?", "account_edit.display_name.placeholder": "Lí ê顯示ê名是lí ê名佇lí ê個人資料kap時間線出現ê方式。", "account_edit.display_name.title": "顯示ê名", "account_edit.featured_hashtags.item": "hashtag", "account_edit.featured_hashtags.placeholder": "幫tsān別lâng認捌,kap緊緊接近使用lí收藏ê主題。", "account_edit.featured_hashtags.title": "特色ê hashtag", + "account_edit.field_delete_modal.confirm": "Lí敢確定beh thâi掉tsit ê自訂ê框á?Tsit ê動作bē當改倒轉。", + "account_edit.field_delete_modal.delete_button": "Thâi掉", + "account_edit.field_delete_modal.title": "敢beh thâi掉自訂ê框á?", + "account_edit.field_edit_modal.add_title": "加自訂ê框á", + "account_edit.field_edit_modal.edit_title": "編自訂ê框á", + "account_edit.field_edit_modal.limit_header": "超過建議ê字數限制ah", + "account_edit.field_edit_modal.limit_message": "行動設備ê用者有可能bē當看著lí所有ê框á。", + "account_edit.field_edit_modal.link_emoji_warning": "Lán無建議佇URL內底用自訂ê emoji。為著避免用者舞花去,自訂ê框á若包含自訂emoji kap URL,kan-ta ē顯示做文字。", + "account_edit.field_edit_modal.name_hint": "例:「個人網站」", + "account_edit.field_edit_modal.name_label": "標簽", + "account_edit.field_edit_modal.url_warning": "若beh加連結,請佇起頭包含 {protocol}。", + "account_edit.field_edit_modal.value_hint": "例:「https://example.me」", + "account_edit.field_edit_modal.value_label": "值", + "account_edit.field_reorder_modal.drag_cancel": "Giú ê動作取消ah。框á「{item}」予lâng khǹg ah。", + "account_edit.field_reorder_modal.drag_end": "框á「{item}」予lâng khǹg ah。", + "account_edit.field_reorder_modal.drag_instructions": "Beh重整理自訂ê框á,請tshi̍h空白khí或者是Enter。Teh suá振動ê時,請用方向khí來kā框á suá kah頂懸á是下底。請koh tshi̍h空白khí或者是Enter來kā框á khǹg佇新ê位置,á是tshi̍h ESC khí取消。", + "account_edit.field_reorder_modal.drag_move": "框á「{item}」suá振動ah。", + "account_edit.field_reorder_modal.drag_over": "框á「{item}」已經suá kah「{over}」面頂。", + "account_edit.field_reorder_modal.drag_start": "有揀框á「{item}」。", + "account_edit.field_reorder_modal.handle_label": "Giú框á「{item}」", + "account_edit.field_reorder_modal.title": "重整理框á", + "account_edit.image_alt_modal.add_title": "加添說明文字", + "account_edit.image_alt_modal.details_content": "著愛:
    • 照圖描述家kī
    • 用第三人稱(比如用「阿明」,毋是「我」)
    • 束結,幾个詞就夠ah
    毋通:
    • 用「……ê相片」結束,這對讀螢幕程式是加講ê
    例:
    • 「阿明穿青ê siá-tsuh,koh掛眼鏡」
    ", + "account_edit.image_alt_modal.details_title": "撇步:個人資料ê相片 ê 說明文字", + "account_edit.image_alt_modal.edit_title": "編說明文字", + "account_edit.image_alt_modal.text_hint": "ALT說明文字通幫tsān讀螢幕程式ê用者知影lí ê內容。", + "account_edit.image_alt_modal.text_label": "替代文字", + "account_edit.image_delete_modal.confirm": "Lí敢確定beh thâi掉tsit幅圖?Tsit ê動作bē當改倒轉。", + "account_edit.image_delete_modal.delete_button": "Thâi掉", + "account_edit.image_delete_modal.title": "Kám beh thâi掉影像?", + "account_edit.image_edit.add_button": "加圖片", + "account_edit.image_edit.alt_add_button": "加添說明文字", + "account_edit.image_edit.alt_edit_button": "編說明文字", + "account_edit.image_edit.remove_button": "Suá掉圖片", + "account_edit.image_edit.replace_button": "取代圖片", "account_edit.name_modal.add_title": "加添顯示ê名", "account_edit.name_modal.edit_title": "編顯示ê名", "account_edit.profile_tab.button_label": "自訂", @@ -172,6 +211,24 @@ "account_edit.profile_tab.subtitle": "自訂lí ê個人資料ê分頁kap顯示ê內容。", "account_edit.profile_tab.title": "個人資料分頁設定", "account_edit.save": "儲存", + "account_edit.upload_modal.back": "轉去", + "account_edit.upload_modal.done": "做好ah", + "account_edit.upload_modal.next": "後一步", + "account_edit.upload_modal.step_crop.zoom": "伸kiu", + "account_edit.upload_modal.step_upload.button": "瀏覽檔案", + "account_edit.upload_modal.step_upload.dragging": "Giú kàu tsia傳上去", + "account_edit.upload_modal.step_upload.header": "揀圖片", + "account_edit.upload_modal.step_upload.hint": "WEBP、PNG、GIF á是 JPG 格式,上大 {limit}MB。{br}圖會伸kiu kàu {width}x{height} px。", + "account_edit.upload_modal.title_add": "加個人資料ê相", + "account_edit.upload_modal.title_replace": "替換個人資料ê相", + "account_edit.verified_modal.details": "用驗證連kàu個人網站ê連結來加添lí ê Mastodon個人檔案ê通信ê程度。下kha是運作ê方法:", + "account_edit.verified_modal.invisible_link.details": "加連結kàu lí ê網頁頭(header)。上重要ê部份是 rel=\"me\",伊防止通過用者生成ê網站內容來做假包。Lí甚至佇網頁ê header毋免用 {tag},反轉用link標簽,但是HTML定著佇無執行JavaScript ê時陣,就ē當接近使用。", + "account_edit.verified_modal.invisible_link.summary": "Án-tsuánn khàm掉tsit ê連結?", + "account_edit.verified_modal.step1.header": "Khóo-pih 下kha ê HTML程式碼,貼佇lí ê網站ê網頁頭(header)底", + "account_edit.verified_modal.step2.details": "Lí若有加lí ê網站成做自訂ê框á,lí需要thâi掉,koh加tse來啟動驗證。", + "account_edit.verified_modal.step2.header": "Kā lí ê網站加做自訂ê框á", + "account_edit.verified_modal.title": "Án-tsuánn加驗證過ê連結", + "account_edit_tags.add_tag": "加 #{tagName}", "account_edit_tags.column_title": "編收藏ê hashtag", "account_edit_tags.help_text": "收藏ê hashtag幫tsān用者發現kap hām lí ê個人資料互動。In會成做過濾器,佇lí ê個人資料頁ê活動內底出現。", "account_edit_tags.search_placeholder": "編輯hashtag……", @@ -274,6 +331,8 @@ "callout.dismiss": "忽略", "carousel.current": "頁面 {current, number} / {max, number}", "carousel.slide": "{max, number} 頁內底ê第 {current, number} 頁", + "character_counter.recommended": "{currentLength}/{maxLength} ê建議字數", + "character_counter.required": "{currentLength}/{maxLength} 字數", "closed_registrations.other_server_instructions": "因為Mastodon非中心化,所以lí ē當tī別ê服侍器建立口座,iáu ē當kap tsit ê服侍器來往。", "closed_registrations_modal.description": "Tann bē當tī {domain} 建立新ê口座,m̄-koh著記得,lí bô需要 {domain} 服侍器ê帳號,mā ē當用 Mastodon。", "closed_registrations_modal.find_another_server": "Tshuē別ê服侍器", @@ -290,6 +349,8 @@ "collections.accounts.empty_description": "加lí跟tuè ê口座,上tsē {count} ê", "collections.accounts.empty_title": "收藏內底無半項", "collections.collection_description": "說明", + "collections.collection_language": "語言", + "collections.collection_language_none": "無", "collections.collection_name": "名", "collections.collection_topic": "主題", "collections.confirm_account_removal": "Lí確定beh對收藏suá掉tsit ê口座?", @@ -303,10 +364,15 @@ "collections.create_collection": "建立收藏", "collections.delete_collection": "Thâi掉收藏", "collections.description_length_hint": "限制 100 字", + "collections.detail.accept_inclusion": "OK", "collections.detail.accounts_heading": "口座", + "collections.detail.author_added_you": "{author} kā lí加kàu tsit ê收藏", "collections.detail.curated_by_author": "{author} 揀ê", "collections.detail.curated_by_you": "Lí揀ê", "collections.detail.loading": "載入收藏……", + "collections.detail.other_accounts_in_collection": "Tsit ê收藏內ê別lâng:", + "collections.detail.revoke_inclusion": "Kā我suá掉", + "collections.detail.sensitive_note": "Tsit ê收藏包含對一寡用者敏感ê口座kap內容。", "collections.detail.share": "分享tsit ê收藏", "collections.edit_details": "編輯詳細", "collections.error_loading_collections": "佇載入lí ê收藏ê時陣出tshê。", @@ -321,10 +387,14 @@ "collections.old_last_post_note": "頂改佇超過一禮拜進前PO文", "collections.remove_account": "Suá掉tsit ê口座", "collections.report_collection": "檢舉tsit ê收藏", + "collections.revoke_collection_inclusion": "Kā我對收藏內底suá掉", + "collections.revoke_inclusion.confirmation": "Lí已經對「{collection}」hőng suá掉", + "collections.revoke_inclusion.error": "出tshê ah,請小等leh koh試。", "collections.search_accounts_label": "Tshuē口座來加添……", "collections.search_accounts_max_reached": "Lí已經加kàu口座數ê盡磅ah。", "collections.sensitive": "敏感ê", "collections.topic_hint": "加 hashtag,幫tsān別lâng了解tsit ê收藏ê主題。", + "collections.topic_special_chars_hint": "儲存ê時陣ê suá掉特殊字元", "collections.view_collection": "看收藏", "collections.view_other_collections_by_user": "看tsit ê用者ê別ê收藏", "collections.visibility_public": "公共ê", @@ -439,11 +509,14 @@ "confirmations.quiet_post_quote_info.message": "Nā是引用無tī公共時間線內底êPO文,lí êPO文bē當tī趨勢ê時間線顯示。", "confirmations.quiet_post_quote_info.title": "引用無tī公開ê時間線內底顯示ê PO文", "confirmations.redraft.confirm": "Thâi掉了後重寫", - "confirmations.redraft.message": "Lí kám確定behthâi掉tsit篇PO文了後koh重寫?收藏kap轉PO ē無去,而且原底ê PO文ê回應ē變孤立。", + "confirmations.redraft.message": "Lí kám確定beh thâi掉tsit篇PO文了後koh重寫?收藏kap轉PO ē無去,而且原底ê PO文ê回應ē變孤立。", "confirmations.redraft.title": "Kám beh thâi掉koh重寫PO文?", "confirmations.remove_from_followers.confirm": "Suá掉跟tuè lí ê", "confirmations.remove_from_followers.message": "{name} ē停止跟tuè lí。Lí kám確定beh繼續?", "confirmations.remove_from_followers.title": "Kám beh suá掉跟tuè lí ê?", + "confirmations.revoke_collection_inclusion.confirm": "Kā我suá掉", + "confirmations.revoke_collection_inclusion.message": "Tsit ê行動永永有效,了後建立收藏ê bē當kā lí重加kàu收藏內底。", + "confirmations.revoke_collection_inclusion.title": "敢beh kā家kī對收藏內底suá掉?", "confirmations.revoke_quote.confirm": "Thâi掉PO文", "confirmations.revoke_quote.message": "Tsit ê動作bē當復原。", "confirmations.revoke_quote.title": "Kám beh thâi掉PO文?", @@ -679,6 +752,7 @@ "keyboard_shortcuts.direct": "Phah開私人提起ê欄", "keyboard_shortcuts.down": "佇列單內kā suá khah 下kha", "keyboard_shortcuts.enter": "Phah開PO文", + "keyboard_shortcuts.explore": "拍開趨勢ê時間線", "keyboard_shortcuts.favourite": "收藏PO文", "keyboard_shortcuts.favourites": "Phah開收藏ê列單", "keyboard_shortcuts.federated": "Phah開聯邦ê時間線", @@ -765,6 +839,7 @@ "navigation_bar.automated_deletion": "自動thâi PO文", "navigation_bar.blocks": "封鎖ê用者", "navigation_bar.bookmarks": "冊籤", + "navigation_bar.collections": "收藏", "navigation_bar.direct": "私人ê提起", "navigation_bar.domain_blocks": "封鎖ê域名", "navigation_bar.favourites": "Siōng kah意", @@ -801,8 +876,8 @@ "notification.annual_report.view": "Kā #Wrapstodon 看māi。", "notification.favourite": "{name} kah意lí ê PO文", "notification.favourite.name_and_others_with_link": "{name} kap{count, plural, other {另外 # ê lâng}}kah意lí ê PO文", - "notification.favourite_pm": "{name} kah意lí ê私人提起", - "notification.favourite_pm.name_and_others_with_link": "{name} kap{count, plural, other {另外 # ê lâng}}kah意lí ê私人提起", + "notification.favourite_pm": "{name} kah意lí ê私人ê提起", + "notification.favourite_pm.name_and_others_with_link": "{name} kap{count, plural, other {另外 # ê lâng}}kah意lí ê私人ê提起", "notification.follow": "{name}跟tuè lí", "notification.follow.name_and_others": "{name} kap{count, plural, other {另外 # ê lâng}}跟tuè lí", "notification.follow_request": "{name} 請求跟tuè lí", @@ -905,23 +980,23 @@ "notifications.policy.filter_not_following_hint": "直到lí手動允准in", "notifications.policy.filter_not_following_title": "Lí無跟tuè ê lâng", "notifications.policy.filter_private_mentions_hint": "通知ē受過濾,除非是tī lí ê提起ê回應內底,á是lí跟tuè送PO文ê lâng", - "notifications.policy.filter_private_mentions_title": "家kī直接送來ê私人提起", + "notifications.policy.filter_private_mentions_title": "家kī直接送來ê私人ê提起", "notifications.policy.title": "管理通知tuì……", "notifications_permission_banner.enable": "啟用桌面ê通知", "notifications_permission_banner.how_to_control": "Nā beh佇Mastodon關起來ê時陣收通知,請啟用桌面通知。若準啟用,Lí ē當通過面頂ê {icon} 鈕á,準準控制siánn物互動ê類型ē生桌面通知。", "notifications_permission_banner.title": "逐ê著看", "onboarding.follows.back": "轉去", - "onboarding.follows.done": "做好ah", "onboarding.follows.empty": "可惜,tsit-má無半條結果通顯示。Lí ē當試用tshiau-tshuē á是瀏覽探索ê頁,來tshuē beh跟tuè ê lâng,或者是sió等leh koh試。", + "onboarding.follows.next": "後一步:設定lí ê個人資料", "onboarding.follows.search": "Tshiau-tshuē", "onboarding.follows.title": "請跟tuè lâng來開始。", "onboarding.profile.discoverable": "Hōo我ê個人資料通tshuē著", "onboarding.profile.discoverable_hint": "Nā lí揀beh佇Mastodon開hōo lâng發現ê功能,lí ê PO文通顯示佇tshiau-tshuē結果kap趨勢,而且你ê個人資料可能ē推薦hōo kap lí有相siâng興趣ê別lâng。", "onboarding.profile.display_name": "顯示ê名", "onboarding.profile.display_name_hint": "Lí ê全名á是別號……", + "onboarding.profile.finish": "完成", "onboarding.profile.note": "個人紹介", "onboarding.profile.note_hint": "Lí ē當 @mention 別lâng á是用 #hashtag……", - "onboarding.profile.save_and_continue": "儲存了後繼續", "onboarding.profile.title": "個人資料ê設定", "onboarding.profile.upload_avatar": "Kā個人資料ê相片傳起去。", "onboarding.profile.upload_header": "Kā個人資料ê橫條á ê圖傳起去", @@ -1068,6 +1143,9 @@ "sign_in_banner.mastodon_is": "Mastodon是跟tuè siánn物當teh發生ê上贊ê方法。", "sign_in_banner.sign_in": "登入", "sign_in_banner.sso_redirect": "登入á是註冊", + "skip_links.hotkey": "組合khí {hotkey}", + "skip_links.skip_to_content": "跳kàu主內容", + "skip_links.skip_to_navigation": "跳kàu主導覽", "status.admin_account": "Phah開 @{name} ê管理界面", "status.admin_domain": "Phah開 {domain} ê管理界面", "status.admin_status": "Tī管理界面內底看tsit篇PO文", @@ -1118,7 +1196,7 @@ "status.quote_error.limited_account_hint.title": "Tsit ê口座予 {domain} ê管理員tshàng起來ah。", "status.quote_error.muted_account_hint.title": "因為lí有消音 @{name},tsit篇PO文受khàm掉。", "status.quote_error.not_available": "PO文bē當看", - "status.quote_error.pending_approval": "PO文當咧送", + "status.quote_error.pending_approval": "PO文當leh送", "status.quote_error.pending_approval_popout.body": "佇Mastodon,lí ē當控制PO文kám beh hōo lâng引用。Tsit篇PO文teh等原文作者允准。", "status.quote_error.revoked": "PO文已經hōo作者thâi掉", "status.quote_followers_only": "Kan-ta tuè我ê ē當引用PO文", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 54025d2f06b84a..3be1d0981468b8 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ga naar profiel", "account.hide_reblogs": "Boosts van @{name} verbergen", "account.in_memoriam": "In memoriam.", - "account.joined_long": "Geregistreerd op {date}", "account.joined_short": "Geregistreerd op", "account.languages": "Getoonde talen wijzigen", "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "{item} bewerken", "account_edit.column_button": "Klaar", "account_edit.column_title": "Profiel bewerken", + "account_edit.custom_fields.name": "veld", "account_edit.custom_fields.placeholder": "Voeg je voornaamwoorden, externe links of iets anders toe dat je wilt delen.", + "account_edit.custom_fields.reorder_button": "Velden opnieuw ordenen", + "account_edit.custom_fields.tip_content": "Je kunt gemakkelijk je Mastodon-account geloofwaardig maken door links naar websites die van jou zijn te laten verifiëren.", + "account_edit.custom_fields.tip_title": "Tip: Geverifieerde links toevoegen", "account_edit.custom_fields.title": "Extra velden", + "account_edit.custom_fields.verified_hint": "Hoe voeg ik een geverifieerde link toe?", "account_edit.display_name.placeholder": "Je weergavenaam wordt op jouw profiel en op tijdlijnen weergegeven.", "account_edit.display_name.title": "Weergavenaam", "account_edit.featured_hashtags.item": "hashtags", "account_edit.featured_hashtags.placeholder": "Geef anderen een overzicht van en snel toegang tot je favoriete onderwerpen.", "account_edit.featured_hashtags.title": "Uitgelichte hashtags", + "account_edit.field_delete_modal.confirm": "Weet je zeker dat je dit aangepaste veld wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "account_edit.field_delete_modal.delete_button": "Verwijderen", + "account_edit.field_delete_modal.title": "Aangepast veld verwijderen?", + "account_edit.field_edit_modal.add_title": "Aangepast veld toevoegen", + "account_edit.field_edit_modal.edit_title": "Aangepast veld bewerken", + "account_edit.field_edit_modal.limit_header": "Aanbevolen tekenlimiet overschreden", + "account_edit.field_edit_modal.limit_message": "Mobiele gebruikers zien mogelijk het veld niet volledig.", + "account_edit.field_edit_modal.link_emoji_warning": "We raden aan om geen lokale emoji in combinatie met URL's te gebruiken. Aangepaste velden die beide bevatten worden alleen als tekst weergegeven, in plaats van als een link. Dit om verwarring voor de gebruiker te voorkomen.", + "account_edit.field_edit_modal.name_hint": "Bijv. \"Persoonlijke website\"", + "account_edit.field_edit_modal.name_label": "Label", + "account_edit.field_edit_modal.url_warning": "Voeg {protocol} aan het begin toe om een link toe te voegen.", + "account_edit.field_edit_modal.value_hint": "Bijv. \"https://example.me\"", + "account_edit.field_edit_modal.value_label": "Waarde", + "account_edit.field_reorder_modal.drag_cancel": "Slepen is geannuleerd. Veld \"{item}\" werd geschrapt.", + "account_edit.field_reorder_modal.drag_end": "Veld \"{item}\" werd geschrapt.", + "account_edit.field_reorder_modal.drag_instructions": "Druk op spatie of enter om de aangepaste velden te herschikken. Gebruik de pijltjestoetsen om het veld omhoog of omlaag te verplaatsen. Druk opnieuw op spatie of enter om het veld op diens nieuwe positie te laten vallen, of druk op escape om te annuleren.", + "account_edit.field_reorder_modal.drag_move": "Veld \"{item}\" is verplaatst.", + "account_edit.field_reorder_modal.drag_over": "Veld \"{item}\" is over \"{over}\" geplaatst.", + "account_edit.field_reorder_modal.drag_start": "Opgepakt veld \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Veld \"{item}\" slepen", + "account_edit.field_reorder_modal.title": "Velden herschikken", + "account_edit.image_alt_modal.add_title": "Alt-tekst toevoegen", + "account_edit.image_alt_modal.details_content": "DOEN:
    • Beschrijf jezelf zoals afgebeeld
    • Schrijf in de derde persoon (bijv. “Alex” in plaats van “ik”)
    • Wees beknopt - een paar woorden is vaak genoeg
    NIET DOEN:
    • Begin met “Foto van” - dit is overbodig voor schermlezers
    VOORBEELD:
    • “Alex draagt een groen shirt en een bril”
    ", + "account_edit.image_alt_modal.details_title": "Tips: Alt-tekst voor profielfoto's", + "account_edit.image_alt_modal.edit_title": "Alt-tekst bewerken", + "account_edit.image_alt_modal.text_hint": "Alt-text helpt gebruikers van schermlezers jouw inhoud te begrijpen.", + "account_edit.image_alt_modal.text_label": "Alt-tekst", + "account_edit.image_delete_modal.confirm": "Weet je zeker dat je deze afbeelding wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "account_edit.image_delete_modal.delete_button": "Verwijderen", + "account_edit.image_delete_modal.title": "Afbeelding verwijderen?", + "account_edit.image_edit.add_button": "Afbeelding toevoegen", + "account_edit.image_edit.alt_add_button": "Alt-tekst toevoegen", + "account_edit.image_edit.alt_edit_button": "Alt-tekst bewerken", + "account_edit.image_edit.remove_button": "Afbeelding verwijderen", + "account_edit.image_edit.replace_button": "Afbeelding vervangen", "account_edit.name_modal.add_title": "Weergavenaam toevoegen", "account_edit.name_modal.edit_title": "Weergavenaam bewerken", "account_edit.profile_tab.button_label": "Aanpassen", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "De tabbladen op je profiel aanpassen en wat er op wordt weergegeven.", "account_edit.profile_tab.title": "Instellingen voor tabblad Profiel", "account_edit.save": "Opslaan", + "account_edit.upload_modal.back": "Terug", + "account_edit.upload_modal.done": "Klaar", + "account_edit.upload_modal.next": "Volgende", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Door bestanden bladeren", + "account_edit.upload_modal.step_upload.dragging": "Hierheen slepen om te uploaden", + "account_edit.upload_modal.step_upload.header": "Kies een afbeelding", + "account_edit.upload_modal.step_upload.hint": "WEBP-, PNG-, GIF- of JPG-formaat, tot max. {limit}MB.{br}Afbeelding wordt geschaald naar {width}x{height}px.", + "account_edit.upload_modal.title_add": "Profielfoto toevoegen", + "account_edit.upload_modal.title_replace": "Profielfoto vervangen", + "account_edit.verified_modal.details": "Maak je Mastodonprofiel geloofwaardig door links naar persoonlijke websites te verifiëren. Zo werkt het:", + "account_edit.verified_modal.invisible_link.details": "Voeg de link aan de HTML van je website toe. Het belangrijkste onderdeel is rel=\"me\", waarmee wordt voorkomen dat websites met user-generated content geïmpersoneerd kunnen worden. Je kunt zelfs een -tag gebruiken binnen de -tag van je website in plaats van {tag}, maar de HTML moet zonder JavaScript toegankelijk zijn.", + "account_edit.verified_modal.invisible_link.summary": "Hoe maak ik de link onzichtbaar?", + "account_edit.verified_modal.step1.header": "Kopieer de onderstaande HTML-code en plak deze binnen de -tag van je website", + "account_edit.verified_modal.step2.details": "Als je je website al als een aangepast veld hebt toegevoegd, moet je deze verwijderen en opnieuw toevoegen om de verificatie te activeren.", + "account_edit.verified_modal.step2.header": "Voeg je website toe als een aangepast veld", + "account_edit.verified_modal.title": "Hoe voeg je een geverifieerde link toe", "account_edit_tags.add_tag": "#{tagName} toevoegen", "account_edit_tags.column_title": "Uitgelichte hashtags bewerken", "account_edit_tags.help_text": "Uitgelichte hashtags helpen gebruikers je profiel te ontdekken en om er interactie mee te communiceren. Ze verschijnen als filters op je Profielpagina onder het tabblad Activiteit.", @@ -275,6 +331,8 @@ "callout.dismiss": "Afwijzen", "carousel.current": "Bericht {current, number} / {max, number}", "carousel.slide": "Bericht {current, number} van {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} aanbevolen tekens", + "character_counter.required": "{currentLength}/{maxLength} tekens", "closed_registrations.other_server_instructions": "Omdat Mastodon gedecentraliseerd is, kun je op een andere server een account registreren en vanaf daar nog steeds met deze server communiceren.", "closed_registrations_modal.description": "Momenteel is het niet mogelijk om op {domain} een account aan te maken. Hou echter in gedachte dat om Mastodon te kunnen gebruiken het niet een vereiste is om op {domain} een account te hebben.", "closed_registrations_modal.find_another_server": "Een andere server zoeken", @@ -291,6 +349,8 @@ "collections.accounts.empty_description": "Tot {count} accounts die je volgt toevoegen", "collections.accounts.empty_title": "Deze verzameling is leeg", "collections.collection_description": "Omschrijving", + "collections.collection_language": "Taal", + "collections.collection_language_none": "Geen", "collections.collection_name": "Naam", "collections.collection_topic": "Onderwerp", "collections.confirm_account_removal": "Weet je zeker dat je dit account uit deze verzameling wilt verwijderen?", @@ -304,10 +364,15 @@ "collections.create_collection": "Verzameling aanmaken", "collections.delete_collection": "Verzameling verwijderen", "collections.description_length_hint": "Maximaal 100 karakters", + "collections.detail.accept_inclusion": "Oké", "collections.detail.accounts_heading": "Accounts", + "collections.detail.author_added_you": "{author} heeft je aan deze verzameling toegevoegd", "collections.detail.curated_by_author": "Samengesteld door {author}", "collections.detail.curated_by_you": "Samengesteld door jou", "collections.detail.loading": "Verzameling laden…", + "collections.detail.other_accounts_in_collection": "Anderen in deze verzameling:", + "collections.detail.revoke_inclusion": "Verwijder mij", + "collections.detail.sensitive_note": "Deze verzameling bevat accounts en inhoud die mogelijk gevoelig zijn voor sommige gebruikers.", "collections.detail.share": "Deze verzameling delen", "collections.edit_details": "Gegevens bewerken", "collections.error_loading_collections": "Er is een fout opgetreden bij het laden van je verzamelingen.", @@ -322,10 +387,14 @@ "collections.old_last_post_note": "Meer dan een week geleden voor het laatst een bericht geplaatst", "collections.remove_account": "Dit account verwijderen", "collections.report_collection": "Deze verzameling rapporteren", + "collections.revoke_collection_inclusion": "Mezelf uit deze verzameling verwijderen", + "collections.revoke_inclusion.confirmation": "Je bent uit \"{collection}\" verwijderd", + "collections.revoke_inclusion.error": "Er is een fout opgetreden. Probeer het later opnieuw.", "collections.search_accounts_label": "Naar accounts zoeken om toe te voegen…", "collections.search_accounts_max_reached": "Je hebt het maximum aantal accounts toegevoegd", "collections.sensitive": "Gevoelig", "collections.topic_hint": "Voeg een hashtag toe die anderen helpt het hoofdonderwerp van deze verzameling te begrijpen.", + "collections.topic_special_chars_hint": "Speciale tekens worden bij het opslaan verwijderd", "collections.view_collection": "Verzameling bekijken", "collections.view_other_collections_by_user": "Bekijk andere verzamelingen van deze gebruiker", "collections.visibility_public": "Openbaar", @@ -445,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Volger verwijderen", "confirmations.remove_from_followers.message": "{name} zal je niet meer volgen. Weet je zeker dat je wilt doorgaan?", "confirmations.remove_from_followers.title": "Volger verwijderen?", + "confirmations.revoke_collection_inclusion.confirm": "Verwijder mij", + "confirmations.revoke_collection_inclusion.message": "Deze actie is definitief en de curator kan je later niet opnieuw aan de verzameling toevoegen.", + "confirmations.revoke_collection_inclusion.title": "Jezelf uit deze collectie verwijderen?", "confirmations.revoke_quote.confirm": "Bericht verwijderen", "confirmations.revoke_quote.message": "Deze actie kan niet ongedaan worden gemaakt.", "confirmations.revoke_quote.title": "Bericht verwijderen?", @@ -767,6 +839,7 @@ "navigation_bar.automated_deletion": "Automatisch berichten verwijderen", "navigation_bar.blocks": "Geblokkeerde gebruikers", "navigation_bar.bookmarks": "Bladwijzers", + "navigation_bar.collections": "Verzamelingen", "navigation_bar.direct": "Privéberichten", "navigation_bar.domain_blocks": "Geblokkeerde servers", "navigation_bar.favourites": "Favorieten", @@ -913,17 +986,17 @@ "notifications_permission_banner.how_to_control": "Om meldingen te ontvangen wanneer Mastodon niet open staat. Je kunt precies bepalen welke soort interacties wel of geen desktopmeldingen geven via de bovenstaande {icon} knop.", "notifications_permission_banner.title": "Mis nooit meer iets", "onboarding.follows.back": "Terug", - "onboarding.follows.done": "Klaar", "onboarding.follows.empty": "Helaas kunnen op dit moment geen resultaten worden getoond. Je kunt proberen te zoeken of op de verkenningspagina te bladeren om mensen te vinden die je kunt volgen, of probeer het later opnieuw.", + "onboarding.follows.next": "Volgende: je profiel instellen", "onboarding.follows.search": "Zoeken", "onboarding.follows.title": "Volg mensen om te beginnen", "onboarding.profile.discoverable": "Maak mijn profiel vindbaar", "onboarding.profile.discoverable_hint": "Wanneer je akkoord gaat met het vindbaar zijn op Mastodon, verschijnen je berichten in zoekresultaten en kunnen ze trending worden, en je profiel kan aan andere mensen worden aanbevolen wanneer ze vergelijkbare interesses hebben.", "onboarding.profile.display_name": "Weergavenaam", "onboarding.profile.display_name_hint": "Jouw volledige naam of een leuke bijnaam…", + "onboarding.profile.finish": "Voltooien", "onboarding.profile.note": "Biografie", "onboarding.profile.note_hint": "Je kunt andere mensen @vermelden of #hashtags gebruiken…", - "onboarding.profile.save_and_continue": "Opslaan en doorgaan", "onboarding.profile.title": "Profiel instellen", "onboarding.profile.upload_avatar": "Profielfoto uploaden", "onboarding.profile.upload_header": "Omslagfoto voor het profiel uploaden", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 9d7ca37c0d29c2..feb88565f8cfb2 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -73,7 +73,6 @@ "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}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Rediger {item}", "account_edit.column_button": "Ferdig", "account_edit.column_title": "Rediger profil", + "account_edit.custom_fields.name": "felt", "account_edit.custom_fields.placeholder": "Legg til pronomen, lenkjer eller kva du elles vil dela.", + "account_edit.custom_fields.reorder_button": "Omorganiser felt", + "account_edit.custom_fields.tip_content": "Du kan auka truverdet til Mastodon-kontoen din ved å stadfesta lenker til nettstader du eig.", + "account_edit.custom_fields.tip_title": "Tips: Legg til stadfesta lenker", "account_edit.custom_fields.title": "Eigne felt", + "account_edit.custom_fields.verified_hint": "Korleis legg eg til ei stadfesta lenke?", "account_edit.display_name.placeholder": "Det synlege namnet ditt er det som syner på profilen din og i tidsliner.", "account_edit.display_name.title": "Synleg namn", "account_edit.featured_hashtags.item": "emneknaggar", "account_edit.featured_hashtags.placeholder": "Hjelp andre å finna og få rask tilgang til favorittemna dine.", "account_edit.featured_hashtags.title": "Utvalde emneknaggar", + "account_edit.field_delete_modal.confirm": "Vil du sletta dette tilpassa feltet? Du kan ikkje angra.", + "account_edit.field_delete_modal.delete_button": "Slett", + "account_edit.field_delete_modal.title": "Slett tilpassa felt?", + "account_edit.field_edit_modal.add_title": "Legg til eit tilpassa felt", + "account_edit.field_edit_modal.edit_title": "Rediger tilpassa felt", + "account_edit.field_edit_modal.limit_header": "Over maksgrensa for teikn", + "account_edit.field_edit_modal.limit_message": "Det er ikkje sikkert mobilbrukarar ser heile feltet ditt.", + "account_edit.field_edit_modal.link_emoji_warning": "Me rår frå å bruka eigne smilefjes kombinert med adresser. Tilpassa felt som inneheld båe, vil syna som berre tekst i staden for ei lenke, slik at lesarane ikkje blir forvirra.", + "account_edit.field_edit_modal.name_hint": "Til dømes «Personleg nettstad»", + "account_edit.field_edit_modal.name_label": "Etikett", + "account_edit.field_edit_modal.url_warning": "Skriv {protocol} i starten for å leggja til ei lenke.", + "account_edit.field_edit_modal.value_hint": "Til dømes «https://nettstad.no»", + "account_edit.field_edit_modal.value_label": "Verdi", + "account_edit.field_reorder_modal.drag_cancel": "Du avbraut draginga. Feltet «{item}» vart sleppt.", + "account_edit.field_reorder_modal.drag_end": "Feltet «{item}» vart sleppt.", + "account_edit.field_reorder_modal.drag_instructions": "For å flytta eigne felt, trykkjer du mellomrom eller enter. Bruk piltastane når du dreg for å flytta feltet opp eller ned. Trykk mellomrom eller enter ein gong til for å sleppa feltet på den nye plassen, eller trykk escape for å avbryta.", + "account_edit.field_reorder_modal.drag_move": "Feltet «{item}» vart flytt.", + "account_edit.field_reorder_modal.drag_over": "Feltet «{item}» vart flytt over «{over}».", + "account_edit.field_reorder_modal.drag_start": "Plukka opp feltet «{item}».", + "account_edit.field_reorder_modal.handle_label": "Dra feltet «{item}»", + "account_edit.field_reorder_modal.title": "Flytt felt", + "account_edit.image_alt_modal.add_title": "Legg til skildring", + "account_edit.image_alt_modal.details_content": "JA:
    • Skildre deg sjølv på biletet
    • Bruk tredjeperson (til dømes «Anne» i staden for «meg»)
    • Skriv stutt
    NEI:
    • Start med «bilete av». Skjermlesarar treng ikkje det.
    DØME:
    • «Anne har på seg ei grøn skjorte og briller»
    ", + "account_edit.image_alt_modal.details_title": "Tips: Skrildre profilbilete", + "account_edit.image_alt_modal.edit_title": "Rediger skildring", + "account_edit.image_alt_modal.text_hint": "Skildringar hjelper folk som bruker skjermlesarar å forstå deg.", + "account_edit.image_alt_modal.text_label": "Skildring", + "account_edit.image_delete_modal.confirm": "Vil du sletta dette biletet? Du kan ikkje angra.", + "account_edit.image_delete_modal.delete_button": "Slett", + "account_edit.image_delete_modal.title": "Slett bilete?", + "account_edit.image_edit.add_button": "Legg til bilete", + "account_edit.image_edit.alt_add_button": "Legg til skildring", + "account_edit.image_edit.alt_edit_button": "Rediger skildring", + "account_edit.image_edit.remove_button": "Fjern bilete", + "account_edit.image_edit.replace_button": "Erstatt bilete", "account_edit.name_modal.add_title": "Legg til synleg namn", "account_edit.name_modal.edit_title": "Endre synleg namn", "account_edit.profile_tab.button_label": "Tilpass", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Tilpass fanene på profilen din og kva dei syner.", "account_edit.profile_tab.title": "Innstillingar for profilfane", "account_edit.save": "Lagre", + "account_edit.upload_modal.back": "Tilbake", + "account_edit.upload_modal.done": "Ferdig", + "account_edit.upload_modal.next": "Neste", + "account_edit.upload_modal.step_crop.zoom": "Forstørre", + "account_edit.upload_modal.step_upload.button": "Sjå gjennom filer", + "account_edit.upload_modal.step_upload.dragging": "Slepp for å lasta opp", + "account_edit.upload_modal.step_upload.header": "Vel eit bilete", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF eller JPG-format, opp til {limit}MB.{br}Biletet blir skalert til {width}*{height} punkt.", + "account_edit.upload_modal.title_add": "Legg til profilbilete", + "account_edit.upload_modal.title_replace": "Byt ut profilbilete", + "account_edit.verified_modal.details": "Auk truverdet til Mastodon-profilen din ved å stadfesta lenker til personlege nettstader. Slik verkar det:", + "account_edit.verified_modal.invisible_link.details": "Den viktige delen er rel=\"me\", som på nettstader med brukargenerert innhald vil hindra at andre kan låst som dei er deg. Du kan til og med bruka link i staden for {tag} i toppteksten til sida, men HTML-koden må vera tilgjengeleg utan å måtte køyra JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Korleis gjer eg lenka usynleg?", + "account_edit.verified_modal.step1.header": "Kopier HTML-koden under og lim han inn i toppfeltet på nettstaden din", + "account_edit.verified_modal.step2.details": "Viss du allereie har lagt til nettsida di i eit tilpassa felt, må du sletta ho og leggja ho til på nytt for å setja i gang stadfestinga.", + "account_edit.verified_modal.step2.header": "Legg til nettstaden din som eige felt", + "account_edit.verified_modal.title": "Korleis legg eg til ei stadfesta lenke", "account_edit_tags.add_tag": "Legg til #{tagName}", "account_edit_tags.column_title": "Rediger utvalde emneknaggar", "account_edit_tags.help_text": "Utvalde emneknaggar hjelper folk å oppdaga og samhandla med profilen din. Dei blir viste som filter på aktivitetsoversikta på profilsida di.", @@ -275,6 +331,8 @@ "callout.dismiss": "Avvis", "carousel.current": "Side {current, number} / {max, number}", "carousel.slide": "Side {current, number} av {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} tilrådde teikn", + "character_counter.required": "{currentLength}/{maxLength} teikn", "closed_registrations.other_server_instructions": "Sidan Mastodon er desentralisert kan du lage ein brukar på ein anna tenar og framleis interagere med denne.", "closed_registrations_modal.description": "Det er ikkje mogleg å opprette ein konto på {domain} nett no, men hugs at du ikkje treng ein konto på akkurat {domain} for å nytte Mastodon.", "closed_registrations_modal.find_another_server": "Finn ein annan tenar", @@ -304,10 +362,15 @@ "collections.create_collection": "Lag ei samling", "collections.delete_collection": "Slett samlinga", "collections.description_length_hint": "Maks 100 teikn", + "collections.detail.accept_inclusion": "Ok", "collections.detail.accounts_heading": "Kontoar", + "collections.detail.author_added_you": "{author} la deg til i denne samlinga", "collections.detail.curated_by_author": "Kuratert av {author}", "collections.detail.curated_by_you": "Kuratert av deg", "collections.detail.loading": "Lastar inn samling…", + "collections.detail.other_accounts_in_collection": "Andre i denne samlinga:", + "collections.detail.revoke_inclusion": "Fjern meg", + "collections.detail.sensitive_note": "Denne samlinga inneheld kontoar og innhald som kan vera ømtolige for nokre menneske.", "collections.detail.share": "Del denne samlinga", "collections.edit_details": "Rediger detaljar", "collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.", @@ -322,10 +385,14 @@ "collections.old_last_post_note": "Sist lagt ut for over ei veke sidan", "collections.remove_account": "Fjern denne kontoen", "collections.report_collection": "Rapporter denne samlinga", + "collections.revoke_collection_inclusion": "Fjern meg frå denne samlinga", + "collections.revoke_inclusion.confirmation": "Du er fjerna frå «{collection}»", + "collections.revoke_inclusion.error": "Noko gjekk gale, prøv att seinare.", "collections.search_accounts_label": "Søk etter kontoar å leggja til…", "collections.search_accounts_max_reached": "Du har nådd grensa for kor mange kontoar du kan leggja til", "collections.sensitive": "Ømtolig", "collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.", + "collections.topic_special_chars_hint": "Spesialteikn vil bli fjerna ved lagring", "collections.view_collection": "Sjå samlinga", "collections.view_other_collections_by_user": "Sjå andre samlingar frå denne personen", "collections.visibility_public": "Offentleg", @@ -445,6 +512,9 @@ "confirmations.remove_from_followers.confirm": "Fjern fylgjar", "confirmations.remove_from_followers.message": "{name} vil ikkje fylgja deg meir. Vil du halda fram?", "confirmations.remove_from_followers.title": "Fjern fylgjar?", + "confirmations.revoke_collection_inclusion.confirm": "Fjern meg", + "confirmations.revoke_collection_inclusion.message": "Denne handlinga er endeleg, og kuratoren kan ikkje leggja deg til samlinga på nytt seinare.", + "confirmations.revoke_collection_inclusion.title": "Vil du fjerna deg sjølv frå denne samlinga?", "confirmations.revoke_quote.confirm": "Fjern innlegget", "confirmations.revoke_quote.message": "Du kan ikkje angra denne handlinga.", "confirmations.revoke_quote.title": "Fjern innlegget?", @@ -767,6 +837,7 @@ "navigation_bar.automated_deletion": "Automatisk sletting av innlegg", "navigation_bar.blocks": "Blokkerte brukarar", "navigation_bar.bookmarks": "Bokmerke", + "navigation_bar.collections": "Samlingar", "navigation_bar.direct": "Private omtaler", "navigation_bar.domain_blocks": "Skjulte domene", "navigation_bar.favourites": "Favorittar", @@ -913,17 +984,17 @@ "notifications_permission_banner.how_to_control": "Aktiver skrivebordsvarsel for å få varsel når Mastodon ikkje er open. Du kan nøye bestemme kva samhandlingar som skal føre til skrivebordsvarsel gjennom {icon}-knappen ovanfor etter at varsel er aktivert.", "notifications_permission_banner.title": "Gå aldri glipp av noko", "onboarding.follows.back": "Tilbake", - "onboarding.follows.done": "Ferdig", "onboarding.follows.empty": "Me kan ikkje visa deg nokon resultat no. Du kan prøva å søkja eller bla gjennom utforsk-sida for å finna folk å fylgja, eller du kan prøva att seinare.", + "onboarding.follows.next": "Neste: Set opp profilen din", "onboarding.follows.search": "Søk", "onboarding.follows.title": "Fylg folk for å koma i gang", "onboarding.profile.discoverable": "Gjer profilen min synleg", "onboarding.profile.discoverable_hint": "Når du vel å gjera profilen din synleg på Mastodon, vil innlegga dine syna i søkjeresultat og populære innlegg, og profilen din kan bli føreslegen for folk med liknande interesser som deg.", "onboarding.profile.display_name": "Synleg namn", "onboarding.profile.display_name_hint": "Det fulle namnet eller kallenamnet ditt…", + "onboarding.profile.finish": "Fullfør", "onboarding.profile.note": "Om meg", "onboarding.profile.note_hint": "Du kan @nemna folk eller #emneknaggar…", - "onboarding.profile.save_and_continue": "Lagre og hald fram", "onboarding.profile.title": "Profiloppsett", "onboarding.profile.upload_avatar": "Last opp profilbilete", "onboarding.profile.upload_header": "Last opp profiltoppbilete", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index a6ab9f30e1614f..c1a1e33e8452e5 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -4,19 +4,26 @@ "about.default_locale": "Standard", "about.disclaimer": "Mastodon er gratis, åpen kildekode-programvare og et varemerke fra Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Årsak ikke tilgjengelig", - "about.domain_blocks.preamble": "Mastodon lar deg normalt sett se innholdet fra og samhandle med brukere fra enhver annen tjener i fødiverset. Dette er unntakene som har blitt lagt inn på denne tjeneren.", - "about.domain_blocks.silenced.explanation": "Du vil vanligvis ikke se profiler og innhold fra denne tjeneren, med mindre du eksplisitt søker dem opp eller velger å følge dem.", + "about.domain_blocks.preamble": "Mastodon lar deg normalt sett se innholdet fra og samhandle med brukere fra enhver annen server i fødiverset. Dette er unntakene som har blitt lagt inn på denne serveren.", + "about.domain_blocks.silenced.explanation": "Du vil vanligvis ikke se profiler og innhold fra denne serveren, med mindre du eksplisitt søker dem opp eller velger å følge dem.", "about.domain_blocks.silenced.title": "Begrenset", - "about.domain_blocks.suspended.explanation": "Ikke noe innhold fra denne tjeneren vil bli behandlet, lagret eller utvekslet. Det gjør det umulig å samhandle eller kommunisere med brukere fra denne tjeneren.", + "about.domain_blocks.suspended.explanation": "Ikke noe innhold fra denne serveren vil bli behandlet, lagret eller utvekslet. Det gjør det umulig å samhandle eller kommunisere med brukere fra denne serveren.", "about.domain_blocks.suspended.title": "Suspendert", "about.language_label": "Språk", - "about.not_available": "Denne informasjonen er ikke gjort tilgjengelig på denne tjeneren.", - "about.powered_by": "Desentraliserte sosiale medier drevet av {mastodon}", + "about.not_available": "Denne informasjonen er ikke gjort tilgjengelig på denne serveren.", + "about.powered_by": "Desentralisert sosialt medie drevet av {mastodon}", "about.rules": "Regler for serveren", "account.account_note_header": "Personlig notat", + "account.activity": "Aktivitet", + "account.add_note": "Legg til et personlig notat", "account.add_or_remove_from_list": "Legg til eller fjern fra lister", + "account.badges.admin": "Administrator", + "account.badges.blocked": "Blokkert", "account.badges.bot": "Automatisert", + "account.badges.domain_blocked": "Blokkert domene", "account.badges.group": "Gruppe", + "account.badges.muted": "Dempet", + "account.badges.muted_until": "Dempet til {until}", "account.block": "Blokker @{name}", "account.block_domain": "Blokker domenet {domain}", "account.block_short": "Blokker", @@ -27,7 +34,9 @@ "account.direct": "Nevn @{name} privat", "account.disable_notifications": "Slutt å varsle meg når @{name} legger ut innlegg", "account.domain_blocking": "Blokkerer domene", + "account.edit_note": "Rediger personlig notat", "account.edit_profile": "Rediger profil", + "account.edit_profile_short": "Rediger", "account.enable_notifications": "Varsle meg når @{name} legger ut innlegg", "account.endorse": "Vis frem på profilen", "account.familiar_followers_many": "Fulgt av {name1}, {name2}, og {othersCount, plural, one {en annen du kjenner} other {# andre du kjenner}}", @@ -35,11 +44,22 @@ "account.familiar_followers_two": "Fulgt av {name1} og {name2}", "account.featured": "Utvalgt", "account.featured.accounts": "Profiler", + "account.featured.collections": "Samlinger", "account.featured.hashtags": "Emneknagger", "account.featured_tags.last_status_at": "Siste innlegg {date}", "account.featured_tags.last_status_never": "Ingen Innlegg", + "account.field_overflow": "Vis fullt innhold", + "account.filters.all": "All aktivitet", + "account.filters.boosts_toggle": "Vis fremhevinger", + "account.filters.posts_boosts": "Innlegg og fremhevinger", + "account.filters.posts_only": "Innlegg", + "account.filters.posts_replies": "Innlegg og svar", + "account.filters.replies_toggle": "Vis svar", "account.follow": "Følg", "account.follow_back": "Følg tilbake", + "account.follow_back_short": "Følg tilbake", + "account.follow_request_cancel": "Avbryt forespørsel", + "account.follow_request_cancel_short": "Avbryt", "account.followers": "Følgere", "account.followers.empty": "Ingen følger denne brukeren ennå.", "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}", @@ -57,6 +77,24 @@ "account.locked_info": "Denne kontoens personvernstatus er satt til låst. Eieren vurderer manuelt hvem som kan følge dem.", "account.media": "Media", "account.mention": "Nevn @{name}", + "account.menu.add_to_list": "Legg til i liste…", + "account.menu.block": "Blokker kontor", + "account.menu.block_domain": "Blokker {domain}", + "account.menu.copied": "Kopierte kontolenke til utklippstavle", + "account.menu.copy": "Kopier lenke", + "account.menu.direct": "Nevn privat", + "account.menu.hide_reblogs": "Skjul fremhevinger i tidslinje", + "account.menu.mention": "Nevn", + "account.menu.mute": "Demp konto", + "account.menu.note.description": "Synlig bare for deg", + "account.menu.open_original_page": "Se på {domain}", + "account.menu.remove_follower": "Fjern følger", + "account.menu.report": "Rapporter konto", + "account.menu.share": "Del…", + "account.menu.show_reblogs": "Vis fremhevinger i tidslinje", + "account.menu.unblock": "Opphev blokkering av konto", + "account.menu.unblock_domain": "Opphev blokkering av {domain}", + "account.menu.unmute": "Opphev demping av konto", "account.moved_to": "{name} har angitt at deres nye konto nå er:", "account.mute": "Demp @{name}", "account.mute_notifications_short": "Demp varsler", @@ -64,7 +102,21 @@ "account.muted": "Dempet", "account.muting": "Demper", "account.mutual": "Dere følger hverandre", + "account.name.help.domain": "{domain} er serveren som inneholder profilen og innleggene for brukeren.", + "account.name.help.domain_self": "{domain} er serveren som inneholder profilen og innleggene dine.", + "account.name.help.header": "Et håndtak er som en e-postadresse", + "account.name.help.username": "{username} er denne kontoens brukernavn på deres server. Noen på en annen server kan ha det samme brukernavnet.", + "account.name.help.username_self": "{username} er ditt brukernavn på denne serveren. Noen på en annen server kan ha det samme brukernavnet.", + "account.name_info": "Hva betyr dette?", "account.no_bio": "Ingen beskrivelse oppgitt.", + "account.node_modal.callout": "Personlige notater er bare synlige for deg.", + "account.node_modal.edit_title": "Rediger personlig notat", + "account.node_modal.error_unknown": "Klarte ikke å lagre notatet", + "account.node_modal.field_label": "Personlig notat", + "account.node_modal.save": "Lagre", + "account.node_modal.title": "Legg til et personlig notat", + "account.note.edit_button": "Rediger", + "account.note.title": "Personlig notat (bare synlig for deg)", "account.open_original_page": "Gå til originalsiden", "account.posts": "Innlegg", "account.posts_with_replies": "Innlegg med svar", @@ -75,6 +127,8 @@ "account.share": "Del @{name} sin profil", "account.show_reblogs": "Vis fremhevinger fra @{name}", "account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}", + "account.timeline.pinned": "Festet", + "account.timeline.pinned.view_all": "Vis alle festede innlegg", "account.unblock": "Opphev blokkering av @{name}", "account.unblock_domain": "Opphev blokkering av {domain}", "account.unblock_domain_short": "Opphev blokkering", @@ -84,6 +138,19 @@ "account.unmute": "Opphev demping av @{name}", "account.unmute_notifications_short": "Opphev demping av varsler", "account.unmute_short": "Opphev demping", + "account_edit.bio.placeholder": "Legg til en kort introduksjon for å hjelpe andre med å identifisere deg.", + "account_edit.button.add": "Legg til {item}", + "account_edit.button.delete": "Slett {item}", + "account_edit.button.edit": "Rediger {item}", + "account_edit.column_button": "Ferdig", + "account_edit.column_title": "Rediger profil", + "account_edit.custom_fields.name": "felt", + "account_edit.custom_fields.reorder_button": "Omorganiser felter", + "account_edit.custom_fields.tip_content": "Du kan enkelt øke troverdighet til Mastodon-kontoen din ved å verifisere koblinger til nettsider du eier.", + "account_edit.custom_fields.tip_title": "Legg til bekreftede lenker", + "account_edit.custom_fields.title": "Egendefinerte felter", + "account_edit.custom_fields.verified_hint": "Hvordan legger jeg til en verifisert lenke?", + "account_edit.save": "Lagre", "account_note.placeholder": "Klikk for å legge til et notat", "admin.dashboard.daily_retention": "Andel brukere som er aktive, per dag etter registrering", "admin.dashboard.monthly_retention": "Andel brukere som er aktive, per måned etter registrering", @@ -107,6 +174,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv dette for folk med synsproblemer…", "alt_text_modal.done": "Ferdig", "announcement.announcement": "Kunngjøring", + "annual_report.announcement.action_build": "Lag min Wrapstodon", + "annual_report.announcement.action_dismiss": "Nei takk", + "annual_report.announcement.action_view": "Se Wrapstodon", + "annual_report.summary.close": "Lukk", + "annual_report.summary.copy_link": "Kopier lenke", "annual_report.summary.most_used_app.most_used_app": "mest brukte applikasjoner", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brukte evne knagg", "annual_report.summary.new_posts.new_posts": "nye innlegg", @@ -137,11 +209,20 @@ "bundle_modal_error.close": "Lukk", "bundle_modal_error.message": "Noe gikk galt mens denne komponenten ble lastet inn.", "bundle_modal_error.retry": "Prøv igjen", + "carousel.current": "Side {current, number} / {max, number}", + "carousel.slide": "Side {current, number} av {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} anbefalte tegn", + "character_counter.required": "{currentLength}/{maxLength} tegn", "closed_registrations.other_server_instructions": "Siden Mastodon er desentralizert, kan du opprette en konto på en annen server og fortsatt kommunisere med denne.", "closed_registrations_modal.description": "Opprettelse av en konto på {domain} er for tiden ikke mulig, men vær oppmerksom på at du ikke trenger en konto spesifikt på {domain} for å kunne bruke Mastodon.", "closed_registrations_modal.find_another_server": "Finn en annen server", "closed_registrations_modal.preamble": "Mastodon er desentralisert, så uansett hvor du oppretter kontoen din, vil du kunne følge og samhandle med alle på denne serveren. Du kan til og med kjøre serveren selv!", "closed_registrations_modal.title": "Registrerer deg på Mastodon", + "collection.share_modal.share_link_label": "Invitasjonslenke for deling", + "collection.share_modal.share_via_post": "Publiser på Mastodon", + "collection.share_modal.share_via_system": "Del med…", + "collection.share_modal.title": "Del samling", + "collection.share_modal.title_new": "Del din nye samling!", "column.about": "Om", "column.blocks": "Blokkerte brukere", "column.bookmarks": "Bokmerker", @@ -206,6 +287,7 @@ "confirmations.delete_list.title": "Slett liste?", "confirmations.discard_edit_media.confirm": "Forkast", "confirmations.discard_edit_media.message": "Du har ulagrede endringer i mediebeskrivelsen eller i forhåndsvisning, forkast dem likevel?", + "confirmations.follow_to_collection.title": "Følg konto?", "confirmations.follow_to_list.confirm": "Følg og legg til i liste", "confirmations.follow_to_list.message": "Du må følge {name} for å kunne legge vedkommende til i en liste.", "confirmations.follow_to_list.title": "Følg bruker?", @@ -217,13 +299,29 @@ "confirmations.missing_alt_text.secondary": "Legg ut likevel", "confirmations.missing_alt_text.title": "Legg til bildebeskrivelse?", "confirmations.mute.confirm": "Demp", + "confirmations.private_quote_notify.cancel": "Tilbake til redigering", + "confirmations.private_quote_notify.confirm": "Publiser innlegg", + "confirmations.private_quote_notify.do_not_show_again": "Ikke vis meg denne meldingen igjen", + "confirmations.private_quote_notify.message": "Personen du siterer og andre som er nevnt vil bli varslet og vil kunne se innlegget ditt, selv om de ikke følger deg.", + "confirmations.private_quote_notify.title": "Del med følgere og nevnte brukere?", + "confirmations.quiet_post_quote_info.got_it": "Forstått", "confirmations.redraft.confirm": "Slett og skriv på nytt", "confirmations.redraft.message": "Er du sikker på at du vil slette dette innlegget og lagre det på nytt? Favoritter og fremhevinger vil gå tapt, og svar til det originale innlegget vil bli foreldreløse.", "confirmations.redraft.title": "Slett og skriv på nytt?", "confirmations.remove_from_followers.confirm": "Fjern følger", "confirmations.remove_from_followers.message": "{name} vil ikke lenger følge deg. Er du sikker på at du vil fortsette?", "confirmations.remove_from_followers.title": "Fjern følger?", + "confirmations.revoke_collection_inclusion.confirm": "Fjern meg", + "confirmations.revoke_collection_inclusion.title": "Fjern deg selv fra denne samlingen?", + "confirmations.revoke_quote.confirm": "Fjern innlegg", + "confirmations.revoke_quote.message": "Denne handlingen kan ikke angres.", + "confirmations.revoke_quote.title": "Fjern innlegg?", + "confirmations.unblock.confirm": "Opphev blokkering", + "confirmations.unblock.title": "Opphev blokkering av {name}?", "confirmations.unfollow.confirm": "Slutt å følge", + "confirmations.unfollow.title": "Slutt å følge {name}?", + "confirmations.withdraw_request.confirm": "Trekk tilbake forespørsel", + "confirmations.withdraw_request.title": "Trekk tilbake forespørsel om å følge {name}?", "content_warning.hide": "Skjul innlegg", "content_warning.show": "Vis likevel", "content_warning.show_more": "Vis mer", @@ -643,7 +741,6 @@ "notifications_permission_banner.how_to_control": "For å motta varsler når Mastodon ikke er åpne, aktiver desktop varsler. Du kan kontrollere nøyaktig hvilke typer interaksjoner genererer skrivebordsvarsler gjennom {icon} -knappen ovenfor når de er aktivert.", "notifications_permission_banner.title": "Aldri gå glipp av noe", "onboarding.follows.back": "Tilbake", - "onboarding.follows.done": "Ferdig", "onboarding.follows.empty": "Dessverre kan ingen resultater vises akkurat nå. Du kan prøve å bruke søk eller bla gjennom utforske-siden for å finne folk å følge, eller prøve igjen senere.", "onboarding.follows.search": "Søk", "onboarding.follows.title": "Følg folk for å komme i gang", @@ -652,7 +749,6 @@ "onboarding.profile.display_name_hint": "Ditt fulle navn eller ditt morsomme navn…", "onboarding.profile.note": "Om meg", "onboarding.profile.note_hint": "Du kan @nevne andre eller #emneknagger…", - "onboarding.profile.save_and_continue": "Lagre og fortsett", "onboarding.profile.title": "Konfigurering av profil", "onboarding.profile.upload_avatar": "Last opp profilbilde", "onboarding.profile.upload_header": "Last opp profiltoppbilde", @@ -742,7 +838,9 @@ "report_notification.categories.spam": "Søppelpost", "report_notification.categories.spam_sentence": "spam", "report_notification.categories.violation": "Regelbrudd", + "report_notification.categories.violation_sentence": "regel brudd", "report_notification.open": "Åpne rapport", + "search.clear": "Tøm søk", "search.no_recent_searches": "Ingen søk nylig", "search.placeholder": "Søk", "search.quick_action.account_search": "Profiler som samsvarer med {x}", @@ -763,6 +861,7 @@ "search_results.all": "Alle", "search_results.hashtags": "Emneknagger", "search_results.no_results": "Ingen resultater.", + "search_results.no_search_yet": "Søk etter innlegg, profiler eller hashtags.", "search_results.see_all": "Se alle", "search_results.statuses": "Innlegg", "search_results.title": "Søk etter \"{q}\"", @@ -778,13 +877,23 @@ "status.admin_account": "Åpne moderatorgrensesnittet for @{name}", "status.admin_domain": "Åpne moderatorgrensesnittet for {domain}", "status.admin_status": "Åpne dette innlegget i moderatorgrensesnittet", + "status.all_disabled": "Fremheving og sitering er deaktivert", "status.block": "Blokker @{name}", "status.bookmark": "Bokmerke", "status.cancel_reblog_private": "Fjern fremheving", + "status.cannot_quote": "Du har ikke tilgang til å sitere dette innlegget", "status.cannot_reblog": "Denne posten kan ikke fremheves", + "status.contains_quote": "Inneholder sitat", + "status.context.loading": "Laster inn flere svar", + "status.context.loading_error": "Klarte ikke å laste inn nye svar", + "status.context.loading_success": "Nye svar er lastet inn", + "status.context.more_replies_found": "Flere svar funnet", + "status.context.retry": "Prøv igjen", + "status.context.show": "Vis", "status.continued_thread": "Fortsettelse av samtale", "status.copy": "Kopier lenken til innlegget", "status.delete": "Slett", + "status.delete.success": "Innlegget er slettet", "status.detailed_status": "Detaljert samtalevisning", "status.direct": "Nevn @{name} privat", "status.direct_indicator": "Privat omtale", @@ -805,17 +914,29 @@ "status.mute_conversation": "Demp samtale", "status.open": "Utvid dette innlegget", "status.pin": "Fest på profilen", + "status.quote_error.blocked_account_hint.title": "Dette innlegget er skjult fordi du har blokkert @{name}.", + "status.quote_error.blocked_domain_hint.title": "Dette innlegget er skjult fordi du har blokkert {domain}.", "status.quote_error.filtered": "Skjult på grunn av et av filterne dine", + "status.quote_error.limited_account_hint.action": "Vis likevel", + "status.quote_error.limited_account_hint.title": "Denne kontoen har blitt skjult av moderatorene til {domain}.", + "status.quote_error.muted_account_hint.title": "Dette innlegget er skjult fordi du har dempet @{name}.", + "status.quote_error.not_available": "Innlegg utilgjengelig", + "status.quote_error.pending_approval": "Ventende innlegg", "status.read_more": "Les mer", "status.reblog": "Fremhev", "status.reblogged_by": "Fremhevet av {name}", "status.reblogs.empty": "Ingen har fremhevet dette innlegget enda. Når noen gjør det, vil de dukke opp her.", "status.redraft": "Slett og skriv på nytt", "status.remove_bookmark": "Fjern bokmerke", + "status.remove_favourite": "Fjern fra favoritter", + "status.remove_quote": "Fjern", + "status.replied_in_thread": "Svarte i tråd", "status.replied_to": "Som svar til {name}", "status.reply": "Svar", "status.replyAll": "Svar til samtale", "status.report": "Rapporter @{name}", + "status.request_quote": "Send forespørsel om å sitere", + "status.revoke_quote": "Fjern innlegget mitt fra @{name}' innlegg", "status.sensitive_warning": "Følsomt innhold", "status.share": "Del", "status.show_less_all": "Vis mindre for alle", @@ -831,8 +952,13 @@ "subscribed_languages.save": "Lagre endringer", "subscribed_languages.target": "Endre abonnerte språk for {target}", "tabs_bar.home": "Hjem", + "tabs_bar.menu": "Meny", "tabs_bar.notifications": "Varslinger", + "tabs_bar.publish": "Nytt Innlegg", + "tabs_bar.search": "Søk", + "tag.remove": "Fjern", "terms_of_service.title": "Bruksvilkår", + "terms_of_service.upcoming_changes_on": "Kommende endringer på {date}", "time_remaining.days": "{number, plural,one {# dag} other {# dager}} igjen", "time_remaining.hours": "{number, plural, one {# time} other {# timer}} igjen", "time_remaining.minutes": "{number, plural, one {# minutt} other {# minutter}} igjen", @@ -848,6 +974,8 @@ "upload_button.label": "Legg til media", "upload_error.limit": "Filopplastingsgrensen er oversteget.", "upload_error.poll": "Filopplasting er ikke tillatt for avstemninger.", + "upload_error.quote": "Filopplasting er ikke tillatt med sitat.", + "upload_form.drag_and_drop.instructions": "For å plukke opp et medievedlegg, trykk på mellomrom eller enter. Bruk piltastene for å flytte medievedlegget i ønsket retning. Trykk mellomrom eller enter igjen for å slippe vedlegget på nytt sted, eller trykk på esc for å avbryte.", "upload_form.edit": "Rediger", "upload_progress.label": "Laster opp...", "upload_progress.processing": "Behandler…", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 9abbc0324d34a7..a00a2ab6c11404 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -11,6 +11,8 @@ "about.powered_by": "Malhum social descentralizat propulsat per {mastodon}", "about.rules": "Règlas del servidor", "account.account_note_header": "Nòta personala", + "account.activity": "Activitat", + "account.add_note": "Apondre una nòta personala", "account.add_or_remove_from_list": "Ajustar o tirar de las listas", "account.badges.bot": "Robòt", "account.badges.group": "Grop", @@ -51,6 +53,12 @@ "account.locked_info": "L’estatut de privacitat del compte es configurat sus clavat. Lo proprietari causís qual pòt sègre son compte.", "account.media": "Mèdias", "account.mention": "Mencionar @{name}", + "account.menu.mention": "Mencionar", + "account.menu.mute": "Silenciar lo compte", + "account.menu.note.description": "Sonque visibla per vos", + "account.menu.remove_follower": "Tirar lo seguidor", + "account.menu.report": "Senhalar lo compte", + "account.menu.share": "Partejar…", "account.moved_to": "{name} indiquèt que son nòu compte es ara :", "account.mute": "Rescondre @{name}", "account.mute_notifications_short": "Amudir las notificacions", @@ -58,6 +66,10 @@ "account.muted": "Mes en silenci", "account.mutual": "Vos seguissètz", "account.no_bio": "Cap de descripcion pas fornida.", + "account.node_modal.field_label": "Nòta personala", + "account.node_modal.save": "Enregistrar", + "account.node_modal.title": "Apondre una nòta personala", + "account.note.edit_button": "Modificar", "account.open_original_page": "Dobrir la pagina d’origina", "account.posts": "Tuts", "account.posts_with_replies": "Tuts e responsas", @@ -67,6 +79,7 @@ "account.requests_to_follow_you": "Demanda a vos sègre", "account.share": "Partejar lo perfil a @{name}", "account.show_reblogs": "Mostrar los partatges de @{name}", + "account.timeline.pinned": "Penjat", "account.unblock": "Desblocar @{name}", "account.unblock_domain": "Desblocar {domain}", "account.unblock_domain_short": "Desblocar", @@ -76,6 +89,10 @@ "account.unmute": "Quitar de rescondre @{name}", "account.unmute_notifications_short": "Restablir las notificacions", "account.unmute_short": "Tornar afichar", + "account_edit.column_title": "Modificar lo perfil", + "account_edit.custom_fields.name": "camp", + "account_edit.field_delete_modal.delete_button": "Suprimir", + "account_edit.field_edit_modal.value_label": "Valor", "account_note.placeholder": "Clicar per ajustar una nòta", "admin.dashboard.retention.average": "Mejana", "admin.dashboard.retention.cohort": "Mes d’inscripcion", @@ -91,6 +108,8 @@ "alt_text_modal.change_thumbnail": "Cambiar de miniatura", "alt_text_modal.done": "Acabat", "announcement.announcement": "Anóncia", + "annual_report.summary.close": "Tampar", + "annual_report.summary.copy_link": "Copiar lo ligam", "attachments_list.unprocessed": "(pas tractat)", "audio.hide": "Amagar àudio", "block_modal.show_less": "Ne veire mens", @@ -106,6 +125,9 @@ "bundle_modal_error.retry": "Tornar ensajar", "closed_registrations_modal.find_another_server": "Trobar un autre servidor", "closed_registrations_modal.title": "S’inscriure a Mastodon", + "collections.visibility_public": "Publica", + "collections.visibility_title": "Visibilitat", + "collections.visibility_unlisted": "Non listada", "column.about": "A prepaus", "column.blocks": "Personas blocadas", "column.bookmarks": "Marcadors", @@ -387,7 +409,7 @@ "notification.reblog": "{name} a partejat vòstre estatut", "notification.relationships_severance_event.learn_more": "Ne saber mai", "notification.status": "{name} ven de publicar", - "notification.update": "{name} modiquè sa publicacion", + "notification.update": "{name} modifiquèt sa publicacion", "notifications.clear": "Escafar", "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?", "notifications.column_settings.admin.report": "Senhalaments novèls :", @@ -440,7 +462,9 @@ "privacy.direct.short": "Mencion privada", "privacy.private.long": "Mostrar pas qu’als seguidors", "privacy.private.short": "Seguidors", + "privacy.public.long": "Tot lo monde de e defòra de Mastodon", "privacy.public.short": "Public", + "privacy.unlisted.long": "Rescondut dels resultats de recèrca de Mastodon, las tendéncias e las cronologias publicas", "privacy.unlisted.short": "Public silenciós", "privacy_policy.last_updated": "Darrièra actualizacion {date}", "privacy_policy.title": "Politica de confidencialitat", @@ -593,5 +617,14 @@ "video.fullscreen": "Ecran complèt", "video.hide": "Amagar la vidèo", "video.pause": "Pausa", - "video.play": "Lectura" + "video.play": "Lectura", + "visibility_modal.button_title": "Definir la visibilitat", + "visibility_modal.header": "Visibilitat e interaccion", + "visibility_modal.instructions": "Contrarotlatz qui pòt interagir amb aquesta publicacion. Podètz tanben aplicar de paramètres a totas las publicacions futuras en anant a Preferéncias > Paramètres per defaut de publicacion.", + "visibility_modal.privacy_label": "Visibilitat", + "visibility_modal.quote_followers": "Sonque pels seguidors", + "visibility_modal.quote_label": "Qual pòt citar", + "visibility_modal.quote_nobody": "Sonque ieu", + "visibility_modal.quote_public": "Tot lo monde", + "visibility_modal.save": "Enregistrar" } diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 11c7022100343c..68dd9d6f68421e 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -61,7 +61,6 @@ "account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ", "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ", "account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ", - "account.joined_long": "{date} ਨੂੰ ਜੁਆਇਨ ਕੀਤਾ", "account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ", "account.media": "ਮੀਡੀਆ", "account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ", @@ -552,11 +551,9 @@ "notifications.policy.filter_not_following_hint": "ਜਦ ਤੱਕ ਤੁਸੀਂ ਉਹਨਾਂ ਨੂੰ ਖੁਦ ਮਨਜ਼ੂਰੀ ਨਹੀਂ ਦਿੰਦੇ", "notifications_permission_banner.enable": "ਡੈਸਕਟਾਪ ਸੂਚਨਾਵਾਂ ਸਮਰੱਥ ਕਰੋ", "onboarding.follows.back": "ਪਿੱਛੇ", - "onboarding.follows.done": "ਮੁਕੰਮਲ", "onboarding.follows.search": "ਖੋਜੋ", "onboarding.profile.display_name": "ਦਿਖਾਇਆ ਜਾਣ ਵਾਲਾ ਨਾਂ", "onboarding.profile.note": "ਜਾਣਕਾਰੀ", - "onboarding.profile.save_and_continue": "ਸੰਭਾਲੋ ਅਤੇ ਜਾਰੀ ਰੱਖੋ", "onboarding.profile.title": "ਪਰੋਫਾਈਲ ਸੈਟਅੱਪ", "onboarding.profile.upload_avatar": "ਪਰੋਫਾਈਲ ਤਸਵੀਰ ਅੱਪਲੋਡ ਕਰੋ", "poll.closed": "ਬੰਦ ਹੈ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 0ef974c4b2b2e9..88b21f47caecf7 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -57,7 +57,6 @@ "account.go_to_profile": "Przejdź do profilu", "account.hide_reblogs": "Ukryj podbicia od @{name}", "account.in_memoriam": "Ku pamięci.", - "account.joined_long": "Dołączył(a) dnia {date}", "account.joined_short": "Dołączył(a)", "account.languages": "Zmień subskrybowane języki", "account.link_verified_on": "Własność tego odnośnika została potwierdzona {date}", @@ -733,7 +732,6 @@ "notifications_permission_banner.how_to_control": "Aby otrzymywać powiadomienia, gdy Mastodon nie jest otwarty, włącz powiadomienia na pulpicie. Możesz wybrać, które dokładnie typy interakcji generują powiadomienia na pulpicie za pomocą przycisku {icon} powyżej po ich włączeniu.", "notifications_permission_banner.title": "Nigdy niczego nie przegapisz", "onboarding.follows.back": "Wróć", - "onboarding.follows.done": "Gotowe", "onboarding.follows.empty": "Niestety, w tej chwili nie można nic wyświetlić. Możesz użyć wyszukiwania lub przeglądać stronę główną, aby znaleźć osoby, które chcesz obserwować, albo spróbuj ponownie później.", "onboarding.follows.search": "Szukaj", "onboarding.follows.title": "Zaobserwuj kogoś, aby zacząć", @@ -743,7 +741,6 @@ "onboarding.profile.display_name_hint": "Twoje imię lub pseudonim…", "onboarding.profile.note": "Opis", "onboarding.profile.note_hint": "Możesz @wzmiankować innych lub dodawać #hashtagi…", - "onboarding.profile.save_and_continue": "Zapisz i kontynuuj", "onboarding.profile.title": "Ustawienia profilu", "onboarding.profile.upload_avatar": "Dodaj zdjęcie profilowe", "onboarding.profile.upload_header": "Dodaj baner", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index ded303958f1eba..03e356313ecfbd 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "Entrou em {date}", "account.joined_short": "Entrou", "account.languages": "Mudar idiomas inscritos", "account.link_verified_on": "A propriedade deste link foi verificada em {date}", @@ -151,17 +150,39 @@ "account_edit.button.edit": "Editar {item}", "account_edit.column_button": "Feito", "account_edit.column_title": "Editar perfil", + "account_edit.custom_fields.name": "Campo", "account_edit.custom_fields.placeholder": "Insira seus pronomes, links externos ou qualquer coisa que queira compartilhar.", + "account_edit.custom_fields.reorder_button": "Reordenar campos", + "account_edit.custom_fields.tip_content": "Você pode facilmente dar credibilidade à sua conta Mastodon verificando os links para os seus sites.", + "account_edit.custom_fields.tip_title": "Dica: Adicionando links verificados", "account_edit.custom_fields.title": "Campos personalizados", + "account_edit.custom_fields.verified_hint": "Como adiciono um link verificado?", "account_edit.display_name.placeholder": "Seu nome de exibição é a forma com que seu nome aparece em seu perfil e em suas linhas do tempo.", "account_edit.display_name.title": "Nome de exibição", "account_edit.featured_hashtags.item": "hashtags", "account_edit.featured_hashtags.placeholder": "Ajude outros a identificar e ter acesso rápido a seus tópicos favoritos.", "account_edit.featured_hashtags.title": "Hashtags em destaque", + "account_edit.field_delete_modal.confirm": "Tem certeza que deseja excluir este campo personalizado? Esta ação não pode ser desfeita.", + "account_edit.field_delete_modal.delete_button": "Excluir", + "account_edit.field_delete_modal.title": "Excluir campo personalizado?", + "account_edit.field_edit_modal.add_title": "Adicionar campo personalizado", + "account_edit.field_edit_modal.edit_title": "Editar campo personalizado", + "account_edit.field_edit_modal.limit_header": "Limite recomendado de caracteres excedido", + "account_edit.field_edit_modal.limit_message": "Usuários de dispositivos móveis podem não ver seu campo completo.", + "account_edit.field_edit_modal.link_emoji_warning": "Recomendamos não utilizar emojis personalizados combinados com URLs. Campos personalizados contendo ambos serão exibidos apenas como texto em vez de link, para evitar confusão dos usuários.", + "account_edit.field_edit_modal.name_hint": "Ex. \"Site pessoal\"", + "account_edit.field_edit_modal.name_label": "Descrição", + "account_edit.field_edit_modal.url_warning": "Para adicionar um link, por favor inclia {protocol} no início.", + "account_edit.field_edit_modal.value_hint": "Ex.: \"https://example.me\"", + "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "O arrasto foi cancelado. O campo \"{item}\" foi descartado.", + "account_edit.field_reorder_modal.drag_end": "O campo \"{item}\" foi descartado.", "account_edit.name_modal.add_title": "Inserir nome de exibição", "account_edit.name_modal.edit_title": "Editar nome de exibição", "account_edit.profile_tab.button_label": "Personalizar", + "account_edit.profile_tab.hint.description": "Essas configurações definem o que os usuários veem no {servidor} nos apps oficiais, mas podem não se aplicar a usuários em servidores e apps de terceiros.", "account_edit.profile_tab.hint.title": "Exibições divergem", + "account_edit.profile_tab.show_featured.description": "'Em Destaque' é uma aba opcional onde você pode exibir outras contas.", "account_edit.profile_tab.show_featured.title": "Mostrar aba \"Destaque\"", "account_edit.profile_tab.show_media.description": "\"Mídia\" é uma aba opcional que mostra seus posts, contendo imagens ou vídeos.", "account_edit.profile_tab.show_media.title": "Mostrar aba \"Mídia\"", @@ -170,6 +191,13 @@ "account_edit.profile_tab.subtitle": "Personalizar as abas em seu perfil e o que elas exibem.", "account_edit.profile_tab.title": "Configurações da aba de perfil", "account_edit.save": "Salvar", + "account_edit.verified_modal.details": "Dê credibilidade ao seu perfil do Mastodon, verificando links para sites pessoais. Veja como funciona:", + "account_edit.verified_modal.invisible_link.summary": "Como posso tornar o link invisível?", + "account_edit.verified_modal.step1.header": "Copie o código HTML abaixo e cole no cabeçalho do seu site", + "account_edit.verified_modal.step2.details": "Se já adicionou seu site como um campo personalizado, deverá excluí-lo e adicioná-lo novamente para acionar a verificação.", + "account_edit.verified_modal.step2.header": "Adicione seu site como um campo personalizado", + "account_edit.verified_modal.title": "Como adicionar um link verificado", + "account_edit_tags.add_tag": "Adicionar #{tagName}", "account_edit_tags.column_title": "Editar hashtags em destaque", "account_edit_tags.help_text": "Hashtags em destaque ajudam os usuários a descobrir e interagir com seu perfil. Elas aparecem como filtros na visualização de Atividade da sua página de Perfil.", "account_edit_tags.search_placeholder": "Insira uma hashtag…", @@ -272,6 +300,8 @@ "callout.dismiss": "Rejeitar", "carousel.current": "Slide {current, number} / {max, number}", "carousel.slide": "Slide {current, number} de {max, number}", + "character_counter.recommended": "{currentLength}/{maxLength} caracteres sugeridos", + "character_counter.required": "{currentLength}/{maxLength} caracteres", "closed_registrations.other_server_instructions": "Como o Mastodon é descentralizado, você pode criar uma conta em outro servidor e ainda pode interagir com este.", "closed_registrations_modal.description": "Não é possível criar uma conta em {domain} no momento, mas atente que você não precisa de uma conta especificamente em {domain} para usar o Mastodon.", "closed_registrations_modal.find_another_server": "Encontrar outro servidor", @@ -301,10 +331,15 @@ "collections.create_collection": "Criar coleção", "collections.delete_collection": "Eliminar coleção", "collections.description_length_hint": "Limite de 100 caracteres", + "collections.detail.accept_inclusion": "OK", "collections.detail.accounts_heading": "Contas", + "collections.detail.author_added_you": "{author} adicionou você a esta coleção", "collections.detail.curated_by_author": "Curadoria de {author}", "collections.detail.curated_by_you": "Curadoria por você", "collections.detail.loading": "Carregando coleção…", + "collections.detail.other_accounts_in_collection": "Outros nesta coleção:", + "collections.detail.revoke_inclusion": "Remover-me", + "collections.detail.sensitive_note": "Esta coleção contém contas e conteúdo que podem ser sensíveis a alguns usuários.", "collections.detail.share": "Compartilhar esta coleção", "collections.edit_details": "Editar detalhes", "collections.error_loading_collections": "Houve um erro ao tentar carregar suas coleções.", @@ -319,6 +354,9 @@ "collections.old_last_post_note": "Publicado pela última vez semana passada", "collections.remove_account": "Remover esta conta", "collections.report_collection": "Denunciar esta coleção", + "collections.revoke_collection_inclusion": "Remover-me desta coleção", + "collections.revoke_inclusion.confirmation": "Você foi removido de \"{collection}\"", + "collections.revoke_inclusion.error": "Houve um erro, por favor tente novamente mais tarde.", "collections.search_accounts_label": "Buscar contas para adicionar…", "collections.search_accounts_max_reached": "Você acrescentou o numero máximo de contas", "collections.sensitive": "Sensível", @@ -442,6 +480,9 @@ "confirmations.remove_from_followers.confirm": "Remover seguidor", "confirmations.remove_from_followers.message": "{name} vai parar de te seguir. Tem certeza de que deseja continuar?", "confirmations.remove_from_followers.title": "Remover seguidor?", + "confirmations.revoke_collection_inclusion.confirm": "Remover-me", + "confirmations.revoke_collection_inclusion.message": "Esta ação é permanente e o curador não poderá adicionar-lhe de volta à coleção mais tarde.", + "confirmations.revoke_collection_inclusion.title": "Remover-se desta coleção?", "confirmations.revoke_quote.confirm": "Remover publicação", "confirmations.revoke_quote.message": "Esta ação não pode ser desfeita.", "confirmations.revoke_quote.title": "Remover publicação?", @@ -677,6 +718,7 @@ "keyboard_shortcuts.direct": "Abrir coluna de menções privadas", "keyboard_shortcuts.down": "mover para baixo", "keyboard_shortcuts.enter": "Abrir publicação", + "keyboard_shortcuts.explore": "Abrir linha do tempo atual", "keyboard_shortcuts.favourite": "Favoritar publicação", "keyboard_shortcuts.favourites": "Abrir lista de favoritos", "keyboard_shortcuts.federated": "abrir linha global", @@ -763,6 +805,7 @@ "navigation_bar.automated_deletion": "Publicação de eliminação automático", "navigation_bar.blocks": "Usuários bloqueados", "navigation_bar.bookmarks": "Salvos", + "navigation_bar.collections": "Coleções", "navigation_bar.direct": "Menções privadas", "navigation_bar.domain_blocks": "Domínios bloqueados", "navigation_bar.favourites": "Favoritos", @@ -909,7 +952,6 @@ "notifications_permission_banner.how_to_control": "Para receber notificações quando o Mastodon não estiver aberto, ative as notificações no computador. Você pode controlar precisamente quais tipos de interações geram notificações no computador através do botão {icon}.", "notifications_permission_banner.title": "Nunca perca nada", "onboarding.follows.back": "Voltar", - "onboarding.follows.done": "Feito", "onboarding.follows.empty": "Infelizmente, não é possível mostrar resultados agora. Você pode tentar usar a busca ou navegar na página de exploração para encontrar pessoas para seguir, ou tentar novamente mais tarde.", "onboarding.follows.search": "Buscar", "onboarding.follows.title": "Comece seguindo pessoas", @@ -919,7 +961,6 @@ "onboarding.profile.display_name_hint": "Seu nome completo ou apelido…", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Você pode @mencionar outras pessoas ou usar #hashtags…", - "onboarding.profile.save_and_continue": "Salvar e continuar", "onboarding.profile.title": "Configuração do perfil", "onboarding.profile.upload_avatar": "Enviar imagem de perfil", "onboarding.profile.upload_header": "Carregar cabeçalho do perfil", @@ -1066,6 +1107,8 @@ "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está acontecendo.", "sign_in_banner.sign_in": "Entrar", "sign_in_banner.sso_redirect": "Entrar ou Registrar-se", + "skip_links.skip_to_content": "Conteúdo principal", + "skip_links.skip_to_navigation": "Navegação principal", "status.admin_account": "Abrir interface de moderação para @{name}", "status.admin_domain": "Abrir interface de moderação para {domain}", "status.admin_status": "Abrir esta publicação na interface de moderação", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 9ee10e95d49694..d51565bb25e9fe 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -2,12 +2,12 @@ "about.blocks": "Servidores moderados", "about.contact": "Contacto:", "about.default_locale": "Padrão", - "about.disclaimer": "O Mastodon é um ‘software’ livre, de código aberto e uma marca registada de Mastodon gGmbH.", + "about.disclaimer": "O Mastodon é um 'software' livre, de código aberto e marca registada de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Motivo não disponível", - "about.domain_blocks.preamble": "O Mastodon ver e interagir com o conteúdo de utilizadores de qualquer outra instância no fediverso. Estas são as exceções desta instância em específico.", - "about.domain_blocks.silenced.explanation": "Normalmente não verás perfis e conteúdos deste servidor, a não ser que os procures explicitamente ou optes por segui-los.", + "about.domain_blocks.preamble": "O Mastodon, geralmente, permite-lhe ver conteúdo e interagir com utilizadores de qualquer outro servidor na fediverso. Estas são as exceções aplicadas neste servidor em particular.", + "about.domain_blocks.silenced.explanation": "Normalmente não verá perfis e conteúdos deste servidor, a não ser que os procures explicitamente ou opte por segui-los.", "about.domain_blocks.silenced.title": "Limitados", - "about.domain_blocks.suspended.explanation": "Nenhum dado deste servidor será processado, armazenado ou trocado, tornando impossível qualquer interação ou comunicação com os utilizadores a partir deste servidor.", + "about.domain_blocks.suspended.explanation": "Nenhum dado deste servidor será processado, armazenado ou trocado, impossibilitando qualquer interação ou comunicação com os utilizadores a partir deste servidor.", "about.domain_blocks.suspended.title": "Suspensos", "about.language_label": "Idioma", "about.not_available": "Esta informação não foi disponibilizada neste servidor.", @@ -73,7 +73,6 @@ "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}", @@ -132,7 +131,7 @@ "account.show_reblogs": "Mostrar partilhas de @{name}", "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}", "account.timeline.pinned": "Fixado", - "account.timeline.pinned.view_all": "Ver todos as publicações fixadas", + "account.timeline.pinned.view_all": "Ver todas as publicações fixadas", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear o domínio {domain}", "account.unblock_domain_short": "Desbloquear", @@ -142,7 +141,7 @@ "account.unmute": "Desocultar @{name}", "account.unmute_notifications_short": "Desocultar notificações", "account.unmute_short": "Desocultar", - "account_edit.bio.placeholder": "Adicione uma breve introdução para ajudar à sua identificação por outros.", + "account_edit.bio.placeholder": "Adicione uma breve apresentação para ajudar os outros a identificá-lo.", "account_edit.bio.title": "Bio", "account_edit.bio_modal.add_title": "Adicionar biografia", "account_edit.bio_modal.edit_title": "Editar biografia", @@ -151,19 +150,79 @@ "account_edit.button.edit": "Editar {item}", "account_edit.column_button": "Concluído", "account_edit.column_title": "Editar Perfil", + "account_edit.custom_fields.name": "campo", "account_edit.custom_fields.placeholder": "Adicione os seus pronomes, hiperligações externas ou qualquer outra coisa que queira partilhar.", + "account_edit.custom_fields.reorder_button": "Reordenar campos", + "account_edit.custom_fields.tip_content": "Pode adicionar facilmente credibilidade à sua conta Mastodon, verificando ligações para qualquer website que possua.", + "account_edit.custom_fields.tip_title": "Dica: Adicionando links verificados", "account_edit.custom_fields.title": "Campos personalizados", - "account_edit.display_name.placeholder": "Como o seu nome vai aparecer no seu perfil e nas linhas do tempo.", + "account_edit.custom_fields.verified_hint": "Como adiciono um link verificado?", + "account_edit.display_name.placeholder": "O seu nome de exibição é como o seu nome aparece no seu perfil e nas linhas do tempo.", "account_edit.display_name.title": "Nome a mostrar", "account_edit.featured_hashtags.item": "etiquetas", "account_edit.featured_hashtags.placeholder": "Ajude à sua identificação por outros e tenha acesso rápido aos seus tópicos favoritos.", "account_edit.featured_hashtags.title": "Etiquetas em destaque", + "account_edit.field_delete_modal.confirm": "Tem certeza de que deseja excluir este campo personalizado? Esta ação não pode ser desfeita.", + "account_edit.field_delete_modal.delete_button": "Excluir", + "account_edit.field_delete_modal.title": "Excluir campo personalizado?", + "account_edit.field_edit_modal.add_title": "Adicionar campo personalizado", + "account_edit.field_edit_modal.edit_title": "Editar campo personalizado", + "account_edit.field_edit_modal.limit_header": "Limite de caracteres recomendado excedido", + "account_edit.field_edit_modal.limit_message": "Os utilizadores de dispositivos móveis podem não conseguir ver o seu campo na totalidade.", + "account_edit.field_edit_modal.link_emoji_warning": "Não recomendamos o uso de emojis personalizados em combinação com URLs. Campos personalizados que contenham ambos serão exibidos apenas como texto, em vez de como hiperligação, para evitar confusão aos utilizadores.", + "account_edit.field_edit_modal.name_hint": "Ex.: \"Site pessoal\"", + "account_edit.field_edit_modal.name_label": "Rótulo", + "account_edit.field_edit_modal.value_hint": "Ex.: \"https://exemplo.eu\"", + "account_edit.field_edit_modal.value_label": "Valor", + "account_edit.field_reorder_modal.drag_cancel": "O arrastamento foi cancelado. O campo \"{item}\" foi largado.", + "account_edit.field_reorder_modal.drag_end": "O campo \"{item}\" foi largado.", + "account_edit.field_reorder_modal.drag_instructions": "Para reorganizar os campos personalizados, prima a tecla de espaço ou enter. Enquanto arrasta, utilize as teclas de setas para mover o campo para cima ou para baixo. Prima novamente a tecla de espaço ou enter para largar o campo na nova posição, ou prima escape para cancelar.", + "account_edit.field_reorder_modal.drag_move": "O campo \"{item}\" foi movido.", + "account_edit.field_reorder_modal.drag_over": "O campo \"{item}\" foi movido para cima de \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Apanhou o campo \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Arrastar o campo \"{item}\"", + "account_edit.field_reorder_modal.title": "Reordenar campos", + "account_edit.image_alt_modal.add_title": "Adicionar texto alternativo", + "account_edit.image_alt_modal.edit_title": "Editar texto alternativo", + "account_edit.image_alt_modal.text_hint": "O texto alternativo ajuda os utilizadores que usam um leitor de ecrã a entender seu conteúdo.", + "account_edit.image_alt_modal.text_label": "Texto alternativo", + "account_edit.image_delete_modal.confirm": "Tem a certeza que pretende eliminar esta imagem? Esta ação é irreversível.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "Eliminar imagem?", + "account_edit.image_edit.add_button": "Adicionar imagem", + "account_edit.image_edit.alt_add_button": "Adicionar texto alternativo", + "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", + "account_edit.image_edit.remove_button": "Remover imagem", + "account_edit.image_edit.replace_button": "Substituir imagem", "account_edit.name_modal.add_title": "Adicionar nome a mostrar", "account_edit.name_modal.edit_title": "Editar o nome a mostrar", "account_edit.profile_tab.button_label": "Personalizar", "account_edit.profile_tab.hint.description": "Estas configurações personalizam o que os utilizadores veem no {server} nas aplicações oficiais, mas podem não se aplicar aos utilizadores de outros servidores nem aplicações de terceiros.", "account_edit.profile_tab.hint.title": "A apresentação ainda pode variar", + "account_edit.profile_tab.show_featured.description": "\"Destaques\" é uma aba opcional onde pode mostrar outras contas.", + "account_edit.profile_tab.show_featured.title": "Exibir aba “Destaques”", + "account_edit.profile_tab.show_media.description": "\"Mídia\" é uma aba opcional que mostra as suas publicações contendo imagens ou vídeos.", + "account_edit.profile_tab.show_media.title": "Exibir aba \"Mídia\"", + "account_edit.profile_tab.show_media_replies.description": "Quando ativada, a aba \"Mídia\" exibe tanto as suas publicações quanto as suas respostas às publicações de outras pessoas.", + "account_edit.profile_tab.subtitle": "Personalize as abas do seu perfil e o que elas exibem.", + "account_edit.profile_tab.title": "Configurações da aba do perfil", "account_edit.save": "Guardar", + "account_edit.upload_modal.back": "Voltar", + "account_edit.upload_modal.done": "Concluído", + "account_edit.upload_modal.next": "Seguinte", + "account_edit.upload_modal.step_crop.zoom": "Ampliação", + "account_edit.upload_modal.step_upload.button": "Explorar ficheiros", + "account_edit.upload_modal.step_upload.dragging": "Solte para transferir", + "account_edit.upload_modal.step_upload.header": "Escolha uma imagem", + "account_edit.upload_modal.title_add": "Adicionar foto de perfil", + "account_edit.upload_modal.title_replace": "Substituir foto de perfil", + "account_edit.verified_modal.details": "Adicione credibilidade ao seu perfil no Mastodon verificando links para sites pessoais. Veja como funciona:", + "account_edit.verified_modal.invisible_link.details": "Adicione o link ao seu cabeçalho. A parte importante é rel=\"me\", que evita a personificação em sites com conteúdo gerado por utilizadores. Você também pode usar uma tag de link no cabeçalho da página em vez de {tag}, mas o HTML deve ser acessível sem executar JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Como faço para tornar o link invisível?", + "account_edit.verified_modal.step1.header": "Copie o código HTML abaixo e cole no cabeçalho do seu site", + "account_edit.verified_modal.step2.details": "Se já adicionou o seu site como um campo personalizado, será necessário excluí-lo e adicioná-lo novamente para acionar a verificação.", + "account_edit.verified_modal.step2.header": "Adicione o seu site como um campo personalizado", + "account_edit.verified_modal.title": "Como adicionar um link verificado", "account_edit_tags.add_tag": "Adicionar #{tagName}", "account_edit_tags.column_title": "Editar etiquetas em destaque", "account_edit_tags.help_text": "As etiquetas destacadas ajudam os utilizadores a descobrir e interagir com o seu perfil. Aparecem como filtros na vista de atividade da sua página de perfil.", @@ -196,13 +255,13 @@ "annual_report.announcement.action_build": "Criar o meu Wrapstodon", "annual_report.announcement.action_dismiss": "Não, obrigado", "annual_report.announcement.action_view": "Ver o meu Wrapstodon", - "annual_report.announcement.description": "Descobre mais sobre o teu envolvimento com o Mastodon durante o último ano.", + "annual_report.announcement.description": "Descubra mais sobre a sua interação no Mastodon ao longo do último ano.", "annual_report.announcement.title": "Chegou o Wrapstodon {year}", "annual_report.nav_item.badge": "Novo", "annual_report.shared_page.donate": "Doar", "annual_report.shared_page.footer": "Gerado com {heart} pela equipa do Mastodon", "annual_report.shared_page.footer_server_info": "{username} utiliza {domain}, uma das muitas comunidades baseadas no Mastodon.", - "annual_report.summary.archetype.booster.desc_public": "{name} permaneceu à procura de publicações para partilhar, promovendo outros criadores com uma precisão perfeita.", + "annual_report.summary.archetype.booster.desc_public": "{name} manteve a procura de publicações para impulsionar, amplificando outros criadores com precisão perfeita.", "annual_report.summary.archetype.booster.desc_self": "Permaneceu à procura de publicações para partilhar, promovendo outros criadores com uma precisão perfeita.", "annual_report.summary.archetype.booster.name": "O Arqueiro", "annual_report.summary.archetype.die_drei_fragezeichen": "???", @@ -219,7 +278,7 @@ "annual_report.summary.archetype.replier.desc_self": "Respondeu frequentemente às publicações de outras pessoas, polinizando o Mastodon com novas discussões.", "annual_report.summary.archetype.replier.name": "A Borboleta", "annual_report.summary.archetype.reveal": "Revelar o meu arquétipo", - "annual_report.summary.archetype.reveal_description": "Obrigado por fazer parte do Mastodon! É hora de descobrir qual arquétipo você encarnou em {year}.", + "annual_report.summary.archetype.reveal_description": "Obrigado por fazer parte do Mastodon! É hora de descobrir qual arquétipo foi incorporado em {year}.", "annual_report.summary.archetype.title_public": "Arquétipo de {name}", "annual_report.summary.archetype.title_self": "O seu arquétipo", "annual_report.summary.close": "Fechar", @@ -241,7 +300,7 @@ "annual_report.summary.share_on_mastodon": "Partilhar no Mastodon", "attachments_list.unprocessed": "(não processado)", "audio.hide": "Ocultar áudio", - "block_modal.remote_users_caveat": "Vamos pedir ao servidor {domain} para respeitar a tua decisão. No entanto, não é garantido o seu cumprimento, uma vez que alguns servidores podem tratar os bloqueios de forma diferente. As publicações públicas podem continuar a ser visíveis para utilizadores não autenticados.", + "block_modal.remote_users_caveat": "Solicitaremos ao servidor {domain} que respeite a sua decisão. No entanto, o cumprimento não é garantido, sendo que alguns servidores podem gerir bloqueios de forma diferente. As publicações públicas podem continuar visíveis para utilizadores não autenticados.", "block_modal.show_less": "Mostrar menos", "block_modal.show_more": "Mostrar mais", "block_modal.they_cant_mention": "Ele não o pode mencionar nem seguir.", @@ -255,7 +314,7 @@ "bundle_column_error.copy_stacktrace": "Copiar relatório de erros", "bundle_column_error.error.body": "A página solicitada não pôde ser sintetizada. Isto pode ser devido a uma falha no nosso código ou a um problema de compatibilidade com o navegador.", "bundle_column_error.error.title": "Ó, não!", - "bundle_column_error.network.body": "Houve um erro ao tentar carregar esta página. Isto pode ocorrer devido a um problema temporário com a tua conexão à internet ou a este servidor.", + "bundle_column_error.network.body": "Ocorreu um erro ao tentar carregar esta página. Isto poderá dever-se a um problema temporário na tua ligação à Internet ou neste servidor.", "bundle_column_error.network.title": "Erro de rede", "bundle_column_error.retry": "Tenta de novo", "bundle_column_error.return": "Voltar à página inicial", @@ -270,10 +329,11 @@ "character_counter.recommended": "{currentLength}/{maxLength} caracteres recomendados", "character_counter.required": "{currentLength}/{maxLength} caracteres", "closed_registrations.other_server_instructions": "Visto que o Mastodon é descentralizado, podes criar uma conta noutro servidor e interagir com este na mesma.", - "closed_registrations_modal.description": "Neste momento não é possível criar uma conta em {domain}, mas lembramos que não é preciso ter uma conta especificamente em {domain} para usar o Mastodon.", + "closed_registrations_modal.description": "Criar uma conta em {domain} não é atualmente possível, mas tenha em atenção que não é necessário ter uma conta especificamente em {domain} para usar o Mastodon.", "closed_registrations_modal.find_another_server": "Procurar outro servidor", - "closed_registrations_modal.preamble": "O Mastodon é descentralizado, por isso não importa onde a tua conta é criada, pois continuarás a poder acompanhar e interagir com qualquer um neste servidor. Podes até alojar o teu próprio servidor!", + "closed_registrations_modal.preamble": "O Mastodon é descentralizado, por isso, não importa onde crie a sua conta: poderá seguir e interagir com qualquer utilizador neste servidor. Pode até alojá-lo você próprio!", "closed_registrations_modal.title": "Criar uma conta no Mastodon", + "collection.share_modal.share_link_label": "Link de convite para partilha", "collection.share_modal.share_via_post": "Publicar no Mastodon", "collection.share_modal.share_via_system": "Compartilhar com…", "collection.share_modal.title": "Partilhar coleção", @@ -297,10 +357,15 @@ "collections.create_collection": "Criar coleção", "collections.delete_collection": "Eliminar coleção", "collections.description_length_hint": "Limite de 100 caracteres", + "collections.detail.accept_inclusion": "OK / Aceitar", "collections.detail.accounts_heading": "Contas", + "collections.detail.author_added_you": "{author} adicionou-o a esta coleção", "collections.detail.curated_by_author": "Curado por {author}", "collections.detail.curated_by_you": "Curado por si", "collections.detail.loading": "A carregar a coleção…", + "collections.detail.other_accounts_in_collection": "Outros nesta coleção:", + "collections.detail.revoke_inclusion": "Remover-me", + "collections.detail.sensitive_note": "Esta coleção contém contas e conteúdos que podem ser sensíveis para alguns utilizadores.", "collections.detail.share": "Partilhar esta coleção", "collections.edit_details": "Editar detalhes", "collections.error_loading_collections": "Ocorreu um erro ao tentar carregar as suas coleções.", @@ -315,10 +380,14 @@ "collections.old_last_post_note": "Última publicação há mais de uma semana", "collections.remove_account": "Remover esta conta", "collections.report_collection": "Denunciar esta coleção", + "collections.revoke_collection_inclusion": "Remover-me desta coleção", + "collections.revoke_inclusion.confirmation": "Foi removido da coleção \"{collection}\"", + "collections.revoke_inclusion.error": "Ocorreu um erro, por favor tente novamente mais tarde.", "collections.search_accounts_label": "Procurar contas para adicionar…", "collections.search_accounts_max_reached": "Já adicionou o máximo de contas", "collections.sensitive": "Sensível", "collections.topic_hint": "Adicione uma etiqueta para ajudar outros a entender o tópico principal desta coleção.", + "collections.topic_special_chars_hint": "Os carateres especiais serão removidos ao guardar", "collections.view_collection": "Ver coleções", "collections.view_other_collections_by_user": "Ver outras coleções deste utilizador", "collections.visibility_public": "Pública", @@ -328,7 +397,7 @@ "collections.visibility_unlisted_hint": "Visível para qualquer pessoa com uma hiperligação. Não aparece nos resultados de pesquisa e recomendações.", "column.about": "Sobre", "column.blocks": "Utilizadores bloqueados", - "column.bookmarks": "Marcadores", + "column.bookmarks": "Favoritos", "column.collections": "As minhas coleções", "column.community": "Cronologia local", "column.create_list": "Criar lista", @@ -438,6 +507,9 @@ "confirmations.remove_from_followers.confirm": "Remover seguidor", "confirmations.remove_from_followers.message": "{name} vai parar de seguir-te. Tens a certeza que prentedes continuar?", "confirmations.remove_from_followers.title": "Remover seguidor?", + "confirmations.revoke_collection_inclusion.confirm": "Remover-me", + "confirmations.revoke_collection_inclusion.message": "Esta ação é permanente, e o curador não poderá adicioná-lo novamente à coleção mais tarde.", + "confirmations.revoke_collection_inclusion.title": "Remover-se desta coleção?", "confirmations.revoke_quote.confirm": "Remover publicação", "confirmations.revoke_quote.message": "Esta ação é irreversível.", "confirmations.revoke_quote.title": "Remover publicação?", @@ -455,6 +527,7 @@ "conversation.open": "Ver conversa", "conversation.with": "Com {names}", "copy_icon_button.copied": "Copiado para a área de transferência", + "copy_icon_button.copy_this_text": "Copiar link para a área de transferência", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar para a área de transferência", "directory.federated": "Do fediverso conhecido", @@ -672,6 +745,7 @@ "keyboard_shortcuts.direct": "Abrir coluna de menções privadas", "keyboard_shortcuts.down": "mover para baixo na lista", "keyboard_shortcuts.enter": "abrir publicação", + "keyboard_shortcuts.explore": "Abrir linha do tempo em destaque", "keyboard_shortcuts.favourite": "assinalar como favorita", "keyboard_shortcuts.favourites": "abrir lista de favoritos", "keyboard_shortcuts.federated": "abrir a cronologia federada", @@ -758,6 +832,7 @@ "navigation_bar.automated_deletion": "Eliminação automática de publicações", "navigation_bar.blocks": "Utilizadores bloqueados", "navigation_bar.bookmarks": "Itens salvos", + "navigation_bar.collections": "Coleções", "navigation_bar.direct": "Menções privadas", "navigation_bar.domain_blocks": "Domínios escondidos", "navigation_bar.favourites": "Favoritos", @@ -794,7 +869,7 @@ "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} assinalou a tua publicação como favorita", "notification.favourite.name_and_others_with_link": "{name} e {count, plural, one {# outro} other {# outros}} assinalaram a tua publicação como favorita", - "notification.favourite_pm": "{name} assinalou como favorita a tua menção privada", + "notification.favourite_pm": "{name} assinalou como favorita a sua menção privada", "notification.favourite_pm.name_and_others_with_link": "{name} e {count, plural, one {# outro favoritou} other {# outros favoritaram}} a tua menção privada", "notification.follow": "{name} começou a seguir-te", "notification.follow.name_and_others": "{name} e {count, plural, one {# outro seguiram-te} other {# outros seguiram-te}}", @@ -904,17 +979,17 @@ "notifications_permission_banner.how_to_control": "Para receberes notificações quando o Mastodon não estiver aberto, ativa as notificações no ambiente de trabalho. Após isso, podes controlar precisamente que tipos de interações geram notificações no ambiente de trabalho através do botão {icon} acima.", "notifications_permission_banner.title": "Nunca percas nada", "onboarding.follows.back": "Voltar", - "onboarding.follows.done": "Concluído", "onboarding.follows.empty": "Infelizmente não é possível mostrar resultados neste momento. Podes tentar pesquisar ou navegar na página \"Explorar\" para encontrares pessoas para seguires ou tentar novamente mais tarde.", + "onboarding.follows.next": "A seguir: configure o seu perfil", "onboarding.follows.search": "Pesquisar", "onboarding.follows.title": "Segue pessoas para começar", "onboarding.profile.discoverable": "Permitir que o meu perfil seja descoberto", "onboarding.profile.discoverable_hint": "Quando opta pela possibilidade de ser descoberto no Mastodon, as suas mensagens podem aparecer nos resultados de pesquisa e nos destaques, e o seu perfil pode ser sugerido a pessoas com interesses semelhantes aos seus.", "onboarding.profile.display_name": "Nome a apresentar", "onboarding.profile.display_name_hint": "O teu nome completo ou o teu nome divertido…", + "onboarding.profile.finish": "Terminar", "onboarding.profile.note": "Biografia", "onboarding.profile.note_hint": "Podes @mencionar outras pessoas e usar #etiquetas…", - "onboarding.profile.save_and_continue": "Guardar e continuar", "onboarding.profile.title": "Configuração do perfil", "onboarding.profile.upload_avatar": "Enviar foto de perfil", "onboarding.profile.upload_header": "Enviar cabeçalho do perfil", @@ -989,7 +1064,7 @@ "report.forward": "Reencaminhar para {target}", "report.forward_hint": "A conta pertence a outro servidor. Enviar uma cópia anónima da denúncia para esse servidor também?", "report.mute": "Ocultar", - "report.mute_explanation": "Não verás as publicações dele. Ele não poderá ver as tuas publicações nem seguir-te. Ele não saberá que o ocultaste.", + "report.mute_explanation": "Não verá as publicações dele. Ele não poderá ver as suas publicações nem segui-lo. Ele não saberá que o ocultou.", "report.next": "Seguinte", "report.placeholder": "Comentários adicionais", "report.reasons.dislike": "Não gosto disto", @@ -1061,6 +1136,9 @@ "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está a acontecer.", "sign_in_banner.sign_in": "Iniciar sessão", "sign_in_banner.sso_redirect": "Inicia a sessão ou cria uma conta", + "skip_links.hotkey": "Tecla de atalho {hotkey}", + "skip_links.skip_to_content": "Ir para o conteúdo principal", + "skip_links.skip_to_navigation": "Ir para a navegação principal", "status.admin_account": "Abrir a interface de moderação para @{name}", "status.admin_domain": "Abrir interface de moderação para {domain}", "status.admin_status": "Abrir esta publicação na interface de moderação", @@ -1133,7 +1211,7 @@ "status.reblogs.empty": "Ainda ninguém partilhou esta publicação. Quando alguém o fizer, aparecerá aqui.", "status.reblogs_count": "{count, plural, one {{counter} partilha} other {{counter} partilhas}}", "status.redraft": "Eliminar e reescrever", - "status.remove_bookmark": "Retirar dos marcadores", + "status.remove_bookmark": "Remover marcador", "status.remove_favourite": "Remover dos favoritos", "status.remove_quote": "Remover", "status.replied_in_thread": "Responder na conversa", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index bdfbb605d4cc70..fd7dfcc56927b2 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -1,6 +1,7 @@ { "about.blocks": "Servere moderate", "about.contact": "Contact:", + "about.default_locale": "Standard", "about.disclaimer": "Mastodon este o aplicație gratuită, cu sursă deschisă și o marcă înregistrată a Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Motivul nu este disponibil", "about.domain_blocks.preamble": "Mastodon îți permite în general să vezi conținut de la și să interacționezi cu utilizatori de pe oricare server în fediverse. Acestea sunt excepțiile care au fost făcute pe acest server.", @@ -8,22 +9,33 @@ "about.domain_blocks.silenced.title": "Limitat", "about.domain_blocks.suspended.explanation": "Nicio informație de la acest server nu va fi procesată, stocată sau trimisă, făcând imposibilă orice interacțiune sau comunicare cu utilizatorii de pe acest server.", "about.domain_blocks.suspended.title": "Suspendat", + "about.language_label": "Limbă", "about.not_available": "Această informație nu a fost pusă la dispoziție pe acest server.", "about.powered_by": "Media socială descentralizată furnizată de {mastodon}", "about.rules": "Reguli server", "account.account_note_header": "Notă personală", + "account.activity": "Activități", + "account.add_note": "Adaugă o notă personală", "account.add_or_remove_from_list": "Adaugă sau elimină din liste", + "account.badges.admin": "Admin", + "account.badges.blocked": "Blocat", "account.badges.bot": "Robot", + "account.badges.domain_blocked": "Domeniu blocat", "account.badges.group": "Grup", + "account.badges.muted": "Silențios", + "account.badges.muted_until": "Silențios până la {until}", "account.block": "Blochează pe @{name}", "account.block_domain": "Blochează domeniul {domain}", "account.block_short": "Blochează", "account.blocked": "Blocat", + "account.blocking": "Blocarea", "account.cancel_follow_request": "Retrage cererea de urmărire", "account.copy": "Copiază link-ul profilului", "account.direct": "Menționează pe @{name} în privat", "account.disable_notifications": "Nu îmi mai trimite notificări când postează @{name}", + "account.edit_note": "Editare notă personală", "account.edit_profile": "Modifică profilul", + "account.edit_profile_short": "Editare", "account.enable_notifications": "Trimite-mi o notificare când postează @{name}", "account.endorse": "Promovează pe profil", "account.featured_tags.last_status_at": "Ultima postare pe {date}", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 7424a73f12eada..44dd60505ac61c 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -725,7 +725,6 @@ "notifications_permission_banner.how_to_control": "Чтобы получать уведомления, даже когда Mastodon закрыт, включите уведомления на рабочем столе. После того как вы их включите, вы сможете тонко настроить виды взаимодействий, о которых вы будете оповещены через уведомления на рабочем столе, нажав на кнопку {icon} выше.", "notifications_permission_banner.title": "Будьте в курсе происходящего", "onboarding.follows.back": "Назад", - "onboarding.follows.done": "Готово", "onboarding.follows.empty": "К сожалению, на данный момент предложения отсутствуют. Чтобы найти, на кого подписаться, вы можете просматривать раздел «Актуальное» или воспользоваться поиском.", "onboarding.follows.search": "Поиск", "onboarding.follows.title": "Начните подписываться на людей", @@ -735,7 +734,6 @@ "onboarding.profile.display_name_hint": "Ваше полное имя или псевдоним…", "onboarding.profile.note": "О себе", "onboarding.profile.note_hint": "Вы можете @упоминать других людей, а также использовать #хештеги…", - "onboarding.profile.save_and_continue": "Сохранить и продолжить", "onboarding.profile.title": "Создайте свой профиль", "onboarding.profile.upload_avatar": "Загрузить фото профиля", "onboarding.profile.upload_header": "Загрузить обложку профиля", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index ab9444c0b37314..d05c906d68d2bf 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -562,12 +562,10 @@ "notifications_permission_banner.how_to_control": "Pro retzire notìficas cando Mastodon no est abertu, abilita is notìficas de iscrivania. Podes controllare cun pretzisione is castas de interatziones chi ingendrant notìficas de iscrivania pro mèdiu de su butone {icon} in subra, cando sunt abilitadas.", "notifications_permission_banner.title": "Non ti perdas mai nudda", "onboarding.follows.back": "A coa", - "onboarding.follows.done": "Fatu", "onboarding.follows.search": "Chirca", "onboarding.follows.title": "Sighi a gente pro cumintzare", "onboarding.profile.display_name": "Nòmine visìbile", "onboarding.profile.note": "Biografia", - "onboarding.profile.save_and_continue": "Sarva e sighi", "onboarding.profile.title": "Cunfiguratzione de profilu", "onboarding.profile.upload_avatar": "Càrriga una fotografia de profilu", "picture_in_picture.restore": "Torra·ddu a ue fiat", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 6f1f37f8bf45d9..126fc4d0720990 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -636,7 +636,6 @@ "notifications_permission_banner.how_to_control": "Mastodon විවෘතව නොමැති විට දැනුම්දීම් ලබා ගැනීමට, ඩෙස්ක්ටොප් දැනුම්දීම් සක්‍රීය කරන්න. ඒවා සක්‍රීය කළ පසු ඉහත {icon} බොත්තම හරහා ඩෙස්ක්ටොප් දැනුම්දීම් ජනනය කරන්නේ කුමන ආකාරයේ අන්තර්ක්‍රියාද යන්න ඔබට නිශ්චිතවම පාලනය කළ හැකිය.", "notifications_permission_banner.title": "කිසිවක් අතපසු නොකරන්න", "onboarding.follows.back": "ආපසු", - "onboarding.follows.done": "කළා", "onboarding.follows.empty": "අවාසනාවකට, දැන් ප්‍රතිඵල කිසිවක් පෙන්විය නොහැක. අනුගමනය කිරීමට පුද්ගලයින් සොයා ගැනීමට ඔබට සෙවීම භාවිතා කිරීමට හෝ ගවේෂණ පිටුව බ්‍රවුස් කිරීමට උත්සාහ කළ හැකිය, නැතහොත් පසුව නැවත උත්සාහ කරන්න.", "onboarding.follows.search": "සෙවීම", "onboarding.follows.title": "ආරම්භ කිරීමට පුද්ගලයින් අනුගමනය කරන්න", @@ -646,7 +645,6 @@ "onboarding.profile.display_name_hint": "ඔබේ සම්පූර්ණ නම හෝ ඔබේ විනෝදජනක නම…", "onboarding.profile.note": "ජෛව", "onboarding.profile.note_hint": "ඔබට වෙනත් පුද්ගලයින් හෝ #හැෂ් ටැග්…@සඳහන් කළ හැක.", - "onboarding.profile.save_and_continue": "සුරකින්න සහ ඉදිරියට යන්න", "onboarding.profile.title": "පැතිකඩ සැකසුම", "onboarding.profile.upload_avatar": "පැතිකඩ පින්තූරය උඩුගත කරන්න", "onboarding.profile.upload_header": "පැතිකඩ ශීර්ෂය උඩුගත කරන්න", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index b5c7605285c0d9..0d073db623793b 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -634,7 +634,6 @@ "notifications_permission_banner.how_to_control": "Ak chcete dostávať upozornenia, keď Mastodon nie je otvorený, povoľte upozornenia na ploche. Po ich zapnutí môžete presne kontrolovať, ktoré typy interakcií generujú upozornenia na ploche, a to prostredníctvom tlačidla {icon} vyššie.", "notifications_permission_banner.title": "Nenechajte si nič ujsť", "onboarding.follows.back": "Späť", - "onboarding.follows.done": "Hotovo", "onboarding.follows.empty": "Žiaľ, momentálne sa nedajú zobraziť žiadne výsledky. Môžete skúsiť použiť vyhľadávanie alebo navštíviť stránku objavovania a nájsť ľudí, ktorých chcete sledovať, alebo to skúste znova neskôr.", "onboarding.follows.search": "Hľadať", "onboarding.follows.title": "Pre začiatok nasleduj ľudí", @@ -644,7 +643,6 @@ "onboarding.profile.display_name_hint": "Vaše celé meno alebo pokojne aj vtipná prezývka…", "onboarding.profile.note": "Niečo o vás", "onboarding.profile.note_hint": "Môžete @označiť iných ľudí alebo #hashtagy…", - "onboarding.profile.save_and_continue": "Uložiť a pokračovať", "onboarding.profile.title": "Nastavenie profilu", "onboarding.profile.upload_avatar": "Nahrať profilový obrázok", "onboarding.profile.upload_header": "Nahrať obrázok v záhlaví profilu", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index ae960395408a00..b13ae55d10579b 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -55,7 +55,6 @@ "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}", @@ -706,7 +705,6 @@ "notifications_permission_banner.how_to_control": "Če želite prejemati obvestila, ko Mastodon ni odprt, omogočite namizna obvestila. Natančno lahko nadzirate, katere vrste interakcij naj tvorijo namizna obvestila; ko so omogočena, za to uporabite gumb {icon} zgoraj.", "notifications_permission_banner.title": "Nikoli ne zamudite ničesar", "onboarding.follows.back": "Nazaj", - "onboarding.follows.done": "Opravljeno", "onboarding.follows.empty": "Žal trenutno ni mogoče prikazati nobenih rezultatov. Lahko poskusite z iskanjem ali brskanjem po strani za raziskovanje, da poiščete osebe, ki jim želite slediti, ali poskusite znova pozneje.", "onboarding.follows.search": "Išči", "onboarding.follows.title": "Vaš prvi korak je, da sledite ljudem", @@ -716,7 +714,6 @@ "onboarding.profile.display_name_hint": "Vaše polno ime ali lažno ime ...", "onboarding.profile.note": "Biografija", "onboarding.profile.note_hint": "Lahko @omenite druge osebe ali dodate #ključnike ...", - "onboarding.profile.save_and_continue": "Shrani in nadaljuj", "onboarding.profile.title": "Nastavitev profila", "onboarding.profile.upload_avatar": "Naloži sliko profila", "onboarding.profile.upload_header": "Naloži glavo profila", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 5c4974e1828f95..076830d3f31d1e 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Përpunoni Profil", "account_edit.custom_fields.name": "fushë", "account_edit.custom_fields.placeholder": "Shtoni përemrat tuaj, lidhje të jashme, ose gjithçka tjetë që do të donit të ndanit me të tjerë.", + "account_edit.custom_fields.reorder_button": "Rirenditi fushat", "account_edit.custom_fields.tip_content": "Mundeni të shtoni kollak besueshmëri për llogarinë tuaj Mastodon duke verifikuar lidhje për te çfarëdo sajti që është pronë e juaja.", "account_edit.custom_fields.tip_title": "Ndihmëz: Duke shtuar lidhje të verifikuara", "account_edit.custom_fields.title": "Fusha vetjake", @@ -167,10 +167,35 @@ "account_edit.field_delete_modal.title": "Të fshihet fushë e përshtatur?", "account_edit.field_edit_modal.add_title": "Shtoni fushë të përshtatur", "account_edit.field_edit_modal.edit_title": "Përpunoni fushë të përshtatur", + "account_edit.field_edit_modal.limit_header": "U tejkalua kufi i rekomanduar shenjash", + "account_edit.field_edit_modal.limit_message": "Përdorues me celular mund të mos e shohin të plotë fushën tuaj.", + "account_edit.field_edit_modal.link_emoji_warning": "Rekomandojmë të mos përdoren emoji të përshtatur tok me url-ra. Fusha të përshtatura që i përmbajnë të dyja llojetn do t’i shfaqin si tekst, në vend se si një lidhje, për të parandaluar ngatërrim të përdoruesve.", "account_edit.field_edit_modal.name_hint": "P.sh., “Sajt personal”", "account_edit.field_edit_modal.name_label": "Etiketë", - "account_edit.field_edit_modal.value_hint": "P.sh., “shembull.me”", + "account_edit.field_edit_modal.url_warning": "Që të shtoni një lidhje, ju lutemi, përfshini {protocol} në fillim.", + "account_edit.field_edit_modal.value_hint": "P.sh., “https://example.me”", "account_edit.field_edit_modal.value_label": "Vlerë", + "account_edit.field_reorder_modal.drag_cancel": "Tërheqja u anulua. Fusha “{item}” u la jashtë.", + "account_edit.field_reorder_modal.drag_instructions": "Që të risistemoni fusha vetjake, shtypni tastin Space, ose Enter. Teksa tërhiqen, përdorni tastet shigjetë që të lëvizn fushat lart ose poshtë. Shtypni sërish tastin Space ose Enter që të lihet fusha te pozicioni i saj i ri, ose shtypni tastin Esc, që të anulohet.", + "account_edit.field_reorder_modal.drag_move": "Fusha “{item}” u lëviz.", + "account_edit.field_reorder_modal.drag_over": "Fusha “{item}” u kaluar sipër “{over}”.", + "account_edit.field_reorder_modal.drag_start": "U mor fusha “{item}”.", + "account_edit.field_reorder_modal.handle_label": "Tërhiqni fushën “{item}”", + "account_edit.field_reorder_modal.title": "Risistemoni fusha", + "account_edit.image_alt_modal.add_title": "Shtoni tekst alternativ", + "account_edit.image_alt_modal.details_content": "MIRË:
    • Përshkruani veten si në figurë
    • Përdorni gjuhë në vetën e tretë (p.sh., “Gimi”, në vend se “unë”)
    • Jini konciz – pak fjalë shpesh mjaftojnë
    KEQ:
    • T’ia filloni me “Foto e ” – është përsëritje për lexues ekrani
    SHEMBULL:
    • “Gimi me një këmishë të gjelbër dhe syze”
    ", + "account_edit.image_alt_modal.details_title": "Ndihmëza: Teskt alternativ për figura profilesh", + "account_edit.image_alt_modal.edit_title": "Përpunoni tekst alternativ", + "account_edit.image_alt_modal.text_hint": "Teskti alternativ ndihmon përdorue lexuesish ekrani të kuptojnë lëndën tuaj.", + "account_edit.image_alt_modal.text_label": "Tekst alternativ", + "account_edit.image_delete_modal.confirm": "Jeni i sigurt se doni të fshihet kjo figurë? Ky veprim s’mund të zhbëhet.", + "account_edit.image_delete_modal.delete_button": "Fshije", + "account_edit.image_delete_modal.title": "Të fshihet figura?", + "account_edit.image_edit.add_button": "Shtoni figurë", + "account_edit.image_edit.alt_add_button": "Shtoni tekst alternativ", + "account_edit.image_edit.alt_edit_button": "Përpunoni tekst alternativ", + "account_edit.image_edit.remove_button": "Hiqe figurën", + "account_edit.image_edit.replace_button": "Zëvendësoje figurën", "account_edit.name_modal.add_title": "Shtoni emër në ekran", "account_edit.name_modal.edit_title": "Përpunoni emër në ekran", "account_edit.profile_tab.button_label": "Përshtateni", @@ -185,6 +210,16 @@ "account_edit.profile_tab.subtitle": "Përshtatni skedat në profilin tuaj dhe ato çka shfaqet në to.", "account_edit.profile_tab.title": "Rregullime skede profili", "account_edit.save": "Ruaje", + "account_edit.upload_modal.back": "Mbrapsht", + "account_edit.upload_modal.done": "U bë", + "account_edit.upload_modal.next": "Pasuesja", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Shfletoni kartela", + "account_edit.upload_modal.step_upload.dragging": "Lëreni, që të ngarkohet", + "account_edit.upload_modal.step_upload.header": "Zgjidhni një figurë", + "account_edit.upload_modal.step_upload.hint": "Format WEBP, PNG, GIF ose JPG, deri në {limit}MB.{br}Figura do të ripërmasohet në {width}x{height}px.", + "account_edit.upload_modal.title_add": "Shtoni figurë profili", + "account_edit.upload_modal.title_replace": "Zëvendësoni figurë profili", "account_edit.verified_modal.details": "Shtoni besueshmëri te profili juaj Mastodon duke verifikuar lidhje për te sajte personalë. Ja se si funksionon:", "account_edit.verified_modal.invisible_link.details": "Shtojeni lidhjen te kryet tuaja. Pjesa e rëndësishme është rel=\"me\", e cila pengon imitime në sajte me lëndë të prodhuar nga përdoruesit. Mundeni madje të përdorni një etiketë lidhjeje te kryet e faqes në vend të {tag}, por HTML-ja duhet të jetë e përdorshme pa ekzekutim të JavaScript-it.", "account_edit.verified_modal.invisible_link.summary": "Si ta bëj lidhjen të padukshme?", @@ -309,6 +344,8 @@ "collections.account_count": "{count, plural, one {# llogari} other {# llogari}}", "collections.accounts.empty_title": "Ky koleksion është i zbrazët", "collections.collection_description": "Përshkrim", + "collections.collection_language": "Gjuhë", + "collections.collection_language_none": "Asnjë", "collections.collection_name": "Emër", "collections.collection_topic": "Temë", "collections.confirm_account_removal": "Jeni i sigurt se doni të hiqet kjo llogari nga ky koleksion?", @@ -328,6 +365,7 @@ "collections.detail.curated_by_you": "Nën kujdesin tuaj", "collections.detail.loading": "Po ngarkohet koleksion…", "collections.detail.other_accounts_in_collection": "Të tjerë në këtë koleksion:", + "collections.detail.revoke_inclusion": "Hiqmëni", "collections.detail.sensitive_note": "Ky koleksion përmban llogari dhe lëndë që mund të jetë me spec për disa përdorues.", "collections.detail.share": "Ndajeni këtë koleksion me të tjerë", "collections.edit_details": "Përpunoni hollësi", @@ -343,10 +381,14 @@ "collections.old_last_post_note": "Të postuarat e fundit gjatë një jave më parë", "collections.remove_account": "Hiqe këtë llogari", "collections.report_collection": "Raportojeni këtë koleksion", + "collections.revoke_collection_inclusion": "Hiqmëni nga ky koleksion", + "collections.revoke_inclusion.confirmation": "U hoqët nga “{collection}”", + "collections.revoke_inclusion.error": "Pati një gabim, ju lutemi, riprovoni më vonë.", "collections.search_accounts_label": "Kërkoni për llogari për shtim…", "collections.search_accounts_max_reached": "Keni shtuar numrin maksimum të llogarive", "collections.sensitive": "Rezervat", "collections.topic_hint": "Shtoni një hashtag që ndihmon të tjerët të kuptojnë temën kryesore të këtij koleksion.", + "collections.topic_special_chars_hint": "Shenjat e posaçme do të hiqen, kur ruhet", "collections.view_collection": "Shiheni koleksionin", "collections.view_other_collections_by_user": "Shihni koleksione të tjera nga ky përdorues", "collections.visibility_public": "Publik", @@ -466,6 +508,9 @@ "confirmations.remove_from_followers.confirm": "Hiqe ndjekësin", "confirmations.remove_from_followers.message": "{name} do të reshtë së ndjekuri ju. Jeni i sigurt se doni të vazhdohet?", "confirmations.remove_from_followers.title": "Të hiqet ndjekësi?", + "confirmations.revoke_collection_inclusion.confirm": "Hiqmëni", + "confirmations.revoke_collection_inclusion.message": "Ky veprim është i përhershëm dhe mirëmbajtësi s’do të jetë në gjendje t’ju rishtojë te koleksioni më vonë.", + "confirmations.revoke_collection_inclusion.title": "Të hiqeni nga ky koleksion?", "confirmations.revoke_quote.confirm": "Hiqe postimin", "confirmations.revoke_quote.message": "Ky veprim s’mund të zhbëhet.", "confirmations.revoke_quote.title": "Të hiqet postimi?", @@ -935,17 +980,17 @@ "notifications_permission_banner.how_to_control": "Për të marrë njoftime, kur Mastodon-i s’është i hapur, aktivizoni njoftime në desktop. Përmes butoni {icon} më sipër, mund të kontrolloni me përpikëri cilat lloje ndërveprimesh prodhojnë njoftime në desktop, pasi të jenë aktivizuar.", "notifications_permission_banner.title": "Mos t’ju shpëtojë gjë", "onboarding.follows.back": "Mbrapsht", - "onboarding.follows.done": "U bë", "onboarding.follows.empty": "Mjerisht, s’mund të shfaqen përfundime tani. Mund të provoni të përdorni kërkimin, ose të shfletoni faqen e eksplorimit, që të gjeni persona për ndjekje, ose të riprovoni më vonë.", + "onboarding.follows.next": "Pasuesi: Ujdisni profilin tuaj", "onboarding.follows.search": "Kërkoni", "onboarding.follows.title": "Që t’ia filloni, ndiqni persona", "onboarding.profile.discoverable": "Bëje profilin tim të zbulueshëm", "onboarding.profile.discoverable_hint": "Kur zgjidhni të jeni i zbulueshëm në Mastodon, postimet tuaja mund të shfaqen në përfundime kërkimesh dhe gjëra në modë dhe profili juaj mund t’u sugjerohet njerëzve me interesa të ngjashme me ju.", "onboarding.profile.display_name": "Emër në ekran", "onboarding.profile.display_name_hint": "Emri juaj i plotë, ose ç’të doni…", + "onboarding.profile.finish": "Përfundoje", "onboarding.profile.note": "Jetëshkrim", "onboarding.profile.note_hint": "Mund të @përmendni persona të tjerë, ose #hashtagë…", - "onboarding.profile.save_and_continue": "Ruaje dhe vazhdo", "onboarding.profile.title": "Udjisje profili", "onboarding.profile.upload_avatar": "Ngarkoni foto profili", "onboarding.profile.upload_header": "Ngarkoni krye profili", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 69ea0a1ee74af0..c9d62e2a9ff252 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -483,7 +483,6 @@ "onboarding.profile.display_name_hint": "Vaše puno ime ili nadimak…", "onboarding.profile.note": "Biografija", "onboarding.profile.note_hint": "Možete da @pomenete druge ljude ili #heš oznake…", - "onboarding.profile.save_and_continue": "Sačuvaj i nastavi", "onboarding.profile.title": "Podešavanje profila", "onboarding.profile.upload_avatar": "Otpremi sliku profila", "onboarding.profile.upload_header": "Otpremi zaglavlje profila", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index e6059201479aca..7313eaca462dcc 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -483,7 +483,6 @@ "onboarding.profile.display_name_hint": "Ваше пуно име или надимак…", "onboarding.profile.note": "Биографија", "onboarding.profile.note_hint": "Можете да @поменете друге људе или #хеш ознаке…", - "onboarding.profile.save_and_continue": "Сачувај и настави", "onboarding.profile.title": "Подешавање профила", "onboarding.profile.upload_avatar": "Отпреми слику профила", "onboarding.profile.upload_header": "Отпреми заглавље профила", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 95df6fade65707..17613601e9884f 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -48,6 +48,13 @@ "account.featured.hashtags": "Fyrkantstaggar", "account.featured_tags.last_status_at": "Senaste inlägg den {date}", "account.featured_tags.last_status_never": "Inga inlägg", + "account.field_overflow": "Visa hela innehållet", + "account.filters.all": "All aktivitet", + "account.filters.boosts_toggle": "Visa förstärkningar", + "account.filters.posts_boosts": "Inlägg och förstärkningar", + "account.filters.posts_only": "Inlägg", + "account.filters.posts_replies": "Inlägg och svar", + "account.filters.replies_toggle": "Visa svar", "account.follow": "Följ", "account.follow_back": "Följ tillbaka", "account.follow_back_short": "Följ tillbaka", @@ -66,13 +73,30 @@ "account.go_to_profile": "Gå till profilen", "account.hide_reblogs": "Dölj boostar från @{name}", "account.in_memoriam": "Till minne av.", - "account.joined_long": "Gick med {date}", "account.joined_short": "Gick med", "account.languages": "Ändra vilka språk du helst vill se i ditt flöde", "account.link_verified_on": "Ägarskap för denna länk kontrollerades den {date}", "account.locked_info": "Detta konto har låst integritetsstatus. Ägaren väljer manuellt vem som kan följa det.", "account.media": "Media", "account.mention": "Nämn @{name}", + "account.menu.add_to_list": "Lägg till i lista…", + "account.menu.block": "Blockera konto", + "account.menu.block_domain": "Blockera {domain}", + "account.menu.copied": "Kopierade kontolänk till urklipp", + "account.menu.copy": "Kopiera länk", + "account.menu.direct": "Nämn privat", + "account.menu.hide_reblogs": "Dölj förstärkningar i tidslinjen", + "account.menu.mention": "Nämn", + "account.menu.mute": "Tysta konto", + "account.menu.note.description": "Endast synlig för dig", + "account.menu.open_original_page": "Visa på {domain}", + "account.menu.remove_follower": "Ta bort följare", + "account.menu.report": "Rapportera konto", + "account.menu.share": "Dela…", + "account.menu.show_reblogs": "Visa förstärkningar i tidslinjen", + "account.menu.unblock": "Avblockera konto", + "account.menu.unblock_domain": "Avblockera {domain}", + "account.menu.unmute": "Avtysta konto", "account.moved_to": "{name} har indikerat att hen har ett nytt konto:", "account.mute": "Tysta @{name}", "account.mute_notifications_short": "Stäng av aviseringsljud", @@ -80,7 +104,15 @@ "account.muted": "Tystad", "account.muting": "Stänger av ljud", "account.mutual": "Ni följer varandra", + "account.name.help.domain": "{domain} är servern som är värd för användarens profil och inlägg.", + "account.name.help.domain_self": "{domain} är din server som är värd för användarens profil och inlägg.", + "account.name.help.footer": "Precis som du kan skicka e-post till personer med olika e-postklienter, du kan interagera med personer på andra Mastodon-servrar – och med vem som helst på andra sociala appar som drivs av samma uppsättning regler som Mastodon använder (ActivityPub-protokollet).", "account.no_bio": "Ingen beskrivning angiven.", + "account.node_modal.field_label": "Personlig anteckning", + "account.node_modal.save": "Spara", + "account.node_modal.title": "Lägg till en personlig anteckning", + "account.note.edit_button": "Redigera", + "account.note.title": "Personlig anteckning (endast synlig för dig)", "account.open_original_page": "Öppna den ursprungliga sidan", "account.posts": "Inlägg", "account.posts_with_replies": "Inlägg och svar", @@ -91,6 +123,8 @@ "account.share": "Dela @{name}s profil", "account.show_reblogs": "Visa boostar från @{name}", "account.statuses_counter": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}", + "account.timeline.pinned": "Fastnålad", + "account.timeline.pinned.view_all": "Visa alla fästa inlägg", "account.unblock": "Avblockera @{name}", "account.unblock_domain": "Avblockera {domain}", "account.unblock_domain_short": "Avblockera", @@ -100,8 +134,43 @@ "account.unmute": "Sluta tysta @{name}", "account.unmute_notifications_short": "Aktivera aviseringsljud", "account.unmute_short": "Avtysta", + "account_edit.bio.placeholder": "Lägg till en kort introduktion för att hjälpa andra att identifiera dig.", + "account_edit.bio.title": "Biografi", + "account_edit.bio_modal.add_title": "Lägg till biografi", + "account_edit.bio_modal.edit_title": "Redigera biografi", + "account_edit.button.add": "Lägg till {item}", + "account_edit.button.delete": "Radera {item}", + "account_edit.button.edit": "Redigera {item}", "account_edit.column_button": "Klar", "account_edit.column_title": "Redigera profil", + "account_edit.custom_fields.name": "fält", + "account_edit.custom_fields.placeholder": "Lägg till dina pronomen, externa länkar eller något annat du vill dela.", + "account_edit.custom_fields.reorder_button": "Ändra ordning för fält", + "account_edit.custom_fields.tip_content": "Du kan enkelt lägga till trovärdighet till ditt Mastodon-konto genom att verifiera länkar till alla webbplatser du äger.", + "account_edit.custom_fields.tip_title": "Tips: Lägga till verifierade länkar", + "account_edit.custom_fields.title": "Tilläggsfält", + "account_edit.custom_fields.verified_hint": "Hur lägger jag till en verifierad länk?", + "account_edit.display_name.placeholder": "Visningsnamnet är hur ditt namn ser ut på din profil och i tidslinjer.", + "account_edit.display_name.title": "Visningsnamn", + "account_edit.featured_hashtags.item": "hashtaggar", + "account_edit.featured_hashtags.placeholder": "Hjälp andra att identifiera, och få snabb tillgång till, dina favoritämnen.", + "account_edit.featured_hashtags.title": "Utvalda hashtaggar", + "account_edit.field_delete_modal.confirm": "Är du säker på att du vill ta bort detta tilläggsfält? Denna åtgärd kan inte ångras.", + "account_edit.field_delete_modal.delete_button": "Radera", + "account_edit.field_delete_modal.title": "Radera tilläggsfält?", + "account_edit.field_edit_modal.add_title": "Lägg till tilläggsfält", + "account_edit.field_edit_modal.edit_title": "Redigera tilläggsfält", + "account_edit.field_edit_modal.limit_header": "Rekommenderad teckengräns överskriden", + "account_edit.field_edit_modal.limit_message": "Mobilanvändare kanske inte ser ditt fält i sin helhet.", + "account_edit.field_edit_modal.name_hint": "T.ex. “Personlig webbplats”", + "account_edit.field_edit_modal.name_label": "Etikett", + "account_edit.field_edit_modal.url_warning": "För att lägga till en länk, vänligen inkludera {protocol} i början.", + "account_edit.field_edit_modal.value_hint": "T.ex. \"https://example.me”", + "account_edit.image_edit.add_button": "Lägg till bild", + "account_edit.image_edit.alt_add_button": "Lägg till alternativtext", + "account_edit.image_edit.alt_edit_button": "Redigera alternativtext", + "account_edit.image_edit.remove_button": "Ta bort bild", + "account_edit.image_edit.replace_button": "Ersätt bild", "account_edit.profile_tab.button_label": "Anpassa", "account_note.placeholder": "Klicka för att lägga till anteckning", "admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering", @@ -208,14 +277,19 @@ "closed_registrations_modal.preamble": "Mastodon är decentraliserat så oavsett var du skapar ditt konto kommer du att kunna följa och interagera med någon på denna server. Du kan också köra din egen server!", "closed_registrations_modal.title": "Registrera sig på Mastodon", "collections.accounts.empty_description": "Lägg till upp till {count} konton som du följer", + "collections.collection_language": "Språk", + "collections.collection_language_none": "Inga", "collections.create_a_collection_hint": "Skapa en samling för att rekommendera eller dela dina favoritkonton med andra.", "collections.create_collection": "Skapa samling", "collections.delete_collection": "Radera samling", + "collections.detail.accept_inclusion": "Okej", "collections.detail.accounts_heading": "Konton", + "collections.detail.revoke_inclusion": "Ta bort mig", "collections.error_loading_collections": "Det uppstod ett fel när dina samlingar skulle laddas.", "collections.hints.accounts_counter": "{count} / {max} konton", "collections.no_collections_yet": "Inga samlingar än.", "collections.remove_account": "Ta bort detta konto", + "collections.revoke_inclusion.error": "Ett fel uppstod, försök igen senare.", "collections.search_accounts_label": "Sök efter konton för att lägga till…", "collections.search_accounts_max_reached": "Du har lagt till maximalt antal konton", "collections.view_collection": "Visa samling", @@ -320,6 +394,7 @@ "confirmations.remove_from_followers.confirm": "Ta bort följare", "confirmations.remove_from_followers.message": "{name} kommer att sluta följa dig. Är du säker på att du vill fortsätta?", "confirmations.remove_from_followers.title": "Ta bort följare?", + "confirmations.revoke_collection_inclusion.confirm": "Ta bort mig", "confirmations.revoke_quote.confirm": "Ta bort inlägg", "confirmations.revoke_quote.message": "Denna åtgärd kan inte ångras.", "confirmations.revoke_quote.title": "Ta bort inlägg?", @@ -428,6 +503,10 @@ "featured_carousel.current": "Inlägg {current, number} / {max, number}", "featured_carousel.header": "{count, plural,one {Fäst inlägg} other {Fästa inlägg}}", "featured_carousel.slide": "Inlägg {current, number} av {max, number}", + "featured_tags.suggestions": "På senare tid har du skrivit om {items}. Lägg till dessa som utvalda hashtaggar?", + "featured_tags.suggestions.add": "Lägg till", + "featured_tags.suggestions.added": "Hantera dina utvalda hashtaggar när som helst under Redigera profil > Utvalda hashtaggar.", + "featured_tags.suggestions.dismiss": "Nej tack", "filter_modal.added.context_mismatch_explanation": "Denna filterkategori gäller inte för det sammanhang där du har tillgång till det här inlägget. Om du vill att inlägget ska filtreras även i detta sammanhang måste du redigera filtret.", "filter_modal.added.context_mismatch_title": "Misspassning av sammanhang!", "filter_modal.added.expired_explanation": "Denna filterkategori har utgått, du måste ändra utgångsdatum för att den ska kunna tillämpas.", @@ -783,17 +862,17 @@ "notifications_permission_banner.how_to_control": "För att ta emot aviseringar när Mastodon inte är öppet, aktivera skrivbordsaviseringar. När de är aktiverade kan du styra exakt vilka typer av interaktioner som aviseras via {icon} -knappen ovan.", "notifications_permission_banner.title": "Missa aldrig något", "onboarding.follows.back": "Tillbaka", - "onboarding.follows.done": "Färdig", "onboarding.follows.empty": "Tyvärr kan inga resultat visas just nu. Du kan prova att använda sökfunktionen eller utforska sidan för att hitta personer att följa, eller försök igen senare.", + "onboarding.follows.next": "Nästa: Ställ in din profil", "onboarding.follows.search": "Sök", "onboarding.follows.title": "Följ människor för att komma igång", "onboarding.profile.discoverable": "Gör min profil upptäckbar", "onboarding.profile.discoverable_hint": "När du väljer att vara upptäckbar på Mastodon kan dina inlägg visas i sök- och trendresultat, och din profil kan föreslås för personer med liknande intressen som du.", "onboarding.profile.display_name": "Visningsnamn", "onboarding.profile.display_name_hint": "Fullständigt namn eller ditt roliga namn…", + "onboarding.profile.finish": "Avsluta", "onboarding.profile.note": "Bio", "onboarding.profile.note_hint": "Du kan @nämna andra personer eller #hashtags…", - "onboarding.profile.save_and_continue": "Spara och fortsätt", "onboarding.profile.title": "Konfiguration av profil", "onboarding.profile.upload_avatar": "Ladda upp profilbild", "onboarding.profile.upload_header": "Ladda upp profilbanner", diff --git a/app/javascript/mastodon/locales/szl.json b/app/javascript/mastodon/locales/szl.json index 577a3e8a327f48..9f03864932df84 100644 --- a/app/javascript/mastodon/locales/szl.json +++ b/app/javascript/mastodon/locales/szl.json @@ -1,9 +1,9 @@ { "about.blocks": "Moderowane serwery", "about.contact": "Kōntakt:", - "about.disclaimer": "Mastodōn je wolnym a ôtwartozdrzōdłowym ôprogramowaniym ôraz znakiym towarowym ôd Mastodon gGmbH.", - "about.domain_blocks.no_reason_available": "Grund niydostympny", - "about.domain_blocks.preamble": "Mastodōn normalniy pozwŏlŏ na ôglōndaniy treściōw a interakcyje ze używŏczami inkszych serwerōw we fediverse, ale sōm ôd tygo wyjōntki, kere bōły poczyniōne na tym serwerze.", + "about.disclaimer": "Mastodon je wolne a ôtwartozdrzōdłowe ôprogramowanie i towarowy znak ôd Mastodon gGmbH.", + "about.domain_blocks.no_reason_available": "Brak prziczyny", + "about.domain_blocks.preamble": "Mastodon z wiynksza dŏwŏ ôglōndać treści i kōmunikować sie ze używŏczami inkszych serwerōw we fediverse. To sōm wyjōntki, co fungujōm na tym kōnkretnym serwerze.", "about.domain_blocks.silenced.explanation": "Normalniy niy bydziesz widzieć profilōw a treściōw ze tygo serwera. Ôboczysz je ino jak specjalniy bydziesz ich szukać abo jak je zaôbserwujesz.", "about.domain_blocks.silenced.title": "Ôgraniczone", "about.domain_blocks.suspended.explanation": "Żŏdne dane ze tygo serwera niy bydōm przetwarzane, przechowywane abo wymieniane, beztoż wszelakŏ interakcyjŏ abo komunikacyjŏ ze używŏczami tygo serwera bydzie niymożliwŏ.", @@ -15,16 +15,24 @@ "account.block": "Zablokuj @{name}", "account.block_domain": "Zablokuj domena {domain}", "account.cancel_follow_request": "Withdraw follow request", - "account.media": "Mydia", + "account.media": "Media", "account.mute": "Wycisz @{name}", - "account.posts": "Toots", - "account.posts_with_replies": "Toots and replies", - "account_note.placeholder": "Click to add a note", + "account.posts": "Posty", + "account.posts_with_replies": "Posty i ôdpowiedzi", + "account_note.placeholder": "Wybier, żeby przidac notka", + "column.bookmarks": "Zokłodki", + "column.direct": "Prywatne spōmniynia", + "column.favourites": "Spamiyntane", + "column.firehose": "Kanały na żywo", + "column.home": "Przodek", + "column.lists": "Wykazy", + "column.notifications": "Uwiadōmiynia", "column.pins": "Pinned toot", - "community.column_settings.media_only": "Media only", + "community.column_settings.media_only": "Ino media", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", - "compose_form.placeholder": "What is on your mind?", + "compose_form.placeholder": "Co nowego?", + "compose_form.publish": "Wyślij", "compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.spoiler.unmarked": "Text is not hidden", "confirmations.delete.message": "Are you sure you want to delete this status?", @@ -33,6 +41,11 @@ "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.", "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "explore.title": "Popularne", + "explore.trending_links": "Nowiny", + "explore.trending_statuses": "Posty", + "explore.trending_tags": "Etykety", + "followed_tags": "Torowane etykety", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.blocked": "to open blocked users list", @@ -63,7 +76,10 @@ "keyboard_shortcuts.toot": "to start a brand new toot", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.up": "to move up in the list", + "navigation_bar.bookmarks": "Zokłodki", "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.more": "Wiyncyj", + "navigation_bar.preferences": "Sztalōnki", "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "notification.reblog": "{name} boosted your status", "notifications.column_settings.status": "New toots:", @@ -72,6 +88,7 @@ "report.submit": "Submit report", "report.target": "Report {target}", "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", + "search.search_or_paste": "Szukej abo wraź URL", "search_results.statuses": "Toots", "sign_in_banner.sign_in": "Sign in", "status.admin_status": "Open this status in the moderation interface", @@ -81,5 +98,6 @@ "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", + "trends.trending_now": "Prawie popularne", "upload_progress.label": "Uploading…" } diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 52887b84515d3c..0377c38240798a 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -14,9 +14,15 @@ "about.powered_by": "สื่อสังคมแบบกระจายศูนย์ที่ขับเคลื่อนโดย {mastodon}", "about.rules": "กฎของเซิร์ฟเวอร์", "account.account_note_header": "หมายเหตุส่วนบุคคล", + "account.add_note": "เพิ่มโน้ตส่วนบุคคล", "account.add_or_remove_from_list": "เพิ่มหรือเอาออกจากรายการ", + "account.badges.admin": "แอดมิน", + "account.badges.blocked": "บล็อกอยู่", "account.badges.bot": "อัตโนมัติ", + "account.badges.domain_blocked": "โดเมนที่ถูกบล็อก", "account.badges.group": "กลุ่ม", + "account.badges.muted": "ปิดเสียงบัญชีแล้ว", + "account.badges.muted_until": "ปิดเสียงบัญชีจนถึง {until}", "account.block": "ปิดกั้น @{name}", "account.block_domain": "ปิดกั้นโดเมน {domain}", "account.block_short": "ปิดกั้น", @@ -27,10 +33,12 @@ "account.direct": "กล่าวถึง @{name} แบบส่วนตัว", "account.disable_notifications": "หยุดแจ้งเตือนฉันเมื่อ @{name} โพสต์", "account.domain_blocking": "กำลังปิดกั้นโดเมน", + "account.edit_note": "แก้ไขโน้ตส่วนบุคคล", "account.edit_profile": "แก้ไขโปรไฟล์", "account.edit_profile_short": "แก้ไข", "account.enable_notifications": "แจ้งเตือนฉันเมื่อ @{name} โพสต์", "account.endorse": "แสดงในโปรไฟล์", + "account.familiar_followers_many": "ติดตามโดย {name1}, {name2}, และ {othersCount, plural, one {อีกหนึ่งผู้ใช้ที่คุณรู้จัก} other {# ผู้ใช้อื่น ๆ ที่คุณรู้จัก}}", "account.familiar_followers_one": "ติดตามโดย {name1}", "account.familiar_followers_two": "ติดตามโดย {name1} และ {name2}", "account.featured": "น่าสนใจ", @@ -48,6 +56,7 @@ "account.followers": "ผู้ติดตาม", "account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้", "account.followers_counter": "{count, plural, other {{counter} ผู้ติดตาม}}", + "account.followers_you_know_counter": "{counter} ที่คุณรู้จัก", "account.following": "กำลังติดตาม", "account.following_counter": "{count, plural, other {{counter} กำลังติดตาม}}", "account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร", @@ -61,6 +70,7 @@ "account.locked_info": "มีการตั้งสถานะความเป็นส่วนตัวของบัญชีนี้เป็นล็อคอยู่ เจ้าของตรวจทานผู้ที่สามารถติดตามเขาด้วยตนเอง", "account.media": "สื่อ", "account.mention": "กล่าวถึง @{name}", + "account.menu.remove_follower": "ลบผู้ติดตาม", "account.moved_to": "{name} ได้ระบุว่าบัญชีใหม่ของเขาในตอนนี้คือ:", "account.mute": "ซ่อน @{name}", "account.mute_notifications_short": "ซ่อนการแจ้งเตือน", @@ -75,6 +85,7 @@ "account.remove_from_followers": "เอา {name} ออกจากผู้ติดตาม", "account.report": "รายงาน @{name}", "account.requested_follow": "{name} ได้ขอติดตามคุณ", + "account.requests_to_follow_you": "ส่งคำขอติดตามคุณ", "account.share": "แชร์โปรไฟล์ของ @{name}", "account.show_reblogs": "แสดงการดันจาก @{name}", "account.statuses_counter": "{count, plural, other {{counter} โพสต์}}", @@ -110,6 +121,7 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "อธิบายสิ่งนี้สำหรับผู้คนที่มีความบกพร่องทางการมองเห็น…", "alt_text_modal.done": "เสร็จสิ้น", "announcement.announcement": "ประกาศ", + "annual_report.summary.followers.new_followers": "{count, plural, other {ผู้ติดตามใหม่}}", "annual_report.summary.most_used_app.most_used_app": "แอปที่ใช้มากที่สุด", "annual_report.summary.most_used_hashtag.most_used_hashtag": "แฮชแท็กที่ใช้มากที่สุด", "annual_report.summary.new_posts.new_posts": "โพสต์ใหม่", @@ -144,6 +156,7 @@ "closed_registrations_modal.find_another_server": "ค้นหาเซิร์ฟเวอร์อื่น", "closed_registrations_modal.preamble": "Mastodon เป็นแบบกระจายศูนย์ ดังนั้นไม่ว่าคุณจะสร้างบัญชีของคุณที่ใด คุณจะสามารถติดตามและโต้ตอบกับใครก็ตามในเซิร์ฟเวอร์นี้ คุณยังสามารถโฮสต์บัญชีด้วยตนเองได้อีกด้วย!", "closed_registrations_modal.title": "การลงทะเบียนใน Mastodon", + "collections.accounts.empty_description": "สามารถเพิ่มได้สูงสุด {count} บัญชีที่คุณติดตาม", "column.about": "เกี่ยวกับ", "column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่", "column.bookmarks": "ที่คั่นหน้า", @@ -212,7 +225,11 @@ "confirmations.discard_draft.post.title": "ละทิ้งโพสต์แบบร่างของคุณ?", "confirmations.discard_edit_media.confirm": "ละทิ้ง", "confirmations.discard_edit_media.message": "คุณมีการเปลี่ยนแปลงคำอธิบายหรือตัวอย่างสื่อที่ยังไม่ได้บันทึก ละทิ้งการเปลี่ยนแปลงเหล่านั้นต่อไป?", + "confirmations.follow_to_collection.confirm": "ติดตามและเพิ่มไปยังคอลเล็กชัน", + "confirmations.follow_to_collection.message": "คุณต้องติดตาม {name} ถึงจะสามารถเพิ่มพวกเขาลงคอลเล็กชันได้", + "confirmations.follow_to_collection.title": "ติดตามบัญชีนี้ไหม?", "confirmations.follow_to_list.confirm": "ติดตามและเพิ่มไปยังรายการ", + "confirmations.follow_to_list.message": "คุณต้องติดตาม {name} ถึงจะสามารถเพิ่มพวกเขาลงลิสต์ได้", "confirmations.follow_to_list.title": "ติดตามผู้ใช้?", "confirmations.logout.confirm": "ออกจากระบบ", "confirmations.logout.message": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบ?", @@ -223,12 +240,15 @@ "confirmations.missing_alt_text.title": "เพิ่มข้อความแสดงแทน?", "confirmations.mute.confirm": "ซ่อน", "confirmations.private_quote_notify.confirm": "เผยแพร่โพสต์", + "confirmations.private_quote_notify.message": "คนที่คุณอ้างอิงและผู้ที่ถูกกล่าวถึงคนอื่น ๆ จะถูกแจ้งให้ทราบและพวกเขาจะสามารถดูโพสต์ของคุณได้ ถึงแม้พวกเขาจะไม่ได้ติดตามคุณอยู่", + "confirmations.private_quote_notify.title": "แชร์ให้กับผู้ติดตามและผู้ใช้ที่ถูกกล่าวถึงไหม?", "confirmations.quiet_post_quote_info.dismiss": "ไม่ต้องเตือนฉันอีก", "confirmations.quiet_post_quote_info.got_it": "เข้าใจแล้ว", "confirmations.redraft.confirm": "ลบแล้วร่างใหม่", "confirmations.redraft.message": "คุณแน่ใจหรือไม่ว่าต้องการลบโพสต์นี้แล้วร่างโพสต์ใหม่? รายการโปรดและการดันจะสูญหาย และการตอบกลับโพสต์ดั้งเดิมจะไม่มีความเกี่ยวพัน", "confirmations.redraft.title": "ลบแล้วร่างโพสต์ใหม่?", "confirmations.remove_from_followers.confirm": "เอาผู้ติดตามออก", + "confirmations.remove_from_followers.message": "{name} จะหยุดติดตามคุณ คุณแน่ใจไหมว่าจะดำเนินการต่อ", "confirmations.remove_from_followers.title": "เอาผู้ติดตามออก?", "confirmations.revoke_quote.confirm": "เอาโพสต์ออก", "confirmations.revoke_quote.title": "เอาโพสต์ออก?", @@ -316,6 +336,7 @@ "empty_column.notification_requests": "โล่งทั้งหมด! ไม่มีสิ่งใดที่นี่ เมื่อคุณได้รับการแจ้งเตือนใหม่ การแจ้งเตือนจะปรากฏที่นี่ตามการตั้งค่าของคุณ", "empty_column.notifications": "คุณยังไม่มีการแจ้งเตือนใด ๆ เมื่อผู้คนอื่น ๆ โต้ตอบกับคุณ คุณจะเห็นการแจ้งเตือนที่นี่", "empty_column.public": "ไม่มีสิ่งใดที่นี่! เขียนบางอย่างเป็นสาธารณะ หรือติดตามผู้ใช้จากเซิร์ฟเวอร์อื่น ๆ ด้วยตนเองเพื่อเติมเส้นเวลาให้เต็ม", + "error.no_hashtag_feed_access": "ลงทะเบียนหรือลงชื่อเข้าใช้เพื่อดูและติดตามแฮชแท็กนี้", "error.unexpected_crash.explanation": "เนื่องจากข้อบกพร่องในโค้ดของเราหรือปัญหาความเข้ากันได้ของเบราว์เซอร์ จึงไม่สามารถแสดงหน้านี้ได้อย่างถูกต้อง", "error.unexpected_crash.explanation_addons": "ไม่สามารถแสดงหน้านี้ได้อย่างถูกต้อง ข้อผิดพลาดนี้มีแนวโน้มว่าเกิดจากส่วนเสริมของเบราว์เซอร์หรือเครื่องมือการแปลอัตโนมัติ", "error.unexpected_crash.next_steps": "ลองรีเฟรชหน้า หากนั่นไม่ช่วย คุณอาจยังสามารถใช้ Mastodon ได้ผ่านเบราว์เซอร์อื่นหรือแอปเนทีฟ", @@ -371,6 +392,8 @@ "follow_suggestions.view_all": "ดูทั้งหมด", "follow_suggestions.who_to_follow": "ติดตามใครดี", "followed_tags": "แฮชแท็กที่ติดตาม", + "followers.hide_other_followers": "ผู้ใช้นี้เลือกที่จะไม่แสดงรายชื่อผู้ติดตามคนอื่น ๆ ของพวกเขา", + "following.hide_other_following": "ผู้ใช้นี้เลือกที่จะไม่แสดงรายชื่อคนอื่น ๆ ที่พวกเขาติดตาม", "footer.about": "เกี่ยวกับ", "footer.about_this_server": "เกี่ยวกับ", "footer.directory": "ไดเรกทอรีโปรไฟล์", @@ -551,7 +574,9 @@ "navigation_bar.privacy_and_reach": "ความเป็นส่วนตัวและการเข้าถึง", "navigation_bar.search": "ค้นหา", "navigation_bar.search_trends": "ค้นหา / กำลังนิยม", + "navigation_panel.collapse_followed_tags": "ยุบเมนูแฮชแท็กที่ติดตามอยู่", "navigation_panel.collapse_lists": "ยุบเมนูรายการ", + "navigation_panel.expand_followed_tags": "ขยายเมนูแฮชแท็กที่ติดตามอยู่", "navigation_panel.expand_lists": "ขยายเมนูรายการ", "not_signed_in_indicator.not_signed_in": "คุณจำเป็นต้องเข้าสู่ระบบเพื่อเข้าถึงทรัพยากรนี้", "notification.admin.report": "{name} ได้รายงาน {target}", @@ -674,8 +699,8 @@ "notifications_permission_banner.how_to_control": "เพื่อรับการแจ้งเตือนเมื่อ Mastodon ไม่ได้เปิด เปิดใช้งานการแจ้งเตือนบนเดสก์ท็อป คุณสามารถควบคุมชนิดของการโต้ตอบที่สร้างการแจ้งเตือนบนเดสก์ท็อปได้อย่างแม่นยำผ่านปุ่ม {icon} ด้านบนเมื่อเปิดใช้งานการแจ้งเตือน", "notifications_permission_banner.title": "ไม่พลาดสิ่งใด", "onboarding.follows.back": "ย้อนกลับ", - "onboarding.follows.done": "เสร็จสิ้น", "onboarding.follows.empty": "น่าเสียดาย ไม่สามารถแสดงผลลัพธ์ได้ในตอนนี้ คุณสามารถลองใช้การค้นหาหรือเรียกดูหน้าสำรวจเพื่อค้นหาผู้คนที่จะติดตาม หรือลองอีกครั้งในภายหลัง", + "onboarding.follows.next": "ต่อไป: ตั้งค่าโปรไฟล์ของคุณ", "onboarding.follows.search": "ค้นหา", "onboarding.follows.title": "ติดตามผู้คนเพื่อเริ่มต้นใช้งาน", "onboarding.profile.discoverable": "ทำให้โปรไฟล์ของฉันสามารถค้นพบได้", @@ -684,7 +709,6 @@ "onboarding.profile.display_name_hint": "ชื่อเต็มของคุณหรือชื่อแบบสนุกสนานของคุณ…", "onboarding.profile.note": "ชีวประวัติ", "onboarding.profile.note_hint": "คุณสามารถ @กล่าวถึง ผู้คนอื่น ๆ หรือ #แฮชแท็ก…", - "onboarding.profile.save_and_continue": "บันทึกและดำเนินการต่อ", "onboarding.profile.title": "การตั้งค่าโปรไฟล์", "onboarding.profile.upload_avatar": "อัปโหลดรูปภาพโปรไฟล์", "onboarding.profile.upload_header": "อัปโหลดส่วนหัวโปรไฟล์", @@ -853,9 +877,11 @@ "status.open": "ขยายโพสต์นี้", "status.pin": "ปักหมุดในโปรไฟล์", "status.quote_error.limited_account_hint.action": "แสดงต่อไป", + "status.quote_followers_only": "เฉพาะผู้ติดตามเท่านั้นที่สามารถอ้างอิงโพสต์นี้ได้", "status.quote_post_author": "อ้างอิงโพสต์โดย @{name}", "status.read_more": "อ่านเพิ่มเติม", "status.reblog": "ดัน", + "status.reblog_private": "แชร์อีกครั้งกับผู้ติดตามของคุณ", "status.reblogged_by": "{name} ได้ดัน", "status.reblogs.empty": "ยังไม่มีใครดันโพสต์นี้ เมื่อใครสักคนดัน เขาจะปรากฏที่นี่", "status.redraft": "ลบแล้วร่างใหม่", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ea92132687c214..9f93fbf24b9898 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -28,12 +28,12 @@ "account.block_domain": "{domain} alan adını engelle", "account.block_short": "Engelle", "account.blocked": "Engellendi", - "account.blocking": "Engelleme", - "account.cancel_follow_request": "Takip isteğini geri çek", - "account.copy": "Gönderi bağlantısını kopyala", + "account.blocking": "Engelli", + "account.cancel_follow_request": "Takibi bırak", + "account.copy": "Profil bağlantısını kopyala", "account.direct": "@{name} kullanıcısından özel olarak bahset", "account.disable_notifications": "@{name} kişisinin gönderi bildirimlerini kapat", - "account.domain_blocking": "Alan adını engelleme", + "account.domain_blocking": "Alan adını engelle", "account.edit_note": "Kişisel notu düzenle", "account.edit_profile": "Profili düzenle", "account.edit_profile_short": "Düzenle", @@ -73,7 +73,6 @@ "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", @@ -153,6 +152,7 @@ "account_edit.column_title": "Profili Düzenle", "account_edit.custom_fields.name": "alan", "account_edit.custom_fields.placeholder": "Zamirlerinizi, harici bağlantılarınızı veya paylaşmak istediğiniz diğer bilgileri ekleyin.", + "account_edit.custom_fields.reorder_button": "Alanları yeniden sırala", "account_edit.custom_fields.tip_content": "Sahip olduğunuz web sitelerine bağlantıları doğrulayarak Mastodon hesabınıza kolayca güvenilirlik katabilirsiniz.", "account_edit.custom_fields.tip_title": "İpucu: Doğrulanmış bağlantılar ekleme", "account_edit.custom_fields.title": "Özel alanlar", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Özel alanı sil?", "account_edit.field_edit_modal.add_title": "Özel alan ekle", "account_edit.field_edit_modal.edit_title": "Özel alanı düzenle", + "account_edit.field_edit_modal.limit_header": "Önerilen karakter sınırı aşıldı", + "account_edit.field_edit_modal.limit_message": "Mobil cihaz kullanıcıları sahayı tam olarak görmeyebilir.", + "account_edit.field_edit_modal.link_emoji_warning": "Url'lerle birlikte özel emoji kullanmamanızı öneririz. Her ikisini de içeren özel alanlar, kullanıcıların kafasını karıştırmamak için bağlantı yerine yalnızca metin olarak görüntülenir.", "account_edit.field_edit_modal.name_hint": "Örn. \"Kişisel web sitesi\"", "account_edit.field_edit_modal.name_label": "Etiket", - "account_edit.field_edit_modal.value_hint": "Örn. \"example.me\"", + "account_edit.field_edit_modal.url_warning": "Bağlantı eklemek için lütfen başlangıca {protocol} ekleyin.", + "account_edit.field_edit_modal.value_hint": "Örn. \"https://example.me\"", "account_edit.field_edit_modal.value_label": "Değer", + "account_edit.field_reorder_modal.drag_cancel": "Sürükleme iptal edildi. \"{item}\" alanı bırakıldı.", + "account_edit.field_reorder_modal.drag_end": "\"{item}\" alanı bırakıldı.", + "account_edit.field_reorder_modal.drag_instructions": "Özel alanları yeniden düzenlemek için boşluk tuşuna veya Enter tuşuna basın. Sürüklerken, ok tuşlarını kullanarak alanı yukarı veya aşağı taşıyın. Alanı yeni konumuna bırakmak için boşluk tuşuna veya Enter tuşuna tekrar basın veya iptal etmek için Escape tuşuna basın.", + "account_edit.field_reorder_modal.drag_move": "\"{item}\" alanı hareket ettirildi.", + "account_edit.field_reorder_modal.drag_over": "\"{item}\" alanı \"{over}\" üzerine hareket ettirildi.", + "account_edit.field_reorder_modal.drag_start": "\"{item}\" alanı seçildi.", + "account_edit.field_reorder_modal.handle_label": "\"{item}\" alanını sürükle", + "account_edit.field_reorder_modal.title": "Alanları yeniden düzenle", + "account_edit.image_alt_modal.add_title": "Alternatif metin ekle", + "account_edit.image_alt_modal.details_content": "Yapılması gerekenler:
    • Kendinizi resimdeki gibi tanımlayın
    • Üçüncü şahıs zamirleri kullanın (ör. “ben” yerine “Alex”)
    • Kısa ve öz olun – genellikle birkaç kelime yeterlidir
    Yapılmaması gerekenler:
    • “Fotoğrafında” ifadesiyle başlamayın – bu, ekran okuyucular için gereksizdir
    Örnek:
    • \"Yeşil gömlek ve gözlük takan Alex
    ", + "account_edit.image_alt_modal.details_title": "İpuçları: Profil fotoğrafları için alternatif metinler", + "account_edit.image_alt_modal.edit_title": "Alternatif metni düzenle", + "account_edit.image_alt_modal.text_hint": "Alternatif metin ekran okuyucu kullanan kullanıcıların içeriği anlamasına yardımcı olur.", + "account_edit.image_alt_modal.text_label": "Alternatif metin", + "account_edit.image_delete_modal.confirm": "Bu görseli silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", + "account_edit.image_delete_modal.delete_button": "Sil", + "account_edit.image_delete_modal.title": "Resim silinsin mi?", + "account_edit.image_edit.add_button": "Görsel ekle", + "account_edit.image_edit.alt_add_button": "Alternatif metin ekle", + "account_edit.image_edit.alt_edit_button": "Alternatif metni düzenle", + "account_edit.image_edit.remove_button": "Görseli kaldır", + "account_edit.image_edit.replace_button": "Görseli değiştir", "account_edit.name_modal.add_title": "Görünen ad ekle", "account_edit.name_modal.edit_title": "Görünen adı düzenle", "account_edit.profile_tab.button_label": "Özelleştir", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Profilinizdeki sekmeleri ve bunların görüntülediği bilgileri özelleştirin.", "account_edit.profile_tab.title": "Profil sekme ayarları", "account_edit.save": "Kaydet", + "account_edit.upload_modal.back": "Geri", + "account_edit.upload_modal.done": "Tamamlandı", + "account_edit.upload_modal.next": "İleri", + "account_edit.upload_modal.step_crop.zoom": "Yakınlaştır", + "account_edit.upload_modal.step_upload.button": "Dosyalara göz at", + "account_edit.upload_modal.step_upload.dragging": "Yüklemek için bırakın", + "account_edit.upload_modal.step_upload.header": "Bir resim seç", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF veya JPG formatında, en fazla {limit} MB.{br}Görsel {width}x{height} piksel boyutuna getirilir.", + "account_edit.upload_modal.title_add": "Profil fotoğrafı ekle", + "account_edit.upload_modal.title_replace": "Profil fotoğrafını değiştir", "account_edit.verified_modal.details": "Kişisel web sitelerine bağlantıları doğrulayarak Mastodon profilinize güvenilirlik katın. İşte böyle çalışıyor:", "account_edit.verified_modal.invisible_link.details": "Bağlantıyı başlığınıza ekleyin. Önemli olan kısım, kullanıcı tarafından oluşturulan içeriğe sahip web sitelerinde kimlik sahtekarlığını önleyen rel=\"me\" özniteliğidir. {tag} yerine sayfanın başlığında bir bağlantı etiketi bile kullanabilirsiniz, ancak HTML, JavaScript çalıştırılmadan erişilebilir olmalıdır.", "account_edit.verified_modal.invisible_link.summary": "Bağlantıyı nasıl görünmez hale getirebilirim?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Takip ettiğiniz hesapların sayısını {count} kadar artırın", "collections.accounts.empty_title": "Bu koleksiyon boş", "collections.collection_description": "Açıklama", + "collections.collection_language": "Dil", + "collections.collection_language_none": "Hiçbiri", "collections.collection_name": "Ad", "collections.collection_topic": "Konu", "collections.confirm_account_removal": "Bu hesabı bu koleksiyondan çıkarmak istediğinizden emin misiniz?", @@ -326,10 +364,15 @@ "collections.create_collection": "Koleksiyon oluştur", "collections.delete_collection": "Koleksiyonu sil", "collections.description_length_hint": "100 karakterle sınırlı", + "collections.detail.accept_inclusion": "Tamam", "collections.detail.accounts_heading": "Hesaplar", + "collections.detail.author_added_you": "{author} sizi koleksiyonuna ekledi", "collections.detail.curated_by_author": "{author} tarafından derlenen", "collections.detail.curated_by_you": "Sizin derledikleriniz", "collections.detail.loading": "Koleksiyon yükleniyor…", + "collections.detail.other_accounts_in_collection": "Bu koleksiyondaki diğer kişiler:", + "collections.detail.revoke_inclusion": "Beni çıkar", + "collections.detail.sensitive_note": "Bu koleksiyon bazı kullanıcılar için hassas olabilecek hesap ve içerik içerebilir.", "collections.detail.share": "Bu koleksiyonu paylaş", "collections.edit_details": "Ayrıntıları düzenle", "collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.", @@ -344,10 +387,14 @@ "collections.old_last_post_note": "Son gönderi bir haftadan önce", "collections.remove_account": "Bu hesabı çıkar", "collections.report_collection": "Bu koleksiyonu bildir", + "collections.revoke_collection_inclusion": "Beni bu koleksiyondan çıkar", + "collections.revoke_inclusion.confirmation": "\"{collection}\" koleksiyonundan çıkarıldınız", + "collections.revoke_inclusion.error": "Bir hata oluştu, lütfen daha sonra tekrar deneyin.", "collections.search_accounts_label": "Eklemek için hesap arayın…", "collections.search_accounts_max_reached": "Maksimum hesabı eklediniz", "collections.sensitive": "Hassas", "collections.topic_hint": "Bu koleksiyonun ana konusunu başkalarının anlamasına yardımcı olacak bir etiket ekleyin.", + "collections.topic_special_chars_hint": "Kaydederken özel karakterler silinecektir", "collections.view_collection": "Koleksiyonu görüntüle", "collections.view_other_collections_by_user": "Bu kullanıcının diğer koleksiyonlarını görüntüle", "collections.visibility_public": "Herkese açık", @@ -467,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Takipçi kaldır", "confirmations.remove_from_followers.message": "{name} sizi takip etmeyi bırakacaktır. Devam etmek istediğinize emin misiniz?", "confirmations.remove_from_followers.title": "Takipçiyi kaldır?", + "confirmations.revoke_collection_inclusion.confirm": "Beni çıkar", + "confirmations.revoke_collection_inclusion.message": "Bu eylem kalıcıdır ve koleksiyonu derleyen kişi daha sonra sizi koleksiyona tekrar ekleyemeyecektir.", + "confirmations.revoke_collection_inclusion.title": "Kendini bu koleksiyondan çıkar?", "confirmations.revoke_quote.confirm": "Gönderiyi kaldır", "confirmations.revoke_quote.message": "Bu işlem geri alınamaz.", "confirmations.revoke_quote.title": "Gönderiyi silmek ister misiniz?", @@ -578,6 +628,10 @@ "featured_carousel.header": "{count, plural, one {{counter} Sabitlenmiş Gönderi} other {{counter} Sabitlenmiş Gönderi}}", "featured_carousel.slide": "Gönderi {current, number} / {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Son zamanlarda {items} hakkında gönderileriniz var. Bunlar öne çıkan etiketler olarak eklensin mi?", + "featured_tags.suggestions.add": "Ekle", + "featured_tags.suggestions.added": "Öne çıkan etiketlerinizi istediğiniz zaman Profil Düzenle > Öne çıkan etiketler bölümünden yönetebilirsiniz.", + "featured_tags.suggestions.dismiss": "Hayır teşekkürler", "filter_modal.added.context_mismatch_explanation": "Bu süzgeç kategorisi, bu gönderide eriştiğin bağlama uymuyor. Eğer gönderinin bu bağlamda da filtrelenmesini istiyorsanız, süzgeci düzenlemeniz gerekiyor.", "filter_modal.added.context_mismatch_title": "Bağlam uyumsuzluğu!", "filter_modal.added.expired_explanation": "Bu süzgeç kategorisinin süresi dolmuş, süzgeci uygulamak için bitiş tarihini değiştirmeniz gerekiyor.", @@ -936,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Mastodon açık olmadığında bildirim almak için masaüstü bildirimlerini etkinleştirin. Etkinleştirildikten sonra, yukarıdaki{icon} düğmesi aracılığıyla hangi etkileşim türlerinin masaüstü bildirimi oluşturacağını tam olarak kontrol edebilirsiniz.", "notifications_permission_banner.title": "Hiçbir şeyi kaçırmayın", "onboarding.follows.back": "Geri", - "onboarding.follows.done": "Tamamlandı", "onboarding.follows.empty": "Maalesef şu an bir sonuç gösterilemiyor. Takip edilecek kişileri bulmak için arama veya keşfet sayfasına gözatmayı kullanabilirsiniz veya daha sonra tekrar deneyin.", + "onboarding.follows.next": "Sonraki: Profilinizi ayarlayın", "onboarding.follows.search": "Ara", "onboarding.follows.title": "Başlamak için insanları takip edin", "onboarding.profile.discoverable": "Profilimi keşfedilebilir yap", "onboarding.profile.discoverable_hint": "Mastodon'da keşfedilebilirliği etkinleştirdiğinizde, gönderileriniz arama sonuçlarında ve trendlerde görünebilir aynı zamanda profiliniz sizinle benzer ilgi alanlarına sahip kişilere önerilebilir.", "onboarding.profile.display_name": "Görünen isim", "onboarding.profile.display_name_hint": "Tam adınız veya kullanıcı adınız…", + "onboarding.profile.finish": "Tamamla", "onboarding.profile.note": "Kişisel bilgiler", "onboarding.profile.note_hint": "Diğer insanlara @değinebilir veya #etiketler kullanabilirsiniz…", - "onboarding.profile.save_and_continue": "Kaydet ve ilerle", "onboarding.profile.title": "Profilini ayarla", "onboarding.profile.upload_avatar": "Profil resmi yükle", "onboarding.profile.upload_header": "Profil başlığı yükle", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 3aa940e9524e13..67fe49725ae640 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -68,7 +68,6 @@ "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}", @@ -739,7 +738,6 @@ "notifications_permission_banner.how_to_control": "Щоб отримувати сповіщення, коли Mastodon не відкрито, увімкніть сповіщення стільниці. Ви можете контролювати, які типи взаємодій створюють сповіщення через кнопку {icon} вгорі після їхнього увімкнення.", "notifications_permission_banner.title": "Не проґавте нічого", "onboarding.follows.back": "Назад", - "onboarding.follows.done": "Готово", "onboarding.follows.empty": "На жаль, жоден результат не може бути показаний просто зараз. Ви можете спробувати скористатися пошуком або переглядом сторінки огляду, щоб знайти людей для слідкування або повторіть спробу пізніше.", "onboarding.follows.search": "Пошук", "onboarding.follows.title": "Слідкуйте за людьми, щоб почати", @@ -749,7 +747,6 @@ "onboarding.profile.display_name_hint": "Ваше повне ім'я або ваш псевдонім…", "onboarding.profile.note": "Біографія", "onboarding.profile.note_hint": "Ви можете @згадувати інших людей або #гештеґи…", - "onboarding.profile.save_and_continue": "Зберегти і продовжити", "onboarding.profile.title": "Налаштування профілю", "onboarding.profile.upload_avatar": "Завантажити зображення профілю", "onboarding.profile.upload_header": "Завантажити заголовок профілю", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index c2a73a77e45ab3..bbaae71d13e39e 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -73,7 +73,6 @@ "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}", @@ -153,6 +152,7 @@ "account_edit.column_title": "Sửa hồ sơ", "account_edit.custom_fields.name": "trường", "account_edit.custom_fields.placeholder": "Thêm nghề nghiệp, liên kết ngoài hoặc bất kỳ gì mà bạn muốn.", + "account_edit.custom_fields.reorder_button": "Sắp xếp trường", "account_edit.custom_fields.tip_content": "Bạn có thể dễ dàng tăng độ tin cậy cho tài khoản Mastodon của mình bằng cách xác minh liên kết đến bất kỳ trang web nào bạn sở hữu.", "account_edit.custom_fields.tip_title": "Mẹo: Thêm liên kết xác minh", "account_edit.custom_fields.title": "Trường tùy chỉnh", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "Xóa trường tùy chỉnh?", "account_edit.field_edit_modal.add_title": "Thêm trường tùy chỉnh", "account_edit.field_edit_modal.edit_title": "Sửa trường tùy chỉnh", + "account_edit.field_edit_modal.limit_header": "Đã vượt giới hạn ký tự đề xuất", + "account_edit.field_edit_modal.limit_message": "Người dùng di động sẽ không thể thấy đầy đủ trường.", + "account_edit.field_edit_modal.link_emoji_warning": "Không nên dùng emoji tùy chỉnh với url. Trường tùy chỉnh chứa cả hai sẽ chỉ hiển thị văn bản, để ngăn chặn việc bối rối.", "account_edit.field_edit_modal.name_hint": "Vd: “Website cá nhân”", "account_edit.field_edit_modal.name_label": "Nhãn", - "account_edit.field_edit_modal.value_hint": "Vd: “example.me”", + "account_edit.field_edit_modal.url_warning": "Để thêm một liên kết, vui lòng đặt {protocol} ở đầu.", + "account_edit.field_edit_modal.value_hint": "Vd: “https://example.me”", "account_edit.field_edit_modal.value_label": "Giá trị", + "account_edit.field_reorder_modal.drag_cancel": "Đã hủy kéo. Trường \"{item}\" đã được thả.", + "account_edit.field_reorder_modal.drag_end": "Trường \"{item}\" đã được thả.", + "account_edit.field_reorder_modal.drag_instructions": "Để sắp xếp lại các trường tùy chỉnh, nhấn phím cách hoặc phím Enter. Trong khi kéo, sử dụng các phím mũi tên để di chuyển trường lên hoặc xuống. Nhấn phím cách hoặc phím Enter một lần nữa để thả trường vào vị trí mới, hoặc nhấn phím Escape để hủy bỏ.", + "account_edit.field_reorder_modal.drag_move": "Trường \"{item}\" đã được di chuyển.", + "account_edit.field_reorder_modal.drag_over": "Trường \"{item}\" đã được di chuyển lên trên \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Đã chọn trường \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Kéo trường \"{item}\"", + "account_edit.field_reorder_modal.title": "Sắp xếp lại trường", + "account_edit.image_alt_modal.add_title": "Thêm văn bản thay thế", + "account_edit.image_alt_modal.details_content": "NÊN:
    • Mô tả bản thân bạn trong hình
    • Dùng ngôn ngữ góc nhìn thứ ba (ví dụ “Alex” thay vì “tôi”)
    • Súc tích – một vài từ là đủ
    KHÔNG NÊN:
    • Bắt đầu bằng “Hình ảnh của” – điều này là thừa đối với trình đọc màn hình.
    VÍ DỤ:
    • “Alex mặc áo xanh và đeo kính”
    ", + "account_edit.image_alt_modal.details_title": "Gợi ý: Văn bản thay thế của ảnh đại diện", + "account_edit.image_alt_modal.edit_title": "Sửa văn bản thay thế", + "account_edit.image_alt_modal.text_hint": "Văn bản thay thế giúp người dùng phần mềm đọc màn hình hiểu được nội dung của bạn.", + "account_edit.image_alt_modal.text_label": "Văn bản thay thế", + "account_edit.image_delete_modal.confirm": "Bạn có chắc chắn muốn xóa hình ảnh này? Hành động này không thể hoàn tác.", + "account_edit.image_delete_modal.delete_button": "Xóa", + "account_edit.image_delete_modal.title": "Xóa hình ảnh?", + "account_edit.image_edit.add_button": "Thêm ảnh", + "account_edit.image_edit.alt_add_button": "Thêm văn bản thay thế", + "account_edit.image_edit.alt_edit_button": "Sửa văn bản thay thế", + "account_edit.image_edit.remove_button": "Gỡ ảnh", + "account_edit.image_edit.replace_button": "Thay thế ảnh", "account_edit.name_modal.add_title": "Thêm tên gọi", "account_edit.name_modal.edit_title": "Sửa tên gọi", "account_edit.profile_tab.button_label": "Tùy chỉnh", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Tùy chỉnh tab trên hồ sơ của bạn và những gì chúng hiển thị.", "account_edit.profile_tab.title": "Thiết lập tab hồ sơ", "account_edit.save": "Lưu", + "account_edit.upload_modal.back": "Quay lại", + "account_edit.upload_modal.done": "Xong", + "account_edit.upload_modal.next": "Kế tiếp", + "account_edit.upload_modal.step_crop.zoom": "Phóng to", + "account_edit.upload_modal.step_upload.button": "Duyệt tệp", + "account_edit.upload_modal.step_upload.dragging": "Thả để tải lên", + "account_edit.upload_modal.step_upload.header": "Chọn ảnh", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF hoặc JPG, tối đa {limit}MB.{br}Sẽ bị thu xuống {width}x{height}px.", + "account_edit.upload_modal.title_add": "Thêm ảnh đại diện", + "account_edit.upload_modal.title_replace": "Thay thế ảnh đại diện", "account_edit.verified_modal.details": "Tăng độ tin cậy cho hồ sơ Mastodon của bạn bằng cách xác minh liên kết đến trang web cá nhân. Cách thức thực hiện như sau:", "account_edit.verified_modal.invisible_link.details": "Thêm liên kết trên header của trang. Quan trọng là rel=\"me\" giúp ngăn chặn việc mạo danh trên các trang web có nội dung do người dùng tạo. Bạn cũng có thể sử dụng một thẻ link thay vì {tag}, nhưng HTML phải có thể truy cập được mà không cần thực thi JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Làm thế nào để ẩn liên kết?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "Thêm tối đa {count} tài khoản mà bạn theo dõi", "collections.accounts.empty_title": "Gói khởi đầu này trống", "collections.collection_description": "Mô tả", + "collections.collection_language": "Ngôn ngữ", + "collections.collection_language_none": "Không", "collections.collection_name": "Tên", "collections.collection_topic": "Chủ đề", "collections.confirm_account_removal": "Bạn có chắc muốn gỡ tài khoản này khỏi gói khởi đầu?", @@ -326,12 +364,14 @@ "collections.create_collection": "Tạo gói khởi đầu", "collections.delete_collection": "Xóa gói khởi đầu", "collections.description_length_hint": "Giới hạn 100 ký tự", + "collections.detail.accept_inclusion": "Okay", "collections.detail.accounts_heading": "Tài khoản", "collections.detail.author_added_you": "{author} đã thêm bạn vào gói khởi đầu này", "collections.detail.curated_by_author": "Tuyển chọn bởi {author}", "collections.detail.curated_by_you": "Tuyển chọn bởi bạn", "collections.detail.loading": "Đang tải gói khởi đầu…", "collections.detail.other_accounts_in_collection": "Những người khác trong gói khởi đầu này:", + "collections.detail.revoke_inclusion": "Xóa tôi", "collections.detail.sensitive_note": "Gói khởi đầu này chứa các tài khoản và nội dung có thể nhạy cảm đối với một số người.", "collections.detail.share": "Chia sẻ gói khởi đầu này", "collections.edit_details": "Sửa chi tiết", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "Đăng lần cuối hơn một tuần trước", "collections.remove_account": "Gỡ tài khoản này", "collections.report_collection": "Báo cáo gói khởi đầu này", + "collections.revoke_collection_inclusion": "Xóa tôi khỏi gói khởi đầu này", + "collections.revoke_inclusion.confirmation": "Bạn đã được gỡ khỏi \"{collection}\"", + "collections.revoke_inclusion.error": "Đã có lỗi, xin vui lòng thử lại.", "collections.search_accounts_label": "Tìm tài khoản để thêm…", "collections.search_accounts_max_reached": "Bạn đã đạt đến số lượng tài khoản tối đa", "collections.sensitive": "Nhạy cảm", "collections.topic_hint": "Thêm hashtag giúp người khác hiểu chủ đề chính của gói khởi đầu này.", + "collections.topic_special_chars_hint": "Những ký tự đặc biệt sẽ bị xóa khi lưu", "collections.view_collection": "Xem gói khởi đầu", "collections.view_other_collections_by_user": "Xem những gói khởi đầu khác từ tài khoản này", "collections.visibility_public": "Công khai", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "Xóa người theo dõi", "confirmations.remove_from_followers.message": "{name} sẽ không còn theo dõi bạn.Bạn có chắc tiếp tục?", "confirmations.remove_from_followers.title": "Xóa người theo dõi?", + "confirmations.revoke_collection_inclusion.confirm": "Xóa tôi", + "confirmations.revoke_collection_inclusion.message": "Thao tác này là vĩnh viễn, người tuyển chọn sẽ không thể thêm lại bạn về sau này.", + "confirmations.revoke_collection_inclusion.title": "Xóa bạn khỏi gói khởi đầu này?", "confirmations.revoke_quote.confirm": "Gỡ tút", "confirmations.revoke_quote.message": "Hành động này không thể hoàn tác.", "confirmations.revoke_quote.title": "Gỡ tút?", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, other {Tút đã ghim}}", "featured_carousel.slide": "Tút {current, number} trong {max, number}", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "Gần đây bạn đã đăng về {items}. Thêm cái này vào hashtag thường dùng?", + "featured_tags.suggestions.add": "Thêm", + "featured_tags.suggestions.added": "Quản lý hashtag bạn thường dùng vào bất cứ lúc nào tại Sửa hồ sơ > Hashtag thường dùng.", + "featured_tags.suggestions.dismiss": "Không, cảm ơn", "filter_modal.added.context_mismatch_explanation": "Danh mục bộ lọc này không áp dụng cho ngữ cảnh mà bạn đã truy cập tút này. Nếu bạn muốn tút cũng được lọc trong ngữ cảnh này, bạn sẽ phải chỉnh sửa bộ lọc.", "filter_modal.added.context_mismatch_title": "Bối cảnh không phù hợp!", "filter_modal.added.expired_explanation": "Danh mục bộ lọc này đã hết hạn, bạn sẽ cần thay đổi ngày hết hạn để áp dụng.", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "Hãy bật thông báo trên màn hình để không bỏ lỡ những thông báo từ Mastodon. Sau khi bật, bạn có thể lựa chọn từng loại thông báo khác nhau bằng nút {icon} bên dưới.", "notifications_permission_banner.title": "Không bỏ lỡ điều thú vị nào", "onboarding.follows.back": "Quay lại", - "onboarding.follows.done": "Xong", "onboarding.follows.empty": "Không có kết quả có thể được hiển thị lúc này. Bạn có thể thử sử dụng tính năng tìm kiếm hoặc duyệt qua trang khám phá để tìm những người theo dõi hoặc thử lại sau.", + "onboarding.follows.next": "Tiếp theo: Thiết lập tài khoản", "onboarding.follows.search": "Tìm kiếm", "onboarding.follows.title": "Tìm người để theo dõi", "onboarding.profile.discoverable": "Bật khám phá cho hồ sơ của tôi", "onboarding.profile.discoverable_hint": "Khi bạn bật khám phá trên Mastodon, các tút của bạn có thể xuất hiện trong kết quả tìm kiếm và xu hướng, đồng thời hồ sơ của bạn sẽ được đề xuất cho những người có cùng sở thích với bạn.", "onboarding.profile.display_name": "Tên gọi", "onboarding.profile.display_name_hint": "Tên thật hay biệt danh đều được…", + "onboarding.profile.finish": "Hoàn thành", "onboarding.profile.note": "Giới thiệu", "onboarding.profile.note_hint": "Bạn có thể @aiđó hoặc #hashtags…", - "onboarding.profile.save_and_continue": "Lưu và tiếp tục", "onboarding.profile.title": "Thiết lập hồ sơ", "onboarding.profile.upload_avatar": "Tải lên ảnh đại diện", "onboarding.profile.upload_header": "Tải lên ảnh bìa", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index d8d2dd76b4ecd6..c49c882c29d635 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -73,7 +73,6 @@ "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} 检查", @@ -151,13 +150,53 @@ "account_edit.button.edit": "编辑 {item}", "account_edit.column_button": "完成", "account_edit.column_title": "修改个人资料", + "account_edit.custom_fields.name": "字段", "account_edit.custom_fields.placeholder": "添加你的人称代词、外部链接,或其他你想分享的内容。", + "account_edit.custom_fields.reorder_button": "重新排序字段", + "account_edit.custom_fields.tip_content": "通过验证任意你拥有网站的链接,你可以轻松增加 Mastodon 账号的可信度。", + "account_edit.custom_fields.tip_title": "小贴士:添加已验证的链接", "account_edit.custom_fields.title": "自定义字段", + "account_edit.custom_fields.verified_hint": "我如何添加已验证的链接?", "account_edit.display_name.placeholder": "你的显示名称是指你的名字在个人资料及时间线上出现时的样子。", "account_edit.display_name.title": "显示名称", "account_edit.featured_hashtags.item": "话题标签", "account_edit.featured_hashtags.placeholder": "帮助其他人认识并快速访问你最喜欢的话题。", "account_edit.featured_hashtags.title": "精选话题标签", + "account_edit.field_delete_modal.confirm": "你确定要删除此自定义字段吗?此操作无法撤消。", + "account_edit.field_delete_modal.delete_button": "删除", + "account_edit.field_delete_modal.title": "删除自定义字段?", + "account_edit.field_edit_modal.add_title": "添加自定义字段", + "account_edit.field_edit_modal.edit_title": "编辑自定义字段", + "account_edit.field_edit_modal.limit_header": "已超过建议字数限制", + "account_edit.field_edit_modal.limit_message": "移动端用户可能无法完整看见字段内容。", + "account_edit.field_edit_modal.link_emoji_warning": "我们建议不要同时使用自定义表情和网址。同时包含两者的自定义字段将会显示为纯文本而不是链接形式,以避免用户混淆。", + "account_edit.field_edit_modal.name_hint": "例如:“个人网站”", + "account_edit.field_edit_modal.name_label": "标签", + "account_edit.field_edit_modal.url_warning": "要添加链接,请在开头加上 {protocol}。", + "account_edit.field_edit_modal.value_hint": "例如:“https://example.me”", + "account_edit.field_edit_modal.value_label": "值", + "account_edit.field_reorder_modal.drag_cancel": "拖拽已终止。字段“{item}”已被丢弃。", + "account_edit.field_reorder_modal.drag_end": "字段“{item}”已被丢弃。", + "account_edit.field_reorder_modal.drag_instructions": "要重新排列自定义字段,请按空格键或回车键。在拖拽时,使用方向键将字段向上或向下移动。再次按空格键或回车键可将字段放置在新位置,或按 Esc 键取消。", + "account_edit.field_reorder_modal.drag_move": "字段“{item}”已被移动。", + "account_edit.field_reorder_modal.drag_over": "字段“{item}”已被移动到“{over}”上方。", + "account_edit.field_reorder_modal.drag_start": "已选中字段“{item}”。", + "account_edit.field_reorder_modal.handle_label": "拖拽字段“{item}”", + "account_edit.field_reorder_modal.title": "重新排列字段", + "account_edit.image_alt_modal.add_title": "添加替代文本", + "account_edit.image_alt_modal.details_content": "建议这么做:
    • 根据图片内容描述你自己的样子
    • 文本使用第三人称(例如称呼自己为“Alex”而不是“我”)
    • 保持简洁:通常只需要简单描述一下
    不建议这么做:
    • 以“……的图片”开头或结尾:屏幕阅读器自己会说这是图片
    示例:
    • “穿绿色衬衫戴着眼镜的Alex”
    ", + "account_edit.image_alt_modal.details_title": "小贴士:为头像添加替代文本的注意事项", + "account_edit.image_alt_modal.edit_title": "编辑替代文本", + "account_edit.image_alt_modal.text_hint": "替代文本可以帮助使用屏幕阅读器的用户理解你的内容。", + "account_edit.image_alt_modal.text_label": "替代文本", + "account_edit.image_delete_modal.confirm": "确定要删除此图片吗?此操作无法撤销。", + "account_edit.image_delete_modal.delete_button": "删除", + "account_edit.image_delete_modal.title": "删除图片吗?", + "account_edit.image_edit.add_button": "添加图片", + "account_edit.image_edit.alt_add_button": "添加替代文本", + "account_edit.image_edit.alt_edit_button": "编辑替代文本", + "account_edit.image_edit.remove_button": "移除图片", + "account_edit.image_edit.replace_button": "替换图片", "account_edit.name_modal.add_title": "添加显示名称", "account_edit.name_modal.edit_title": "编辑显示名称", "account_edit.profile_tab.button_label": "自定义", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "自定义你个人资料的标签页及其显示的内容。", "account_edit.profile_tab.title": "个人资料标签页设置", "account_edit.save": "保存", + "account_edit.upload_modal.back": "上一步", + "account_edit.upload_modal.done": "完成", + "account_edit.upload_modal.next": "下一步", + "account_edit.upload_modal.step_crop.zoom": "缩放", + "account_edit.upload_modal.step_upload.button": "浏览文件", + "account_edit.upload_modal.step_upload.dragging": "将文件拖放到此处开始上传", + "account_edit.upload_modal.step_upload.header": "选择图片", + "account_edit.upload_modal.step_upload.hint": "支持 WEBP、PNG、GIF 或 JPG 格式,最大 {limit} MB。{br}图片将被缩放至 {width}×{height}px。", + "account_edit.upload_modal.title_add": "添加头像", + "account_edit.upload_modal.title_replace": "替换头像", + "account_edit.verified_modal.details": "要增加 Mastodon 个人资料的可信度,可以验证指向个人网站的链接。运作方式如下:", + "account_edit.verified_modal.invisible_link.details": "将链接添加到标头(header)中。其中很重要的部分是 rel=\"me\",可以防止他人通过用户生成内容模仿。你甚至可以在页面标头中使用 link 标签而不是 {tag},但包含该部分的 HTML 必须在没有 JavaScript 执行环境下访问时依然存在。", + "account_edit.verified_modal.invisible_link.summary": "如何隐藏此链接?", + "account_edit.verified_modal.step1.header": "复制下面的 HTML 代码并粘贴到你网站的标头(header)中", + "account_edit.verified_modal.step2.details": "如果已经将你的网站添加到自定义字段中,你需要删除并重新添加以触发验证。", + "account_edit.verified_modal.step2.header": "将你的网站添加到自定义字段", + "account_edit.verified_modal.title": "如何添加已验证的链接", "account_edit_tags.add_tag": "添加 #{tagName}", "account_edit_tags.column_title": "编辑精选话题标签", "account_edit_tags.help_text": "精选话题标签可以帮助他人发现并与你的个人资料互动。这些标签会作为过滤器条件出现在你个人资料页面的活动视图中。", @@ -293,6 +349,8 @@ "collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个", "collections.accounts.empty_title": "收藏列表为空", "collections.collection_description": "说明", + "collections.collection_language": "语言", + "collections.collection_language_none": "无", "collections.collection_name": "名称", "collections.collection_topic": "话题", "collections.confirm_account_removal": "你确定要将从收藏列表中移除此账号吗?", @@ -306,10 +364,15 @@ "collections.create_collection": "创建收藏列表", "collections.delete_collection": "删除收藏列表", "collections.description_length_hint": "100字限制", + "collections.detail.accept_inclusion": "确定", "collections.detail.accounts_heading": "账号", + "collections.detail.author_added_you": "{author} 将你添加到了此收藏列表", "collections.detail.curated_by_author": "由 {author} 精心挑选", "collections.detail.curated_by_you": "由你精心挑选", "collections.detail.loading": "正在加载收藏列表…", + "collections.detail.other_accounts_in_collection": "此收藏列表中的其他人:", + "collections.detail.revoke_inclusion": "移除我", + "collections.detail.sensitive_note": "此收藏列表可能包含某些对部分用户而言为敏感内容的账号或内容。", "collections.detail.share": "分享此收藏列表", "collections.edit_details": "编辑详情", "collections.error_loading_collections": "加载你的收藏列表时发生错误。", @@ -324,10 +387,14 @@ "collections.old_last_post_note": "上次发言于一周多以前", "collections.remove_account": "移除此账号", "collections.report_collection": "举报此收藏列表", + "collections.revoke_collection_inclusion": "将自己从此收藏列表中移除", + "collections.revoke_inclusion.confirmation": "你已被从“{collection}”中移除", + "collections.revoke_inclusion.error": "出现错误,请稍后重试。", "collections.search_accounts_label": "搜索要添加的账号…", "collections.search_accounts_max_reached": "你添加的账号数量已达上限", "collections.sensitive": "敏感内容", "collections.topic_hint": "添加话题标签,帮助他人了解此收藏列表的主题。", + "collections.topic_special_chars_hint": "保存时将自动移除特殊字符", "collections.view_collection": "查看收藏列表", "collections.view_other_collections_by_user": "查看此用户的其他收藏列表", "collections.visibility_public": "公开", @@ -447,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "移除关注者", "confirmations.remove_from_followers.message": "{name} 将停止关注你。你确定要继续吗?", "confirmations.remove_from_followers.title": "移除关注者?", + "confirmations.revoke_collection_inclusion.confirm": "移除我", + "confirmations.revoke_collection_inclusion.message": "此操作是永久的,且此收藏列表的制作者之后将无法再将你添加到其中。", + "confirmations.revoke_collection_inclusion.title": "将自己从此收藏列表中移除吗?", "confirmations.revoke_quote.confirm": "移除嘟文", "confirmations.revoke_quote.message": "此操作无法撤销。", "confirmations.revoke_quote.title": "移除嘟文?", @@ -558,6 +628,10 @@ "featured_carousel.header": "{count, plural, other {# 条置顶嘟文}}", "featured_carousel.slide": "第 {current, number} 条嘟文,共 {max, number} 条", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "你最近发布了有关{items}的内容。要将这些添加为精选的话题标签吗?", + "featured_tags.suggestions.add": "添加", + "featured_tags.suggestions.added": "你可以在修改个人资料 > 精选的话题标签下随时管理你精选的话题标签。", + "featured_tags.suggestions.dismiss": "不了,谢谢", "filter_modal.added.context_mismatch_explanation": "这条过滤规则不适用于你当前访问此嘟文的场景。要在此场景下过滤嘟文,你必须编辑此过滤规则。", "filter_modal.added.context_mismatch_title": "场景不匹配!", "filter_modal.added.expired_explanation": "此过滤规则类别已过期,你需要修改到期日期才能应用。", @@ -916,17 +990,17 @@ "notifications_permission_banner.how_to_control": "启用桌面通知以在 Mastodon 未打开时接收通知。你可以通过交互通过上面的 {icon} 按钮来精细控制可以发送桌面通知的交互类型。", "notifications_permission_banner.title": "精彩不容错过", "onboarding.follows.back": "返回", - "onboarding.follows.done": "完成", "onboarding.follows.empty": "很抱歉,现在无法显示任何结果。你可以尝试使用搜索或浏览探索页面来查找要关注的人,或稍后再试。", + "onboarding.follows.next": "下一步:设置你的个人资料", "onboarding.follows.search": "搜索", "onboarding.follows.title": "关注用户,玩转 Mastodon", "onboarding.profile.discoverable": "让我的账号可被他人发现", "onboarding.profile.discoverable_hint": "当你在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果与热门中,你的账号可能会被推荐给与你兴趣相似的人。", "onboarding.profile.display_name": "昵称", "onboarding.profile.display_name_hint": "你的全名或昵称…", + "onboarding.profile.finish": "完成", "onboarding.profile.note": "简介", "onboarding.profile.note_hint": "你可以提及 @其他人 或使用 #话题…", - "onboarding.profile.save_and_continue": "保存并继续", "onboarding.profile.title": "设置个人资料", "onboarding.profile.upload_avatar": "上传头像", "onboarding.profile.upload_header": "上传账号封面图", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 4b6bd119eadae7..e7e449a23973b3 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -547,7 +547,6 @@ "notifications_permission_banner.enable": "啟用桌面通知", "notifications_permission_banner.how_to_control": "只要啟用桌面通知,便可在 Mastodon 網站沒有打開時收到通知。在已經啟用桌面通知的時候,你可以透過上面的 {icon} 按鈕準確控制哪些類型的互動會產生桌面通知。", "notifications_permission_banner.title": "不放過任何事情", - "onboarding.follows.done": "完成", "onboarding.follows.empty": "很遺憾,現在無法顯示任何結果。你可以嘗試搜尋或瀏覽探索頁面來找使用者來追蹤,或者稍後再試。", "onboarding.follows.search": "搜尋", "onboarding.profile.discoverable": "將個人檔案設為可被搜尋", @@ -556,7 +555,6 @@ "onboarding.profile.display_name_hint": "你的全名或暱稱…", "onboarding.profile.note": "簡介", "onboarding.profile.note_hint": "你可以 @提及他人 或使用 #標籤…", - "onboarding.profile.save_and_continue": "儲存並繼續", "onboarding.profile.title": "個人檔案設定", "onboarding.profile.upload_avatar": "上載個人檔案頭像", "onboarding.profile.upload_header": "上載個人檔案橫幅圖片", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index f380953cae3235..06fabfced7fdbd 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -73,7 +73,6 @@ "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} 檢查此連結的擁有者權限", @@ -153,6 +152,7 @@ "account_edit.column_title": "編輯個人檔案", "account_edit.custom_fields.name": "欄位", "account_edit.custom_fields.placeholder": "加入您的稱謂、外部連結、或其他您想分享的。", + "account_edit.custom_fields.reorder_button": "重新排序欄位", "account_edit.custom_fields.tip_content": "您能透過驗證任何您擁有網站之連結,以輕鬆增加您 Mastodon 帳號之可信度。", "account_edit.custom_fields.tip_title": "小撇步:新增驗證連結", "account_edit.custom_fields.title": "自訂欄位", @@ -167,10 +167,36 @@ "account_edit.field_delete_modal.title": "是否刪除自訂欄位?", "account_edit.field_edit_modal.add_title": "新增自訂欄位", "account_edit.field_edit_modal.edit_title": "編輯自訂欄位", + "account_edit.field_edit_modal.limit_header": "已超過建議字數限制", + "account_edit.field_edit_modal.limit_message": "行動裝置使用者可能無法看見您完整欄位。", + "account_edit.field_edit_modal.link_emoji_warning": "我們不建議於 URL 中使用自訂 emoji 表情符號。為了避免使用者混淆,包含兩者之自訂欄位將僅顯示為文字,而不是顯示為連結。", "account_edit.field_edit_modal.name_hint": "例如:「個人網站」", "account_edit.field_edit_modal.name_label": "標籤", - "account_edit.field_edit_modal.value_hint": "例如「example.me」", + "account_edit.field_edit_modal.url_warning": "如欲新增連結,請於開頭包含 {protocol}。", + "account_edit.field_edit_modal.value_hint": "例如「https://example.me」", "account_edit.field_edit_modal.value_label": "值", + "account_edit.field_reorder_modal.drag_cancel": "拖放操作已取消。欄位「{item}」已放置。", + "account_edit.field_reorder_modal.drag_end": "欄位「{item}」已放置。", + "account_edit.field_reorder_modal.drag_instructions": "請按空白鍵或 Enter 重新整理自訂欄位。使用方向鍵上下移動欄位。按下空白鍵或 Enter 鍵於新位置放置欄位,或按下 ESC 鍵取消。", + "account_edit.field_reorder_modal.drag_move": "欄位「{item}」已移動。", + "account_edit.field_reorder_modal.drag_over": "欄位「{item}」已移動至「{over}」上方。", + "account_edit.field_reorder_modal.drag_start": "已選取欄位「{item}」。", + "account_edit.field_reorder_modal.handle_label": "拖放欄位「{item}」", + "account_edit.field_reorder_modal.title": "重新整理欄位", + "account_edit.image_alt_modal.add_title": "新增 ALT 說明文字", + "account_edit.image_alt_modal.details_content": "要:
    • 根據圖片描述自己
    • 使用第三人稱(例如用「小明」代替「我」)
    • 言簡意賅,寥寥數語足矣
    不要:
    • 從「照片」開頭,這對螢幕閱讀程式來說是多餘的
    範例:
    • 「小明穿著綠色襯衫,戴著眼鏡」
    ", + "account_edit.image_alt_modal.details_title": "小撇步:個人檔案照片的 ALT 說明文字", + "account_edit.image_alt_modal.edit_title": "編輯 ALT 說明文字", + "account_edit.image_alt_modal.text_hint": "ALT 說明文字可協助螢幕閱讀程式使用者理解您的內容。", + "account_edit.image_alt_modal.text_label": "ALT 說明文字", + "account_edit.image_delete_modal.confirm": "您確定要刪除此圖片嗎?此動作無法復原。", + "account_edit.image_delete_modal.delete_button": "刪除", + "account_edit.image_delete_modal.title": "是否刪除圖片?", + "account_edit.image_edit.add_button": "新增圖片", + "account_edit.image_edit.alt_add_button": "新增 ALT 說明文字", + "account_edit.image_edit.alt_edit_button": "編輯 ALT 說明文字", + "account_edit.image_edit.remove_button": "移除圖片", + "account_edit.image_edit.replace_button": "替換圖片", "account_edit.name_modal.add_title": "新增顯示名稱", "account_edit.name_modal.edit_title": "編輯顯示名稱", "account_edit.profile_tab.button_label": "自訂", @@ -185,6 +211,16 @@ "account_edit.profile_tab.subtitle": "自訂您個人檔案之分頁與內容。", "account_edit.profile_tab.title": "個人檔案分頁設定", "account_edit.save": "儲存", + "account_edit.upload_modal.back": "上一步", + "account_edit.upload_modal.done": "完成", + "account_edit.upload_modal.next": "下一步", + "account_edit.upload_modal.step_crop.zoom": "縮放", + "account_edit.upload_modal.step_upload.button": "瀏覽檔案", + "account_edit.upload_modal.step_upload.dragging": "拖曳以上傳", + "account_edit.upload_modal.step_upload.header": "選擇圖片", + "account_edit.upload_modal.step_upload.hint": "WEBP、PNG、GIF 或 JPG 格式,最大 {limit}MB。{br}圖片將會縮放至 {width}x{height} 像素。", + "account_edit.upload_modal.title_add": "新增個人檔案照片", + "account_edit.upload_modal.title_replace": "更換個人檔案照片", "account_edit.verified_modal.details": "藉由驗證連結至個人網站增加您 Mastodon 個人檔案之可信度。運作方式如下:", "account_edit.verified_modal.invisible_link.details": "新增連結至您的標頭 (header)。其重要的部分是 rel=\"me\" ,防止透過使用者產生內容模擬。您甚至能使用頁面標頭之 link 標籤取代頁面中的 {tag},但 HTML 必須能於不執行 JavaScript 情況下所存取。", "account_edit.verified_modal.invisible_link.summary": "如何隱藏此連結?", @@ -313,6 +349,8 @@ "collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號", "collections.accounts.empty_title": "此收藏名單是空的", "collections.collection_description": "說明", + "collections.collection_language": "語言", + "collections.collection_language_none": "無", "collections.collection_name": "名稱", "collections.collection_topic": "主題", "collections.confirm_account_removal": "您是否確定要自此收藏名單中移除此帳號?", @@ -326,12 +364,14 @@ "collections.create_collection": "建立收藏名單", "collections.delete_collection": "刪除收藏名單", "collections.description_length_hint": "100 字限制", + "collections.detail.accept_inclusion": "Okay", "collections.detail.accounts_heading": "帳號", "collections.detail.author_added_you": "{author} 將您加入至此收藏名單", "collections.detail.curated_by_author": "由 {author} 精選", "collections.detail.curated_by_you": "由您精選", "collections.detail.loading": "讀取收藏名單中...", "collections.detail.other_accounts_in_collection": "此收藏名單中其他人:", + "collections.detail.revoke_inclusion": "移除我", "collections.detail.sensitive_note": "此收藏名單可能包含對某些使用者敏感之帳號或內容。", "collections.detail.share": "分享此收藏名單", "collections.edit_details": "編輯詳細資料", @@ -347,10 +387,14 @@ "collections.old_last_post_note": "上次發表嘟文已超過一週", "collections.remove_account": "移除此帳號", "collections.report_collection": "檢舉此收藏名單", + "collections.revoke_collection_inclusion": "將我自此收藏名單中移除", + "collections.revoke_inclusion.confirmation": "您已自「{collection}」中被移除", + "collections.revoke_inclusion.error": "發生錯誤,請稍候重試。", "collections.search_accounts_label": "搜尋帳號以加入...", "collections.search_accounts_max_reached": "您新增之帳號數已達上限", "collections.sensitive": "敏感內容", "collections.topic_hint": "新增主題標籤以協助其他人瞭解此收藏名單之主題。", + "collections.topic_special_chars_hint": "特殊字元將於儲存時被移除", "collections.view_collection": "檢視收藏名單", "collections.view_other_collections_by_user": "檢視此使用者之其他收藏名單", "collections.visibility_public": "公開", @@ -470,6 +514,9 @@ "confirmations.remove_from_followers.confirm": "移除跟隨者", "confirmations.remove_from_followers.message": "{name} 將停止跟隨您。您確定要繼續嗎?", "confirmations.remove_from_followers.title": "是否移除該跟隨者?", + "confirmations.revoke_collection_inclusion.confirm": "移除我", + "confirmations.revoke_collection_inclusion.message": "此操作永久有效,且該收藏名單之擁有者無法稍候將您再次加入至此收藏名單。", + "confirmations.revoke_collection_inclusion.title": "是否將您自此收藏名單中移除?", "confirmations.revoke_quote.confirm": "移除嘟文", "confirmations.revoke_quote.message": "此動作無法復原。", "confirmations.revoke_quote.title": "是否移除該嘟文?", @@ -581,6 +628,10 @@ "featured_carousel.header": "{count, plural, other {# 則釘選嘟文}}", "featured_carousel.slide": "{max, number} 則嘟文中之第 {current, number} 則", "featured_tags.more_items": "+{count}", + "featured_tags.suggestions": "最近您曾發有關 {items} 之嘟文。是否新增其為推薦主題標籤?", + "featured_tags.suggestions.add": "新增", + "featured_tags.suggestions.added": "於 編輯個人檔案 > 推薦主題標籤 隨時管理您的推薦主題標籤。", + "featured_tags.suggestions.dismiss": "不需要,謝謝", "filter_modal.added.context_mismatch_explanation": "此過濾器類別不是用您所存取嘟文的情境。若您想要此嘟文被於此情境被過濾,您必須編輯過濾器。", "filter_modal.added.context_mismatch_title": "不符合情境!", "filter_modal.added.expired_explanation": "此過濾器類別已失效,您需要更新過期日期以套用。", @@ -939,17 +990,17 @@ "notifications_permission_banner.how_to_control": "啟用桌面通知以於 Mastodon 沒有開啟的時候接收通知。啟用桌面通知後,您可以透過上面的 {icon} 按鈕準確的控制哪些類型的互動會產生桌面通知。", "notifications_permission_banner.title": "不要錯過任何東西!", "onboarding.follows.back": "返回", - "onboarding.follows.done": "完成", "onboarding.follows.empty": "很遺憾,目前未能顯示任何結果。您可以嘗試使用搜尋、瀏覽探索頁面以找尋人們跟隨、或稍候再試。", + "onboarding.follows.next": "下一步:設定您的個人檔案", "onboarding.follows.search": "搜尋", "onboarding.follows.title": "開始跟隨一些人吧", "onboarding.profile.discoverable": "使我的個人檔案可以被找到", "onboarding.profile.discoverable_hint": "當您於 Mastodon 上選擇加入可發現性時,您的嘟文可能會顯示於搜尋結果與趨勢中。您的個人檔案可能會被推薦至與您志趣相投的人。", "onboarding.profile.display_name": "顯示名稱", "onboarding.profile.display_name_hint": "完整名稱或暱稱...", + "onboarding.profile.finish": "完成", "onboarding.profile.note": "個人簡介", "onboarding.profile.note_hint": "您可以 @mention 其他人或者使用 #主題標籤...", - "onboarding.profile.save_and_continue": "儲存並繼續", "onboarding.profile.title": "個人檔案設定", "onboarding.profile.upload_avatar": "上傳個人檔案大頭貼", "onboarding.profile.upload_header": "上傳個人檔案封面圖片", diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 4d150ad241621f..87e63702c5ff18 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -114,6 +114,9 @@ export const accountDefaultValues: AccountShape = { last_status_at: '', locked: false, noindex: false, + show_featured: true, + show_media: true, + show_media_replies: true, note: '', note_emojified: '', note_plain: 'string', diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index b84e59d442fcd4..6e0036a4e0d2ce 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -11,6 +11,7 @@ import { setComposeQuotePolicy, pasteLinkCompose, cancelPasteLinkCompose, + setDragUploadEnabled, } from '@/mastodon/actions/compose_typed'; import { timelineDelete } from 'mastodon/actions/timelines_typed'; @@ -85,6 +86,7 @@ const initialState = ImmutableMap({ is_submitting: false, is_changing_upload: false, is_uploading: false, + isDragDisabled: false, should_redirect_to_compose_page: false, progress: 0, isUploadingThumbnail: false, @@ -167,6 +169,7 @@ function clearAll(state) { normalizePrivacy(map); map.set('quoted_status_id', null); map.set('quote_policy', state.get('default_quote_policy')); + map.set('isDragDisabled', false); }); } @@ -472,6 +475,8 @@ export const composeReducer = (state = initialState, action) => { return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state; } else if (cancelPasteLinkCompose.match(action)) { return state.set('fetching_link', null); + } else if (setDragUploadEnabled.match(action)) { + return state.set('isDragDisabled', !action.payload); } switch(action.type) { diff --git a/app/javascript/mastodon/reducers/slices/collections.ts b/app/javascript/mastodon/reducers/slices/collections.ts index c3bec4b1c6cd1e..47bbd5e6ba88de 100644 --- a/app/javascript/mastodon/reducers/slices/collections.ts +++ b/app/javascript/mastodon/reducers/slices/collections.ts @@ -1,3 +1,4 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { importFetchedAccounts } from '@/mastodon/actions/importer'; @@ -9,6 +10,7 @@ import { apiDeleteCollection, apiAddCollectionItem, apiRemoveCollectionItem, + apiRevokeCollectionInclusion, } from '@/mastodon/api/collections'; import type { ApiCollectionJSON, @@ -17,9 +19,11 @@ import type { } from '@/mastodon/api_types/collections'; import { me } from '@/mastodon/initial_state'; import { + createAppAsyncThunk, createAppSelector, createDataLoadingThunk, } from '@/mastodon/store/typed_functions'; +import { inputToHashtag } from '@/mastodon/utils/hashtags'; type QueryStatus = 'idle' | 'loading' | 'error'; @@ -34,17 +38,69 @@ interface CollectionState { status: QueryStatus; } >; + editor: EditorState; +} + +interface EditorState { + id: string | null; + name: string; + description: string; + topic: string; + language: string | null; + discoverable: boolean; + sensitive: boolean; + accountIds: string[]; +} + +interface UpdateEditorFieldPayload { + field: K; + value: EditorState[K]; } const initialState: CollectionState = { collections: {}, accountCollections: {}, + editor: { + id: null, + name: '', + description: '', + topic: '', + language: null, + discoverable: true, + sensitive: false, + accountIds: [], + }, }; const collectionSlice = createSlice({ name: 'collections', initialState, - reducers: {}, + reducers: { + init(state, action: PayloadAction) { + const collection = action.payload; + + state.editor = { + id: collection?.id ?? null, + name: collection?.name ?? '', + description: collection?.description ?? '', + topic: inputToHashtag(collection?.tag?.name ?? ''), + language: collection?.language ?? '', + discoverable: collection?.discoverable ?? true, + sensitive: collection?.sensitive ?? false, + accountIds: getCollectionItemIds(collection?.items ?? []), + }; + }, + reset(state) { + state.editor = initialState.editor; + }, + updateEditorField( + state: CollectionState, + action: PayloadAction>, + ) { + const { field, value } = action.payload; + state.editor[field] = value; + }, + }, extraReducers(builder) { /** * Fetching account collections @@ -102,6 +158,7 @@ const collectionSlice = createSlice({ builder.addCase(updateCollection.fulfilled, (state, action) => { const { collection } = action.payload; state.collections[collection.id] = collection; + state.editor = initialState.editor; }); /** @@ -130,6 +187,7 @@ const collectionSlice = createSlice({ const { collection } = actions.payload; state.collections[collection.id] = collection; + state.editor = initialState.editor; if (state.accountCollections[collection.account_id]) { state.accountCollections[collection.account_id]?.collectionIds.unshift( @@ -158,7 +216,10 @@ const collectionSlice = createSlice({ * Removing an account from a collection */ - builder.addCase(removeCollectionItem.fulfilled, (state, action) => { + const removeAccountFromCollection = ( + state: CollectionState, + action: { meta: { arg: { itemId: string; collectionId: string } } }, + ) => { const { itemId, collectionId } = action.meta.arg; const collection = state.collections[collectionId]; @@ -167,7 +228,17 @@ const collectionSlice = createSlice({ (item) => item.id !== itemId, ); } - }); + }; + + builder.addCase( + removeCollectionItem.fulfilled, + removeAccountFromCollection, + ); + + builder.addCase( + revokeCollectionInclusion.fulfilled, + removeAccountFromCollection, + ); }, }); @@ -218,7 +289,16 @@ export const removeCollectionItem = createDataLoadingThunk( apiRemoveCollectionItem(collectionId, itemId), ); +export const revokeCollectionInclusion = createAppAsyncThunk( + `${collectionSlice.name}/revokeCollectionInclusion`, + ({ collectionId, itemId }: { collectionId: string; itemId: string }) => + apiRevokeCollectionInclusion(collectionId, itemId), +); + export const collections = collectionSlice.reducer; +export const collectionEditorActions = collectionSlice.actions; +export const updateCollectionEditorField = + collectionSlice.actions.updateEditorField; /** * Selectors @@ -257,3 +337,8 @@ export const selectAccountCollections = createAppSelector( } satisfies AccountCollectionQuery; }, ); + +const onlyExistingIds = (id?: string): id is string => !!id; + +export const getCollectionItemIds = (items?: ApiCollectionJSON['items']) => + items?.map((item) => item.account_id).filter(onlyExistingIds) ?? []; diff --git a/app/javascript/mastodon/reducers/slices/profile_edit.ts b/app/javascript/mastodon/reducers/slices/profile_edit.ts index e4840c642dedf9..2437352fbd2970 100644 --- a/app/javascript/mastodon/reducers/slices/profile_edit.ts +++ b/app/javascript/mastodon/reducers/slices/profile_edit.ts @@ -1,17 +1,16 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { debounce } from 'lodash'; - +import { fetchAccount } from '@/mastodon/actions/accounts'; import { apiDeleteFeaturedTag, + apiDeleteProfileAvatar, + apiDeleteProfileHeader, apiGetCurrentFeaturedTags, apiGetProfile, apiGetTagSuggestions, apiPatchProfile, apiPostFeaturedTag, } from '@/mastodon/api/accounts'; -import { apiGetSearch } from '@/mastodon/api/search'; import type { ApiAccountFieldJSON } from '@/mastodon/api_types/accounts'; import type { ApiProfileJSON, @@ -21,7 +20,6 @@ import type { ApiFeaturedTagJSON, ApiHashtagJSON, } from '@/mastodon/api_types/tags'; -import type { AppDispatch } from '@/mastodon/store'; import { createAppAsyncThunk, createAppSelector, @@ -56,40 +54,16 @@ export interface ProfileEditState { profile?: ProfileData; tagSuggestions?: ApiHashtagJSON[]; isPending: boolean; - search: { - query: string; - isLoading: boolean; - results?: ApiHashtagJSON[]; - }; } const initialState: ProfileEditState = { isPending: false, - search: { - query: '', - isLoading: false, - }, }; const profileEditSlice = createSlice({ name: 'profileEdit', initialState, - reducers: { - setSearchQuery(state, action: PayloadAction) { - if (state.search.query === action.payload) { - return; - } - - state.search.query = action.payload; - state.search.isLoading = true; - state.search.results = undefined; - }, - clearSearch(state) { - state.search.query = ''; - state.search.isLoading = false; - state.search.results = undefined; - }, - }, + reducers: {}, extraReducers(builder) { builder.addCase(fetchProfile.fulfilled, (state, action) => { state.profile = action.payload; @@ -109,24 +83,45 @@ const profileEditSlice = createSlice({ state.isPending = false; }); - builder.addCase(addFeaturedTag.pending, (state) => { + builder.addCase(uploadImage.pending, (state) => { state.isPending = true; }); - builder.addCase(addFeaturedTag.rejected, (state) => { + builder.addCase(uploadImage.rejected, (state) => { state.isPending = false; }); - builder.addCase(addFeaturedTag.fulfilled, (state, action) => { + builder.addCase(uploadImage.fulfilled, (state, action) => { + state.profile = action.payload; + state.isPending = false; + }); + + builder.addCase(deleteImage.pending, (state) => { + state.isPending = true; + }); + builder.addCase(deleteImage.rejected, (state) => { + state.isPending = false; + }); + builder.addCase(deleteImage.fulfilled, (state) => { + state.isPending = false; + }); + + builder.addCase(addFeaturedTags.pending, (state) => { + state.isPending = true; + }); + builder.addCase(addFeaturedTags.rejected, (state) => { + state.isPending = false; + }); + builder.addCase(addFeaturedTags.fulfilled, (state, action) => { if (!state.profile) { return; } state.profile.featuredTags = [ ...state.profile.featuredTags, - transformTag(action.payload), + ...action.payload.map(transformTag), ].toSorted((a, b) => a.name.localeCompare(b.name)); if (state.tagSuggestions) { state.tagSuggestions = state.tagSuggestions.filter( - (tag) => tag.name !== action.meta.arg.name, + (tag) => !action.meta.arg.names.includes(tag.name), ); } state.isPending = false; @@ -148,37 +143,10 @@ const profileEditSlice = createSlice({ ); state.isPending = false; }); - - builder.addCase(fetchSearchResults.pending, (state) => { - state.search.isLoading = true; - }); - builder.addCase(fetchSearchResults.rejected, (state) => { - state.search.isLoading = false; - state.search.results = undefined; - }); - builder.addCase(fetchSearchResults.fulfilled, (state, action) => { - state.search.isLoading = false; - const searchResults: ApiHashtagJSON[] = []; - const currentTags = new Set( - (state.profile?.featuredTags ?? []).map((tag) => tag.name), - ); - - for (const tag of action.payload) { - if (currentTags.has(tag.name)) { - continue; - } - searchResults.push(tag); - if (searchResults.length >= 10) { - break; - } - } - state.search.results = searchResults; - }); }, }); export const profileEdit = profileEditSlice.reducer; -export const { clearSearch } = profileEditSlice.actions; const transformTag = (result: ApiFeaturedTagJSON): TagData => ({ id: result.id, @@ -220,8 +188,77 @@ export const fetchProfile = createDataLoadingThunk( export const patchProfile = createDataLoadingThunk( `${profileEditSlice.name}/patchProfile`, (params: Partial) => apiPatchProfile(params), - transformProfile, - { useLoadingBar: false }, + (response, { dispatch }) => { + dispatch(fetchAccount(response.id)); + return transformProfile(response); + }, + { + useLoadingBar: false, + condition(_, { getState }) { + return !getState().profileEdit.isPending; + }, + }, +); + +export type ImageLocation = 'avatar' | 'header'; + +export const selectImageInfo = createAppSelector( + [ + (state) => state.profileEdit.profile, + (_, location: ImageLocation) => location, + ], + (profile, location) => { + if (!profile) { + return {}; + } + + return { + src: profile[location], + static: profile[`${location}Static`], + alt: profile[`${location}Description`], + }; + }, +); + +export const uploadImage = createDataLoadingThunk( + `${profileEditSlice.name}/uploadImage`, + (arg: { location: ImageLocation; imageBlob: Blob; altText: string }) => { + const formData = new FormData(); + formData.append(arg.location, arg.imageBlob); + if (arg.altText) { + formData.append(`${arg.location}_description`, arg.altText); + } + + return apiPatchProfile(formData); + }, + (response, { dispatch }) => { + dispatch(fetchAccount(response.id)); + return transformProfile(response); + }, + { + useLoadingBar: false, + }, +); + +export const deleteImage = createDataLoadingThunk( + `${profileEditSlice.name}/deleteImage`, + (arg: { location: ImageLocation }) => { + if (arg.location === 'avatar') { + return apiDeleteProfileAvatar(); + } else { + return apiDeleteProfileHeader(); + } + }, + async (_, { dispatch, getState }) => { + await dispatch(fetchProfile()); + const accountId = getState().profileEdit.profile?.id; + if (accountId) { + dispatch(fetchAccount(accountId)); + } + }, + { + useLoadingBar: false, + }, ); export const selectFieldById = createAppSelector( @@ -313,47 +350,14 @@ export const fetchSuggestedTags = createDataLoadingThunk( { useLoadingBar: false }, ); -export const addFeaturedTag = createDataLoadingThunk( +export const addFeaturedTags = createDataLoadingThunk( `${profileEditSlice.name}/addFeaturedTag`, - ({ name }: { name: string }) => apiPostFeaturedTag(name), - { - condition(arg, { getState }) { - const state = getState(); - return ( - !!state.profileEdit.profile && - !state.profileEdit.profile.featuredTags.some( - (tag) => tag.name === arg.name, - ) - ); - }, - }, + ({ names }: { names: string[] }) => + Promise.all(names.map((n) => apiPostFeaturedTag(n))), + { useLoadingBar: false }, ); export const deleteFeaturedTag = createDataLoadingThunk( `${profileEditSlice.name}/deleteFeaturedTag`, ({ tagId }: { tagId: string }) => apiDeleteFeaturedTag(tagId), ); - -const debouncedFetchSearchResults = debounce( - async (dispatch: AppDispatch, query: string) => { - await dispatch(fetchSearchResults({ q: query })); - }, - 300, -); - -export const updateSearchQuery = createAppAsyncThunk( - `${profileEditSlice.name}/updateSearchQuery`, - (query: string, { dispatch }) => { - dispatch(profileEditSlice.actions.setSearchQuery(query)); - - if (query.trim().length > 0) { - void debouncedFetchSearchResults(dispatch, query); - } - }, -); - -export const fetchSearchResults = createDataLoadingThunk( - `${profileEditSlice.name}/fetchSearchResults`, - ({ q }: { q: string }) => apiGetSearch({ q, type: 'hashtags', limit: 11 }), - (result) => result.hashtags, -); diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index ae9ea345820e72..add56179308efc 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -28,6 +28,7 @@ import { } from '../actions/timelines_typed'; import { compareId } from '../compare_id'; +/** @type {ImmutableMap} */ const initialState = ImmutableMap(); const initialTimeline = ImmutableMap({ @@ -36,7 +37,9 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + /** @type {ImmutableList} */ pendingItems: ImmutableList(), + /** @type {ImmutableList} */ items: ImmutableList(), }); @@ -197,6 +200,7 @@ const reconnectTimeline = (state, usePendingItems) => { }); }; +/** @type {import('@reduxjs/toolkit').Reducer} */ export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_LOAD_PENDING: diff --git a/app/javascript/mastodon/utils/checks.test.ts b/app/javascript/mastodon/utils/checks.test.ts new file mode 100644 index 00000000000000..862a2a0abfedd4 --- /dev/null +++ b/app/javascript/mastodon/utils/checks.test.ts @@ -0,0 +1,21 @@ +import { isUrlWithoutProtocol } from './checks'; + +describe('isUrlWithoutProtocol', () => { + test.concurrent.each([ + ['example.com', true], + ['sub.domain.co.uk', true], + ['example', false], // No dot + ['example..com', false], // Consecutive dots + ['example.com.', false], // Trailing dot + ['example.c', false], // TLD too short + ['example.123', false], // Numeric TLDs are not valid + ['example.com/path', true], // Paths are allowed + ['example.com?query=string', true], // Query strings are allowed + ['example.com#fragment', true], // Fragments are allowed + ['example .com', false], // Spaces are not allowed + ['example://com', false], // Protocol inside the string is not allowed + ['example.com^', false], // Invalid characters not allowed + ])('should return %s for input "%s"', (input, expected) => { + expect(isUrlWithoutProtocol(input)).toBe(expected); + }); +}); diff --git a/app/javascript/mastodon/utils/checks.ts b/app/javascript/mastodon/utils/checks.ts index 8b05ac24a7feda..d5d528bdc6eb08 100644 --- a/app/javascript/mastodon/utils/checks.ts +++ b/app/javascript/mastodon/utils/checks.ts @@ -9,3 +9,29 @@ export function isValidUrl( return false; } } + +/** + * Checks if the input string is probably a URL without a protocol. Note this is not full URL validation, + * and is mostly used to detect link-like inputs. + * @see https://www.xjavascript.com/blog/check-if-a-javascript-string-is-a-url/ + * @param input The input string to check + */ +export function isUrlWithoutProtocol(input: string): boolean { + if (!input.length || input.includes(' ') || input.includes('://')) { + return false; + } + + try { + const url = new URL(`http://${input}`); + const { host } = url; + return ( + host !== '' && // Host is not empty + host.includes('.') && // Host contains at least one dot + !host.endsWith('.') && // No trailing dot + !host.includes('..') && // No consecutive dots + /\.[\w]{2,}$/.test(host) // TLD is at least 2 characters + ); + } catch {} + + return false; +} diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 58421817ade347..5f736fa80c5d90 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) { return initialState?.features.includes(feature) ?? false; } -type ClientFeatures = 'collections' | 'profile_editing'; +type ClientFeatures = 'collections'; export function isClientFeatureEnabled(feature: ClientFeatures) { try { diff --git a/app/javascript/mastodon/utils/hashtags.test.ts b/app/javascript/mastodon/utils/hashtags.test.ts new file mode 100644 index 00000000000000..05b79b1d52c175 --- /dev/null +++ b/app/javascript/mastodon/utils/hashtags.test.ts @@ -0,0 +1,28 @@ +import { inputToHashtag } from './hashtags'; + +describe('inputToHashtag', () => { + test.concurrent.each([ + ['', ''], + // Prepend or keep hashtag + ['mastodon', '#mastodon'], + ['#mastodon', '#mastodon'], + // Preserve trailing whitespace + ['mastodon ', '#mastodon '], + [' ', '# '], + // Collapse whitespace & capitalise first character + ['cats of mastodon', '#catsOfMastodon'], + ['x y z', '#xYZ'], + [' mastodon', '#mastodon'], + // Preserve initial casing + ['Log in', '#LogIn'], + ['#NaturePhotography', '#NaturePhotography'], + // Normalise hash symbol variant + ['#nature', '#nature'], + ['#Nature Photography', '#NaturePhotography'], + // Allow special characters + ['hello-world', '#hello-world'], + ['hello,world', '#hello,world'], + ])('for input "%s", return "%s"', (input, expected) => { + expect(inputToHashtag(input)).toBe(expected); + }); +}); diff --git a/app/javascript/mastodon/utils/hashtags.ts b/app/javascript/mastodon/utils/hashtags.ts index 0c5505c6c9a088..963ca483693b85 100644 --- a/app/javascript/mastodon/utils/hashtags.ts +++ b/app/javascript/mastodon/utils/hashtags.ts @@ -27,3 +27,42 @@ const buildHashtagRegex = () => { export const HASHTAG_PATTERN_REGEX = buildHashtagPatternRegex(); export const HASHTAG_REGEX = buildHashtagRegex(); + +export const trimHashFromStart = (input: string) => { + return input.startsWith('#') || input.startsWith('#') + ? input.slice(1) + : input; +}; + +/** + * Formats an input string as a hashtag: + * - Prepends `#` unless present + * - Strips spaces (except at the end, to allow typing it) + * - Capitalises first character after stripped space + */ +export const inputToHashtag = (input: string): string => { + if (!input) { + return ''; + } + + const trailingSpace = /\s+$/.exec(input)?.[0] ?? ''; + const trimmedInput = input.trimEnd(); + const withoutHash = trimHashFromStart(trimmedInput); + + // Split by space, filter empty strings, and capitalise the start of each word but the first + const words = withoutHash + .split(/\s+/) + .filter((word) => word.length > 0) + .map((word, index) => + index === 0 + ? word + : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ); + + return `#${words.join('')}${trailingSpace}`; +}; + +export const hasSpecialCharacters = (input: string) => { + // Regex matches any character NOT a letter/digit, except the # + return /[^a-zA-Z0-9# ]/.test(input); +}; diff --git a/app/javascript/mastodon/utils/time.test.ts b/app/javascript/mastodon/utils/time.test.ts new file mode 100644 index 00000000000000..f1b206b4245aa9 --- /dev/null +++ b/app/javascript/mastodon/utils/time.test.ts @@ -0,0 +1,36 @@ +import { DAY, HOUR, MINUTE, relativeTimeParts, SECOND } from './time'; + +describe('relativeTimeParts', () => { + const now = Date.now(); + + test.concurrent.each([ + // Now + [0, { value: 0, unit: 'second' }], + + // Past + [-30 * SECOND, { value: -30, unit: 'second' }], + [-90 * SECOND, { value: -2, unit: 'minute' }], + [-30 * MINUTE, { value: -30, unit: 'minute' }], + [-90 * MINUTE, { value: -2, unit: 'hour' }], + [-5 * HOUR, { value: -5, unit: 'hour' }], + [-24 * HOUR, { value: -1, unit: 'day' }], + [-36 * HOUR, { value: -1, unit: 'day' }], + [-47 * HOUR, { value: -2, unit: 'day' }], + [-3 * DAY, { value: -3, unit: 'day' }], + + // Future + [SECOND, { value: 1, unit: 'second' }], + [59 * SECOND, { value: 59, unit: 'second' }], + [MINUTE, { value: 1, unit: 'minute' }], + [MINUTE + SECOND, { value: 1, unit: 'minute' }], + [59 * MINUTE, { value: 59, unit: 'minute' }], + [HOUR, { value: 1, unit: 'hour' }], + [HOUR + MINUTE, { value: 1, unit: 'hour' }], + [23 * HOUR, { value: 23, unit: 'hour' }], + [DAY, { value: 1, unit: 'day' }], + [DAY + HOUR, { value: 1, unit: 'day' }], + [2 * DAY, { value: 2, unit: 'day' }], + ])('should return correct value and unit for %d ms', (input, expected) => { + expect(relativeTimeParts(now + input, now)).toMatchObject(expected); + }); +}); diff --git a/app/javascript/mastodon/utils/time.ts b/app/javascript/mastodon/utils/time.ts new file mode 100644 index 00000000000000..c7ed115d248a75 --- /dev/null +++ b/app/javascript/mastodon/utils/time.ts @@ -0,0 +1,246 @@ +import type { IntlShape } from 'react-intl'; +import { defineMessages } from 'react-intl'; + +export const SECOND = 1000; +export const MINUTE = SECOND * 60; +export const HOUR = MINUTE * 60; +export const DAY = HOUR * 24; + +export const MAX_TIMEOUT = 2147483647; // Maximum delay for setTimeout in browsers (approximately 24.8 days) + +export type TimeUnit = 'second' | 'minute' | 'hour' | 'day'; + +export function relativeTimeParts( + ts: number, + now = Date.now(), +): { value: number; unit: TimeUnit; delta: number } { + const delta = ts - now; + const absDelta = Math.abs(delta); + + if (absDelta < MINUTE) { + return { value: Math.floor(delta / SECOND), unit: 'second', delta }; + } + + if (absDelta < HOUR) { + return { value: Math.floor(delta / MINUTE), unit: 'minute', delta }; + } + + if (absDelta < DAY) { + return { value: Math.floor(delta / HOUR), unit: 'hour', delta }; + } + + // Round instead of use floor as days are big enough that the value is usually off by a few hours. + return { value: Math.round(delta / DAY), unit: 'day', delta }; +} + +export function isToday(ts: number, now = Date.now()): boolean { + const date = new Date(ts); + const nowDate = new Date(now); + return ( + date.getDate() === nowDate.getDate() && + date.getMonth() === nowDate.getMonth() && + date.getFullYear() === nowDate.getFullYear() + ); +} + +export function isSameYear(ts: number, now = Date.now()): boolean { + const date = new Date(ts); + const nowDate = new Date(now); + return date.getFullYear() === nowDate.getFullYear(); +} + +export function unitToTime(unit: TimeUnit): number { + switch (unit) { + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + } +} + +const timeMessages = defineMessages({ + today: { id: 'relative_time.today', defaultMessage: 'today' }, + just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + just_now_full: { + id: 'relative_time.full.just_now', + defaultMessage: 'just now', + }, + seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, + seconds_full: { + id: 'relative_time.full.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', + }, + minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, + minutes_full: { + id: 'relative_time.full.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', + }, + hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, + hours_full: { + id: 'relative_time.full.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', + }, + days: { id: 'relative_time.days', defaultMessage: '{number}d' }, + days_full: { + id: 'relative_time.full.days', + defaultMessage: '{number, plural, one {# day} other {# days}} ago', + }, + moments_remaining: { + id: 'time_remaining.moments', + defaultMessage: 'Moments remaining', + }, + seconds_remaining: { + id: 'time_remaining.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} left', + }, + minutes_remaining: { + id: 'time_remaining.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', + }, + hours_remaining: { + id: 'time_remaining.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} left', + }, + days_remaining: { + id: 'time_remaining.days', + defaultMessage: '{number, plural, one {# day} other {# days}} left', + }, +}); + +const DAYS_LIMIT = 7; +const NOW_SECONDS = 10; + +export function formatTime({ + timestamp, + intl, + now = Date.now(), + noTime = false, + short = false, +}: { + timestamp: number; + intl: Pick; + now?: number; + noTime?: boolean; + short?: boolean; +}) { + const { value, unit } = relativeTimeParts(timestamp, now); + + // If we're only showing days, show "today" for the current day. + if (noTime && isToday(timestamp, now)) { + return intl.formatMessage(timeMessages.today); + } + + if (value > 0) { + return formatFuture({ value, unit, intl }); + } + + if (unit === 'day' && value < -DAYS_LIMIT) { + return formatAbsoluteTime({ timestamp, intl, now }); + } + + return formatRelativePastTime({ value, unit, intl, short }); +} + +export function formatAbsoluteTime({ + timestamp, + intl, + now = Date.now(), +}: { + timestamp: number; + intl: Pick; + now?: number; +}) { + return intl.formatDate(timestamp, { + month: 'short', + day: 'numeric', + // Only show the year if it's different from the current year. + year: isSameYear(timestamp, now) ? undefined : 'numeric', + }); +} + +export function formatFuture({ + unit, + value, + intl, +}: { + value: number; + unit: TimeUnit; + intl: Pick; +}) { + if (unit === 'day') { + return intl.formatMessage(timeMessages.days_remaining, { number: value }); + } + + if (unit === 'hour') { + return intl.formatMessage(timeMessages.hours_remaining, { + number: value, + }); + } + + if (unit === 'minute') { + return intl.formatMessage(timeMessages.minutes_remaining, { + number: value, + }); + } + + if (value > NOW_SECONDS) { + return intl.formatMessage(timeMessages.seconds_remaining, { + number: value, + }); + } + + return intl.formatMessage(timeMessages.moments_remaining); +} + +export function formatRelativePastTime({ + value, + unit, + intl, + short = false, +}: { + value: number; + unit: TimeUnit; + intl: Pick; + short?: boolean; +}) { + const absValue = Math.abs(value); + if (unit === 'day') { + return intl.formatMessage( + short ? timeMessages.days : timeMessages.days_full, + { + number: absValue, + }, + ); + } + + if (unit === 'hour') { + return intl.formatMessage( + short ? timeMessages.hours : timeMessages.hours_full, + { + number: absValue, + }, + ); + } + + if (unit === 'minute') { + return intl.formatMessage( + short ? timeMessages.minutes : timeMessages.minutes_full, + { number: absValue }, + ); + } + + if (absValue >= NOW_SECONDS) { + return intl.formatMessage( + short ? timeMessages.seconds : timeMessages.seconds_full, + { number: absValue }, + ); + } + + return intl.formatMessage( + short ? timeMessages.just_now : timeMessages.just_now_full, + ); +} diff --git a/app/javascript/material-icons/400-20px/mood-fill.svg b/app/javascript/material-icons/400-20px/mood-fill.svg index febf012902326c..81a779d4f53f2c 100644 --- a/app/javascript/material-icons/400-20px/mood-fill.svg +++ b/app/javascript/material-icons/400-20px/mood-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-20px/mood.svg b/app/javascript/material-icons/400-20px/mood.svg index 898697c4cdbd2e..c4dd3904cc955a 100644 --- a/app/javascript/material-icons/400-20px/mood.svg +++ b/app/javascript/material-icons/400-20px/mood.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-20px/warning-fill.svg b/app/javascript/material-icons/400-20px/warning-fill.svg index a4fc7efc5321d7..f36c8d87d17646 100644 --- a/app/javascript/material-icons/400-20px/warning-fill.svg +++ b/app/javascript/material-icons/400-20px/warning-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-20px/warning.svg b/app/javascript/material-icons/400-20px/warning.svg index d67ad689aacef8..5c4a6ccd87ab64 100644 --- a/app/javascript/material-icons/400-20px/warning.svg +++ b/app/javascript/material-icons/400-20px/warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/account_circle-fill.svg b/app/javascript/material-icons/400-24px/account_circle-fill.svg index 1bf9d57a31a82e..5a5517d9805967 100644 --- a/app/javascript/material-icons/400-24px/account_circle-fill.svg +++ b/app/javascript/material-icons/400-24px/account_circle-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/account_circle.svg b/app/javascript/material-icons/400-24px/account_circle.svg index ce59194be0b8ef..70a0ccde4f37de 100644 --- a/app/javascript/material-icons/400-24px/account_circle.svg +++ b/app/javascript/material-icons/400-24px/account_circle.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/alternate_email-fill.svg b/app/javascript/material-icons/400-24px/alternate_email-fill.svg index 7648cf9755e9a2..d586e40677b303 100644 --- a/app/javascript/material-icons/400-24px/alternate_email-fill.svg +++ b/app/javascript/material-icons/400-24px/alternate_email-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/alternate_email.svg b/app/javascript/material-icons/400-24px/alternate_email.svg index 7648cf9755e9a2..d586e40677b303 100644 --- a/app/javascript/material-icons/400-24px/alternate_email.svg +++ b/app/javascript/material-icons/400-24px/alternate_email.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/badge-fill.svg b/app/javascript/material-icons/400-24px/badge-fill.svg index 2f7175b7f12b88..88ac07340ca5af 100644 --- a/app/javascript/material-icons/400-24px/badge-fill.svg +++ b/app/javascript/material-icons/400-24px/badge-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/badge.svg b/app/javascript/material-icons/400-24px/badge.svg index d413363a4cc91a..977a04acfa7fde 100644 --- a/app/javascript/material-icons/400-24px/badge.svg +++ b/app/javascript/material-icons/400-24px/badge.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block-fill.svg b/app/javascript/material-icons/400-24px/block-fill.svg index 2d3801613c97af..8b708f27d8eeb5 100644 --- a/app/javascript/material-icons/400-24px/block-fill.svg +++ b/app/javascript/material-icons/400-24px/block-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block.svg b/app/javascript/material-icons/400-24px/block.svg index e9df4cdd35dfbc..8d798c9eab585c 100644 --- a/app/javascript/material-icons/400-24px/block.svg +++ b/app/javascript/material-icons/400-24px/block.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/breaking_news-fill.svg b/app/javascript/material-icons/400-24px/breaking_news-fill.svg index 633ca48d57d6b4..38f563b524eec0 100644 --- a/app/javascript/material-icons/400-24px/breaking_news-fill.svg +++ b/app/javascript/material-icons/400-24px/breaking_news-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/breaking_news.svg b/app/javascript/material-icons/400-24px/breaking_news.svg index c043f11a8b0207..9a1242b4437adc 100644 --- a/app/javascript/material-icons/400-24px/breaking_news.svg +++ b/app/javascript/material-icons/400-24px/breaking_news.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/contact_mail-fill.svg b/app/javascript/material-icons/400-24px/contact_mail-fill.svg index c42c799955cc9a..45edf82458a4a7 100644 --- a/app/javascript/material-icons/400-24px/contact_mail-fill.svg +++ b/app/javascript/material-icons/400-24px/contact_mail-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/contact_mail.svg b/app/javascript/material-icons/400-24px/contact_mail.svg index 4547c48ec5b7b4..4d54d1157018e6 100644 --- a/app/javascript/material-icons/400-24px/contact_mail.svg +++ b/app/javascript/material-icons/400-24px/contact_mail.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/database-fill.svg b/app/javascript/material-icons/400-24px/database-fill.svg index 3520f6961446e6..977a1abd1d551e 100644 --- a/app/javascript/material-icons/400-24px/database-fill.svg +++ b/app/javascript/material-icons/400-24px/database-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/error-fill.svg b/app/javascript/material-icons/400-24px/error-fill.svg index 5125e9acce627a..92a5cf4abe2a97 100644 --- a/app/javascript/material-icons/400-24px/error-fill.svg +++ b/app/javascript/material-icons/400-24px/error-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/error.svg b/app/javascript/material-icons/400-24px/error.svg index 86c4555326886c..9a83c9306239ec 100644 --- a/app/javascript/material-icons/400-24px/error.svg +++ b/app/javascript/material-icons/400-24px/error.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/find_in_page-fill.svg b/app/javascript/material-icons/400-24px/find_in_page-fill.svg index 146f838a278417..b180354536b83e 100644 --- a/app/javascript/material-icons/400-24px/find_in_page-fill.svg +++ b/app/javascript/material-icons/400-24px/find_in_page-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/find_in_page.svg b/app/javascript/material-icons/400-24px/find_in_page.svg index f21c2786cab932..1133db1429ce99 100644 --- a/app/javascript/material-icons/400-24px/find_in_page.svg +++ b/app/javascript/material-icons/400-24px/find_in_page.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/format_quote.svg b/app/javascript/material-icons/400-24px/format_quote.svg index c354385ea93845..17a51085683a7d 100644 --- a/app/javascript/material-icons/400-24px/format_quote.svg +++ b/app/javascript/material-icons/400-24px/format_quote.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/format_quote_off.svg b/app/javascript/material-icons/400-24px/format_quote_off.svg index 8410c3924bd4bc..38a92eca2e6ff8 100644 --- a/app/javascript/material-icons/400-24px/format_quote_off.svg +++ b/app/javascript/material-icons/400-24px/format_quote_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/forward_5-fill.svg b/app/javascript/material-icons/400-24px/forward_5-fill.svg index bc0119a640e6b0..d0b8eb42da1562 100644 --- a/app/javascript/material-icons/400-24px/forward_5-fill.svg +++ b/app/javascript/material-icons/400-24px/forward_5-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/forward_5.svg b/app/javascript/material-icons/400-24px/forward_5.svg index bc0119a640e6b0..d0b8eb42da1562 100644 --- a/app/javascript/material-icons/400-24px/forward_5.svg +++ b/app/javascript/material-icons/400-24px/forward_5.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/group-fill.svg b/app/javascript/material-icons/400-24px/group-fill.svg index c0d6cef5c555ad..6ba4667f67f014 100644 --- a/app/javascript/material-icons/400-24px/group-fill.svg +++ b/app/javascript/material-icons/400-24px/group-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/group.svg b/app/javascript/material-icons/400-24px/group.svg index dbc2c937e4c4f0..03590ed6244a8f 100644 --- a/app/javascript/material-icons/400-24px/group.svg +++ b/app/javascript/material-icons/400-24px/group.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/help-fill.svg b/app/javascript/material-icons/400-24px/help-fill.svg index 6fd48ca5dd1f97..afef63fa60d8fc 100644 --- a/app/javascript/material-icons/400-24px/help-fill.svg +++ b/app/javascript/material-icons/400-24px/help-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/help.svg b/app/javascript/material-icons/400-24px/help.svg index 0f10691c552974..8aa823bf06eafb 100644 --- a/app/javascript/material-icons/400-24px/help.svg +++ b/app/javascript/material-icons/400-24px/help.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/inbox-fill.svg b/app/javascript/material-icons/400-24px/inbox-fill.svg index 15ae2d8f3c4068..f9b740f2143e29 100644 --- a/app/javascript/material-icons/400-24px/inbox-fill.svg +++ b/app/javascript/material-icons/400-24px/inbox-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/inbox.svg b/app/javascript/material-icons/400-24px/inbox.svg index 32c727e8104990..042b6ee9e89a1b 100644 --- a/app/javascript/material-icons/400-24px/inbox.svg +++ b/app/javascript/material-icons/400-24px/inbox.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/info-fill.svg b/app/javascript/material-icons/400-24px/info-fill.svg index 0232e17ad0a438..149547220860f0 100644 --- a/app/javascript/material-icons/400-24px/info-fill.svg +++ b/app/javascript/material-icons/400-24px/info-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/info.svg b/app/javascript/material-icons/400-24px/info.svg index 05606f4e59e3de..a0b97f8a5f6551 100644 --- a/app/javascript/material-icons/400-24px/info.svg +++ b/app/javascript/material-icons/400-24px/info.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/list_alt-fill.svg b/app/javascript/material-icons/400-24px/list_alt-fill.svg index 6aa8b508236b31..b84dba8fd68c67 100644 --- a/app/javascript/material-icons/400-24px/list_alt-fill.svg +++ b/app/javascript/material-icons/400-24px/list_alt-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/list_alt.svg b/app/javascript/material-icons/400-24px/list_alt.svg index cca8ab195554a9..b3aad84d73480b 100644 --- a/app/javascript/material-icons/400-24px/list_alt.svg +++ b/app/javascript/material-icons/400-24px/list_alt.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/lock-fill.svg b/app/javascript/material-icons/400-24px/lock-fill.svg index 0815d784181ce1..fd34be4c360897 100644 --- a/app/javascript/material-icons/400-24px/lock-fill.svg +++ b/app/javascript/material-icons/400-24px/lock-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/lock.svg b/app/javascript/material-icons/400-24px/lock.svg index 20b9e3984ec95a..ecc5cd8992ba09 100644 --- a/app/javascript/material-icons/400-24px/lock.svg +++ b/app/javascript/material-icons/400-24px/lock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/manufacturing-fill.svg b/app/javascript/material-icons/400-24px/manufacturing-fill.svg index f19180759c0df3..53b05e010fc8fc 100644 --- a/app/javascript/material-icons/400-24px/manufacturing-fill.svg +++ b/app/javascript/material-icons/400-24px/manufacturing-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/manufacturing.svg b/app/javascript/material-icons/400-24px/manufacturing.svg index f19180759c0df3..53b05e010fc8fc 100644 --- a/app/javascript/material-icons/400-24px/manufacturing.svg +++ b/app/javascript/material-icons/400-24px/manufacturing.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/mood-fill.svg b/app/javascript/material-icons/400-24px/mood-fill.svg index 9480d0fb92aaa3..29ac126f1a19f8 100644 --- a/app/javascript/material-icons/400-24px/mood-fill.svg +++ b/app/javascript/material-icons/400-24px/mood-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/mood.svg b/app/javascript/material-icons/400-24px/mood.svg index 33a7d74db9eb4f..af196c6fdfaed9 100644 --- a/app/javascript/material-icons/400-24px/mood.svg +++ b/app/javascript/material-icons/400-24px/mood.svg @@ -1 +1 @@ - + diff --git a/app/javascript/material-icons/400-24px/music_note-fill.svg b/app/javascript/material-icons/400-24px/music_note-fill.svg index b10ad1921ad5b3..6ce5eff0017a71 100644 --- a/app/javascript/material-icons/400-24px/music_note-fill.svg +++ b/app/javascript/material-icons/400-24px/music_note-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/music_note.svg b/app/javascript/material-icons/400-24px/music_note.svg index b10ad1921ad5b3..6ce5eff0017a71 100644 --- a/app/javascript/material-icons/400-24px/music_note.svg +++ b/app/javascript/material-icons/400-24px/music_note.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person-fill.svg b/app/javascript/material-icons/400-24px/person-fill.svg index 73ef1efc1040ac..321281d2cf3b29 100644 --- a/app/javascript/material-icons/400-24px/person-fill.svg +++ b/app/javascript/material-icons/400-24px/person-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person.svg b/app/javascript/material-icons/400-24px/person.svg index a3f6b246c83140..c304462dba412a 100644 --- a/app/javascript/material-icons/400-24px/person.svg +++ b/app/javascript/material-icons/400-24px/person.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_add-fill.svg b/app/javascript/material-icons/400-24px/person_add-fill.svg index 3fa7f65288b965..d7cba656804972 100644 --- a/app/javascript/material-icons/400-24px/person_add-fill.svg +++ b/app/javascript/material-icons/400-24px/person_add-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_add.svg b/app/javascript/material-icons/400-24px/person_add.svg index 39b592bf04edea..cfaeac0071c368 100644 --- a/app/javascript/material-icons/400-24px/person_add.svg +++ b/app/javascript/material-icons/400-24px/person_add.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_alert-fill.svg b/app/javascript/material-icons/400-24px/person_alert-fill.svg index ddbecc60537fce..4e88d53dd322f0 100644 --- a/app/javascript/material-icons/400-24px/person_alert-fill.svg +++ b/app/javascript/material-icons/400-24px/person_alert-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_alert.svg b/app/javascript/material-icons/400-24px/person_alert.svg index 292ea32154768b..2b81be1c70bc80 100644 --- a/app/javascript/material-icons/400-24px/person_alert.svg +++ b/app/javascript/material-icons/400-24px/person_alert.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_remove-fill.svg b/app/javascript/material-icons/400-24px/person_remove-fill.svg index 239c7a49dcb7c0..e3a9c2f7ad69dd 100644 --- a/app/javascript/material-icons/400-24px/person_remove-fill.svg +++ b/app/javascript/material-icons/400-24px/person_remove-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/person_remove.svg b/app/javascript/material-icons/400-24px/person_remove.svg index 725da3649b62a3..f075b09d61b5ea 100644 --- a/app/javascript/material-icons/400-24px/person_remove.svg +++ b/app/javascript/material-icons/400-24px/person_remove.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/photo_camera-fill.svg b/app/javascript/material-icons/400-24px/photo_camera-fill.svg new file mode 100644 index 00000000000000..6a9918605d8d88 --- /dev/null +++ b/app/javascript/material-icons/400-24px/photo_camera-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/photo_camera.svg b/app/javascript/material-icons/400-24px/photo_camera.svg index 4621435ce097ac..e863b0dfa5cd9a 100644 --- a/app/javascript/material-icons/400-24px/photo_camera.svg +++ b/app/javascript/material-icons/400-24px/photo_camera.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/public-fill.svg b/app/javascript/material-icons/400-24px/public-fill.svg index 104f26e1330579..ccc5efef2ae408 100644 --- a/app/javascript/material-icons/400-24px/public-fill.svg +++ b/app/javascript/material-icons/400-24px/public-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/public.svg b/app/javascript/material-icons/400-24px/public.svg index 104f26e1330579..ccc5efef2ae408 100644 --- a/app/javascript/material-icons/400-24px/public.svg +++ b/app/javascript/material-icons/400-24px/public.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/replace_image-fill.svg b/app/javascript/material-icons/400-24px/replace_image-fill.svg new file mode 100644 index 00000000000000..e80c7b37f37c2c --- /dev/null +++ b/app/javascript/material-icons/400-24px/replace_image-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/replace_image.svg b/app/javascript/material-icons/400-24px/replace_image.svg new file mode 100644 index 00000000000000..a1a223c1447940 --- /dev/null +++ b/app/javascript/material-icons/400-24px/replace_image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/replay_5-fill.svg b/app/javascript/material-icons/400-24px/replay_5-fill.svg index c0c259829ee6fe..d4dd38b30e1ad3 100644 --- a/app/javascript/material-icons/400-24px/replay_5-fill.svg +++ b/app/javascript/material-icons/400-24px/replay_5-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/replay_5.svg b/app/javascript/material-icons/400-24px/replay_5.svg index c0c259829ee6fe..d4dd38b30e1ad3 100644 --- a/app/javascript/material-icons/400-24px/replay_5.svg +++ b/app/javascript/material-icons/400-24px/replay_5.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/safety_check-fill.svg b/app/javascript/material-icons/400-24px/safety_check-fill.svg index b38091a8ec4f2a..affaa146592b9a 100644 --- a/app/javascript/material-icons/400-24px/safety_check-fill.svg +++ b/app/javascript/material-icons/400-24px/safety_check-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/safety_check.svg b/app/javascript/material-icons/400-24px/safety_check.svg index 87bdba21fe3444..3f5d4726a14e75 100644 --- a/app/javascript/material-icons/400-24px/safety_check.svg +++ b/app/javascript/material-icons/400-24px/safety_check.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/share.svg b/app/javascript/material-icons/400-24px/share.svg index 23e617121cdafa..6791f0ff25c246 100644 --- a/app/javascript/material-icons/400-24px/share.svg +++ b/app/javascript/material-icons/400-24px/share.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/shield_question-fill.svg b/app/javascript/material-icons/400-24px/shield_question-fill.svg index c647567a00d0f7..b0de7067ebc3e4 100644 --- a/app/javascript/material-icons/400-24px/shield_question-fill.svg +++ b/app/javascript/material-icons/400-24px/shield_question-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/shield_question.svg b/app/javascript/material-icons/400-24px/shield_question.svg index 342ac0800e525b..dce64741d82c4d 100644 --- a/app/javascript/material-icons/400-24px/shield_question.svg +++ b/app/javascript/material-icons/400-24px/shield_question.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/smart_toy-fill.svg b/app/javascript/material-icons/400-24px/smart_toy-fill.svg index df417f5ff7c4c3..eb3fe24a765a36 100644 --- a/app/javascript/material-icons/400-24px/smart_toy-fill.svg +++ b/app/javascript/material-icons/400-24px/smart_toy-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/smart_toy.svg b/app/javascript/material-icons/400-24px/smart_toy.svg index b84efc73b18ae4..68190f9ced9b2a 100644 --- a/app/javascript/material-icons/400-24px/smart_toy.svg +++ b/app/javascript/material-icons/400-24px/smart_toy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/speed.svg b/app/javascript/material-icons/400-24px/speed.svg index 0837877f42f2f8..8b28d172b41986 100644 --- a/app/javascript/material-icons/400-24px/speed.svg +++ b/app/javascript/material-icons/400-24px/speed.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/visibility-fill.svg b/app/javascript/material-icons/400-24px/visibility-fill.svg index 44b5f4c6061bd3..84b06b82e26dc1 100644 --- a/app/javascript/material-icons/400-24px/visibility-fill.svg +++ b/app/javascript/material-icons/400-24px/visibility-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/visibility.svg b/app/javascript/material-icons/400-24px/visibility.svg index 8fe45d09af6004..546009d226e693 100644 --- a/app/javascript/material-icons/400-24px/visibility.svg +++ b/app/javascript/material-icons/400-24px/visibility.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/warning-fill.svg b/app/javascript/material-icons/400-24px/warning-fill.svg index c3727d3f57a943..c37c0f3abc1717 100644 --- a/app/javascript/material-icons/400-24px/warning-fill.svg +++ b/app/javascript/material-icons/400-24px/warning-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/warning.svg b/app/javascript/material-icons/400-24px/warning.svg index 238299e6064bc5..7fe972ff275df5 100644 --- a/app/javascript/material-icons/400-24px/warning.svg +++ b/app/javascript/material-icons/400-24px/warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index cc23627a15ac9b..aca615414f17aa 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -1 +1,24 @@ -@use 'common'; +@use 'mastodon/variables'; +@use 'mastodon/mixins'; +@use 'fonts/roboto'; +@use 'fonts/roboto-mono'; + +@use 'mastodon/reset'; +@use 'mastodon/theme'; +@use 'mastodon/basics'; +@use 'mastodon/branding'; +@use 'mastodon/containers'; +@use 'mastodon/lists'; +@use 'mastodon/widgets'; +@use 'mastodon/forms'; +@use 'mastodon/accounts'; +@use 'mastodon/components'; +@use 'mastodon/polls'; +@use 'mastodon/modal'; +@use 'mastodon/emoji_picker'; +@use 'mastodon/about'; +@use 'mastodon/tables'; +@use 'mastodon/admin'; +@use 'mastodon/dashboard'; +@use 'mastodon/rtl'; +@use 'mastodon/rich_text'; diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss deleted file mode 100644 index aca615414f17aa..00000000000000 --- a/app/javascript/styles/common.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use 'mastodon/variables'; -@use 'mastodon/mixins'; -@use 'fonts/roboto'; -@use 'fonts/roboto-mono'; - -@use 'mastodon/reset'; -@use 'mastodon/theme'; -@use 'mastodon/basics'; -@use 'mastodon/branding'; -@use 'mastodon/containers'; -@use 'mastodon/lists'; -@use 'mastodon/widgets'; -@use 'mastodon/forms'; -@use 'mastodon/accounts'; -@use 'mastodon/components'; -@use 'mastodon/polls'; -@use 'mastodon/modal'; -@use 'mastodon/emoji_picker'; -@use 'mastodon/about'; -@use 'mastodon/tables'; -@use 'mastodon/admin'; -@use 'mastodon/dashboard'; -@use 'mastodon/rtl'; -@use 'mastodon/rich_text'; diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 5eccacd6eb226f..5a1ab9aaccafa8 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -34,34 +34,6 @@ $fluid-breakpoint: $maximum-width + 20px; counter-increment: list-counter; min-height: 4ch; - button { - background: transparent; - border: 0; - padding: 0; - margin: 0; - text-align: start; - font: inherit; - - &:hover, - &:focus, - &:active { - background: transparent; - } - - &[aria-expanded='false'] .rules-list__hint { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - @supports (-webkit-line-clamp: 2) { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - white-space: normal; - } - } - } - &::before { content: counter(list-counter); position: absolute; @@ -91,6 +63,53 @@ $fluid-breakpoint: $maximum-width + 20px; font-size: 14px; font-weight: 400; color: var(--color-text-secondary); + + // Giving this a focus outline as the hint + // will be focused when toggling the full hint + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + &__toggle-button { + position: relative; + display: inline-flex; + vertical-align: -0.25em; + border: none; + border-radius: 4px; + color: var(--color-text-primary); + background: var(--color-bg-secondary); + + &[hidden] { + display: none; + } + + &[aria-expanded='true'] .icon-expand, + &[aria-expanded='false'] .icon-collapse { + display: none; + } + + .icon { + width: 1lh; + height: 1lh; + } + + &:hover { + background: var(--color-bg-tertiary); + } + + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::before { + // Increase clickable size + content: ''; + position: absolute; + inset: -12px; + } } .capability-icon { diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 289345e251b0af..f3a50ad5062010 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -268,7 +268,7 @@ $content-width: 840px; } } - h2 small { + h1 small { font-size: 12px; display: block; font-weight: 500; @@ -282,22 +282,16 @@ $content-width: 840px; } } - h2 { + :where(h1), + .heading-large { color: var(--color-text-primary); font-size: 24px; line-height: 36px; font-weight: 700; } - h3 { - color: var(--color-text-primary); - font-size: 20px; - line-height: 28px; - font-weight: 400; - margin-bottom: 30px; - } - - h4 { + :where(h2):not(.heading-medium), + .heading-small { text-transform: uppercase; font-size: 13px; font-weight: 700; @@ -307,6 +301,14 @@ $content-width: 840px; border-top: 1px solid var(--color-border-primary); } + .heading-medium { + color: var(--color-text-primary); + font-size: 20px; + line-height: 28px; + font-weight: 400; + margin-bottom: 30px; + } + h6 { font-size: 16px; color: var(--color-text-primary); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5934399e2f3184..253f053c15986f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1390,6 +1390,9 @@ body > [data-popper-placement] { .autosuggest-textarea > .autosuggest-textarea__textarea:lang(#{$lang}) { writing-mode: vertical-lr; min-height: 209px; // writable + max-height: 209px; // suppress autosizing by react-textarea-autosize + overflow-x: auto; + scrollbar-color: unset; } .detailed-status > .status__content > .status__content__text:lang(#{$lang}) { @@ -2089,7 +2092,9 @@ body > [data-popper-placement] { .account { padding: 16px; - &:not(&--without-border) { + // Using :where keeps specificity low, allowing for existing + // .account overrides to still apply + &:where(:not(&--without-border)) { border-bottom: 1px solid var(--color-border-primary); } @@ -4796,7 +4801,6 @@ a.status-card { border: 1px solid var(--color-border-primary); border-radius: 4px 4px 0 0; flex: 0 0 auto; - cursor: pointer; position: relative; z-index: 2; outline: 0; @@ -8957,7 +8961,7 @@ noscript { gap: 8px; $button-breakpoint: 420px; - $button-fallback-breakpoint: #{$button-breakpoint} + 55px; + $button-fallback-breakpoint: $button-breakpoint + 55px; &--desktop { margin-top: 55px; @@ -8981,7 +8985,7 @@ noscript { } @supports (not (container-type: inline-size)) { - @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + @media (min-width: ($button-fallback-breakpoint + 1px)) { display: none; } } @@ -12022,6 +12026,9 @@ noscript { .column-footer { padding: 16px; + position: sticky; + bottom: 0; + background: var(--color-bg-primary); } .lists-scrollable { diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 57c62a29e3579b..5e199273e03b78 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -13,28 +13,28 @@ .logo-container { margin: 50px auto; - h1 { + a { display: flex; justify-content: center; align-items: center; - - .logo { - height: 42px; - margin-inline-end: 10px; + width: min-content; + margin: 0 auto; + padding: 12px 16px; + color: var(--color-text-primary); + text-decoration: none; + outline: 0; + line-height: 32px; + font-weight: 500; + font-size: 14px; + + &:focus-visible { + outline: var(--outline-focus-default); } + } - a { - display: flex; - justify-content: center; - align-items: center; - color: var(--color-text-primary); - text-decoration: none; - outline: 0; - padding: 12px 16px; - line-height: 32px; - font-weight: 500; - font-size: 14px; - } + .logo { + height: 42px; + margin-inline-end: 10px; } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index a38b5480b467b0..5066e8922d1fac 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -77,7 +77,6 @@ code { .input { margin-bottom: 16px; - overflow: hidden; &:last-child { margin-bottom: 0; @@ -483,13 +482,19 @@ code { } } - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: var(--color-text-primary); - display: block; - width: auto; + .input.radio_buttons .radio { + label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: var(--color-text-primary); + display: block; + width: auto; + } + + input[type='radio'] { + accent-color: var(--color-text-brand); + } } .check_boxes { @@ -513,6 +518,16 @@ code { margin: 0; } } + + .checkbox.disabled { + opacity: 0.5; + } + } + + label.checkbox { + input[type='checkbox'] { + accent-color: var(--color-text-brand); + } } .input.static .label_input__wrapper { @@ -535,13 +550,20 @@ code { color: var(--color-text-primary); display: block; width: 100%; - outline: 0; font-family: inherit; resize: vertical; background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-radius: 4px; padding: 10px 16px; + outline: var(--outline-focus-default); + outline-offset: -2px; + outline-color: transparent; + transition: outline-color 0.15s ease-out; + + &:focus { + outline: var(--outline-focus-default); + } &:invalid { box-shadow: none; @@ -625,6 +647,11 @@ code { margin-inline-end: 0; } + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + &:active, &:focus, &:hover { @@ -665,6 +692,11 @@ code { padding-inline-end: 30px; height: 41px; + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + @media screen and (width <= 600px) { font-size: 16px; } @@ -1245,31 +1277,44 @@ code { } .progress-tracker { + --circle-size: 30px; + display: flex; align-items: center; padding-bottom: 30px; margin-bottom: 30px; li { - flex: 0 0 auto; position: relative; - } - .separator { - height: 2px; - background: var(--color-border-primary); - flex: 1 1 auto; + --connector-color: var(--color-border-primary); + --connector-thickness: 2px; &.completed { - background: var(--color-text-brand); + --connector-color: var(--color-bg-brand-base); + } + + &:not(:last-child) { + flex-grow: 1; + + // Connector line between circles + &::after { + content: ''; + display: block; + position: absolute; + inset-inline: var(--circle-size) 0; + background-color: var(--connector-color); + height: 2px; + top: calc((var(--circle-size) - var(--connector-thickness)) / 2); + } } } .circle { box-sizing: border-box; position: relative; - width: 30px; - height: 30px; + width: var(--circle-size); + height: var(--circle-size); border-radius: 50%; border: 2px solid var(--color-border-primary); flex: 0 0 auto; @@ -1290,8 +1335,9 @@ code { padding-top: 10px; text-align: center; width: 100px; - left: 50%; - transform: translateX(-50%); + + // Center-align the label with the circle + transform: translateX(-33.3333%); } li:first-child .label { @@ -1308,15 +1354,15 @@ code { transform: none; } - .active .circle { - border-color: var(--color-text-brand); + [aria-current='step'] .circle { + border-color: var(--color-bg-brand-base); &::before { content: ''; width: 10px; height: 10px; border-radius: 50%; - background: var(--color-text-brand); + background: var(--color-bg-brand-base); position: absolute; left: 50%; top: 50%; @@ -1325,8 +1371,9 @@ code { } .completed .circle { - border-color: var(--color-text-brand); - background: var(--color-text-brand); + color: var(--color-text-on-brand-base); + background: var(--color-bg-brand-base); + border-color: var(--color-bg-brand-base); } } diff --git a/app/javascript/styles/mastodon/reset.scss b/app/javascript/styles/mastodon/reset.scss index 2c3efbddc46c71..b6b58371365aab 100644 --- a/app/javascript/styles/mastodon/reset.scss +++ b/app/javascript/styles/mastodon/reset.scss @@ -35,7 +35,7 @@ body { } ol, ul { - list-style: none; + list-style-type: none; } blockquote, q { diff --git a/app/javascript/styles/mastodon/theme/_dark.scss b/app/javascript/styles/mastodon/theme/_dark.scss index 82a81709282ea8..fd0c315c3e19bb 100644 --- a/app/javascript/styles/mastodon/theme/_dark.scss +++ b/app/javascript/styles/mastodon/theme/_dark.scss @@ -81,6 +81,11 @@ var(--color-bg-brand-base), var(--overlay-strength-brand) )}; + --color-bg-brand-softer-solid: color-mix( + in srgb, + var(--color-bg-primary), + var(--color-bg-brand-base) var(--overlay-strength-brand) + ); // Error --overlay-strength-error: 10%; diff --git a/app/javascript/styles/mastodon/theme/_light.scss b/app/javascript/styles/mastodon/theme/_light.scss index 396f38ba836f3f..d439827b2b44b2 100644 --- a/app/javascript/styles/mastodon/theme/_light.scss +++ b/app/javascript/styles/mastodon/theme/_light.scss @@ -78,6 +78,11 @@ #0012d8, var(--overlay-strength-brand) )}; + --color-bg-brand-softer-solid: color-mix( + in srgb, + var(--color-bg-primary), + var(--color-bg-brand-base) var(--overlay-strength-brand) + ); // Error --overlay-strength-error: 5%; diff --git a/app/javascript/testing/api.ts b/app/javascript/testing/api.ts index 096d2ce30ea14a..ad980a8da97399 100644 --- a/app/javascript/testing/api.ts +++ b/app/javascript/testing/api.ts @@ -51,13 +51,17 @@ export const mockHandlers = { '/packs-dev/emoji/:locale.json', async ({ params }) => { const locale = toSupportedLocale(params.locale); + const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`; + const emojiModules = import.meta.glob( + '../../../../../node_modules/emojibase-data/**/compact.json', + { import: 'default' }, + ); + const path = emojiModules[key]; + if (!path) { + throw new Error(`Unsupported locale: ${locale}`); + } action('fetching emoji data')(locale); - const { default: data } = (await import( - /* @vite-ignore */ - `emojibase-data/${locale}/compact.json` - )) as { - default: CompactEmoji[]; - }; + const data = await path(); return HttpResponse.json([data]); }, diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 8ee7e7d11994e4..f5901fc70614f4 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -45,6 +45,9 @@ export const accountFactory: FactoryFunction = ({ indexable: true, last_status_at: '2023-01-01', locked: false, + show_featured: true, + show_media: true, + show_media_replies: true, mute_expires_at: null, note: 'This is a test user account.', statuses_count: 0, diff --git a/app/lib/access_token_extension.rb b/app/lib/access_token_extension.rb index 6e06f988a5e3db..268232a436ecdc 100644 --- a/app/lib/access_token_extension.rb +++ b/app/lib/access_token_extension.rb @@ -24,6 +24,6 @@ def update_last_used(request, clock = Time) end def push_to_streaming_api - redis.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed? + redis.publish("timeline:access_token:#{id}", { event: :kill }.to_json) if revoked? || destroyed? end end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 734224f5a2854a..2d12b890b6dcb7 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -59,6 +59,8 @@ def klass_for(json) ActivityPub::Activity::Move when 'QuoteRequest' ActivityPub::Activity::QuoteRequest + when 'FeatureRequest' + ActivityPub::Activity::FeatureRequest end end end @@ -165,6 +167,12 @@ def follow_from_object @follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? end + def feature_request_from_object + return @collection_item if instance_variable_defined?(:@collection_item) + + @collection_item = CollectionItem.local.find_by(activity_uri: value_or_id(@object), account_id: @account.id) + end + def fetch_remote_original_status if object_uri.start_with?('http') return if ActivityPub::TagManager.instance.local_uri?(object_uri) diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index ed7d56a51df850..d54f05849c22ce 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -6,6 +6,7 @@ def perform return accept_follow_for_friend if friend_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? + return accept_feature_request! if Mastodon::Feature.collections_federation_enabled? && feature_request_from_object.present? case @object['type'] when 'Follow' @@ -45,9 +46,20 @@ def accept_embedded_quote_request accept_quote!(quote) end + def accept_feature_request! + approval_uri = value_or_id(first_of_value(@json['result'])) + return if approval_uri.nil? || unsupported_uri_scheme?(approval_uri) || non_matching_uri_hosts?(approval_uri, @account.uri) + + collection_item = feature_request_from_object + collection_item.update!(approval_uri:, state: :accepted) + + activity_json = ActiveModelSerializers::SerializableResource.new(collection_item, serializer: ActivityPub::AddFeaturedItemSerializer, adapter: ActivityPub::Adapter).to_json + ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, collection_item.collection.account_id) + end + def accept_quote!(quote) approval_uri = value_or_id(first_of_value(@json['result'])) - return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? || !quote.pending? + return if unsupported_uri_scheme?(approval_uri) || non_matching_uri_hosts?(approval_uri, @account.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/add.rb b/app/lib/activitypub/activity/add.rb index 9e2483983dcdad..cfaf29a3be9477 100644 --- a/app/lib/activitypub/activity/add.rb +++ b/app/lib/activitypub/activity/add.rb @@ -12,6 +12,13 @@ def perform else add_featured end + when @account.collections_url + return unless Mastodon::Feature.collections_federation_enabled? + + add_collection + else + @collection = @account.collections.find_by(uri: value_or_id(@json['target'])) + add_collection_item if @collection && Mastodon::Feature.collections_federation_enabled? end end @@ -30,4 +37,12 @@ def add_featured_tags FeaturedTag.create!(account: @account, name: name) if name.present? end + + def add_collection + ActivityPub::ProcessFeaturedCollectionService.new.call(@account, @object) + end + + def add_collection_item + ActivityPub::ProcessFeaturedItemService.new.call(@collection, @object) + end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 6586b33e02b414..8b604d6e39eaac 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -49,12 +49,14 @@ def audience_cc def process_status @tags = [] @mentions = [] + @tagged_objects = [] @unresolved_mentions = [] @silenced_account_ids = [] @params = {} @raw_mention_uris = [] @quote = nil @quote_uri = nil + @quote_approval_uri = nil process_status_params process_sensitive_words @@ -68,6 +70,7 @@ def process_status ApplicationRecord.transaction do @status = Status.create!(@params.merge(quote: @quote)) attach_tags(@status) + attach_tagged_objects(@status) attach_mentions(@status) attach_counts(@status) end @@ -251,6 +254,13 @@ def attach_tags(status) end end + def attach_tagged_objects(status) + @tagged_objects.each do |tagged_object| + tagged_object.status = status + tagged_object.save + end + end + def attach_mentions(status) @mentions.each do |mention| mention.status = status @@ -280,6 +290,8 @@ def process_tags process_mention tag elsif equals_or_includes?(tag['type'], 'Emoji') process_emoji tag + elsif equals_or_includes?(tag['type'], 'FeaturedCollection') + process_tagged_collection tag end end end @@ -288,10 +300,12 @@ def process_quote @quote_uri = @status_parser.quote_uri return unless @status_parser.quote? - approval_uri = @status_parser.quote_approval_uri - approval_uri = 'http://kmy.blue/ns#LegacyQuote' if approval_uri == 'kmyblue:LegacyQuote' - approval_uri = nil if unsupported_uri_scheme?(approval_uri) || (approval_uri != 'http://kmy.blue/ns#LegacyQuote' && TagManager.instance.local_url?(approval_uri)) - @quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending) + @quote_approval_uri = @status_parser.quote_approval_uri + @quote_approval_uri = 'http://kmy.blue/ns#LegacyQuote' if @quote_approval_uri == 'kmyblue:LegacyQuote' + @quote_approval_uri = nil if + unsupported_uri_scheme?(@quote_approval_uri) || + (@quote_approval_uri != 'http://kmy.blue/ns#LegacyQuote' && TagManager.instance.local_url?(@quote_approval_uri)) + @quote = Quote.new(account: @account, approval_uri: nil, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending) end def process_hashtag(tag) @@ -349,6 +363,15 @@ def process_emoji(tag) end end + def process_tagged_collection(tag) + return if tag['id'].blank? + + # TODO: We probably want to resolve unknown objects and push them to an `@unresolved_tagged_objects` on failure + collection = ActivityPub::TagManager.instance.uri_to_resource(tag['id'], Collection) + + @tagged_objects << TaggedObject.new(uri: ActivityPub::TagManager.instance.uri_for(collection), object: collection, ap_type: 'FeaturedCollection') if collection.present? + end + def process_attachments return [] if @object['attachment'].nil? @@ -474,9 +497,9 @@ def fetch_and_verify_quote return if @quote.nil? embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context']) - ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id], depth: @options[:depth]) + ActivityPub::VerifyQuoteService.new.call(@quote, @quote_approval_uri, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id], depth: @options[:depth]) rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS - ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] }) + ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id], 'approval_uri' => @quote_approval_uri }) end def conversation_from_uri(uri) @@ -609,7 +632,7 @@ def tombstone_exists? def forward_for_reply return unless @status.distributable? && @json['signature'].present? && reply_to_local? - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) + ActivityPub::RawDistributionWorker.perform_async(JSON.generate(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) end def process_conversation! @@ -619,7 +642,7 @@ def process_conversation! return if @json['signature'].blank? - ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id, false) + ActivityPub::ForwardConversationWorker.perform_async(JSON.generate(@json), @status.id, false) end def increment_voters_count! diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 5a237553d6ad9d..4c8eb319692e79 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -2,13 +2,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def perform - if @account.uri == object_uri - delete_person - elsif object_uri == ActivityPub::TagManager::COLLECTIONS[:public] - delete_friend - else - delete_object - end + return delete_person if @account.uri == object_uri + return delete_friend if object_uri == ActivityPub::TagManager::COLLECTIONS[:public] + return delete_feature_authorization! unless !Mastodon::Feature.collections_federation_enabled? || feature_authorization_from_object.nil? + + delete_object end private @@ -72,7 +70,7 @@ def revoke_quote def forward_for_conversation return unless @status.conversation.present? && @status.conversation.local? && @json['signature'].present? - ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id, true) + ActivityPub::ForwardConversationWorker.perform_async(JSON.generate(@json), @status.id, true) end def delete_friend @@ -80,7 +78,18 @@ def delete_friend friend&.destroy end + def delete_feature_authorization! + collection_item = feature_authorization_from_object + DeleteCollectionItemService.new.call(collection_item, revoke: true) + end + def forwarder @forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status) end + + def feature_authorization_from_object + return @collection_item if instance_variable_defined?(:@collection_item) + + @collection_item = CollectionItem.local.find_by(approval_uri: value_or_id(@object), account_id: @account.id) + end end diff --git a/app/lib/activitypub/activity/feature_request.rb b/app/lib/activitypub/activity/feature_request.rb new file mode 100644 index 00000000000000..180eeb492cd565 --- /dev/null +++ b/app/lib/activitypub/activity/feature_request.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity + include Payloadable + + def perform + return unless Mastodon::Feature.collections_federation_enabled? + return if non_matching_uri_hosts?(@account.uri, @json['id']) + + @collection = @account.collections.find_by(uri: value_or_id(@json['instrument'])) + @featured_account = ActivityPub::TagManager.instance.uris_to_local_accounts([value_or_id(@json['object'])]).first + + return if @collection.nil? || @featured_account.nil? + + if AccountPolicy.new(@account, @featured_account).feature? + accept_request! + else + reject_request! + end + end + + private + + def accept_request! + collection_item = @collection.collection_items.create!( + account: @featured_account, + state: :accepted + ) + + queue_delivery!(collection_item, ActivityPub::AcceptFeatureRequestSerializer) + end + + def reject_request! + collection_item = @collection.collection_items.build( + account: @featured_account, + state: :rejected + ) + + queue_delivery!(collection_item, ActivityPub::RejectFeatureRequestSerializer) + end + + def queue_delivery!(collection_item, serializer) + json = JSON.generate(serialize_payload(collection_item, serializer)) + ActivityPub::DeliveryWorker.perform_async(json, @featured_account.id, @account.inbox_url) + end +end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 256d72acb94ecc..f16cdf773363e2 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -49,7 +49,7 @@ def perform end def reject_follow_request!(target_account) - json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer)) + json = serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url) end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 7f7d9308506d49..41ae8a4fbf8fef 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -164,6 +164,6 @@ def write_stream(emoji_reaction) end def render_emoji_reaction(emoji_group) - @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + @render_emoji_reaction ||= { event: :emoji_reaction, payload: emoji_group.to_json }.to_json end end diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 46c45cde276117..9e41bdec654d70 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -31,7 +31,7 @@ def accept_quote_request!(quoted_status) status.quote.update!(activity_uri: @json['id']) status.quote.accept! - json = Oj.dump(serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer)) + json = serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) # Ensure the user is notified @@ -60,7 +60,7 @@ def reject_quote_request!(quoted_status) account: @account, activity_uri: @json['id'] ) - json = Oj.dump(serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer)) + json = serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index 9dce5de98c4529..b6d720f03cc5b7 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -7,6 +7,7 @@ def perform return follow_request_from_object.reject! unless follow_request_from_object.nil? return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil? return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil? + return reject_feature_request! unless feature_request_from_object.nil? case @object['type'] when 'Follow' @@ -47,6 +48,13 @@ def reject_quote!(quote) quote.reject! end + def reject_feature_request! + collection_item = feature_request_from_object + return unless collection_item.account == @account && collection_item.local? + + collection_item.destroy! + end + def relay @relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil? end diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb index f5cbef67571d6f..4a1f8d80be38a3 100644 --- a/app/lib/activitypub/activity/remove.rb +++ b/app/lib/activitypub/activity/remove.rb @@ -12,6 +12,12 @@ def perform else remove_featured end + when @account.collections_url + remove_collection + else + @collection = @account.collections.find_by(uri: @json['target']) + + remove_collection_item if @collection end end @@ -34,4 +40,16 @@ def remove_featured_tags featured_tag = FeaturedTag.by_name(name).find_by(account: @account) featured_tag&.destroy! end + + def remove_collection + collection = @account.collections.find_by(uri: value_or_id(@object)) + + collection&.destroy! + end + + def remove_collection_item + collection_item = @collection.collection_items.find_by(uri: value_or_id(@object)) + + collection_item&.destroy! + end end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index 65950487a963c5..cf11a394a23c62 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -167,7 +167,7 @@ def write_stream(emoji_reaction) end def render_emoji_reaction(emoji_group) - @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + @render_emoji_reaction ||= { event: :emoji_reaction, payload: emoji_group.to_json }.to_json end def shortcode diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index 3b49ae5054ae09..ae8d01f48b129b 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -13,6 +13,8 @@ def perform update_account elsif supported_object_type? || converted_object_type? update_status + elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_federation_enabled? + update_collection end end @@ -48,7 +50,13 @@ def update_status def forward_for_conversation return unless @status.conversation.present? && @status.conversation.local? && @json['signature'].present? - ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id, true) + ActivityPub::ForwardConversationWorker.perform_async(JSON.generate(@json), @status.id, true) + end + + def update_collection + return reject_payload! if non_matching_uri_hosts?(@account.uri, object_uri) + + ActivityPub::ProcessFeaturedCollectionService.new.call(@account, @object) end def object_too_old? diff --git a/app/lib/activitypub/forwarder.rb b/app/lib/activitypub/forwarder.rb index c5ff59fa5ae749..43bbe013b33023 100644 --- a/app/lib/activitypub/forwarder.rb +++ b/app/lib/activitypub/forwarder.rb @@ -20,7 +20,7 @@ def forward! private def payload - @payload ||= Oj.dump(@json) + @payload ||= JSON.generate(@json) end def reblogged_by_account_ids diff --git a/app/lib/activitypub/parser/media_attachment_parser.rb b/app/lib/activitypub/parser/media_attachment_parser.rb index 1f4f43cb15f898..2212c24c4544cd 100644 --- a/app/lib/activitypub/parser/media_attachment_parser.rb +++ b/app/lib/activitypub/parser/media_attachment_parser.rb @@ -30,7 +30,7 @@ def thumbnail_remote_url def description str = @json['summary'].presence || @json['name'].presence - str = str.strip[0...MediaAttachment::MAX_DESCRIPTION_LENGTH] if str.present? + str = str.strip[0...MediaAttachment::MAX_DESCRIPTION_HARD_LENGTH_LIMIT] if str.present? str end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 5c2df054ee2d9e..408d75b92e56cb 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -311,6 +311,14 @@ def uri_to_actor(uri) uri_to_resource(uri, Account) end + def uri_to_local_collection(uri) + path_params = Rails.application.routes.recognize_path(uri) + return unless path_params[:controller] == 'collections' + + # TODO: check account, but this requires handling potentially two different schemes + Collection.find_by(id: path_params[:id]) + end + def uri_to_local_conversation(uri) path_params = Rails.application.routes.recognize_path(uri) return unless path_params[:controller] == 'activitypub/contexts' @@ -328,6 +336,8 @@ def uri_to_resource(uri, klass, url: false) uris_to_local_accounts([uri]).first when 'Conversation' uri_to_local_conversation(uri) + when 'Collection' + uri_to_local_collection(uri) else StatusFinder.new(uri).status end diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index 032abb752502c8..70d8f07b512124 100644 --- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb +++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb @@ -65,7 +65,7 @@ def elasticsearch_version value: version, human_value: version, } - rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error nil end @@ -80,7 +80,7 @@ def libvips_version def ffmpeg_version version_output = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-show_program_version -v 0 -of json').run - version = Oj.load(version_output, mode: :strict, symbol_keys: true).dig(:program_version, :version) + version = JSON.parse(version_output, symbolize_names: true).dig(:program_version, :version) { key: 'ffmpeg', @@ -88,7 +88,7 @@ def ffmpeg_version value: version, human_value: version, } - rescue Terrapin::CommandNotFoundError, Terrapin::ExitStatusError, Oj::ParseError + rescue Terrapin::CommandNotFoundError, Terrapin::ExitStatusError, JSON::ParserError nil end end diff --git a/app/lib/admin/metrics/dimension/space_usage_dimension.rb b/app/lib/admin/metrics/dimension/space_usage_dimension.rb index 1b4d224c770df0..ea48de52ab6bb8 100644 --- a/app/lib/admin/metrics/dimension/space_usage_dimension.rb +++ b/app/lib/admin/metrics/dimension/space_usage_dimension.rb @@ -69,7 +69,7 @@ def search_size unit: 'bytes', human_value: number_to_human_size(value), } - rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error nil end end diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb index 11a1abd7778ebc..1d404950e3c9a2 100644 --- a/app/lib/admin/system_check/elasticsearch_check.rb +++ b/app/lib/admin/system_check/elasticsearch_check.rb @@ -17,7 +17,7 @@ def pass? return true unless Chewy.enabled? running_version.present? && compatible_version? && cluster_health['status'] == 'green' && indexes_match? && specifications_match? && preset_matches? - rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected false end @@ -54,7 +54,7 @@ def message else Admin::SystemCheck::Message.new(:elasticsearch_preset, nil, 'https://docs.joinmastodon.org/admin/elasticsearch/#scaling') end - rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected Admin::SystemCheck::Message.new(:elasticsearch_running_check) end @@ -67,7 +67,7 @@ def cluster_health def running_version @running_version ||= begin Chewy.client.info['version']['number'] - rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error nil end end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index bc6c7561cca464..b8906d339bfe16 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -35,7 +35,7 @@ def redirect_uris def close_streaming_sessions(resource_owner = nil) # TODO: #28793 Combine into a single topic - payload = Oj.dump(event: :kill) + payload = { event: :kill }.to_json scope = access_tokens scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil? scope.in_batches do |tokens| diff --git a/app/lib/connection_pool/shared_connection_pool.rb b/app/lib/connection_pool/shared_connection_pool.rb index c7dd747edabc0f..2a66afbdc999c3 100644 --- a/app/lib/connection_pool/shared_connection_pool.rb +++ b/app/lib/connection_pool/shared_connection_pool.rb @@ -4,7 +4,7 @@ require_relative 'shared_timed_stack' class ConnectionPool::SharedConnectionPool < ConnectionPool - def initialize(options = {}, &block) + def initialize(**, &block) super @available = ConnectionPool::SharedTimedStack.new(@size, &block) diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index 3d788741e764a5..9f711cfde4633e 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -74,7 +74,7 @@ def emoji_map # from emoji_reactions_grouped_by_name (status_stat) return @emoji_map ||= custom_emojis.each_with_object({}) { |e, h| h[e.name] = [e.url, e.static_url] } if custom_emojis.first&.image.blank? - @emoji_map ||= custom_emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] } + @emoji_map ||= custom_emojis.to_h { |e| [e.shortcode, [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))]] } end def tag_for_emoji(shortcode, emoji) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 3fc81a4b2c6717..3cc9076dc48386 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -90,7 +90,7 @@ def push_to_home(account, status, update: false) def unpush_from_home(account, status, update: false) return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) - redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + redis.publish("timeline:#{account.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update true end @@ -125,14 +125,14 @@ def push_to_antenna(antenna, status, update: false) def unpush_from_list(list, status, update: false) return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) - redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + redis.publish("timeline:list:#{list.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update true end def unpush_from_antenna(antenna, status, update: false) return false unless remove_from_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?) - redis.publish("timeline:antenna:#{antenna.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + redis.publish("timeline:antenna:#{antenna.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update true end diff --git a/app/lib/importer/base_importer.rb b/app/lib/importer/base_importer.rb index 8f53be84b13b06..e1457010a7c6f8 100644 --- a/app/lib/importer/base_importer.rb +++ b/app/lib/importer/base_importer.rb @@ -54,7 +54,7 @@ def clean_up! raise ActiveRecord::UnknownPrimaryKey, index.adapter.target if primary_key.nil? ids = documents.pluck('_id') - existence_map = index.adapter.target.where(primary_key => ids).pluck(primary_key).each_with_object({}) { |id, map| map[id.to_s] = true } + existence_map = index.adapter.target.where(primary_key => ids).pluck(primary_key).to_h { |id| [id.to_s, true] } tmp = ids.reject { |id| existence_map[id] } next if tmp.empty? diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index a8004f2925f131..5dba1350ef0121 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -101,7 +101,7 @@ def root_array(root) end def json - @json ||= root_array(Oj.load(@data)).compact.find { |obj| SUPPORTED_TYPES.include?(obj['@type']) } || {} + @json ||= root_array(JSON.parse(@data)).compact.find { |obj| SUPPORTED_TYPES.include?(obj['@type']) } || {} end end @@ -265,7 +265,7 @@ def structured_data next unless structured_data.valid? structured_data - rescue Oj::ParseError, EncodingError + rescue JSON::ParserError, EncodingError Rails.logger.debug { "Invalid JSON-LD in #{@original_url}" } next end.first diff --git a/app/lib/request.rb b/app/lib/request.rb index 81e59fb2ec6891..66d7ece70f7980 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -208,7 +208,7 @@ def re_sign_on_redirect(_response, request) return end - signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri)) + signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding', 'Accept'), @verb, Addressable::URI.parse(request.uri)) request.headers['Signature'] = signature_value end @@ -295,7 +295,7 @@ def open(host, *args) Resolv::DNS.open do |dns| dns.timeouts = 5 addresses = dns.getaddresses(host) - addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2) + addresses = addresses.grep(Resolv::IPv6).take(2) + addresses.grep_v(Resolv::IPv6).take(2) end end diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb index 7761dbe626d5f6..98536fe2dec642 100644 --- a/app/lib/translation_service/deepl.rb +++ b/app/lib/translation_service/deepl.rb @@ -31,7 +31,7 @@ def languages def fetch_languages(type) request(:get, "/v2/languages?type=#{type}") do |res| - Oj.load(res.body_with_limit).map { |language| normalize_language(language['language']) } + JSON.parse(res.body_with_limit).map { |language| normalize_language(language['language']) } end end @@ -68,7 +68,7 @@ def base_url end def transform_response(json) - data = Oj.load(json, mode: :strict) + data = JSON.parse(json) raise UnexpectedResponseError unless data.is_a?(Hash) data['translations'].map do |translation| @@ -78,7 +78,7 @@ def transform_response(json) provider: 'DeepL.com' ) end - rescue Oj::ParseError + rescue JSON::ParserError raise UnexpectedResponseError end end diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 0df8590f870939..ffcd4ed78cc5e1 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -9,7 +9,7 @@ def initialize(base_url, api_key) end def translate(texts, source_language, target_language) - body = Oj.dump(q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) + body = { q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key }.to_json request(:post, '/translate', body: body) do |res| transform_response(res.body_with_limit, source_language) end @@ -17,7 +17,7 @@ def translate(texts, source_language, target_language) def languages request(:get, '/languages') do |res| - languages = Oj.load(res.body_with_limit).to_h do |language| + languages = JSON.parse(res.body_with_limit).to_h do |language| [language['code'], language['targets'].without(language['code'])] end languages[nil] = languages.values.flatten.uniq.sort @@ -45,7 +45,7 @@ def request(verb, path, **) end def transform_response(json, source_language) - data = Oj.load(json, mode: :strict) + data = JSON.parse(json) raise UnexpectedResponseError unless data.is_a?(Hash) data['translatedText'].map.with_index do |text, index| @@ -55,7 +55,7 @@ def transform_response(json, source_language) provider: 'LibreTranslate' ) end - rescue Oj::ParseError + rescue JSON::ParserError raise UnexpectedResponseError end end diff --git a/app/lib/user_settings_serializer.rb b/app/lib/user_settings_serializer.rb index 10d1be04d5a493..e6bf6e20145db9 100644 --- a/app/lib/user_settings_serializer.rb +++ b/app/lib/user_settings_serializer.rb @@ -6,7 +6,7 @@ def self.load(value) if value.blank? {} else - Oj.load(value, symbol_keys: true) + JSON.parse(value, symbolize_names: true) end end @@ -14,6 +14,6 @@ def self.load(value) end def self.dump(value) - Oj.dump(value.as_json) + JSON.generate(value.as_json) end end diff --git a/app/lib/video_metadata_extractor.rb b/app/lib/video_metadata_extractor.rb index 33a6264b4f50aa..3a0f5063682d0a 100644 --- a/app/lib/video_metadata_extractor.rb +++ b/app/lib/video_metadata_extractor.rb @@ -6,10 +6,10 @@ class VideoMetadataExtractor def initialize(path) @path = path - @metadata = Oj.load(ffmpeg_command_output, mode: :strict, symbol_keys: true) + @metadata = JSON.parse(ffmpeg_command_output, symbolize_names: true) parse_metadata - rescue Terrapin::ExitStatusError, Oj::ParseError + rescue Terrapin::ExitStatusError, JSON::ParserError @invalid = true rescue Terrapin::CommandNotFoundError raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffprobe` command. Please install ffmpeg.' # rubocop:disable I18n/RailsI18n/DecorateString -- This error is not user-facing diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb index c39c25e994a34b..aa5bcbe2a7b89d 100644 --- a/app/lib/webfinger.rb +++ b/app/lib/webfinger.rb @@ -12,7 +12,7 @@ class Response def initialize(uri, body) @uri = uri - @json = Oj.load(body, mode: :strict) + @json = JSON.parse(body) validate_response! end @@ -57,7 +57,7 @@ def initialize(uri) def perform Response.new(@uri, body_from_webfinger) - rescue Oj::ParseError + rescue JSON::ParserError raise Webfinger::Error, "Invalid JSON in response for #{@uri}" rescue Addressable::URI::InvalidURIError raise Webfinger::Error, "Invalid URI for #{@uri}" diff --git a/app/lib/webhooks/payload_renderer.rb b/app/lib/webhooks/payload_renderer.rb index 73ae30b5725cae..1be4842cbfd2c0 100644 --- a/app/lib/webhooks/payload_renderer.rb +++ b/app/lib/webhooks/payload_renderer.rb @@ -10,7 +10,7 @@ def initialize(document) def get(path) value = @document.dig(*parse_path(path)) - string = Oj.dump(value) + string = JSON.generate(value) # We want to make sure people can use the variable inside # other strings, so it can't be wrapped in quotes. @@ -58,7 +58,7 @@ class TemplateParser < Parslet::Parser /iox def initialize(json) - @document = DocumentTraverser.new(Oj.load(json)) + @document = DocumentTraverser.new(JSON.parse(json)) end def render(template) diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 5607aa2a58c8c6..d10689c9b8ce68 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -11,30 +11,26 @@ class AdminMailer < ApplicationMailer after_action :set_important_headers!, only: :new_critical_software_updates + around_action :set_locale + default to: -> { @me.user_email } def new_report(report) @report = report - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance, id: @report.id) - end + mail subject: default_i18n_subject(instance: @instance, id: @report.id) end def new_appeal(appeal) @appeal = appeal - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance, username: @appeal.account.username) - end + mail subject: default_i18n_subject(instance: @instance, username: @appeal.account.username) end def new_pending_account(user) @account = user.account - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance, username: @account.username) - end + mail subject: default_i18n_subject(instance: @instance, username: @account.username) end def new_pending_friend_server(friend_server) @@ -50,31 +46,23 @@ def new_trends(links, tags, statuses) @tags = tags @statuses = statuses - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance) - end + mail subject: default_i18n_subject(instance: @instance) end def new_software_updates @software_updates = SoftwareUpdate.by_version - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance) - end + mail subject: default_i18n_subject(instance: @instance) end def new_critical_software_updates @software_updates = SoftwareUpdate.urgent.by_version - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance) - end + mail subject: default_i18n_subject(instance: @instance) end def auto_close_registrations - locale_for_account(@me) do - mail subject: default_i18n_subject(instance: @instance) - end + mail subject: default_i18n_subject(instance: @instance) end private @@ -87,6 +75,10 @@ def set_instance @instance = Rails.configuration.x.local_domain end + def set_locale(&block) + locale_for_account(@me, &block) + end + def set_important_headers! headers( 'Importance' => 'high', diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 54dde1bb0dda46..ecb37509686828 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -15,6 +15,8 @@ class NotificationMailer < ApplicationMailer before_deliver :verify_functional_user + around_action :set_locale + default to: -> { email_address_with_name(@user.email, @me.username) } layout 'mailer' @@ -22,45 +24,33 @@ class NotificationMailer < ApplicationMailer def mention return if @status.blank? - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @status.account.acct) - end + mail subject: default_i18n_subject(name: @status.account.acct) end def quote return if @status.blank? - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @status.account.acct) - end + mail subject: default_i18n_subject(name: @status.account.acct) end def follow - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @account.acct) - end + mail subject: default_i18n_subject(name: @account.acct) end def favourite return if @status.blank? - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @account.acct) - end + mail subject: default_i18n_subject(name: @account.acct) end def reblog return if @status.blank? - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @account.acct) - end + mail subject: default_i18n_subject(name: @account.acct) end def follow_request - locale_for_account(@me) do - mail subject: default_i18n_subject(name: @account.acct) - end + mail subject: default_i18n_subject(name: @account.acct) end private @@ -81,6 +71,10 @@ def set_account @account = @notification.from_account end + def set_locale(&block) + locale_for_account(@me, &block) + end + def verify_functional_user throw(:abort) unless @user.functional? end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index fffc80a8e8c8bc..3ca313ddd0ba8d 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -216,7 +216,6 @@ def failed_2fa(user, remote_ip, user_agent, timestamp) def terms_of_service_changed(user, terms_of_service) @resource = user @terms_of_service = terms_of_service - @markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true) I18n.with_locale(locale) do mail subject: default_i18n_subject diff --git a/app/models/account.rb b/app/models/account.rb index c912d1944a090d..b51fbdca92e5d3 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -15,6 +15,7 @@ # avatar_remote_url :string # avatar_storage_schema_version :integer # avatar_updated_at :datetime +# collections_url :string # discoverable :boolean # display_name :string default(""), not null # domain :string diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index b4ab0d604bdc01..c2c6355a1869a8 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -25,7 +25,10 @@ class AccountMigration < ApplicationRecord before_validation :set_target_account before_validation :set_followers_count + attribute :current_username, :string + normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') } + normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') } validates :acct, presence: true, domain: { acct: true } validate :validate_migration_cooldown @@ -33,7 +36,7 @@ class AccountMigration < ApplicationRecord scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) } - attr_accessor :current_password, :current_username + attr_accessor :current_password def self.cooldown_duration_ago Time.current - COOLDOWN_PERIOD diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index eed17c9a87e5f3..e132571da02045 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -46,6 +46,7 @@ def process_action! ApplicationRecord.transaction do handle_type! process_strike! + create_log! process_reports! end @@ -105,6 +106,12 @@ def handle_suspend! target_account.suspend!(origin: :local) end + def create_log! + # A log entry is only interesting if the warning contains + # custom text from someone. Otherwise it's just noise. + log_action(:create, @warning) if @warning&.text.present? && type == 'none' + end + def text_for_warning [warning_preset&.text, text].compact.join("\n\n") end diff --git a/app/models/collection.rb b/app/models/collection.rb index 3b8ee82a3cfe10..3be633bbf1f7ac 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -5,7 +5,8 @@ # Table name: collections # # id :bigint(8) not null, primary key -# description :text not null +# description :text +# description_html :text # discoverable :boolean not null # item_count :integer default(0), not null # language :string @@ -21,6 +22,8 @@ # class Collection < ApplicationRecord MAX_ITEMS = 25 + NAME_LENGTH_HARD_LIMIT = 256 + DESCRIPTION_LENGTH_HARD_LIMIT = 2048 belongs_to :account belongs_to :tag, optional: true @@ -30,7 +33,16 @@ class Collection < ApplicationRecord has_many :collection_reports, dependent: :delete_all validates :name, presence: true - validates :description, presence: true + validates :name, length: { maximum: 40 }, if: :local? + validates :name, length: { maximum: NAME_LENGTH_HARD_LIMIT }, if: :remote? + validates :description, + presence: true, + length: { maximum: 100 }, + if: :local? + validates :description_html, + presence: true, + length: { maximum: DESCRIPTION_LENGTH_HARD_LIMIT }, + if: :remote? validates :local, inclusion: [true, false] validates :sensitive, inclusion: [true, false] validates :discoverable, inclusion: [true, false] diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index e113b3b5227e88..f7067fb2fcf35c 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -29,10 +29,10 @@ class CollectionItem < ApplicationRecord validates :position, numericality: { only_integer: true, greater_than: 0 } validates :activity_uri, presence: true, if: :local_item_with_remote_account? - validates :approval_uri, absence: true, unless: :local? + validates :approval_uri, presence: true, unless: -> { local? || account&.local? || !accepted? } validates :account, presence: true, if: :accepted? validates :object_uri, presence: true, if: -> { account.nil? } - validates :uri, presence: true, if: :remote? + validates :uri, presence: true, if: :remote_item_with_remote_account? before_validation :set_position, on: :create before_validation :set_activity_uri, only: :create, if: :local_item_with_remote_account? @@ -41,6 +41,7 @@ class CollectionItem < ApplicationRecord scope :with_accounts, -> { includes(account: [:account_stat, :user]) } scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } scope :local, -> { joins(:collection).merge(Collection.local) } + scope :accepted_partial, ->(account) { joins(:account).merge(Account.local).accepted.where(uri: nil, account_id: account.id) } def revoke! update!(state: :revoked) @@ -50,6 +51,10 @@ def local_item_with_remote_account? local? && account&.remote? end + def remote_item_with_remote_account? + remote? && account&.remote? + end + def object_type :featured_item end @@ -57,7 +62,7 @@ def object_type private def set_position - return if position_changed? + return if position.present? && position_changed? self.position = self.class.where(collection_id:).maximum(:position).to_i + 1 end diff --git a/app/models/concerns/account/avatar.rb b/app/models/concerns/account/avatar.rb index a2582b2848c848..d4d789ced17a22 100644 --- a/app/models/concerns/account/avatar.rb +++ b/app/models/concerns/account/avatar.rb @@ -29,7 +29,7 @@ def avatar_styles(file) validates_attachment_size :avatar, less_than: AVATAR_LIMIT remotable_attachment :avatar, AVATAR_LIMIT, suppress_errors: false - validates :avatar_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH } + validates :avatar_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }, if: -> { local? && will_save_change_to_avatar_description? } end def avatar_original_url diff --git a/app/models/concerns/account/header.rb b/app/models/concerns/account/header.rb index 1b70715a9e7bfe..75d3c04542ca34 100644 --- a/app/models/concerns/account/header.rb +++ b/app/models/concerns/account/header.rb @@ -26,7 +26,7 @@ def header_styles(file) validates_attachment_size :header, less_than: HEADER_LIMIT remotable_attachment :header, HEADER_LIMIT, suppress_errors: false - validates :header_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH } + validates :header_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }, if: -> { local? && will_save_change_to_header_description? } end def header_original_url diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index 344b647f6253ad..796f3737503919 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -260,7 +260,7 @@ def remote_followers_hash(url) Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do digest = "\x00" * 32 - followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri| + followers.matches_uri_prefix(url_prefix).pluck_each(:uri) do |uri| Xorcist.xor!(digest, Digest::SHA256.digest(uri)) end digest.unpack1('H*') diff --git a/app/models/concerns/account/mappings.rb b/app/models/concerns/account/mappings.rb index b44ff9c844ad60..76c0f64a1c8d20 100644 --- a/app/models/concerns/account/mappings.rb +++ b/app/models/concerns/account/mappings.rb @@ -5,12 +5,12 @@ module Account::Mappings class_methods do def following_map(target_account_ids, account_id) - Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| - mapping[follow.target_account_id] = { + Follow.where(target_account_id: target_account_ids, account_id: account_id).to_h do |follow| + [follow.target_account_id, { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages, - } + }] end end @@ -36,21 +36,21 @@ def blocked_by_map(target_account_ids, account_id) end def muting_map(target_account_ids, account_id) - Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping| - mapping[mute.target_account_id] = { + Mute.where(target_account_id: target_account_ids, account_id: account_id).to_h do |mute| + [mute.target_account_id, { notifications: mute.hide_notifications?, expires_at: mute.expires_at, - } + }] end end def requested_map(target_account_ids, account_id) - FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| - mapping[follow_request.target_account_id] = { + FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).to_h do |follow_request| + [follow_request.target_account_id, { reblogs: follow_request.show_reblogs?, notify: follow_request.notify?, languages: follow_request.languages, - } + }] end end @@ -69,10 +69,10 @@ def endorsed_map(target_account_ids, account_id) end def account_note_map(target_account_ids, account_id) - AccountNote.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |note, mapping| - mapping[note.target_account_id] = { + AccountNote.where(target_account_id: target_account_ids, account_id: account_id).to_h do |note| + [note.target_account_id, { comment: note.comment, - } + }] end end diff --git a/app/models/concerns/account/suspensions.rb b/app/models/concerns/account/suspensions.rb index 45a4e517ed4905..36c72da20b5bf1 100644 --- a/app/models/concerns/account/suspensions.rb +++ b/app/models/concerns/account/suspensions.rb @@ -36,7 +36,7 @@ def suspend!(date: Time.now.utc, origin: :local, block_email: true) # This terminates all connections for the given account with the streaming # server: - redis.publish("timeline:system:#{id}", Oj.dump(event: :kill)) if local? + redis.publish("timeline:system:#{id}", { event: :kill }.to_json) if local? end def unsuspend! diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 490a34a1d01a54..c4b8e9ba26a25d 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -133,8 +133,8 @@ def invalidate_cache! @should_invalidate_cache = false Rails.cache.delete("filters:v3:#{account_id}") - redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed)) - redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed)) + redis.publish("timeline:#{account_id}", { event: :filters_changed }.to_json) + redis.publish("timeline:system:#{account_id}", { event: :filters_changed }.to_json) end private diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 722be05ba24849..c4ab8b9ed27643 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -59,7 +59,7 @@ def invalid_uri? def blocking?(allow_with_approval: false) blocks = EmailDomainBlock.where(domain: domains_with_variants, allow_with_approval: allow_with_approval).by_domain_length - blocks.each { |block| block.history.add(@attempt_ip) } if @attempt_ip.present? + blocks.each { |block| block.history.add(@attempt_ip.to_s) } if @attempt_ip.present? blocks.any? end @@ -70,7 +70,7 @@ def domains_with_variants segments = uri.normalized_host.split('.') segments.map.with_index { |_, i| segments[i..].join('.') } - end + end.uniq end def extract_uris(domain_or_domains) diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb index 6ab95f21f1f5d6..5721674ef9ccd3 100644 --- a/app/models/form/redirect.rb +++ b/app/models/form/redirect.rb @@ -2,15 +2,20 @@ class Form::Redirect include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Attributes::Normalization - attr_accessor :account, :target_account, :current_password, - :current_username + attr_accessor :account, :target_account, :current_password - attr_reader :acct + attribute :acct, :string + attribute :current_username, :string validates :acct, presence: true, domain: { acct: true } validate :validate_target_account + normalizes :acct, with: ->(value) { value.to_s.strip.gsub(/\A@/, '') }, apply_to_nil: true + normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') } + def valid_with_challenge?(current_user) if current_user.encrypted_password.present? errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password) @@ -24,10 +29,6 @@ def valid_with_challenge?(current_user) valid? end - def acct=(val) - @acct = val.to_s.strip.gsub(/\A@/, '') - end - private def set_target_account diff --git a/app/models/friend_domain.rb b/app/models/friend_domain.rb index 243fc3b7b8845a..8ae4d02e267c8d 100644 --- a/app/models/friend_domain.rb +++ b/app/models/friend_domain.rb @@ -49,7 +49,7 @@ def idle? def follow! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(follow_activity(activity_id)) + payload = follow_activity(activity_id).to_json update!(active_state: :pending, passive_state: :idle, active_follow_activity_id: activity_id) DeliveryFailureTracker.reset!(inbox_url) @@ -58,7 +58,7 @@ def follow! def unfollow! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(unfollow_activity(activity_id)) + payload = unfollow_activity(activity_id).to_json update!(active_state: :idle, passive_state: :idle, active_follow_activity_id: nil) DeliveryFailureTracker.reset!(inbox_url) @@ -69,7 +69,7 @@ def accept! return if they_are_idle? activity_id = passive_follow_activity_id - payload = Oj.dump(accept_follow_activity(activity_id)) + payload = accept_follow_activity(activity_id).to_json update!(passive_state: :accepted, active_state: :idle) DeliveryFailureTracker.reset!(inbox_url) @@ -80,7 +80,7 @@ def reject! return if they_are_idle? activity_id = passive_follow_activity_id - payload = Oj.dump(reject_follow_activity(activity_id)) + payload = reject_follow_activity(activity_id).to_json update!(passive_state: :rejected, active_state: :idle, passive_follow_activity_id: nil) DeliveryFailureTracker.reset!(inbox_url) @@ -101,7 +101,7 @@ def default_inbox_url def delete_for_friend! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(delete_follow_activity(activity_id)) + payload = delete_follow_activity(activity_id).to_json DeliveryFailureTracker.reset!(inbox_url) ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) diff --git a/app/models/invite.rb b/app/models/invite.rb index 2e9371a07419bf..ca692d937e9064 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -22,7 +22,7 @@ class Invite < ApplicationRecord COMMENT_SIZE_LIMIT = 420 ELIGIBLE_CODE_CHARACTERS = [*('a'..'z'), *('A'..'Z'), *('0'..'9')].freeze HOMOGLYPHS = %w(0 1 I l O).freeze - VALID_CODE_CHARACTERS = ELIGIBLE_CODE_CHARACTERS - HOMOGLYPHS + VALID_CODE_CHARACTERS = (ELIGIBLE_CODE_CHARACTERS - HOMOGLYPHS).freeze belongs_to :user, inverse_of: :invites has_many :users, inverse_of: :invite, dependent: nil @@ -37,6 +37,10 @@ def valid_for_use? (max_uses.nil? || uses < max_uses) && !expired? && user&.functional? end + def bypass_approval? + user&.role&.can?(:invite_bypass_approval) + end + private def set_code diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index e0ed5892c3aab4..ac09d181954100 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -40,6 +40,7 @@ class MediaAttachment < ApplicationRecord enum :processing, { queued: 0, in_progress: 1, complete: 2, failed: 3 }, prefix: true MAX_DESCRIPTION_LENGTH = 1_500 + MAX_DESCRIPTION_HARD_LENGTH_LIMIT = 10_000 IMAGE_LIMIT = 16.megabytes VIDEO_LIMIT = 99.megabytes diff --git a/app/models/quote.rb b/app/models/quote.rb index e4f3b823fe4ae6..9625965817a2eb 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -45,12 +45,22 @@ class Quote < ApplicationRecord after_destroy_commit :decrement_counter_caches! after_update_commit :update_counter_caches! - def accept! - update!(state: :accepted) + def accept!(approval_uri: nil, legacy: false) + if approval_uri.present? + update!(state: :accepted, approval_uri:) + elsif legacy + update!(state: :accepted, approval_uri: 'http://kmy.blue/ns#LegacyQuote') + else + update!(state: :accepted) + end reset_parent_cache! if attribute_previously_changed?(:state) end + def pend_legacy! + update!(approval_uri: 'http://kmy.blue/ns#LegacyQuote') + end + def reject! if accepted? update!(state: :revoked, approval_uri: nil) diff --git a/app/models/relay.rb b/app/models/relay.rb index 53221887bdfe22..41a0a2a1aefa32 100644 --- a/app/models/relay.rb +++ b/app/models/relay.rb @@ -31,7 +31,7 @@ def to_log_human_identifier def enable! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(follow_activity(activity_id)) + payload = follow_activity(activity_id).to_json update!(state: :pending, follow_activity_id: activity_id) reset_delivery_tracker @@ -40,7 +40,7 @@ def enable! def disable! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(unfollow_activity(activity_id)) + payload = unfollow_activity(activity_id).to_json update!(state: :idle, follow_activity_id: nil) reset_delivery_tracker diff --git a/app/models/status.rb b/app/models/status.rb index 5d76182eafa445..167caf3c80c9d8 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -52,6 +52,23 @@ class Status < ApplicationRecord include DtlHelper include Status::InteractionPolicyConcern + CACHEABLE_ASSOCIATIONS = [ + :application, + :conversation, + :media_attachments, + :preloadable_poll, + :status_stat, + :tags, + :reference_objects, + :references, + :scheduled_expiration_status, + account: [:account_stat, user: :role], + active_mentions: :account, + tagged_objects: :object, + preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, + quote: { status: { account: [:account_stat, user: :role] } }, + ].freeze + MEDIA_ATTACHMENTS_LIMIT = 4 MEDIA_ATTACHMENTS_LIMIT_WITH_POLL = 4 MEDIA_ATTACHMENTS_LIMIT_FROM_REMOTE = 16 @@ -89,6 +106,7 @@ class Status < ApplicationRecord has_many :mentions, dependent: :destroy, inverse_of: :status has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account' has_many :media_attachments, dependent: :nullify + has_many :tagged_objects, dependent: :destroy has_many :quotes, foreign_key: 'quoted_status_id', inverse_of: :quoted_status, dependent: :nullify has_many :reference_objects, class_name: 'StatusReference', inverse_of: :status, dependent: :destroy has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status @@ -188,34 +206,11 @@ class Status < ApplicationRecord # the `dependent: destroy` callbacks remove relevant records before_destroy :unlink_from_conversations!, prepend: true - cache_associated :application, - :media_attachments, - :conversation, - :status_stat, - :tags, - :preloadable_poll, - :reference_objects, - :references, - :scheduled_expiration_status, - quote: { status: { account: [:account_stat, user: :role] } }, - preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, - account: [:account_stat, user: :role], - active_mentions: :account, - reblog: [ - :application, - :media_attachments, - :conversation, - :status_stat, - :tags, - :preloadable_poll, - :reference_objects, - :scheduled_expiration_status, - quote: { status: { account: [:account_stat, user: :role] } }, - preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, - account: [:account_stat, user: :role], - active_mentions: { account: :account_stat }, - ], - thread: :account + cache_associated( + *CACHEABLE_ASSOCIATIONS, + reblog: [*CACHEABLE_ASSOCIATIONS], + thread: :account + ) delegate :domain, :indexable?, to: :account, prefix: true @@ -410,7 +405,7 @@ def emoji_reactions_grouped_by_name(account = nil, **options) permitted_account_ids = options[:permitted_account_ids] - (Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions| + JSON.parse(status_stat&.emoji_reactions.presence || '[]').tap do |emoji_reactions| if account.present? public_emoji_reactions = [] @@ -440,7 +435,7 @@ def emoji_reactions_grouped_by_name(account = nil, **options) def generate_emoji_reactions_grouped_by_name records = emoji_reactions.group(:name).order(Arel.sql('MIN(created_at) ASC')).select('name, min(custom_emoji_id) as custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids') - Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::EmojiReactionsGroupedByNameSerializer, scope: nil, scope_name: :current_user)) + ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::EmojiReactionsGroupedByNameSerializer, scope: nil, scope_name: :current_user).to_json end def refresh_emoji_reactions_grouped_by_name! @@ -504,7 +499,7 @@ def searchable_visibility class << self def favourites_map(status_ids, account_id) - Favourite.select(:status_id).where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true } + Favourite.select(:status_id).where(status_id: status_ids).where(account_id: account_id).to_h { |f| [f.status_id, true] } end def bookmarks_map(status_ids, account_id) @@ -512,15 +507,15 @@ def bookmarks_map(status_ids, account_id) end def reblogs_map(status_ids, account_id) - unscoped.select(:reblog_of_id).where(reblog_of_id: status_ids).where(account_id: account_id).each_with_object({}) { |s, h| h[s.reblog_of_id] = true } + unscoped.select(:reblog_of_id).where(reblog_of_id: status_ids).where(account_id: account_id).to_h { |s| [s.reblog_of_id, true] } end def mutes_map(conversation_ids, account_id) - ConversationMute.select(:conversation_id).where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true } + ConversationMute.select(:conversation_id).where(conversation_id: conversation_ids).where(account_id: account_id).to_h { |m| [m.conversation_id, true] } end def pins_map(status_ids, account_id) - StatusPin.select(:status_id).where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true } + StatusPin.select(:status_id).where(status_id: status_ids).where(account_id: account_id).to_h { |p| [p.status_id, true] } end def emoji_reaction_allows_map(status_ids, account_id) diff --git a/app/models/tag.rb b/app/models/tag.rb index c7577e59614f5a..047bc20b1936a4 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -42,7 +42,7 @@ class Tag < ApplicationRecord HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}".freeze - HASHTAG_RE = /(?<=^|\s)[##](#{HASHTAG_NAME_PAT})/ + HASHTAG_RE = /(?<=^|[[:space:]])[##](#{HASHTAG_NAME_PAT})/ HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/ diff --git a/app/models/tagged_object.rb b/app/models/tagged_object.rb new file mode 100644 index 00000000000000..7f4b83f43157fe --- /dev/null +++ b/app/models/tagged_object.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: tagged_objects +# +# id :bigint(8) not null, primary key +# ap_type :string not null +# object_type :string +# uri :string +# created_at :datetime not null +# updated_at :datetime not null +# object_id :bigint(8) +# status_id :bigint(8) not null +# +class TaggedObject < ApplicationRecord + belongs_to :status, inverse_of: :tagged_objects + belongs_to :object, polymorphic: true, optional: true +end diff --git a/app/models/terms_of_service.rb b/app/models/terms_of_service.rb index 41afaf10d9cc75..7fb025ea3fd878 100644 --- a/app/models/terms_of_service.rb +++ b/app/models/terms_of_service.rb @@ -31,6 +31,10 @@ def self.current live.first || upcoming.first # For the case when none of the published terms have become effective yet end + def usable_effective_date + effective_date || Time.zone.today + end + def published? published_at.present? end diff --git a/app/models/trends/history.rb b/app/models/trends/history.rb index 21331f00dca466..9e4d173475a717 100644 --- a/app/models/trends/history.rb +++ b/app/models/trends/history.rb @@ -40,11 +40,11 @@ def uses with_redis { |redis| redis.get(key_for(:uses)).to_i } end - def add(account_id) + def add(value) with_redis do |redis| redis.pipelined do |pipeline| pipeline.incrby(key_for(:uses), 1) - pipeline.pfadd(key_for(:accounts), account_id) + pipeline.pfadd(key_for(:accounts), value) pipeline.expire(key_for(:uses), EXPIRE_AFTER) pipeline.expire(key_for(:accounts), EXPIRE_AFTER) end diff --git a/app/models/user.rb b/app/models/user.rb index 3e111d13ee21cd..718087e32937ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -173,12 +173,16 @@ def valid_invitation? invite_id.present? && invite.valid_for_use? end + def valid_bypassing_invitation? + valid_invitation? && invite.bypass_approval? + end + def disable! update!(disabled: true) # This terminates all connections for the given account with the streaming # server: - redis.publish("timeline:system:#{account.id}", Oj.dump(event: :kill)) + redis.publish("timeline:system:#{account.id}", { event: :kill }.to_json) end def enable! @@ -214,8 +218,10 @@ def update_sign_in!(new_sign_in: false) increment(:sign_in_count) if new_sign_in - save(validate: false) unless new_record? - prepare_returning_user! + unless new_record? + save(validate: false) + prepare_returning_user! + end end def disable_css @@ -366,7 +372,7 @@ def revoke_access! # Revoke each access token for the Streaming API, since `update_all`` # doesn't trigger ActiveRecord Callbacks: # TODO: #28793 Combine into a single topic - payload = Oj.dump(event: :kill) + payload = { event: :kill }.to_json redis.pipelined do |pipeline| batch.ids.each do |id| pipeline.publish("timeline:access_token:#{id}", payload) @@ -439,7 +445,7 @@ def set_approved if requires_approval? false else - open_registrations? || valid_invitation? || external? + open_registrations? || valid_bypassing_invitation? || external? end end end @@ -482,18 +488,15 @@ def sign_up_from_ip_requires_approval? end def sign_up_email_requires_approval? - return false if email.blank? - - _, domain = email.split('@', 2) - return false if domain.blank? + return false if email_domain.blank? records = [] # Doing this conditionally is not very satisfying, but this is consistent # with the MX records validations we do and keeps the specs tractable. - records = DomainResource.new(domain).mx unless self.class.skip_mx_check? + records = DomainResource.new(email_domain).mx unless self.class.skip_mx_check? - EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip) + EmailDomainBlock.requires_approval?(records + [email_domain], attempt_ip: sign_up_ip) end def sign_up_username_requires_approval? diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 62308e0e6d1ab3..3998b1fe9d0540 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -38,6 +38,7 @@ class UserRole < ApplicationRecord manage_user_access: (1 << 18), delete_user_data: (1 << 19), view_feeds: (1 << 20), + invite_bypass_approval: (1 << 21), manage_sensitive_words: (1 << 29), manage_ng_words: (1 << 30), }.freeze @@ -54,10 +55,12 @@ module Flags DEFAULT = 0 EVERYONE_ALLOWED = FLAGS[:invite_users] + SAFE = FLAGS[:invite_users] | FLAGS[:invite_bypass_approval] CATEGORIES = { invites: %i( invite_users + invite_bypass_approval ).freeze, moderation: %i( @@ -211,6 +214,6 @@ def validate_position_elevation end def validate_dangerous_permissions - errors.add(:permissions_as_keys, :dangerous) if everyone? && Flags::EVERYONE_ALLOWED & permissions != permissions + errors.add(:permissions_as_keys, :dangerous) if everyone? && Flags::SAFE & permissions != permissions end end diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 25140598a50eba..b13c5c97d477f6 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -29,6 +29,7 @@ class Web::PushSubscription < ApplicationRecord validates_with WebPushKeyValidator delegate :locale, to: :user + delegate :token, to: :access_token, prefix: :associated_access generates_token_for :unsubscribe, expires_in: Web::PushNotificationWorker::TTL @@ -36,10 +37,6 @@ def pushable?(notification) policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification) end - def associated_access_token - access_token.token - end - class << self def unsubscribe_for(application_id, resource_owner) access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id).not_revoked.pluck(:id) diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index f06aeb8674113a..d924e1f2a31eb6 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -60,7 +60,7 @@ def domain_blocking_map Rails.cache.write_multi(to_cache, expires_in: 1.day) # Return formatted value - @accounts.each_with_object({}) { |account, h| h[account.id] = blocks_by_domain[account.domain] } + @accounts.to_h { |account| [account.id, blocks_by_domain[account.domain]] } end def cached diff --git a/app/presenters/tag_relationships_presenter.rb b/app/presenters/tag_relationships_presenter.rb index 922eb7a39ba979..669b35886250d5 100644 --- a/app/presenters/tag_relationships_presenter.rb +++ b/app/presenters/tag_relationships_presenter.rb @@ -8,8 +8,8 @@ def initialize(tags, current_account_id = nil, **options) @following_map = {} @featuring_map = {} else - @following_map = TagFollow.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).each_with_object({}) { |f, h| h[f.tag_id] = true }.merge(options[:following_map] || {}) - @featuring_map = FeaturedTag.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).each_with_object({}) { |f, h| h[f.tag_id] = true }.merge(options[:featuring_map] || {}) + @following_map = TagFollow.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).to_h { |f| [f.tag_id, true] }.merge(options[:following_map] || {}) + @featuring_map = FeaturedTag.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).to_h { |f| [f.tag_id, true] }.merge(options[:featuring_map] || {}) end end end diff --git a/app/serializers/activitypub/accept_feature_request_serializer.rb b/app/serializers/activitypub/accept_feature_request_serializer.rb new file mode 100644 index 00000000000000..99393dac91225c --- /dev/null +++ b/app/serializers/activitypub/accept_feature_request_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class ActivityPub::AcceptFeatureRequestSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :id, :type, :actor, :to, :result + + has_one :virtual_object, key: :object + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#accepts/feature_requests/', object.id].join + end + + def type + 'Accept' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.uri_for(object.collection.account) + end + + def virtual_object + object.activity_uri + end + + def result + ap_account_feature_authorization_url(object.account_id, object) + end +end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 68d055d5420bdb..5daa852995e68b 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -8,7 +8,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer context_extensions :manually_approves_followers, :featured, :also_known_as, :moved_to, :property_value, :discoverable, :suspended, :searchable_by, - :other_setting, :memorial, :indexable, :attribution_domains + :other_setting, :memorial, :indexable, :attribution_domains, :profile_settings context_extensions :interaction_policies if Mastodon::Feature.collections_enabled? @@ -16,7 +16,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :inbox, :outbox, :featured, :featured_tags, :preferred_username, :name, :summary, :url, :manually_approves_followers, - :discoverable, :indexable, :published, :memorial, :searchable_by, :other_setting + :discoverable, :indexable, :published, :memorial, :searchable_by, :other_setting, + :show_featured, :show_media + + attribute :show_media_replies, key: :show_replies_in_media attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? } diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index d7c55de465a5c3..4a371d8bc3b37f 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -159,7 +159,7 @@ def virtual_attachments end def virtual_tags - object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis + object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis + object.tagged_objects.map(&:object) end class NoteLink < ActiveModelSerializers::Model @@ -412,8 +412,9 @@ def name end end - class CustomEmojiSerializer < ActivityPub::EmojiSerializer - end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer; end + + class CollectionSerializer < ActivityPub::FeaturedCollectionSerializer; end class OptionSerializer < ActivityPub::Serializer class RepliesSerializer < ActivityPub::Serializer diff --git a/app/serializers/activitypub/reject_feature_request_serializer.rb b/app/serializers/activitypub/reject_feature_request_serializer.rb new file mode 100644 index 00000000000000..4dbc2cdf11b62c --- /dev/null +++ b/app/serializers/activitypub/reject_feature_request_serializer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ActivityPub::RejectFeatureRequestSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :id, :type, :actor, :to + + has_one :virtual_object, key: :object + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#rejects/feature_requests/', object.id].join + end + + def type + 'Reject' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.uri_for(object.collection.account) + end + + def virtual_object + object.activity_uri + end +end diff --git a/app/serializers/rest/collection_serializer.rb b/app/serializers/rest/collection_serializer.rb index 9296a5cf4ad82f..c3f2b55a84a5c9 100644 --- a/app/serializers/rest/collection_serializer.rb +++ b/app/serializers/rest/collection_serializer.rb @@ -5,7 +5,7 @@ class REST::CollectionSerializer < ActiveModel::Serializer :local, :sensitive, :discoverable, :item_count, :created_at, :updated_at - belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer + belongs_to :tag, serializer: REST::ShallowTagSerializer has_many :items, serializer: REST::CollectionItemSerializer @@ -13,6 +13,12 @@ def id object.id.to_s end + def description + return object.description if object.local? + + Sanitize.fragment(object.description_html, Sanitize::Config::MASTODON_STRICT) + end + def items object.items_for(current_user&.account) end diff --git a/app/serializers/rest/collection_with_accounts_serializer.rb b/app/serializers/rest/collection_with_accounts_serializer.rb index 5d05a32d687e61..be0b9550227cb8 100644 --- a/app/serializers/rest/collection_with_accounts_serializer.rb +++ b/app/serializers/rest/collection_with_accounts_serializer.rb @@ -10,6 +10,6 @@ def collection end def accounts - [object.account] + object.collection_items.map(&:account) + [object.account] + object.collection_items.filter_map(&:account) end end diff --git a/app/serializers/rest/shallow_tag_serializer.rb b/app/serializers/rest/shallow_tag_serializer.rb new file mode 100644 index 00000000000000..cdf2a3e1f90392 --- /dev/null +++ b/app/serializers/rest/shallow_tag_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::ShallowTagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url + + def url + tag_url(object) + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index b64ec294dbf17a..4bed993b28f760 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -32,6 +32,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :ordered_mentions, key: :mentions has_many :tags has_many :emojis, serializer: REST::CustomEmojiSlimSerializer + has_many :tagged_collections, serializer: REST::CollectionSerializer # Due to a ActiveModel::Serializer quirk, if you change any of the following, have a look at # updating `app/serializers/rest/shallow_status_serializer.rb` as well @@ -243,6 +244,10 @@ def expires_at object.scheduled_expiration_status.scheduled_at end + def tagged_collections + object.tagged_objects.filter_map { |tagged_object| tagged_object.object if tagged_object.ap_type == 'FeaturedCollection' } + end + def quote_approval { automatic: object.proper.quote_policy_as_keys(:automatic), @@ -289,13 +294,5 @@ def acct end end - class TagSerializer < ActiveModel::Serializer - include RoutingHelper - - attributes :name, :url - - def url - tag_url(object) - end - end + class TagSerializer < REST::ShallowTagSerializer; end end diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 4fa9c6631ff384..2e9dc4d3f279d1 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -22,6 +22,8 @@ def links { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_page_href }, { rel: 'self', type: 'application/activity+json', href: self_href }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, + { rel: 'https://w3id.org/fep/3b86/Create', template: "#{share_url}?text={content}" }, + { rel: 'https://w3id.org/fep/3b86/Object', template: "#{authorize_interaction_url}?uri={object}" }, ].tap do |x| x << { rel: 'http://webfinger.net/rel/avatar', type: object.avatar.content_type, href: full_asset_url(object.avatar_original_url) } if show_avatar? end diff --git a/app/services/activitypub/fetch_featured_collections_collection_service.rb b/app/services/activitypub/fetch_featured_collections_collection_service.rb new file mode 100644 index 00000000000000..99a45fac3e2024 --- /dev/null +++ b/app/services/activitypub/fetch_featured_collections_collection_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ActivityPub::FetchFeaturedCollectionsCollectionService < BaseService + include JsonLdHelper + + MAX_PAGES = 10 + MAX_ITEMS = 50 + + def call(account, request_id: nil) + return if account.collections_url.blank? || account.suspended? || account.local? + + @request_id = request_id + @account = account + @items, = collection_items(@account.collections_url, max_pages: MAX_PAGES, reference_uri: @account.uri) + process_items(@items) + end + + private + + def process_items(items) + return if items.nil? + + items.take(MAX_ITEMS).each do |collection_json| + if collection_json.is_a?(String) + ActivityPub::FetchRemoteFeaturedCollectionService.new.call(collection_json, request_id: @request_id) + else + ActivityPub::ProcessFeaturedCollectionService.new.call(@account, collection_json, request_id: @request_id) + end + end + end +end diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb index 560cf424e1539f..1fb3f45ce5811f 100644 --- a/app/services/activitypub/fetch_remote_actor_service.rb +++ b/app/services/activitypub/fetch_remote_actor_service.rb @@ -19,7 +19,7 @@ def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, s else body_to_json(prefetched_body, compare_id: uri) end - rescue Oj::ParseError + rescue JSON::ParserError raise Error, "Error parsing JSON-LD document #{uri}" end diff --git a/app/services/activitypub/fetch_remote_featured_collection_service.rb b/app/services/activitypub/fetch_remote_featured_collection_service.rb new file mode 100644 index 00000000000000..babad143e1751a --- /dev/null +++ b/app/services/activitypub/fetch_remote_featured_collection_service.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ActivityPub::FetchRemoteFeaturedCollectionService < BaseService + include JsonLdHelper + + def call(uri, request_id: nil, on_behalf_of: nil) + json = fetch_resource(uri, true, on_behalf_of) + + return unless supported_context?(json) + return unless json['type'] == 'FeaturedCollection' + + # Fetching an unknown account should eventually also fetch its + # collections, so it should be OK to only handle known accounts here + account = Account.find_by(uri: json['attributedTo']) + return unless account + + existing_collection = account.collections.find_by(uri:) + return existing_collection if existing_collection.present? + + ActivityPub::ProcessFeaturedCollectionService.new.call(account, json, request_id:) + end +end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 10059dfe1140cd..35a380c9cfb4fa 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -65,13 +65,14 @@ def call(username, domain, json, options = {}) unless @options[:only_key] || (@account.suspended? && !@account.remote_pending) check_featured_collection! if @json['featured'].present? check_featured_tags_collection! if @json['featuredTags'].present? + check_featured_collections_collection! if @json['featuredCollections'].present? && Mastodon::Feature.collections_federation_enabled? check_links! if @account.fields.any?(&:requires_verification?) end fetch_instance_info @account - rescue Oj::ParseError + rescue JSON::ParserError nil end @@ -142,6 +143,7 @@ def valid_collection_uri(uri) def set_immediate_attributes! @account.featured_collection_url = valid_collection_uri(@json['featured']) + @account.collections_url = valid_collection_uri(@json['featuredCollections']) @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 @@ -153,6 +155,9 @@ def set_immediate_attributes! @account.settings = other_settings @account.master_settings = (@account.master_settings || {}).merge(master_settings(@account.note)) @account.memorial = @json['memorial'] || false + @account.show_featured = @json['showFeatured'] if @json.key?('showFeatured') + @account.show_media = @json['showMedia'] if @json.key?('showMedia') + @account.show_media_replies = @json['showRepliesInMedia'] if @json.key?('showRepliesInMedia') @account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) } end @@ -237,6 +242,10 @@ def check_featured_tags_collection! ActivityPub::SynchronizeFeaturedTagsCollectionWorker.perform_async(@account.id, @json['featuredTags']) end + def check_featured_collections_collection! + ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker.perform_async(@account.id, @options[:request_id]) + end + def check_links! VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), @account.id) end @@ -273,7 +282,7 @@ def image_url_and_description(key) url = first_of_value(value['url']) url = url['href'] if url.is_a?(Hash) description = value['summary'].presence || value['name'].presence - description = description.strip[0...MediaAttachment::MAX_DESCRIPTION_LENGTH] if description.present? + description = description.strip[0...MediaAttachment::MAX_DESCRIPTION_HARD_LENGTH_LIMIT] if description.present? else url = value end diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index e14e840b2029a7..7cb4e256520ac5 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -6,7 +6,7 @@ class ActivityPub::ProcessCollectionService < BaseService def call(body, actor, **options) @account = actor - @json = original_json = Oj.load(body, mode: :strict) + @json = original_json = JSON.parse(body) @options = options return unless @json.is_a?(Hash) @@ -42,7 +42,7 @@ def call(body, actor, **options) else process_items [@json] end - rescue Oj::ParseError + rescue JSON::ParserError nil end diff --git a/app/services/activitypub/process_featured_collection_service.rb b/app/services/activitypub/process_featured_collection_service.rb new file mode 100644 index 00000000000000..2ef555e6bc2d84 --- /dev/null +++ b/app/services/activitypub/process_featured_collection_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class ActivityPub::ProcessFeaturedCollectionService + include JsonLdHelper + include Lockable + include Redisable + + ITEMS_LIMIT = 150 + + def call(account, json, request_id: nil) + @account = account + @json = json + @request_id = request_id + return if non_matching_uri_hosts?(@account.uri, @json['id']) + + with_redis_lock("collection:#{@json['id']}") do + Collection.transaction do + @collection = @account.collections.find_or_initialize_by(uri: @json['id']) + + @collection.update!( + local: false, + name: (@json['name'] || '')[0, Collection::NAME_LENGTH_HARD_LIMIT], + description_html: truncated_summary, + language:, + sensitive: @json['sensitive'], + discoverable: @json['discoverable'], + original_number_of_items: @json['totalItems'] || 0, + tag_name: @json.dig('topic', 'name') + ) + + process_items! + end + + @collection + end + end + + private + + def truncated_summary + text = @json['summaryMap']&.values&.first || @json['summary'] || '' + text[0, Collection::DESCRIPTION_LENGTH_HARD_LIMIT] + end + + def language + @json['summaryMap']&.keys&.first + end + + def process_items! + uris = [] + items = @json['orderedItems'] || [] + items.take(ITEMS_LIMIT).each_with_index do |item_json, index| + uris << value_or_id(item_json) + ActivityPub::ProcessFeaturedItemWorker.perform_async(@collection.id, item_json, index, @request_id) + end + uris.compact! + @collection.collection_items.where.not(uri: uris).delete_all + end +end diff --git a/app/services/activitypub/process_featured_item_service.rb b/app/services/activitypub/process_featured_item_service.rb new file mode 100644 index 00000000000000..961de802c95482 --- /dev/null +++ b/app/services/activitypub/process_featured_item_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class ActivityPub::ProcessFeaturedItemService + include JsonLdHelper + include Lockable + include Redisable + + def call(collection, uri_or_object, position: nil, request_id: nil) + @collection = collection + @request_id = request_id + @item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object + return if non_matching_uri_hosts?(@collection.uri, @item_json['id']) + + with_redis_lock("collection_item:#{@item_json['id']}") do + @collection_item = existing_item || pre_approved_item || new_item + + @collection_item.update!( + uri: @item_json['id'], + object_uri: value_or_id(@item_json['featuredObject']), + position: + ) + + @approval_uri = @item_json['featureAuthorization'] + + verify_authorization! unless @collection_item&.account&.local? + + @collection_item + end + end + + private + + def existing_item + @collection.collection_items.find_by(uri: @item_json['id']) + end + + def pre_approved_item + # This is a local account that has authorized this item already + local_account = ActivityPub::TagManager.instance.uris_to_local_accounts([@item_json['featuredObject']]).first + @collection.collection_items.accepted_partial(local_account).first if local_account.present? + end + + def new_item + @collection.collection_items.new + end + + def verify_authorization! + ActivityPub::VerifyFeaturedItemService.new.call(@collection_item, @approval_uri, request_id: @request_id) + rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS + ActivityPub::VerifyFeaturedItemWorker.perform_in(rand(30..600).seconds, @collection_item.id, @approval_uri, @request_id) + end +end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index b76b0615bae157..cb039c8bfe2f0f 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -62,7 +62,7 @@ def handle_explicit_update! create_edits! end - fetch_and_verify_quote!(@quote, @status_parser.quote_uri) if @quote.present? + fetch_and_verify_quote!(@quote, @quote_approval_uri, @status_parser.quote_uri) if @quote.present? update_references! download_media_files! queue_poll_notifications! @@ -249,9 +249,10 @@ def process_sensitive_words end def read_metadata - @raw_tags = [] + @raw_tags = [] @raw_mentions = [] - @raw_emojis = [] + @raw_tagged_objects = [] + @raw_emojis = [] as_array(@json['tag']).each do |tag| if equals_or_includes?(tag['type'], 'Hashtag') @@ -260,12 +261,15 @@ def read_metadata @raw_mentions << tag['href'] if tag['href'].present? elsif equals_or_includes?(tag['type'], 'Emoji') @raw_emojis << tag + elsif equals_or_includes?(tag['type'], 'FeaturedCollection') + @raw_tagged_objects << tag if tag['id'] end end end def update_metadata! update_tags! + update_tagged_objects! update_mentions! update_emojis! update_quote! @@ -298,6 +302,24 @@ def update_tags! end end + def update_tagged_objects! + current_tagged_objects = @raw_tagged_objects.filter_map do |tagged_object| + url = tagged_object['id'] + + # TODO: We probably want to resolve unknown objects at authoring time + ActivityPub::TagManager.instance.uri_to_resource(url, Collection) + end + + # Any previously-unresolved URI would be resolved here + @status.tagged_objects.upsert_all( + current_tagged_objects.uniq.map { |object| { object_type: object.class.name, object_id: object.id, uri: ActivityPub::TagManager.instance.uri_for(object), ap_type: 'FeaturedCollection' } }, + unique_by: %w(status_id uri) + ) + + # Remove unused links + @status.tagged_objects.where.not(uri: current_tagged_objects.map { |object| ActivityPub::TagManager.instance.uri_for(object) }).delete_all + end + def update_mentions! unresolved_mentions = [] @@ -393,9 +415,9 @@ def update_quote_approval! approval_uri = @status_parser.quote_approval_uri approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri) - quote.update(approval_uri: approval_uri, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri != @status_parser.quote_approval_uri + quote.update(approval_uri: nil, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri.present? && quote.approval_uri != @status_parser.quote_approval_uri - fetch_and_verify_quote!(quote, quote_uri) + fetch_and_verify_quote!(quote, approval_uri, quote_uri) end def update_quote! @@ -411,18 +433,20 @@ def update_quote! # Revoke the quote while we get a chance… maybe this should be a `before_destroy` hook? RevokeQuoteService.new.call(@status.quote) if @status.quote.quoted_account&.local? && @status.quote.accepted? @status.quote.destroy - quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending) + quote = Quote.create(status: @status, approval_uri: nil, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending) @quote_changed = true else quote = @status.quote - quote.update(approval_uri: approval_uri, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri != approval_uri + quote.update(approval_uri: nil, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri.present? && quote.approval_uri != approval_uri end else - quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?) + quote = Quote.create(status: @status, approval_uri: nil, legacy: @status_parser.legacy_quote?) @quote_changed = true end @quote = quote + @quote_approval_uri = approval_uri + quote.save elsif @status.quote.present? @quote = nil @@ -431,11 +455,11 @@ def update_quote! end end - def fetch_and_verify_quote!(quote, quote_uri) + def fetch_and_verify_quote!(quote, approval_uri, quote_uri) embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @activity_json['context']) - ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, prefetched_quoted_object: embedded_quote, request_id: @request_id) + ActivityPub::VerifyQuoteService.new.call(quote, approval_uri, fetchable_quoted_uri: quote_uri, prefetched_quoted_object: embedded_quote, request_id: @request_id) rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS - ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, quote.id, quote_uri, { 'request_id' => @request_id }) + ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, quote.id, quote_uri, { 'request_id' => @request_id, 'approval_uri' => approval_uri }) end def update_counts! diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index 9e0b452929dc98..30f475ed07e1c0 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -62,7 +62,7 @@ def handle_unexpected_outgoing_follows!(expected_followers) end def build_undo_follow_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) + serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json end # Only returns true if the whole collection has been processed diff --git a/app/services/activitypub/verify_featured_item_service.rb b/app/services/activitypub/verify_featured_item_service.rb new file mode 100644 index 00000000000000..6ce524870c41ca --- /dev/null +++ b/app/services/activitypub/verify_featured_item_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ActivityPub::VerifyFeaturedItemService + include JsonLdHelper + + def call(collection_item, approval_uri, request_id: nil) + @collection_item = collection_item + @authorization = fetch_resource(approval_uri, true, raise_on_error: :temporary) + + if @authorization.nil? + @collection_item.update!(state: :rejected) + return + end + + return if non_matching_uri_hosts?(approval_uri, @authorization['interactionTarget']) + return unless matching_type? && matching_collection_uri? + + account = Account.where(uri: @collection_item.object_uri).first + account ||= ActivityPub::FetchRemoteAccountService.new.call(@collection_item.object_uri, request_id:) + return if account.blank? + + @collection_item.update!(account:, approval_uri:, state: :accepted) + end + + private + + def matching_type? + supported_context?(@authorization) && equals_or_includes?(@authorization['type'], 'FeatureAuthorization') + end + + def matching_collection_uri? + @collection_item.collection.uri == @authorization['interactingObject'] + end +end diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb index 6e0a225fde0b90..30f20a3fdfc983 100644 --- a/app/services/activitypub/verify_quote_service.rb +++ b/app/services/activitypub/verify_quote_service.rb @@ -6,24 +6,25 @@ class ActivityPub::VerifyQuoteService < BaseService MAX_SYNCHRONOUS_DEPTH = 2 # Optionally fetch quoted post, and verify the quote is authorized - def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil, depth: nil) + def call(quote, approval_uri, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil, depth: nil) @request_id = request_id @depth = depth || 0 @quote = quote + @approval_uri = approval_uri.presence || @quote.approval_uri @fetching_error = nil fetch_quoted_post_if_needed!(fetchable_quoted_uri, prefetched_body: prefetched_quoted_object) - return quote.accept! if Setting.auto_accept_legacy_quotes && (quote.legacy || (legacy_quote_available? && quote.approval_uri == 'http://kmy.blue/ns#LegacyQuote')) + return quote.accept!(legacy: legacy_quote_available?) if Setting.auto_accept_legacy_quotes && (quote.legacy || (legacy_quote_available? && @approval_uri == 'http://kmy.blue/ns#LegacyQuote')) - return if quote.approval_uri == 'http://kmy.blue/ns#LegacyQuote' + return quote.pend_legacy! if @approval_uri == 'http://kmy.blue/ns#LegacyQuote' return if quote.quoted_account&.local? - return if fast_track_approval! || quote.approval_uri.blank? + return if fast_track_approval! || @approval_uri.blank? - @json = fetch_approval_object(quote.approval_uri, prefetched_body: prefetched_approval) + @json = fetch_approval_object(@approval_uri, prefetched_body: prefetched_approval) return quote.reject! if @json.nil? - return if non_matching_uri_hosts?(quote.approval_uri, value_or_id(@json['attributedTo'])) + return if non_matching_uri_hosts?(@approval_uri, value_or_id(@json['attributedTo'])) return unless matching_type? && matching_quote_uri? # Opportunistically import embedded posts if needed @@ -34,7 +35,7 @@ def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefet return unless matching_quoted_post? && matching_quoted_author? - quote.accept! + quote.accept!(approval_uri: @approval_uri) end private @@ -95,7 +96,7 @@ def import_quoted_post_if_needed!(uri) object = @json['interactionTarget'].merge({ '@context' => @json['@context'] }) # It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations - return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id']) + return if object['id'] != uri || non_matching_uri_hosts?(@approval_uri, object['id']) status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth) diff --git a/app/services/after_block_domain_from_account_service.rb b/app/services/after_block_domain_from_account_service.rb index fc5dc6568103e1..549d5086745844 100644 --- a/app/services/after_block_domain_from_account_service.rb +++ b/app/services/after_block_domain_from_account_service.rb @@ -54,7 +54,7 @@ def reject_follow!(follow) return unless follow.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, @account.id, follow.account.inbox_url) end def notify_of_severed_relationships! diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 49bef727e6a8c9..1c2c6da3ceaf9d 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -22,6 +22,6 @@ def create_notification(follow_request) end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) + serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer).to_json end end diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 9c4b35a10b8e96..35bcb06b804e1c 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -7,6 +7,12 @@ class BackupService < BaseService include ContextHelper CHUNK_SIZE = 1.megabyte + PLACEHOLDER = '!PLACEHOLDER!' + + STREAM_ACTOR = 'actor.json' + STREAM_BOOKMARKS = 'bookmarks.json' + STREAM_LIKES = 'likes.json' + STREAM_OUTBOX = 'outbox.json' attr_reader :account, :backup @@ -20,18 +26,16 @@ def call(backup) private def build_outbox_json!(file) - skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) + skeleton = serialize(collection_presenter(STREAM_OUTBOX, size: account.statuses.count), ActivityPub::CollectionSerializer) skeleton[:@context] = full_context - skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') - add_comma = false + skeleton[:orderedItems] = [PLACEHOLDER] + skeleton = skeleton.to_json + prepend, append = skeleton.split(PLACEHOLDER.to_json) file.write(prepend) - account.statuses.with_includes.reorder(nil).find_in_batches do |statuses| - file.write(',') if add_comma - add_comma = true + account.statuses.with_includes.reorder(nil).find_in_batches.with_index do |statuses, batch| + file.write(',') unless batch.zero? file.write(statuses.map do |status| serializer = status.reblog? ? ActivityPub::AnnounceNoteSerializer : ActivityPub::CreateNoteSerializer @@ -44,7 +48,7 @@ def build_outbox_json!(file) end end - Oj.dump(item) + item.to_json end.join(',')) GC.start @@ -56,17 +60,9 @@ def build_outbox_json!(file) def build_archive! tmp_file = Tempfile.new(%w(archive .zip)) - Zip::File.open(tmp_file, create: true) do |zipfile| - dump_outbox!(zipfile) - dump_media_attachments!(zipfile) - dump_likes!(zipfile) - dump_bookmarks!(zipfile) - dump_actor!(zipfile) - end - - archive_filename = "#{['archive', Time.current.to_fs(:number), SecureRandom.hex(16)].join('-')}.zip" + build_zip_file(tmp_file) - @backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename) + @backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename) @backup.processed = true @backup.save! ensure @@ -74,6 +70,24 @@ def build_archive! tmp_file.unlink end + def build_zip_file(file) + Zip::File.open(file, create: true) do |zip| + dump_outbox!(zip) + dump_media_attachments!(zip) + dump_likes!(zip) + dump_bookmarks!(zip) + dump_actor!(zip) + end + end + + def archive_filename + "#{archive_id}.zip" + end + + def archive_id + [:archive, Time.current.to_fs(:number), SecureRandom.hex(16)].join('-') + end + def dump_media_attachments!(zipfile) MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments| media_attachments.each do |m| @@ -90,7 +104,7 @@ def dump_media_attachments!(zipfile) end def dump_outbox!(zipfile) - zipfile.get_output_stream('outbox.json') do |io| + zipfile.get_output_stream(STREAM_OUTBOX) do |io| build_outbox_json!(io) end end @@ -100,38 +114,34 @@ def dump_actor!(zipfile) actor[:icon][:url] = "avatar#{File.extname(actor[:icon][:url])}" if actor[:icon] actor[:image][:url] = "header#{File.extname(actor[:image][:url])}" if actor[:image] - actor[:outbox] = 'outbox.json' - actor[:likes] = 'likes.json' - actor[:bookmarks] = 'bookmarks.json' + actor[:outbox] = STREAM_OUTBOX + actor[:likes] = STREAM_LIKES + actor[:bookmarks] = STREAM_BOOKMARKS download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? - json = Oj.dump(actor) - - zipfile.get_output_stream('actor.json') do |io| - io.write(json) + zipfile.get_output_stream(STREAM_ACTOR) do |io| + io.write(actor.to_json) end end def dump_likes!(zipfile) - skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + skeleton = serialize(collection_presenter(STREAM_LIKES), ActivityPub::CollectionSerializer) + skeleton.delete(:totalItems) - skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') + skeleton[:orderedItems] = [PLACEHOLDER] + skeleton = skeleton.to_json + prepend, append = skeleton.split(PLACEHOLDER.to_json) - zipfile.get_output_stream('likes.json') do |io| + zipfile.get_output_stream(STREAM_LIKES) do |io| io.write(prepend) - add_comma = false - - Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses| - io.write(',') if add_comma - add_comma = true + favourite_statuses.find_in_batches.with_index do |statuses, batch| + io.write(',') unless batch.zero? io.write(statuses.map do |status| - Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + ActivityPub::TagManager.instance.uri_for(status).to_json end.join(',')) GC.start @@ -141,23 +151,25 @@ def dump_likes!(zipfile) end end + def favourite_statuses + Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites) + end + def dump_bookmarks!(zipfile) - skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + skeleton = serialize(collection_presenter(STREAM_BOOKMARKS), ActivityPub::CollectionSerializer) skeleton.delete(:totalItems) - skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') + skeleton[:orderedItems] = [PLACEHOLDER] + skeleton = skeleton.to_json + prepend, append = skeleton.split(PLACEHOLDER.to_json) - zipfile.get_output_stream('bookmarks.json') do |io| + zipfile.get_output_stream(STREAM_BOOKMARKS) do |io| io.write(prepend) - add_comma = false - Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses| - io.write(',') if add_comma - add_comma = true + bookmark_statuses.find_in_batches.with_index do |statuses, batch| + io.write(',') unless batch.zero? io.write(statuses.map do |status| - Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + ActivityPub::TagManager.instance.uri_for(status).to_json end.join(',')) GC.start @@ -167,12 +179,16 @@ def dump_bookmarks!(zipfile) end end - def collection_presenter + def bookmark_statuses + Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks) + end + + def collection_presenter(id, size: 0) ActivityPub::CollectionPresenter.new( - id: 'outbox.json', - type: :ordered, - size: account.statuses_count, - items: [] + id:, + items: [], + size:, + type: :ordered ) end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index f87d9f868043f7..a691601106bb8d 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -91,7 +91,7 @@ def unpush_from_antenna_timelines(_account, statuses) def unpush_from_public_timelines(status, pipeline) return unless status.public_visibility? && status.id > @status_id_cutoff - payload = Oj.dump(event: :delete, payload: status.id.to_s) + payload = { event: :delete, payload: status.id.to_s }.to_json pipeline.publish('timeline:public', payload) pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 98229d98c0eaaf..cbd0e8e75b50df 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -26,6 +26,6 @@ def create_notification(block) end def build_json(block) - Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) + serialize_payload(block, ActivityPub::BlockSerializer).to_json end end diff --git a/app/services/create_featured_tag_service.rb b/app/services/create_featured_tag_service.rb index 298bdb5928cb72..b8cb5903aa63ff 100644 --- a/app/services/create_featured_tag_service.rb +++ b/app/services/create_featured_tag_service.rb @@ -26,6 +26,6 @@ def call(account, name_or_tag, raise_error: true) private def build_json(featured_tag) - Oj.dump(serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account)) + serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account).to_json end end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 425bdcf850b31b..a162e8869f1ad2 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -128,7 +128,7 @@ def reject_follows! # we have to force it to unfollow them. ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url] end end @@ -140,7 +140,7 @@ def undo_follows! # if the remote account gets un-suspended. ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json, follow.account_id, @account.inbox_url] end end @@ -318,7 +318,7 @@ def record_severed_relationships! end def delete_actor_json - @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true)) + @delete_actor_json ||= serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true).to_json end def delivery_inboxes diff --git a/app/services/delete_collection_item_service.rb b/app/services/delete_collection_item_service.rb index 23f539a6850a9b..47df001d60baf3 100644 --- a/app/services/delete_collection_item_service.rb +++ b/app/services/delete_collection_item_service.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class DeleteCollectionItemService - def call(collection_item) + def call(collection_item, revoke: false) @collection_item = collection_item @collection = collection_item.collection - @collection_item.destroy! + + revoke ? @collection_item.revoke! : @collection_item.destroy! distribute_remove_activity if Mastodon::Feature.collections_federation_enabled? end diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index d39a2a7368f279..c161538c3e5f9a 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -75,11 +75,11 @@ def increment_statistics end def payload - @payload = Oj.dump(serialize_payload(@emoji_reaction, ActivityPub::EmojiReactionSerializer, signer: @emoji_reaction.account)) + @payload = serialize_payload(@emoji_reaction, ActivityPub::EmojiReactionSerializer, signer: @emoji_reaction.account).to_json end def render_emoji_reaction(emoji_group) # @rendered_emoji_reaction ||= InlineRenderer.render(HashObject.new(emoji_group), nil, :emoji_reaction) - @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + @render_emoji_reaction ||= { event: :emoji_reaction, payload: emoji_group.to_json }.to_json end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 05ba941d2a827c..5ea506b46e0f76 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -222,10 +222,10 @@ def warm_payload_cache! end def anonymous_payload - @anonymous_payload ||= Oj.dump( + @anonymous_payload ||= JSON.generate({ event: update? ? :'status.update' : :update, - payload: rendered_status - ) + payload: rendered_status, + }.as_json) end def rendered_status diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 50ed5831eac4f1..41405f530d515c 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -45,6 +45,6 @@ def increment_statistics end def build_json(favourite) - Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) + serialize_payload(favourite, ActivityPub::LikeSerializer).to_json end end diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb index c7d4f7e2927aef..cc71a5f92bb038 100644 --- a/app/services/fetch_oembed_service.rb +++ b/app/services/fetch_oembed_service.rb @@ -86,14 +86,14 @@ def fetch! end validate(parse_for_format(body)) if body.present? - rescue Oj::ParseError, Ox::ParseError + rescue JSON::ParserError, Ox::ParseError nil end def parse_for_format(body) case @format when :json - Oj.load(body, mode: :strict)&.with_indifferent_access + JSON.parse(body)&.with_indifferent_access when :xml Ox.load(body, mode: :hash_no_attrs)&.with_indifferent_access&.dig(:oembed) end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 702fdde9599f46..32d9590c4d3d1f 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -93,7 +93,7 @@ def direct_follow! end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) + serialize_payload(follow_request, ActivityPub::FollowSerializer).to_json end def follow_options diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 1bc7542a05764f..1f375021f34d9e 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -262,7 +262,7 @@ def push_notification! end def push_to_streaming_api! - redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) + redis.publish("timeline:#{@recipient.id}:notifications", { event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification) }.to_json) end def subscribed_to_streaming_api? diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 050abfc0814177..853e2515a1cbf5 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -157,7 +157,7 @@ def searchability def process_status! @status = @account.statuses.new(status_attributes) - process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? @limited_scope : '', circle: @circle, save_records: false) + process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? @limited_scope : '', circle: @circle) safeguard_mentions!(@status) validate_status_ng_rules! validate_status_mentions! @@ -167,6 +167,7 @@ def process_status! UpdateStatusExpirationService.new.call(@status) safeguard_private_mention_quote!(@status) + attach_tagged_objects!(@status) attach_quote!(@status) antispam = Antispam.new(@status) @@ -203,6 +204,10 @@ def attach_quote!(status) end end + def attach_tagged_objects!(status) + ProcessLinksService.new.call(status) + end + def safeguard_mentions!(status) return if @options[:allowed_mentions].nil? diff --git a/app/services/process_links_service.rb b/app/services/process_links_service.rb new file mode 100644 index 00000000000000..e7dfb64c8efd13 --- /dev/null +++ b/app/services/process_links_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ProcessLinksService < BaseService + include Payloadable + + # Scan status for links to ActivityPub objects and attach them to statuses + # @param [Status] status + def call(status) + return unless status.local? + + @status = status + @previous_objects = @status.tagged_objects.includes(:object).to_a + @current_objects = [] + + Status.transaction do + scan_text! + assign_tagged_objects! + end + end + + private + + def scan_text! + urls = @status.text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize } + + urls.each do |url| + # We only support `FeaturedCollection` at this time + + # TODO: We probably want to resolve unknown objects at authoring time + object = ActivityPub::TagManager.instance.uri_to_resource(url.to_s, Collection) + next if object.nil? + + tagged_object = @previous_objects.find { |x| x.object == object || x.uri == url } + tagged_object ||= @current_objects.find { |x| x.object == object || x.uri == url } + tagged_object ||= @status.tagged_objects.new(object: object, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(object)) + + @current_objects << tagged_object + end + end + + def assign_tagged_objects! + return unless @status.persisted? + + @current_objects.each do |object| + object.save if object.new_record? + end + + # If previous objects are no longer contained in the text, remove them to lighten the database + removed_objects = @previous_objects - @current_objects + + TaggedObject.where(id: removed_objects.map(&:id)).delete_all unless removed_objects.empty? + end +end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index f7084b126f7e53..9e5e683516f5be 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -6,12 +6,10 @@ class ProcessMentionsService < BaseService # Scan status for mentions and fetch remote mentioned users, # and create local mention pointers # @param [Status] status - # @param [Boolean] save_records Whether to save records in database - def call(status, limited_type: '', circle: nil, save_records: true) + def call(status, limited_type: '', circle: nil) @status = status @limited_type = limited_type @circle = circle - @save_records = save_records return unless @status.local? @@ -73,7 +71,7 @@ def scan_text! process_mutual! if @limited_type == :mutual process_circle! if @limited_type == :circle - @status.save! if @save_records + @status.save! if @status.persisted? end def assign_mentions! @@ -88,8 +86,10 @@ def assign_mentions! dropped_mentions.each(&:destroy) end + return unless @status.persisted? + @current_mentions.each do |mention| - mention.save if (mention.new_record? || mention.silent_changed?) && @save_records + mention.save if mention.new_record? || mention.silent_changed? end # If previous mentions are no longer contained in the text, convert them diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index bc0000c8c886b0..4ced57bdf27d15 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -17,6 +17,6 @@ def create_notification(follow_request) end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow_request, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb index d76763409d3b0d..ed01b26e16a83c 100644 --- a/app/services/remove_domains_from_followers_service.rb +++ b/app/services/remove_domains_from_followers_service.rb @@ -18,6 +18,6 @@ def create_notification(follow) end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_featured_tag_service.rb b/app/services/remove_featured_tag_service.rb index 4fdd43eb6a6b92..69e8a47bf477a6 100644 --- a/app/services/remove_featured_tag_service.rb +++ b/app/services/remove_featured_tag_service.rb @@ -26,6 +26,6 @@ def call(account, featured_tag_or_tag) private def build_json(featured_tag) - Oj.dump(serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account)) + serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account).to_json end end diff --git a/app/services/remove_from_followers_service.rb b/app/services/remove_from_followers_service.rb index 007d5b1fdd4b00..22fb72cd649cd7 100644 --- a/app/services/remove_from_followers_service.rb +++ b/app/services/remove_from_followers_service.rb @@ -18,6 +18,6 @@ def create_notification(follow) end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index c7329df1a39425..f6864a6649ef52 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -14,7 +14,7 @@ class RemoveStatusService < BaseService # @option [Boolean] :original_removed # @option [Boolean] :skip_streaming def call(status, **options) - @payload = Oj.dump(event: :delete, payload: status.id.to_s) + @payload = { event: :delete, payload: status.id.to_s }.to_json @status = status @account = status.account @options = options @@ -128,7 +128,7 @@ def remove_from_conversation end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign_unsafe: @status.limited_visibility?)) + @signed_activity_json ||= serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign: true, always_sign_unsafe: @status.limited_visibility?).to_json end def remove_reblogs diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 5ddfdeb4f02ce1..6a28e6005cc73a 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -88,17 +88,24 @@ def reported_status_ids has_followers = @target_account.followers.with_domain(domain).exists? visibility = has_followers ? %i(public unlisted public_unlisted login private) : %i(public unlisted public_unlisted) scope = @target_account.statuses.with_discarded - scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain))) + scope.merge!(scope.where(visibility: visibility).or(scope.where(domain_mentions(domain)))) # Allow missing posts to not drop reports that include e.g. a deleted post scope.where(id: Array(@status_ids)).pluck(:id) end + def domain_mentions(domain) + Mention + .joins(:account) + .where(Account.arel_table[:domain].lower.eq domain) + .select(1).arel.exists + end + def reported_collection_ids @target_account.collections.find(Array(@collection_ids)).pluck(:id) end def payload - Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account)) + serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account).to_json end def some_local_account diff --git a/app/services/revoke_collection_item_service.rb b/app/services/revoke_collection_item_service.rb index d299b567f225b5..c0dc70e952cf92 100644 --- a/app/services/revoke_collection_item_service.rb +++ b/app/services/revoke_collection_item_service.rb @@ -19,6 +19,6 @@ def distribute_stamp_deletion! end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true)) + @signed_activity_json ||= serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true).to_json end end diff --git a/app/services/revoke_quote_service.rb b/app/services/revoke_quote_service.rb index 346fba89709ed8..1bc69c1f51cb57 100644 --- a/app/services/revoke_quote_service.rb +++ b/app/services/revoke_quote_service.rb @@ -39,6 +39,6 @@ def inboxes end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true)) + @signed_activity_json ||= serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true).to_json end end diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb index bafe259cde1c57..638384385f9b94 100644 --- a/app/services/software_update_check_service.rb +++ b/app/services/software_update_check_service.rb @@ -20,9 +20,9 @@ def clean_outdated_updates! def fetch_update_notices Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res| - return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200 + return JSON.parse(res.body_with_limit) if res.code == 200 end - rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError + rescue *Mastodon::HTTP_CONNECTION_ERRORS, JSON::ParserError nil end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 666b64cacfb17b..1ec868a961eae9 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -34,7 +34,7 @@ def reject_remote_follows! Follow.where(account: @account).find_in_batches do |follows| ActivityPub::DeliveryWorker.push_bulk(follows) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url] end follows.each(&:destroy) @@ -72,6 +72,6 @@ def remove_from_trends! end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account)) + @signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json end end diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index 256b1b3ae5dfb9..04cc5a011619ca 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -56,12 +56,12 @@ def write_stream(emoji_reaction) end def build_json(emoji_reaction) - @build_json = Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer, signer: emoji_reaction.account)) + @build_json = serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer, signer: emoji_reaction.account).to_json end def render_emoji_reaction(emoji_group) # @rendered_emoji_reaction ||= InlineRenderer.render(emoji_group, nil, :emoji_reaction) - Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + { event: :emoji_reaction, payload: emoji_group.to_json }.to_json end def relay_for_undo_emoji_reaction!(emoji_reaction) diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index c263ac8afe0548..31067618a98201 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -18,6 +18,6 @@ def create_notification(unblock) end def build_json(unblock) - Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) + serialize_payload(unblock, ActivityPub::UndoBlockSerializer).to_json end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index 37917a64f1bc52..2f422c42513a4d 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -18,6 +18,6 @@ def create_notification(favourite) end def build_json(favourite) - Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) + serialize_payload(favourite, ActivityPub::UndoLikeSerializer).to_json end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 385aa0c7b17dba..a77f8c012d0472 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -68,10 +68,10 @@ def send_reject_follow(follow) end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) + serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json end def build_reject_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 1a52e80d246642..95cb18606fbf0f 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -63,6 +63,6 @@ def publish_media_attachments! end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account)) + @signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json end end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index dabd3bf2f47329..0b03f777825824 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -244,8 +244,10 @@ def update_references! def update_metadata! ProcessHashtagsService.new.call(@status) + ProcessMentionsService.new.call(@status) + ProcessLinksService.new.call(@status) - @status.update(limited_scope: :circle) if process_mentions_service.mentions? + @status.update(limited_scope: :circle) if @status.limited_visibility? && process_mentions_service.mentions? end def process_mentions_service diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index d4f5006be3d87c..e45499830d6ab2 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -68,7 +68,7 @@ def deliver_votes! end def build_json(vote) - Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer)) + serialize_payload(vote, ActivityPub::VoteSerializer).to_json end def increment_voters_count! diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb index aafa38318173bb..5441c754f323c4 100644 --- a/app/services/webhook_service.rb +++ b/app/services/webhook_service.rb @@ -17,6 +17,8 @@ def webhooks_for_event end def serialize_event - Oj.dump(ActiveModelSerializers::SerializableResource.new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json) + ActiveModelSerializers::SerializableResource + .new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user) + .to_json end end diff --git a/app/validators/emoji_reaction_validator.rb b/app/validators/emoji_reaction_validator.rb index d49e73a75b00b4..a0a1c0e07faf59 100644 --- a/app/validators/emoji_reaction_validator.rb +++ b/app/validators/emoji_reaction_validator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class EmojiReactionValidator < ActiveModel::Validator - SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze + SUPPORTED_EMOJIS = JSON.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze def validate(emoji_reaction) return if emoji_reaction.name.blank? diff --git a/app/validators/reaction_validator.rb b/app/validators/reaction_validator.rb index 89d83de5a2ccd6..51e6a01f3c2df7 100644 --- a/app/validators/reaction_validator.rb +++ b/app/validators/reaction_validator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ReactionValidator < ActiveModel::Validator - SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze + SUPPORTED_EMOJIS = JSON.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze LIMIT = 8 diff --git a/app/views/admin/fasp/debug/callbacks/index.html.haml b/app/views/admin/fasp/debug/callbacks/index.html.haml index d83ae95fa5af37..98149e93a1bc16 100644 --- a/app/views/admin/fasp/debug/callbacks/index.html.haml +++ b/app/views/admin/fasp/debug/callbacks/index.html.haml @@ -2,7 +2,7 @@ = t('admin.fasp.debug.callbacks.title') - content_for :heading do - %h2= t('admin.fasp.debug.callbacks.title') + %h1= t('admin.fasp.debug.callbacks.title') = render 'admin/fasp/shared/links' - unless @callbacks.empty? diff --git a/app/views/admin/fasp/providers/edit.html.haml b/app/views/admin/fasp/providers/edit.html.haml index f4a799c7770d1a..89f6d18a3a6ce5 100644 --- a/app/views/admin/fasp/providers/edit.html.haml +++ b/app/views/admin/fasp/providers/edit.html.haml @@ -4,7 +4,7 @@ = simple_form_for [:admin, @provider] do |f| = render 'shared/error_messages', object: @provider - %h4= t('admin.fasp.providers.select_capabilities') + %h2= t('admin.fasp.providers.select_capabilities') .fields_group = f.fields_for :capabilities do |cf| diff --git a/app/views/admin/fasp/providers/index.html.haml b/app/views/admin/fasp/providers/index.html.haml index 209f7e803453ba..aa410495542f4e 100644 --- a/app/views/admin/fasp/providers/index.html.haml +++ b/app/views/admin/fasp/providers/index.html.haml @@ -2,7 +2,7 @@ = t('admin.fasp.providers.title') - content_for :heading do - %h2= t('admin.fasp.providers.title') + %h1= t('admin.fasp.providers.title') = render 'admin/fasp/shared/links' - unless @providers.empty? diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml index f76a594534bbf8..e447739247920e 100644 --- a/app/views/admin/roles/_form.html.haml +++ b/app/views/admin/roles/_form.html.haml @@ -40,7 +40,7 @@ %span.hint= t('simple_form.hints.user_role.permissions_as_keys') - (form.object.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions| - %h4= t(category, scope: 'admin.roles.categories') + %h2= t(category, scope: 'admin.roles.categories') = form.input :permissions_as_keys, as: :check_boxes, diff --git a/app/views/admin/rules/edit.html.haml b/app/views/admin/rules/edit.html.haml index 944480fa5a84d8..54f7ce3eb06389 100644 --- a/app/views/admin/rules/edit.html.haml +++ b/app/views/admin/rules/edit.html.haml @@ -8,7 +8,7 @@ %hr.spacer/ - %h4= t('admin.rules.translations') + %h2= t('admin.rules.translations') %p.hint= t('admin.rules.translations_explanation') diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml index c4df98e7041091..4e2ebb43ed61f3 100644 --- a/app/views/admin/settings/about/show.html.haml +++ b/app/views/admin/settings/about/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.about.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_about_path do |f| diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml index 20208df15248f1..87c7375920f54a 100644 --- a/app/views/admin/settings/appearance/show.html.haml +++ b/app/views/admin/settings/appearance/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.appearance.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_appearance_path do |f| diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml index 02da7b2c8ff456..10bb28c20687f1 100644 --- a/app/views/admin/settings/branding/show.html.haml +++ b/app/views/admin/settings/branding/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.branding.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_branding_path do |f| diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml index 873728f0c88cd0..257b666e72872c 100644 --- a/app/views/admin/settings/content_retention/show.html.haml +++ b/app/views/admin/settings/content_retention/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.content_retention.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_content_retention_path do |f| @@ -18,7 +18,7 @@ input_html: { pattern: '[0-9]+' }, wrapper: :with_block_label - %h4= t('admin.settings.content_retention.danger_zone') + %h2= t('admin.settings.content_retention.danger_zone') .fields-group = f.input :content_cache_retention_period, diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index 30f76bde9e8030..770fd9ae71959f 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.discovery.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_discovery_path do |f| @@ -10,7 +10,7 @@ %p.lead= t('admin.settings.discovery.preamble') - %h4= t('admin.settings.discovery.trends') + %h2= t('admin.settings.discovery.trends') .fields-group = f.input :trends, @@ -23,7 +23,7 @@ wrapper: :with_label, recommended: :not_recommended - %h4= t('admin.settings.discovery.public_timelines') + %h2= t('admin.settings.discovery.public_timelines') .fields-row .fields-row__column.fields-row__column-6.fields-group @@ -55,7 +55,7 @@ label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") }, wrapper: :with_label - %h4= t('admin.settings.discovery.privacy') + %h2= t('admin.settings.discovery.privacy') .fields-group = f.input :noindex, @@ -74,7 +74,7 @@ .fields-group = f.input :auto_accept_legacy_quotes, as: :boolean, wrapper: :with_label, kmyblue: true - %h4= t('admin.settings.discovery.emoji_reactions') + %h2= t('admin.settings.discovery.emoji_reactions') .fields-group = f.input :enable_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false @@ -88,7 +88,7 @@ .fields-group = f.input :streaming_other_servers_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true - %h4= t('admin.settings.discovery.visibilities') + %h2= t('admin.settings.discovery.visibilities') .fields-group = f.input :enable_public_visibility, as: :boolean, wrapper: :with_label, kmyblue: true @@ -99,12 +99,12 @@ .fields-group = f.input :enable_local_timeline, as: :boolean, wrapper: :with_label, kmyblue: true - %h4= t('admin.settings.discovery.friend_servers') + %h2= t('admin.settings.discovery.friend_servers') .fields-group = f.input :unlocked_friend, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false - %h4= t('admin.settings.discovery.publish_statistics') + %h2= t('admin.settings.discovery.publish_statistics') .fields-group = f.input :activity_api_enabled, @@ -118,7 +118,7 @@ wrapper: :with_label, recommended: :recommended - %h4= t('admin.settings.security.federation_authentication') + %h2= t('admin.settings.security.federation_authentication') .fields-group = f.input :authorized_fetch, @@ -133,20 +133,20 @@ .fields-group = f.input :check_lts_version_only, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false - %h4= t('admin.settings.discovery.follow_recommendations') + %h2= t('admin.settings.discovery.follow_recommendations') .fields-group = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label - %h4= t('admin.settings.discovery.profile_directory') + %h2= t('admin.settings.discovery.profile_directory') .fields-group = f.input :profile_directory, as: :boolean, wrapper: :with_label - %h4= t('admin.settings.discovery.wrapstodon') + %h2= t('admin.settings.discovery.wrapstodon') .fields-group = f.input :wrapstodon, diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 1ba5629f9734a0..a75ffa82484b79 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -2,7 +2,7 @@ = t('admin.settings.registrations.title') - content_for :heading do - %h2= t('admin.settings.title') + %h1= t('admin.settings.title') = render partial: 'admin/settings/shared/links' = simple_form_for @admin_settings, url: admin_settings_registrations_path do |f| diff --git a/app/views/admin/terms_of_service/drafts/show.html.haml b/app/views/admin/terms_of_service/drafts/show.html.haml index e83bb47c6b57a9..9a527753c07ed5 100644 --- a/app/views/admin/terms_of_service/drafts/show.html.haml +++ b/app/views/admin/terms_of_service/drafts/show.html.haml @@ -2,7 +2,7 @@ = t('admin.terms_of_service.title') - content_for :heading do - %h2= t('admin.terms_of_service.title') + %h1= t('admin.terms_of_service.title') = render partial: 'admin/terms_of_service/links' = simple_form_for @terms_of_service, url: admin_terms_of_service_draft_path, method: :put do |form| diff --git a/app/views/admin/terms_of_service/histories/show.html.haml b/app/views/admin/terms_of_service/histories/show.html.haml index 10921d922f887e..3abfe8ef614b91 100644 --- a/app/views/admin/terms_of_service/histories/show.html.haml +++ b/app/views/admin/terms_of_service/histories/show.html.haml @@ -2,7 +2,7 @@ = t('admin.terms_of_service.history') - content_for :heading do - %h2= t('admin.terms_of_service.title') + %h1= t('admin.terms_of_service.title') = render partial: 'admin/terms_of_service/links' - if @terms_of_service.empty? diff --git a/app/views/admin/terms_of_service/index.html.haml b/app/views/admin/terms_of_service/index.html.haml index f34d1bda44a919..bae26015110624 100644 --- a/app/views/admin/terms_of_service/index.html.haml +++ b/app/views/admin/terms_of_service/index.html.haml @@ -2,7 +2,7 @@ = t('admin.terms_of_service.title') - content_for :heading do - %h2= t('admin.terms_of_service.title') + %h1= t('admin.terms_of_service.title') = render partial: 'links' - if @terms_of_service.present? @@ -31,7 +31,7 @@ %hr.spacer/ - %h3= t('admin.terms_of_service.changelog') + %h2.heading-medium= t('admin.terms_of_service.changelog') .prose = markdown(@terms_of_service.changelog) diff --git a/app/views/admin/webhooks/show.html.haml b/app/views/admin/webhooks/show.html.haml index a389e611029933..c3784559de2b9b 100644 --- a/app/views/admin/webhooks/show.html.haml +++ b/app/views/admin/webhooks/show.html.haml @@ -3,7 +3,7 @@ - content_for :heading do .content__heading__row - %h2 + %h1 %small = material_symbol 'inbox' = t('admin.webhooks.webhook') diff --git a/app/views/auth/confirmations/new.html.haml b/app/views/auth/confirmations/new.html.haml index eaa9d9add090c0..53c97d1dcbab75 100644 --- a/app/views/auth/confirmations/new.html.haml +++ b/app/views/auth/confirmations/new.html.haml @@ -18,6 +18,7 @@ %p.lead= t('auth.confirmations.awaiting_review', domain: site_hostname) - else = simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| + %h1.title= t('auth.resend_confirmation', domain: site_hostname) = render 'shared/error_messages', object: resource .fields-group diff --git a/app/views/auth/passwords/new.html.haml b/app/views/auth/passwords/new.html.haml index 8d5adaf3b987e1..3c72dd23d0a347 100644 --- a/app/views/auth/passwords/new.html.haml +++ b/app/views/auth/passwords/new.html.haml @@ -2,6 +2,7 @@ = t('auth.reset_password') = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| + %h1.title= t('auth.reset_password', domain: site_hostname) = render 'shared/error_messages', object: resource .fields-group diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml index 8f44eee015ccef..e9824d946f2521 100644 --- a/app/views/auth/registrations/_status.html.haml +++ b/app/views/auth/registrations/_status.html.haml @@ -10,7 +10,7 @@ = t('auth.status.redirecting_to', acct: user.account.moved_to_account.pretty_acct) = link_to t('migrations.cancel'), settings_migration_path -%h3= t('auth.status.account_status') +%h2.heading-medium= t('auth.status.account_status') %p.hint - if user.account.suspended? diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 07d6c1af51d3c0..195436b0d4260c 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -7,7 +7,7 @@ - else = render partial: 'status', locals: { user: @user, strikes: @strikes } -%h3= t('auth.security') +%h2.heading-medium= t('auth.security') = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit', novalidate: false }) do |f| = render 'shared/error_messages', object: resource @@ -57,15 +57,15 @@ - unless current_account.suspended? || self_destruct? %hr.spacer/ - %h3= t('auth.migrate_account') + %h2.heading-medium= t('auth.migrate_account') %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) %hr.spacer/ - %h3= t('migrations.incoming_migrations') + %h2.heading-medium= t('migrations.incoming_migrations') %p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) %hr.spacer/ - %h3= t('auth.delete_account') + %h2.heading-medium= t('auth.delete_account') %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml index 1e3d934c375314..54c21e779873ee 100644 --- a/app/views/auth/registrations/rules.html.haml +++ b/app/views/auth/registrations/rules.html.haml @@ -16,7 +16,7 @@ %h1.title= t('auth.rules.title') %p.lead= t('auth.rules.preamble', domain: site_hostname) - %ol.rules-list + %ol.rules-list{ role: 'list' } = render collection: @rule_translations, partial: 'auth/rule_translations/rule_translation' .stacked-actions diff --git a/app/views/auth/rule_translations/_rule_translation.html.haml b/app/views/auth/rule_translations/_rule_translation.html.haml index 32b9cc28af13a9..cbb53acdf46569 100644 --- a/app/views/auth/rule_translations/_rule_translation.html.haml +++ b/app/views/auth/rule_translations/_rule_translation.html.haml @@ -1,4 +1,9 @@ %li - %button{ type: 'button', aria: { expanded: 'false' } } - .rules-list__text= rule_translation.text - .rules-list__hint= rule_translation.hint + .rules-list__text= rule_translation.text + - if rule_translation.hint? + .rules-list__hint{ tabIndex: -1 } + %span.rules-list__hint-text= rule_translation.hint + %button.rules-list__toggle-button{ type: 'button', hidden: true, 'aria-expanded': 'false' } + = material_symbol('keyboard_arrow_up', { class: 'icon-collapse' }) + = material_symbol('more_horiz', { class: 'icon-expand' }) + %span.sr-only= t('auth.rules.read_more') diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 0c28142eb681b1..c55d7139a86887 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -12,6 +12,7 @@ - if use_seamless_external_login? = f.input :email, autofocus: true, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, label: t('simple_form.labels.defaults.username_or_email'), @@ -19,12 +20,14 @@ - else = f.input :email, autofocus: true, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, label: t('simple_form.labels.defaults.email'), wrapper: :with_label .fields-group = f.input :password, + required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'current-password' }, label: t('simple_form.labels.defaults.password'), @@ -46,7 +49,10 @@ - if devise_mapping.omniauthable? && resource_class.omniauth_providers.any? .simple_form.alternative-login - %h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with') + - if omniauth_only? + %h1= t('auth.log_in_with') + - else + %h2= t('auth.or_log_in_with') .actions - resource_class.omniauth_providers.each do |provider| diff --git a/app/views/auth/shared/_progress.html.haml b/app/views/auth/shared/_progress.html.haml index 8fb33854194789..9ee0abdf9058c4 100644 --- a/app/views/auth/shared/_progress.html.haml +++ b/app/views/auth/shared/_progress.html.haml @@ -1,26 +1,35 @@ - progress_index = { rules: 0, details: 1, confirm: 2, confirmed: 3, completed: 4 }[stage.to_sym] -%ol.progress-tracker - %li{ class: progress_index.positive? ? 'completed' : 'active' } +%ol.progress-tracker{ role: 'list', 'aria-label': t('auth.progress.list') } + %li{ + class: progress_index.positive? ? 'completed' : nil, + 'aria-current': progress_index.zero? ? 'step' : nil + } .circle - if progress_index.positive? = check_icon .label= t('auth.progress.rules') - %li.separator{ class: progress_index.positive? ? 'completed' : nil } - %li{ class: [progress_index > 1 && 'completed', progress_index == 1 && 'active'] } + %li{ + class: progress_index > 1 && 'completed', + 'aria-current': progress_index == 1 ? 'step' : nil + } .circle - if progress_index > 1 = check_icon .label= t('auth.progress.details') - %li.separator{ class: progress_index > 1 ? 'completed' : nil } - %li{ class: [progress_index > 2 && 'completed', progress_index == 2 && 'active'] } + %li{ + class: progress_index > 2 && 'completed', + 'aria-current': progress_index == 2 ? 'step' : nil + } .circle - if progress_index > 2 = check_icon .label= t('auth.progress.confirm') - if approved_registrations? - %li.separator{ class: progress_index > 2 ? 'completed' : nil } - %li{ class: [progress_index > 3 && 'completed', progress_index == 3 && 'active'] } + %li{ + class: progress_index > 3 && 'completed', + 'aria-current': progress_index == 3 ? 'step' : nil + } .circle - if progress_index > 3 = check_icon diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml index f4b851c85aedee..832de94819ba29 100644 --- a/app/views/filters/_filter_fields.html.haml +++ b/app/views/filters/_filter_fields.html.haml @@ -43,13 +43,13 @@ %hr.spacer/ - unless f.object.statuses.empty? - %h4= t('filters.edit.statuses') + %h2= t('filters.edit.statuses') %p.muted-hint= t('filters.edit.statuses_hint_html', path: filter_statuses_path(f.object)) %hr.spacer/ -%h4= t('filters.edit.keywords') +%h2= t('filters.edit.keywords') .table-wrapper %table.table.keywords-table diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 08432a177c0031..6e49ed90339bad 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -31,7 +31,7 @@ = yield :heading - else .content__heading__row - %h2= yield :page_title + %h1= yield :page_title - if content_for?(:heading_actions) .content__heading__actions diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml index bdbcc84e1f3d21..c989e6ccd86a44 100644 --- a/app/views/layouts/auth.html.haml +++ b/app/views/layouts/auth.html.haml @@ -4,12 +4,11 @@ - content_for :content do .container-alt .logo-container - %h1 - - if within_authorization_flow? + - if within_authorization_flow? + = logo_as_symbol(:wordmark) + - else + = link_to root_path do = logo_as_symbol(:wordmark) - - else - = link_to root_path do - = logo_as_symbol(:wordmark) .form-container = render 'flashes' diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml index 088a93bc595a14..b7f6e56b3d94b2 100644 --- a/app/views/settings/featured_tags/index.html.haml +++ b/app/views/settings/featured_tags/index.html.haml @@ -2,7 +2,7 @@ = t('settings.featured_tags') - content_for :heading do - %h2= t('settings.profile') + %h1= t('settings.profile') = render partial: 'settings/shared/profile_navigation' = simple_form_for @featured_tag, url: settings_featured_tags_path do |f| @@ -23,7 +23,7 @@ - @featured_tags.each do |featured_tag| .directory__tag %div - %h4 + %h2 = material_symbol 'tag' = featured_tag.display_name %small diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index c3204384363f7d..1c099ae95e99b5 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -69,7 +69,7 @@ = link_to t('appearance.localization.guide_link_text'), t('appearance.localization.guide_link'), target: '_blank', rel: 'noopener' = f.simple_fields_for :settings, current_user.settings do |ff| - %h4= t 'appearance.animations_and_accessibility' + %h2= t 'appearance.animations_and_accessibility' .fields-group = ff.input :'web.use_pending_items', @@ -95,7 +95,7 @@ label: I18n.t('simple_form.labels.defaults.setting_content_font_size'), wrapper: :with_label - %h4= t 'appearance.custom_emoji_and_emoji_reactions' + %h2= t 'appearance.custom_emoji_and_emoji_reactions' .fields-group = ff.input :'web.show_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_recent_emojis'), hint: false @@ -104,14 +104,14 @@ = ff.input :'web.show_emoji_reaction_on_timeline', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_emoji_reaction_on_timeline') = ff.input :'web.show_emoji_reaction_count', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_emoji_reaction_count') - %h4= t 'appearance.remote_server_features' + %h2= t 'appearance.remote_server_features' .fields-group - if Setting.enable_emoji_reaction = ff.input :'web.hide_emoji_reaction_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_emoji_reaction_unavailable_server') = ff.input :'web.hide_status_reference_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_status_reference_unavailable_server') - %h4= t 'appearance.saved_posts' + %h2= t 'appearance.saved_posts' .fields-group = ff.input :'web.bookmark_category_needed', @@ -121,12 +121,12 @@ hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed') = ff.input :'web.show_favourite_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_favourite_menu') - %h4= t 'appearance.status_action_bar' + %h2= t 'appearance.status_action_bar' .fields-group = ff.input :'web.simple_timeline_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_simple_timeline_menu') - %h4= t 'appearance.discovery' + %h2= t 'appearance.discovery' .fields-group = ff.input :'web.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_trends') @@ -134,7 +134,7 @@ - if Setting.enable_local_timeline = ff.input :'web.community_timeline_instead_of_search_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_community_timeline_instead_of_search_menu') - %h4= t 'appearance.boosting_preferences' + %h2= t 'appearance.boosting_preferences' .fields-group = ff.input :'web.reblog_modal', @@ -148,7 +148,7 @@ wrapper: :with_label .flash-message.hidden-on-touch-devices= t('appearance.boosting_preferences_info_html', icon: material_symbol('repeat')) - %h4= t 'appearance.sensitive_content' + %h2= t 'appearance.sensitive_content' .fields-group = ff.input :'web.display_media', @@ -172,7 +172,7 @@ = ff.input :'web.expand_content_warnings', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_expand_spoilers') = ff.input :'web.show_avatar_on_filter', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_avatar_on_filter') - %h4= t 'appearance.advanced_settings' + %h2= t 'appearance.advanced_settings' .fields-group = ff.input :'web.advanced_layout', diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index febcee752ba4fa..4a70ec8bc004f1 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -7,7 +7,7 @@ = simple_form_for current_user, url: settings_preferences_notifications_path, html: { id: :edit_notification } do |f| = render 'shared/error_messages', object: current_user - %h4= t 'notifications.email_events' + %h2= t 'notifications.email_events' %p.hint= t 'notifications.email_events_hint' @@ -24,7 +24,7 @@ = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails') - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies, :manage_federation) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)) - %h4= t 'notifications.administration_emails' + %h2= t 'notifications.administration_emails' .fields-group = ff.input :'notification_emails.report', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.report') if current_user.can?(:manage_reports) diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 60da62b23f5063..160d0ead0a3724 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -29,7 +29,7 @@ - if @dtl_enabled - %h4= t 'preferences.dtl' + %h2= t 'preferences.dtl' %p.hint= t 'preferences.dtl_hint', tag: @dtl_tag @@ -60,7 +60,7 @@ label_method: ->(item) { safe_join([t("simple_form.labels.dtl_force_searchability.#{item}")]) }, wrapper: :with_floating_label - %h4= t 'preferences.public_timelines' + %h2= t 'preferences.public_timelines' .fields-group = f.input :chosen_languages, diff --git a/app/views/settings/preferences/reaching/show.html.haml b/app/views/settings/preferences/reaching/show.html.haml index 1f8041980564b1..9273c9c4f56d53 100644 --- a/app/views/settings/preferences/reaching/show.html.haml +++ b/app/views/settings/preferences/reaching/show.html.haml @@ -8,7 +8,7 @@ = render 'shared/error_messages', object: current_user = f.simple_fields_for :settings, current_user.settings do |ff| - %h4= t 'preferences.visibility' + %h2= t 'preferences.visibility' .fields-group = ff.input :stay_privacy, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stay_privacy'), hint: I18n.t('simple_form.hints.defaults.setting_stay_privacy') @@ -31,7 +31,7 @@ required: false, wrapper: :with_block_label - %h4= t 'preferences.searchability' + %h2= t 'preferences.searchability' - unless Chewy.enabled? %p.hint= t 'preferences.does_not_search' @@ -44,7 +44,7 @@ hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability') - if Chewy.enabled? - %h4= t 'preferences.search' + %h2= t 'preferences.search' .fields-row .fields-group.fields-row__column.fields-row__column-12 diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml index 09985ebdbe9db5..170359d472353d 100644 --- a/app/views/settings/privacy/show.html.haml +++ b/app/views/settings/privacy/show.html.haml @@ -2,14 +2,14 @@ = t('privacy.title') - content_for :heading do - %h2= t('privacy.title') + %h1= t('privacy.title') = simple_form_for @account, url: settings_privacy_path do |f| = render 'shared/error_messages', object: @account %p.lead= t('privacy.hint_html') - %h4= t('privacy.reach') + %h2= t('privacy.reach') %p.lead= t('privacy.reach_hint_html') @@ -23,7 +23,7 @@ .fields-group = f.input :unlocked, as: :boolean, wrapper: :with_label - %h4= t('privacy.search') + %h2= t('privacy.search') %p.lead= t('privacy.search_hint_html') @@ -48,7 +48,7 @@ required: false, wrapper: :with_label - %h4= t('privacy.privacy') + %h2= t('privacy.privacy') %p.lead= t('privacy.privacy_hint_html') diff --git a/app/views/settings/privacy_extra/show.html.haml b/app/views/settings/privacy_extra/show.html.haml index 63d3f67bd3ef50..d6117c21a7e260 100644 --- a/app/views/settings/privacy_extra/show.html.haml +++ b/app/views/settings/privacy_extra/show.html.haml @@ -10,7 +10,7 @@ %p.lead= t('privacy_extra.hint_html') - %h4= t('privacy_extra.post_processing') + %h2= t('privacy_extra.post_processing') %p.lead= t('privacy_extra.post_processing_hint_html') @@ -19,7 +19,7 @@ = ff.input :translatable_private, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_translatable_private') - if Setting.enable_emoji_reaction - %h4= t 'preferences.emoji_reaction_permitting' + %h2= t 'preferences.emoji_reaction_permitting' .fields-row .fields-group.fields-row__column.fields-row__column-12 @@ -37,7 +37,7 @@ .fields-group = ff.input :slip_local_emoji_reaction, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_slip_local_emoji_reaction') - %h4= t 'privacy_extra.stop_deliver' + %h2= t 'privacy_extra.stop_deliver' %p.lead= t('privacy_extra.stop_deliver_hint_html') diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 036603b93eb9c3..ba53ca91ce0570 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -2,7 +2,7 @@ = t('settings.edit_profile') - content_for :heading do - %h2= t('settings.profile') + %h1= t('settings.profile') = render partial: 'settings/shared/profile_navigation' = simple_form_for @account, url: settings_profile_path, html: { id: :edit_profile } do |f| @@ -10,7 +10,7 @@ %p.lead= t('edit_profile.hint_html') - %h4= t('edit_profile.basic_information') + %h2= t('edit_profile.basic_information') .fields-row .fields-row__column.fields-row__column-6 .fields-group @@ -73,7 +73,7 @@ = material_symbol 'delete' = t('generic.delete') - %h4= t('edit_profile.other') + %h2= t('edit_profile.other') .fields-group = f.input :my_actor_type, diff --git a/app/views/settings/verifications/show.html.haml b/app/views/settings/verifications/show.html.haml index b348b7bc03bad2..6e6af16a9016b6 100644 --- a/app/views/settings/verifications/show.html.haml +++ b/app/views/settings/verifications/show.html.haml @@ -2,15 +2,15 @@ = t('verification.verification') - content_for :heading do - %h2= t('settings.profile') + %h1= t('settings.profile') = render partial: 'settings/shared/profile_navigation' .simple_form.form-section - %h3= t('verification.website_verification') + %h2.heading-medium= t('verification.website_verification') %p.lead= t('verification.hint_html') - %h4= t('verification.here_is_how') + %h3.heading-small= t('verification.here_is_how') %p.lead= t('verification.instructions_html') @@ -22,7 +22,7 @@ %p.lead= t('verification.extra_instructions_html') - if @verified_links.any? - %h4= t('verification.verified_links') + %h3.heading-small= t('verification.verified_links') %ul.lead - @verified_links.each do |field| @@ -34,7 +34,7 @@ = simple_form_for @account, url: settings_verification_path, html: { class: 'form-section' } do |f| = render 'shared/error_messages', object: @account - %h3= t('author_attribution.title') + %h2.heading-medium= t('author_attribution.title') %p.lead= t('author_attribution.hint_html') @@ -53,7 +53,7 @@ = logo_as_symbol(:icon) = t('author_attribution.more_from_html', name: author_attribution_name(@account)) - %h4= t('verification.here_is_how') + %h3.heading-small= t('verification.here_is_how') %p.lead= t('author_attribution.instructions') diff --git a/app/views/shared/_web_app.html.haml b/app/views/shared/_web_app.html.haml index 5e6815165f4457..367d1c52d2a8ac 100644 --- a/app/views/shared/_web_app.html.haml +++ b/app/views/shared/_web_app.html.haml @@ -8,7 +8,7 @@ = render_initial_state = vite_typescript_tag 'application.ts', crossorigin: 'anonymous' -.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } } +.notranslate.app-holder#mastodon{ data: { props: default_props.to_json } } %noscript = image_tag frontend_asset_path('images/logo.svg'), alt: 'Mastodon' diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml index c12b43031e5980..1d9717f1f89d7f 100644 --- a/app/views/shares/show.html.haml +++ b/app/views/shares/show.html.haml @@ -2,4 +2,4 @@ = render_initial_state = vite_typescript_tag 'share.tsx', crossorigin: 'anonymous' -#mastodon-compose{ data: { props: Oj.dump(default_props) } } +#mastodon-compose{ data: { props: default_props.to_json } } diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml index 09d0792ea2549e..74c807a89ed50e 100644 --- a/app/views/statuses/embed.html.haml +++ b/app/views/statuses/embed.html.haml @@ -1 +1 @@ -#mastodon-status{ data: { props: Oj.dump(default_props.merge(id: @status.id.to_s)) } } +#mastodon-status{ data: { props: default_props.merge(id: @status.id.to_s).to_json } } diff --git a/app/views/statuses_cleanup/show.html.haml b/app/views/statuses_cleanup/show.html.haml index 47abc1e979d9b2..57dcc8ed9c8f43 100644 --- a/app/views/statuses_cleanup/show.html.haml +++ b/app/views/statuses_cleanup/show.html.haml @@ -24,7 +24,7 @@ .flash-message= t('statuses_cleanup.explanation') - %h4= t('statuses_cleanup.exceptions') + %h2= t('statuses_cleanup.exceptions') .fields-row .fields-row__column.fields-row__column-6.fields-group @@ -72,7 +72,7 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :keep_self_emoji, wrapper: :with_label, label: t('statuses_cleanup.keep_self_emoji'), hint: t('statuses_cleanup.keep_self_emoji_hint') - %h4= t('statuses_cleanup.interaction_exceptions') + %h2= t('statuses_cleanup.interaction_exceptions') .fields-row .fields-row__column.fields-row__column-6.fields-group diff --git a/app/views/terms_of_service_interstitial/show.html.haml b/app/views/terms_of_service_interstitial/show.html.haml index c7fb3595f3b42a..4fed5eba932661 100644 --- a/app/views/terms_of_service_interstitial/show.html.haml +++ b/app/views/terms_of_service_interstitial/show.html.haml @@ -6,8 +6,11 @@ .simple_form %h1.title= t('terms_of_service_interstitial.title', domain: site_hostname) - - effective_date = @terms_of_service.effective_date || Time.zone.today - %p.lead= effective_date.past? ? t('terms_of_service_interstitial.past_preamble_html') : t('terms_of_service_interstitial.future_preamble_html', date: l(effective_date)) + %p.lead< + - if @terms_of_service.usable_effective_date.past? + = t('terms_of_service_interstitial.past_preamble_html') + - else + = t('terms_of_service_interstitial.future_preamble_html', date: l(@terms_of_service.usable_effective_date)) %p.lead= t('user_mailer.terms_of_service_changed.agreement', domain: site_hostname) diff --git a/app/views/user_mailer/terms_of_service_changed.html.haml b/app/views/user_mailer/terms_of_service_changed.html.haml index 2e34eb49901f97..efa4911743931a 100644 --- a/app/views/user_mailer/terms_of_service_changed.html.haml +++ b/app/views/user_mailer/terms_of_service_changed.html.haml @@ -9,7 +9,7 @@ %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } %tr %td.email-inner-card-td.email-prose - %p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_version_url(date: @terms_of_service.effective_date), domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today)) + %p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_version_url(date: @terms_of_service.effective_date), domain: site_hostname, date: l(@terms_of_service.usable_effective_date)) %p %strong= t('user_mailer.terms_of_service_changed.changelog') = markdown(@terms_of_service.changelog) diff --git a/app/views/user_mailer/terms_of_service_changed.text.erb b/app/views/user_mailer/terms_of_service_changed.text.erb index ccf332ce8970b8..933be1122c17a8 100644 --- a/app/views/user_mailer/terms_of_service_changed.text.erb +++ b/app/views/user_mailer/terms_of_service_changed.text.erb @@ -2,7 +2,7 @@ === -<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today)) %> +<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname, date: l(@terms_of_service.usable_effective_date)) %> => <%= terms_of_service_version_url(date: @terms_of_service.effective_date) %> diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb index 8c1eefd93d5bd4..a2535a0ba2a167 100644 --- a/app/workers/activitypub/distribute_poll_update_worker.rb +++ b/app/workers/activitypub/distribute_poll_update_worker.rb @@ -43,7 +43,7 @@ def inboxes end def payload - @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account)) + @payload ||= serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account).to_json end def relay! diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 3be749a5ec914b..af63cdf10f113a 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -65,15 +65,15 @@ def status_reach_finder end def payload - @payload ||= Oj.dump(serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, always_sign_unsafe: always_sign))) + @payload ||= serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, always_sign_unsafe: always_sign)).to_json end def payload_for_misskey - @payload_for_misskey ||= Oj.dump(serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, for_misskey: true))) + @payload_for_misskey ||= serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, for_misskey: true)).to_json end def payload_for_friend - @payload_for_friend ||= Oj.dump(serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, for_friend: true, always_sign_unsafe: always_sign))) + @payload_for_friend ||= serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account, for_friend: true, always_sign_unsafe: always_sign)).to_json end def activity_serializer diff --git a/app/workers/activitypub/emoji_reaction_distribution_worker.rb b/app/workers/activitypub/emoji_reaction_distribution_worker.rb index 06b1c739602ce8..b0da3a8a280c20 100644 --- a/app/workers/activitypub/emoji_reaction_distribution_worker.rb +++ b/app/workers/activitypub/emoji_reaction_distribution_worker.rb @@ -16,7 +16,7 @@ def perform(emoji_reaction_id, options = {}) protected def payload - @payload ||= Oj.dump(serialize_payload(@emoji_reaction, ActivityPub::EmojiReactionSerializer, signer: @account)) + @payload ||= serialize_payload(@emoji_reaction, ActivityPub::EmojiReactionSerializer, signer: @account).to_json end def inboxes diff --git a/app/workers/activitypub/feature_request_worker.rb b/app/workers/activitypub/feature_request_worker.rb index fa895a546d772c..61bc041f503a43 100644 --- a/app/workers/activitypub/feature_request_worker.rb +++ b/app/workers/activitypub/feature_request_worker.rb @@ -17,6 +17,6 @@ def inboxes end def payload - @payload ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account)) + @payload ||= serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account).to_json end end diff --git a/app/workers/activitypub/move_distribution_worker.rb b/app/workers/activitypub/move_distribution_worker.rb index 1680fcc76ec906..255a59cef0cac2 100644 --- a/app/workers/activitypub/move_distribution_worker.rb +++ b/app/workers/activitypub/move_distribution_worker.rb @@ -28,6 +28,6 @@ def inboxes end def signed_payload - @signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account)) + @signed_payload ||= serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account).to_json end end diff --git a/app/workers/activitypub/process_featured_item_worker.rb b/app/workers/activitypub/process_featured_item_worker.rb new file mode 100644 index 00000000000000..b29c7d7aeea64b --- /dev/null +++ b/app/workers/activitypub/process_featured_item_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ActivityPub::ProcessFeaturedItemWorker + include Sidekiq::Worker + include ExponentialBackoff + + sidekiq_options queue: 'pull', retry: 3 + + def perform(collection_id, id_or_json, position = nil, request_id = nil) + collection = Collection.find(collection_id) + + ActivityPub::ProcessFeaturedItemService.new.call(collection, id_or_json, position:, request_id:) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/activitypub/quote_refresh_worker.rb b/app/workers/activitypub/quote_refresh_worker.rb index 7dabfddc8006aa..5db84a1dac1219 100644 --- a/app/workers/activitypub/quote_refresh_worker.rb +++ b/app/workers/activitypub/quote_refresh_worker.rb @@ -10,6 +10,6 @@ def perform(quote_id) return if quote.nil? || quote.updated_at > Quote::BACKGROUND_REFRESH_INTERVAL.ago quote.touch - ActivityPub::VerifyQuoteService.new.call(quote) + ActivityPub::VerifyQuoteService.new.call(quote, quote.approval_uri) end end diff --git a/app/workers/activitypub/quote_request_worker.rb b/app/workers/activitypub/quote_request_worker.rb index 0540492f863768..45e328bb804437 100644 --- a/app/workers/activitypub/quote_request_worker.rb +++ b/app/workers/activitypub/quote_request_worker.rb @@ -17,6 +17,6 @@ def inboxes end def payload - @payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true)) + @payload ||= serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true).to_json end end diff --git a/app/workers/activitypub/refetch_and_verify_quote_worker.rb b/app/workers/activitypub/refetch_and_verify_quote_worker.rb index 1a8dd40541e9d7..762d55e1bee965 100644 --- a/app/workers/activitypub/refetch_and_verify_quote_worker.rb +++ b/app/workers/activitypub/refetch_and_verify_quote_worker.rb @@ -9,7 +9,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker def perform(quote_id, quoted_uri, options = {}) quote = Quote.find(quote_id) - ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id]) + ActivityPub::VerifyQuoteService.new.call(quote, options['approval_uri'], fetchable_quoted_uri: quoted_uri, request_id: options['request_id']) ::DistributionWorker.perform_async(quote.status_id, { 'update' => true }) if quote.state_previously_changed? rescue ActiveRecord::RecordNotFound # Do nothing diff --git a/app/workers/activitypub/status_update_distribution_worker.rb b/app/workers/activitypub/status_update_distribution_worker.rb index 7f23521e09a0dc..ad7f1efe6765bb 100644 --- a/app/workers/activitypub/status_update_distribution_worker.rb +++ b/app/workers/activitypub/status_update_distribution_worker.rb @@ -68,7 +68,7 @@ def activity_for_friend end def delete_activity - @delete_activity ||= Oj.dump(serialize_payload(@status, ActivityPub::DeleteNoteSerializer, signer: @account)) + @delete_activity ||= serialize_payload(@status, ActivityPub::DeleteNoteSerializer, signer: @account).to_json end def distribute_delete_activity! diff --git a/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb new file mode 100644 index 00000000000000..e24fe59b070994 --- /dev/null +++ b/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', lock: :until_executed, lock_ttl: 1.day.to_i + + def perform(account_id, request_id = nil) + account = Account.find(account_id) + + ActivityPub::FetchFeaturedCollectionsCollectionService.new.call(account, request_id:) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index 976f516498dc28..6b6c9056327712 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -23,6 +23,6 @@ def inboxes end def payload - @payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with])) + @payload ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with]).to_json end end diff --git a/app/workers/activitypub/verify_featured_item_worker.rb b/app/workers/activitypub/verify_featured_item_worker.rb new file mode 100644 index 00000000000000..3e96b81c095604 --- /dev/null +++ b/app/workers/activitypub/verify_featured_item_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ActivityPub::VerifyFeaturedItemWorker + include Sidekiq::Worker + include ExponentialBackoff + include JsonLdHelper + + sidekiq_options queue: 'pull', retry: 5 + + def perform(collection_item_id, approval_uri, request_id = nil) + collection_item = CollectionItem.find(collection_item_id) + + ActivityPub::VerifyFeaturedItemService.new.call(collection_item, approval_uri, request_id:) + rescue ActiveRecord::RecordNotFound + # Do nothing + nil + rescue Mastodon::UnexpectedResponseError => e + raise e unless response_error_unsalvageable?(e.response) + end +end diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb index fe7647024e574a..3706ecd427ca5b 100644 --- a/app/workers/poll_expiration_notify_worker.rb +++ b/app/workers/poll_expiration_notify_worker.rb @@ -9,6 +9,7 @@ def perform(poll_id) @poll = Poll.find(poll_id) return if missing_expiration? + requeue! && return if not_due_yet? notify_remote_voters_and_owner! if @poll.local? diff --git a/app/workers/publish_announcement_reaction_worker.rb b/app/workers/publish_announcement_reaction_worker.rb index 03da56550aadb8..7cf7393c1d13bc 100644 --- a/app/workers/publish_announcement_reaction_worker.rb +++ b/app/workers/publish_announcement_reaction_worker.rb @@ -11,7 +11,7 @@ def perform(announcement_id, name) reaction ||= announcement.announcement_reactions.new(name: name) payload = InlineRenderer.render(reaction, nil, :reaction).tap { |h| h[:announcement_id] = announcement_id.to_s } - payload = Oj.dump(event: :'announcement.reaction', payload: payload) + payload = { event: :'announcement.reaction', payload: payload } FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/app/workers/publish_scheduled_announcement_worker.rb b/app/workers/publish_scheduled_announcement_worker.rb index c23eae6af7c4a4..63f1600d341226 100644 --- a/app/workers/publish_scheduled_announcement_worker.rb +++ b/app/workers/publish_scheduled_announcement_worker.rb @@ -12,7 +12,7 @@ def perform(announcement_id) @announcement.publish! unless @announcement.published? payload = InlineRenderer.render(@announcement, nil, :announcement) - payload = Oj.dump(event: :announcement, payload: payload) + payload = { event: :announcement, payload: payload }.to_json FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/app/workers/push_conversation_worker.rb b/app/workers/push_conversation_worker.rb index 23b1469f1110cf..b3990c14797490 100644 --- a/app/workers/push_conversation_worker.rb +++ b/app/workers/push_conversation_worker.rb @@ -9,7 +9,7 @@ def perform(conversation_account_id) message = InlineRenderer.render(conversation, conversation.account, :conversation) timeline_id = "timeline:direct:#{conversation.account_id}" - redis.publish(timeline_id, Oj.dump(event: :conversation, payload: message)) + redis.publish(timeline_id, { event: :conversation, payload: message }.to_json) rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index c32975a986c3c5..63240d50b0bfc5 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -23,10 +23,10 @@ def render_payload! end def message - Oj.dump( + JSON.generate({ event: update? ? :'status.update' : :update, - payload: @payload - ) + payload: @payload, + }.as_json) end def publish! diff --git a/app/workers/scheduler/self_destruct_scheduler.rb b/app/workers/scheduler/self_destruct_scheduler.rb index d7aaef56e73625..4d8ee2cd85fef9 100644 --- a/app/workers/scheduler/self_destruct_scheduler.rb +++ b/app/workers/scheduler/self_destruct_scheduler.rb @@ -55,13 +55,10 @@ def inboxes end def delete_account!(account) - payload = ActiveModelSerializers::SerializableResource.new( - account, - serializer: ActivityPub::DeleteActorSerializer, - adapter: ActivityPub::Adapter - ).as_json - - json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account)) + json = ActivityPub::LinkedDataSignature + .new(deletion_payload(account)) + .sign!(account) + .to_json ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| [json, account.id, inbox_url] @@ -70,4 +67,12 @@ def delete_account!(account) # Do not call `Account#suspend!` because we don't want to issue a deletion request account.update!(suspended_at: Time.now.utc, suspension_origin: :local) end + + def deletion_payload(account) + ActiveModelSerializers::SerializableResource.new( + account, + serializer: ActivityPub::DeleteActorSerializer, + adapter: ActivityPub::Adapter + ).as_json + end end diff --git a/app/workers/unfilter_notifications_worker.rb b/app/workers/unfilter_notifications_worker.rb index cb8a46b8f4e835..7b57a2db136eaa 100644 --- a/app/workers/unfilter_notifications_worker.rb +++ b/app/workers/unfilter_notifications_worker.rb @@ -39,7 +39,7 @@ def decrement_worker_count! end def push_streaming_event! - redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notifications_merged, payload: '1')) + redis.publish("timeline:#{@recipient.id}:notifications", { event: :notifications_merged, payload: '1' }.to_json) end def subscribed_to_streaming_api? diff --git a/app/workers/unpublish_announcement_worker.rb b/app/workers/unpublish_announcement_worker.rb index e58c07554a75e5..1b61bacb24ac2f 100644 --- a/app/workers/unpublish_announcement_worker.rb +++ b/app/workers/unpublish_announcement_worker.rb @@ -5,7 +5,7 @@ class UnpublishAnnouncementWorker include Redisable def perform(announcement_id) - payload = Oj.dump(event: :'announcement.delete', payload: announcement_id.to_s) + payload = { event: :'announcement.delete', payload: announcement_id.to_s }.to_json FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index a1f4e46690c23c..0277aadfd8f344 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -101,7 +101,7 @@ def web_push_request def push_notification_json I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do - Oj.dump(serialized_notification.as_json) + serialized_notification.to_json end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 0c4f1de41e0e08..12709d5f0bf954 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -92,3 +92,6 @@ Sidekiq.strict_args! Redis.raise_deprecations = true + +# Silence deprecation warning from json-schema +JSON::Validator.use_multi_json = false diff --git a/config/initializers/fog_connection_cache.rb b/config/initializers/fog_connection_cache.rb new file mode 100644 index 00000000000000..c9cf310d83a61d --- /dev/null +++ b/config/initializers/fog_connection_cache.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +if ENV['SWIFT_ENABLED'] == 'true' + module PaperclipFogConnectionCache + def connection + @connection ||= begin + key = fog_credentials.hash + Thread.current[:paperclip_fog_connections] ||= {} + Thread.current[:paperclip_fog_connections][key] ||= ::Fog::Storage.new(fog_credentials) + end + end + end + + Rails.application.config.after_initialize do + Paperclip::Storage::Fog.prepend(PaperclipFogConnectionCache) + end +end diff --git a/config/initializers/oj.rb b/config/initializers/oj.rb deleted file mode 100644 index 9b2d908637199d..00000000000000 --- a/config/initializers/oj.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -Oj.default_options = { mode: :compat, time_format: :ruby, use_to_json: true } diff --git a/config/locales/activerecord.es-AR.yml b/config/locales/activerecord.es-AR.yml index 927f9e4742a5c4..4aadbe352d4302 100644 --- a/config/locales/activerecord.es-AR.yml +++ b/config/locales/activerecord.es-AR.yml @@ -26,7 +26,7 @@ es-AR: fields: fields_with_values_missing_labels: contiene valores con etiquetas faltantes username: - invalid: sólo letras, números y subguiones ("_") + invalid: solo letras, números y subguiones ("_") reserved: está reservado admin/webhook: attributes: diff --git a/config/locales/be.yml b/config/locales/be.yml index bcd568fa54faf3..9d06a26030b384 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -810,6 +810,8 @@ be: administrator_description: Карыстальнікі з гэтым дазволам будуць абыходзіць усе абмежаванні delete_user_data: Выдаленне даных карыстальнікаў delete_user_data_description: Дазваляе карыстальнікам без затрымкі выдаляць даныя іншых карыстальнікаў + invite_bypass_approval: Запрашаць карыстальнікаў без разгляду + invite_bypass_approval_description: Дазваляе людзям, якіх запрасілі на сервер гэтыя карыстальнікі, абыходзіць ухвалу мадэратараў invite_users: Запрашэнне карыстальнікаў invite_users_description: Дазваляе запрашаць новых людзей на сервер manage_announcements: Кіраванне аб’явамі @@ -1321,6 +1323,7 @@ be: progress: confirm: Пацвердзіць email details: Вашы даныя + list: Прагрэс рэгістрацыі review: Наш водгук rules: Прыняць правілы providers: @@ -1336,6 +1339,7 @@ be: invited_by: 'Вы можаце далучыцца да %{domain} дзякуючы запрашэнню, якое вы атрымалі ад:' preamble: Правілы вызначаныя мадэратарамі дамена %{domain}. preamble_invited: Перш чым працягнуць, азнаёмцеся з асноўнымі правіламі, усталяванымі мадэратарамі %{domain}. + read_more: Падрабязней title: Некалькі базавых правілаў. title_invited: Вас запрасілі. security: Бяспека diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 46843c122dd0ff..560bc3e6f3ead8 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1363,6 +1363,7 @@ cy: progress: confirm: Cadarnhau'r e-bost details: Eich manylion + list: Y drefn cofrestru review: Ein hadolygiad rules: Derbyn rheolau providers: @@ -1378,6 +1379,7 @@ cy: invited_by: 'Gallwch ymuno â %{domain} diolch i''r gwahoddiad a gawsoch gan:' preamble: Mae'r rhain yn cael eu gosod a'u gorfodi gan y %{domain} cymedrolwyr. preamble_invited: Cyn i chi barhau, ystyriwch y rheolau sylfaenol a osodwyd gan gymedrolwyr %{domain}. + read_more: Darllen rhagor title: Rhai rheolau sylfaenol. title_invited: Rydych wedi derbyn gwahoddiad. security: Diogelwch diff --git a/config/locales/da.yml b/config/locales/da.yml index 2df10f12b11289..c83750d58c2442 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -778,6 +778,8 @@ da: administrator_description: Brugere med denne rolle kan omgå alle tilladelser delete_user_data: Slet brugerdata delete_user_data_description: Tillader brugere at slette andre brugeres data straks + invite_bypass_approval: Invitere brugere uden gennemgang + invite_bypass_approval_description: Gør det muligt for personer, der er inviteret til serveren af disse brugere, at undgå moderatorgodkendelse invite_users: Invitér brugere invite_users_description: Tillader brugere at invitere nye personer til serveren manage_announcements: Administrer annonceringer @@ -1279,6 +1281,7 @@ da: progress: confirm: Bekræft e-mail details: Dine detaljer + list: Status for tilmelding review: Vores gennemgang rules: Acceptér regler providers: @@ -1294,6 +1297,7 @@ da: invited_by: 'Du kan tilmelde dig %{domain} takket være den invitation, du har modtaget fra:' preamble: Disse er opsat og håndhæves af %{domain}-moderatorerne. preamble_invited: Før du fortsætter, bedes du overveje de grundregler, der er fastsat af moderatorerne af %{domain}. + read_more: Læs mere title: Nogle grundregler. title_invited: Du er blevet inviteret. security: Sikkerhed diff --git a/config/locales/de.yml b/config/locales/de.yml index 2315be3f8d7986..4981257442e109 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -778,8 +778,10 @@ de: administrator_description: Beschränkung aller Berechtigungen umgehen delete_user_data: Kontodaten löschen delete_user_data_description: Daten anderer Profile ohne Verzögerung löschen + invite_bypass_approval: Einladungen ohne Überprüfungen + invite_bypass_approval_description: Neue Benutzer*innen zur Registrierung auf diesem Server einladen, ohne manuell genehmigt werden zu müssen invite_users: Einladungen - invite_users_description: Erlaubt bereits registrierten Benutzer*innen, neue Leute zum Server einzuladen + invite_users_description: Neue Benutzer*innen zur Registrierung auf diesem Server einladen manage_announcements: Ankündigungen manage_announcements_description: Ankündigungen dieses Servers verwalten manage_appeals: Einsprüche @@ -805,7 +807,7 @@ de: manage_user_access: Kontozugriff manage_user_access_description: Zwei-Faktor-Authentisierungen anderer können deaktiviert, E-Mail-Adressen geändert und Passwörter zurückgesetzt werden manage_users: Konten - manage_users_description: Erlaubt es Benutzer*innen, die Details anderer Profile einzusehen und diese Accounts zu moderieren + manage_users_description: Details anderer Profile einsehen und Moderation von Konten manage_webhooks: Webhooks manage_webhooks_description: Webhooks für administrative Vorgänge einrichten view_audit_log: Protokoll anzeigen @@ -814,7 +816,7 @@ de: view_dashboard_description: Dashboard und verschiedene Metriken view_devops: DevOps view_devops_description: Auf Sidekiq- und pgHero-Dashboards zugreifen - view_feeds: Live-Feeds und Hashtags anzeigen + view_feeds: Live-Feeds und Hashtags view_feeds_description: Zugriff auf Live-Feeds und Hashtags – unabhängig der Servereinstellungen requires_2fa: Zwei-Faktor-Authentisierung erforderlich title: Rollen @@ -1279,6 +1281,7 @@ de: progress: confirm: E-Mail bestätigen details: Deine Daten + list: Registrierungsfortschritt review: Unsere Überprüfung rules: Serverregeln akzeptieren providers: @@ -1294,6 +1297,7 @@ de: invited_by: 'Du kannst %{domain} beitreten – dank der Einladung von:' preamble: Diese werden von den Moderator*innen von %{domain} festgelegt und durchgesetzt. preamble_invited: Bevor du fortfährst, beachte bitte die Grundregeln der Moderator*innen von %{domain}. + read_more: Weiterlesen title: Einige Grundregeln. title_invited: Du wurdest eingeladen. security: Sicherheit @@ -1668,7 +1672,7 @@ de: validations: images_and_video: Es kann kein Video an einen Beitrag angehängt werden, der bereits Bilder enthält not_found: Medieninhalt(e) %{ids} nicht gefunden oder bereits an einen anderen Beitrag angehängt - not_ready: Dateien, die noch nicht verarbeitet wurden, können nicht angehängt werden. Versuche es gleich noch einmal! + not_ready: Dateien, die noch nicht verarbeitet wurden, können nicht angehängt werden. Warte einen Moment! too_many: Mehr als vier Dateien können nicht angehängt werden migrations: acct: umgezogen nach diff --git a/config/locales/devise.es-AR.yml b/config/locales/devise.es-AR.yml index a4084441a91215..5e8eeead33ab5c 100644 --- a/config/locales/devise.es-AR.yml +++ b/config/locales/devise.es-AR.yml @@ -49,7 +49,7 @@ es-AR: subject: 'Mastodon: instrucciones para cambiar la contraseña' title: Cambiar contraseña two_factor_disabled: - explanation: Ahora es posible iniciar sesión utilizando sólo la dirección de correo electrónico y la contraseña. + explanation: Ahora es posible iniciar sesión utilizando solo la dirección de correo electrónico y la contraseña. subject: 'Mastodon: autenticación de dos factores, deshabilitada' subtitle: Se deshabilitó la autenticación de dos factores para tu cuenta. title: 2FA deshabilitada @@ -76,7 +76,7 @@ es-AR: title: Se eliminó una de tus llaves de seguridad webauthn_disabled: explanation: Se deshabilitó la autenticación con claves de seguridad para tu cuenta. - extra: Ahora es posible iniciar sesión utilizando sólo la clave numérica ("token") generada por la aplicación TOTP emparejada. + extra: Ahora es posible iniciar sesión utilizando solo la clave numérica ("token") generada por la aplicación TOTP emparejada. subject: 'Mastodon: autenticación con llaves de seguridad, deshabilitada' title: Llaves de seguridad deshabilitadas webauthn_enabled: diff --git a/config/locales/devise.no.yml b/config/locales/devise.no.yml index b92e7e8aa35aa0..116705d63d672b 100644 --- a/config/locales/devise.no.yml +++ b/config/locales/devise.no.yml @@ -7,6 +7,7 @@ send_paranoid_instructions: Hvis e-postadressen din finnes i databasen vår vil du om noen få minutter motta en e-post med instruksjoner for bekreftelse. Sjekk spam-mappen din hvis du ikke mottok e-posten. failure: already_authenticated: Du er allerede innlogget. + closed_registrations: Registreringsforsøket ditt har blitt blokkert på grunn av en nettverkspolicy. Hvis du mener dette er feil, kontakt %{email}. inactive: Kontoen din er ikke blitt aktivert ennå. invalid: Ugyldig %{authentication_keys} eller passord. last_attempt: Du har ett forsøk igjen før kontoen din låses. diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml index a499f6503ff2cd..2faa5bd64bcd5e 100644 --- a/config/locales/doorkeeper.de.yml +++ b/config/locales/doorkeeper.de.yml @@ -125,7 +125,7 @@ de: accounts: Konten admin/accounts: Kontenverwaltung admin/all: Alle administrativen Funktionen - admin/reports: Meldungen verwalten + admin/reports: Meldungen all: Voller Zugriff auf dein Mastodon-Konto blocks: Blockierungen bookmarks: Lesezeichen diff --git a/config/locales/doorkeeper.es-AR.yml b/config/locales/doorkeeper.es-AR.yml index 2c6b17ee84d126..d9a6f1df5e8c7c 100644 --- a/config/locales/doorkeeper.es-AR.yml +++ b/config/locales/doorkeeper.es-AR.yml @@ -25,7 +25,7 @@ es-AR: edit: Editar submit: Enviar confirmations: - destroy: "¿Estás seguro?" + destroy: "¿Continuar?" edit: title: Editar aplicación form: @@ -69,7 +69,7 @@ es-AR: buttons: revoke: Revocar confirmations: - revoke: "¿Estás seguro?" + revoke: "¿Continuar?" index: authorized_at: Autorizado el %{date} description_html: Estas son aplicaciones que pueden acceder a tu cuenta usando la API. Si hay aplicaciones que no reconocés acá, o que funcionan de forma sospechosa, podés revocar su acceso. @@ -118,9 +118,9 @@ es-AR: notice: Aplicación revocada. grouped_scopes: access: - read: Acceso de sólo lectura + read: Acceso de solo lectura read/write: Acceso de lectura y escritura - write: Acceso de sólo escritura + write: Acceso de solo escritura title: accounts: Cuentas admin/accounts: Administración de cuentas diff --git a/config/locales/doorkeeper.no.yml b/config/locales/doorkeeper.no.yml index 7b7b9d65342689..59874904b2a05a 100644 --- a/config/locales/doorkeeper.no.yml +++ b/config/locales/doorkeeper.no.yml @@ -60,6 +60,7 @@ error: title: En feil oppstod new: + prompt_html: "%{client_name} ønsker tilgang til kontoen din. Godkjenn denne forespørselen kun hvis du kjenner igjen og stoler på denne kilden." review_permissions: Gå gjennom tillatelser title: Autorisasjon påkrevd show: @@ -134,6 +135,7 @@ media: Mediavedlegg mutes: Dempinger notifications: Varslinger + profile: Din Mastodon-profil push: Push-varslinger reports: Rapporteringer search: Søk diff --git a/config/locales/el.yml b/config/locales/el.yml index 3e0bc3a0601fd7..de95a5b7258c5d 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -14,7 +14,7 @@ el: other: Ακόλουθοι following: one: Ακολουθεί - other: Ακολουθούν + other: Ακολουθεί instance_actor_flash: Αυτός ο λογαριασμός είναι εικονικός και χρησιμοποιείται για να αντιπροσωπεύει τον ίδιο τον διακομιστή και όχι κάποιον μεμονωμένο χρήστη. Χρησιμοποιείται για σκοπούς ομοσπονδίας και δεν πρέπει να ανασταλεί. last_active: τελευταία ενεργός/ή link_verified_on: Η ιδιοκτησία αυτού του συνδέσμου ελέγχθηκε στις %{date} @@ -778,6 +778,8 @@ el: administrator_description: Οι χρήστες με αυτό το δικαίωμα θα παρακάμπτουν κάθε δικαίωμα delete_user_data: Διαγραφή Δεδομένων Χρήστη delete_user_data_description: Επιτρέπει στους χρήστες να διαγράφουν τα δεδομένα άλλων χρηστών χωρίς καθυστέρηση + invite_bypass_approval: Πρόσκληση Χρηστών χωρίς έλεγχο + invite_bypass_approval_description: Επιτρέπει σε άτομα που προσκαλούνται στον διακομιστή από αυτούς τους χρήστες, να παρακάμψουν την έγκριση από συντονιστές invite_users: Πρόσκληση Χρηστών invite_users_description: Επιτρέπει στους χρήστες να προσκαλούν νέα άτομα στον διακομιστή manage_announcements: Διαχείριση Ανακοινώσεων @@ -841,10 +843,10 @@ el: desc: Όταν οι χρήστες σου κάνουν κλικ συνδέσμους σε εξωτερικές ιστοσελίδες, το πρόγραμμα περιήγησής τους μπορεί να στείλει τη διεύθυνση του διακομιστή σας Mastodon ως αναφέρων. Απενεργοποίησέ το αν αυτό θα αναγνώριζε μοναδικά τους χρήστες σου, π.χ. αν αυτός είναι ένας προσωπικός διακομιστής Mastodon. title: Να επιτρέπεται σε εξωτερικούς ιστότοπους να βλέπουν τον διακομιστή Mastodon σου ως πηγή κίνησης appearance: - preamble: Προσάρμοσε την ιστοσελίδα του Mastodon. + preamble: Προσάρμοσε τη διεπαφή ιστού του Mastodon. title: Εμφάνιση branding: - preamble: Η ταυτότητα του διακομιστή σου, τον διαφοροποιεί από άλλους διακομιστές του δικτύου. Αυτές οι πληροφορίες μπορεί να εμφανίζονται σε διάφορα περιβάλλοντα, όπως η ιστοσελίδα του Mastodon, εγγενείς εφαρμογές, σε προεπισκοπήσεις συνδέσμου σε άλλους ιστότοπους και εντός εφαρμογών μηνυμάτων και ούτω καθεξής. Γι' αυτό, είναι καλύτερο να διατηρούνται αυτές οι πληροφορίες σαφείς, σύντομες και συνοπτικές. + preamble: Η ταυτότητα του διακομιστή σου, τον διαφοροποιεί από άλλους διακομιστές του δικτύου. Αυτές οι πληροφορίες μπορεί να εμφανίζονται σε διάφορα περιβάλλοντα, όπως η διεπαφή ιστού του Mastodon, εγγενείς εφαρμογές, σε προεπισκοπήσεις συνδέσμου σε άλλους ιστότοπους και εντός εφαρμογών μηνυμάτων και ούτω καθεξής. Γι' αυτό, είναι καλύτερο να διατηρούνται αυτές οι πληροφορίες σαφείς, σύντομες και συνοπτικές. title: Ταυτότητα captcha_enabled: desc_html: Αυτό βασίζεται σε εξωτερικά scripts από το hCaptcha, όπου υπάρχει ανησυχία πέρι ασφάλειας και ιδιωτηκότητας. Επιπρόσθετα, μπορεί να κάνει τη διαδικασία εγγραφής πολύ λιγότερο προσβάσιμη για κάποια άτομα (ειδικά αυτά με αναπηρίες). Για αυτούς τους λόγους, παρακαλώ σκέψου άλλου τρόπους εγγραφής όπως με αποδοχή ή με πρόσκληση. @@ -1279,6 +1281,7 @@ el: progress: confirm: Επιβεβαίωση email details: Τα στοιχεία σας + list: Πρόοδος εγγραφής review: Η αξιολόγησή μας rules: Αποδοχή κανόνων providers: @@ -1294,6 +1297,7 @@ el: invited_by: 'Μπορείτε να συμμετάσχεις στο %{domain} χάρη στην πρόσκληση που έλαβες από:' preamble: Αυτά ορίζονται και επιβάλλονται από τους συντονιστές του%{domain}. preamble_invited: Πριν συνεχίσεις, παρακαλώ δώσε προσοχή στους βασικούς κανόνες που έχουν οριστεί από τους συντονιστές του %{domain}. + read_more: Διαβάστε περισσότερα title: Ορισμένοι βασικοί κανόνες. title_invited: Έχεις προσκληθεί. security: Ασφάλεια @@ -1433,7 +1437,7 @@ el: content: Λυπούμαστε, κάτι πήγε στραβά από τη δική μας μεριά. title: Η σελίδα αυτή δεν είναι σωστή '503': Η σελίδα δε μπόρεσε να εμφανιστεί λόγω προσωρινού σφάλματος του διακομιστή. - noscript_html: Για να χρησιμοποιήσεις τη δικτυακή εφαρμογή του Mastodon, ενεργοποίησε την Javascript. Εναλλακτικά, δοκίμασε μια από τις εφαρμογές για το Mastodon για την πλατφόρμα σου. + noscript_html: Για να χρησιμοποιήσεις την εφαρμογή ιστού του Mastodon, παρακαλούμε ενεργοποίησε την Javascript. Εναλλακτικά, δοκίμασε μια από τις εφαρμογές για το Mastodon για την πλατφόρμα σου. existing_username_validator: not_found: δεν βρέθηκε τοπικός χρήστης με αυτό το όνομα not_found_multiple: δεν βρέθηκε %{usernames} @@ -1471,7 +1475,7 @@ el: statuses_hint_html: Αυτό το φίλτρο εφαρμόζεται για την επιλογή μεμονωμένων αναρτήσεων, ανεξάρτητα από το αν αντιστοιχούν με τις λέξεις-κλειδιά παρακάτω. Επισκόπηση ή αφαίρεση αναρτήσεων από το φίλτρο. title: Επεξεργασία φίλτρου errors: - deprecated_api_multiple_keywords: Αυτές οι παράμετροι δεν μπορούν να αλλάξουν από αυτήν την εφαρμογή επειδή ισχύουν για περισσότερες από μία λέξεις-κλειδιά φίλτρου. Χρησιμοποίησε μια πιο πρόσφατη εφαρμογή ή την ιστοσελίδα. + deprecated_api_multiple_keywords: Αυτές οι παράμετροι δεν μπορούν να αλλάξουν από αυτήν την εφαρμογή επειδή ισχύουν για περισσότερες από μία λέξεις-κλειδιά φίλτρου. Χρησιμοποίησε μια πιο πρόσφατη εφαρμογή ή τη διεπαφή ιστού. invalid_context: Δόθηκε κενό ή μη έγκυρο περιεχόμενο index: contexts: Φίλτρα σε %{contexts} @@ -1497,7 +1501,7 @@ el: batch: remove: Αφαίρεση από φίλτρο index: - hint: Αυτό το φίλτρο ισχύει για την επιλογή μεμονωμένων αναρτήσεων ανεξάρτητα από άλλα κριτήρια. Μπορείς να προσθέσεις περισσότερες αναρτήσεις σε αυτό το φίλτρο από την ιστοσελίδα. + hint: Αυτό το φίλτρο ισχύει για την επιλογή μεμονωμένων αναρτήσεων ανεξάρτητα από άλλα κριτήρια. Μπορείς να προσθέσεις περισσότερες αναρτήσεις σε αυτό το φίλτρο από τη διεπαφή ιστού. title: Φιλτραρισμένες αναρτήσεις generic: all: Όλα @@ -1601,7 +1605,7 @@ el: blocking: Λίστα αποκλεισμού bookmarks: Σελιδοδείκτες domain_blocking: Λίστα αποκλεισμένων τομέων - following: Λίστα ατόμων που ακολουθείτε + following: Λίστα λογαριασμών που ακολουθείτε lists: Λίστες muting: Λίστα αποσιωπήσεων upload: Μεταφόρτωση diff --git a/config/locales/en.yml b/config/locales/en.yml index 7489a3d53031ba..69ca2a9717dd20 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1007,6 +1007,8 @@ en: administrator_description: Users with this permission will bypass every permission delete_user_data: Delete User Data delete_user_data_description: Allows users to delete other users' data without delay + invite_bypass_approval: Invite Users without review + invite_bypass_approval_description: Allows people invited to the server by these users to bypass moderation approval invite_users: Invite Users invite_users_description: Allows users to invite new people to the server manage_announcements: Manage Announcements @@ -1573,6 +1575,7 @@ en: progress: confirm: Confirm email details: Your details + list: Sign up progress review: Our review rules: Accept rules providers: @@ -1591,6 +1594,7 @@ en: invited_by: 'You can join %{domain} thanks to the invitation you have received from:' preamble: These are set and enforced by the %{domain} moderators. preamble_invited: Before you proceed, please consider the ground rules set by the moderators of %{domain}. + read_more: Read more title: Some ground rules. title_invited: You've been invited. security: Security diff --git a/config/locales/eo.yml b/config/locales/eo.yml index d8700963c2cd07..de46b0a7be197c 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -1404,7 +1404,7 @@ eo: add_new: Aldoni novan errors: limit: Vi jam elstarigis la maksimuman kvanton da kradvortoj - hint_html: "Kio estas la trajtaj kradvortoj? Ili bone videblas en via publika profilo kaj permesas al homoj trarigardi viajn publikajn mesaĝojn specife laŭ tiuj kradvortoj. Ili estas bonaj iloj por sekvi la evoluon de kreadaj laboroj aŭ longdaŭraj projektoj." + hint_html: "Elstarigu viajn plej gravajn kradvortojn en via profilo. Bona ilo por sekvi viaj kreaĵoj aŭ longdaŭraj projektoj, elstarigitaj kradvortoj bone videblas en via publika profilo kaj ebligas rapidan aliron al viaj propraj afiŝoj." filters: contexts: account: Profiloj diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index bbc1fd78e3ba1d..e4c8e85622c4e0 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -40,7 +40,7 @@ es-AR: add_email_domain_block: Bloquear el dominio del correo electrónico approve: Aprobar approved_msg: Se aprobó exitosamente la solicitud de registro de %{username} - are_you_sure: "¿Estás seguro?" + are_you_sure: "¿Continuar?" avatar: Avatar by_domain: Dominio change_email: @@ -144,7 +144,7 @@ es-AR: search_same_ip: Otros usuarios con la misma dirección IP security: Seguridad security_measures: - only_password: Sólo contraseña + only_password: Solo contraseña password_and_2fa: Contraseña y 2FA sensitive: Forzar como sensible sensitized: Marcado como sensible @@ -379,7 +379,7 @@ es-AR: not_permitted: No tenés permiso para realizar esta acción overwrite: Sobreescribir shortcode: Código corto - shortcode_hint: Al menos 2 caracteres, sólo caracteres alfanuméricos y subguiones ("_") + shortcode_hint: Al menos 2 caracteres, solo caracteres alfanuméricos y subguiones ("_") title: Emojis personalizados uncategorized: Sin categoría unlist: No listar @@ -559,7 +559,7 @@ es-AR: back_to_limited: Limitados back_to_warning: Advertencia by_domain: Dominio - confirm_purge: "¿Estás seguro que querés eliminar permanentemente los datos de este dominio?" + confirm_purge: "¿De verdad querés eliminar permanentemente los datos de este dominio?" content_policies: comment: Nota interna description_html: Podés definir políticas de contenido que se aplicarán a todas las cuentas de este dominio y a cualquiera de sus subdominios. @@ -675,16 +675,16 @@ es-AR: mark_as_sensitive_description_html: Los archivos de medios en los mensajes denunciados se marcarán como sensibles y se registrará un incumplimiento para ayudarte a escalar las futuras infracciones de la misma cuenta. other_description_html: Ver más opciones para controlar el comportamiento de la cuenta y personalizar la comunicación de la cuenta denunciada. resolve_description_html: No se tomarán medidas contra la cuenta denunciada, no se registrará el incumplimiento, y se cerrará la denuncia. - silence_description_html: La cuenta será visible sólo para quienes ya la siguen o la busquen manualmente, limitando severamente su alcance. Siempre puede ser revertido. Esto cierra todas las denuncias contra esta cuenta. + silence_description_html: La cuenta será visible solo para quienes ya la siguen o la busquen manualmente, limitando severamente su alcance. Siempre puede ser revertido. Esto cierra todas las denuncias contra esta cuenta. suspend_description_html: La cuenta y todos sus contenidos serán inaccesibles y finalmente eliminados, e interactuar con ella será imposible. Revertible en 30 días. Esto cierra todas las denuncias contra esta cuenta. actions_description_html: Decidí qué medidas tomar para resolver esta denuncia. Si tomás una acción punitiva contra la cuenta denunciada, se le enviará a dicha cuenta una notificación por correo electrónico, excepto cuando se seleccione la categoría Spam. - actions_description_remote_html: Decidí qué medidas tomar para resolver esta denuncia. Esto sólo afectará la forma en que tu servidor se comunica con esta cuenta remota y maneja su contenido. + actions_description_remote_html: Decidí qué medidas tomar para resolver esta denuncia. Esto solo afectará la forma en que tu servidor se comunica con esta cuenta remota y maneja su contenido. actions_no_posts: Esta denuncia no tiene ningún mensaje asociado para eliminar add_to_report: Agregar más a la denuncia already_suspended_badges: local: Ya suspendido en este servidor remote: Ya suspendido en su servidor - are_you_sure: "¿Estás seguro?" + are_you_sure: "¿Continuar?" assign_to_self: Asignármela a mí assigned: Moderador asignado by_target_domain: Dominio de la cuenta denunciada @@ -738,7 +738,7 @@ es-AR: actions: delete_html: Eliminar los mensajes ofensivos mark_as_sensitive_html: Marcar los mensajes ofensivos como sensibles - silence_html: Limitar severamente el alcance de @%{acct} haciendo que su perfil y contenido sólo sean visibles para las personas que ya lo siguen o que busquen manualmente su perfil + silence_html: Limitar severamente el alcance de @%{acct} haciendo que su perfil y contenido solo sean visibles para las personas que ya lo siguen o que busquen manualmente su perfil suspend_html: Suspender @%{acct}, haciendo su perfil y contenido inaccesibles, e imposibilitando la interacción con la cuenta close_report: 'Marcar denuncia #%{id} como resuelta' close_reports_html: Marcar todas las denuncias contra @%{acct} como resueltas @@ -778,6 +778,8 @@ es-AR: administrator_description: Los usuarios con este permiso saltarán todos los permisos delete_user_data: Eliminar datos del usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar a usuarios sin revisión + invite_bypass_approval_description: Permite —a las personas invitadas al servidor por estos usuarios— eludir la aprobación de moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar anuncios @@ -1054,16 +1056,16 @@ es-AR: trends: allow: Permitir approved: Aprobadas - confirm_allow: "¿Estás seguro de que querés permitir las etiquetas seleccionadas?" - confirm_disallow: "¿Estás seguro de que no querés permitir las etiquetas seleccionadas?" + confirm_allow: "¿De verdad querés permitir las etiquetas seleccionadas?" + confirm_disallow: "¿De verdad no querés permitir las etiquetas seleccionadas?" disallow: Rechazar links: allow: Permitir enlace allow_provider: Permitir medio - confirm_allow: "¿Estás seguro de que querés permitir los enlaces seleccionados?" - confirm_allow_provider: "¿Estás seguro de que querés permitir los proveedores seleccionados?" - confirm_disallow: "¿Estás seguro de que no querés permitir los enlaces seleccionados?" - confirm_disallow_provider: "¿Estás seguro de que no querés permitir los proveedores seleccionados?" + confirm_allow: "¿De verdad querés permitir los enlaces seleccionados?" + confirm_allow_provider: "¿De verdad querés permitir los proveedores seleccionados?" + confirm_disallow: "¿De verdad no querés permitir los enlaces seleccionados?" + confirm_disallow_provider: "¿De verdad no querés permitir los proveedores seleccionados?" description_html: Estos son enlaces que actualmente están siendo muy compartidos por cuentas desde las que tu servidor ve los mensajes. Esto puede ayudar a tus usuarios a averiguar qué está pasando en el mundo. No hay enlaces que se muestren públicamente hasta que autoricés al publicador. También podés permitir o rechazar enlaces individuales. disallow: Rechazar enlace disallow_provider: Rechazar medio @@ -1076,7 +1078,7 @@ es-AR: title: Enlaces en tendencia usage_comparison: Compartido %{today} veces hoy, comparado con la/s %{yesterday} vez/veces de ayer not_allowed_to_trend: No se permite la tendencia - only_allowed: Sólo permitidas + only_allowed: Solo permitidas pending_review: Revisión pendiente preview_card_providers: allowed: Los enlaces de este medio pueden ser tendencia @@ -1087,10 +1089,10 @@ es-AR: statuses: allow: Permitir mensaje allow_account: Permitir autor - confirm_allow: "¿Estás seguro de que querés permitir los estados seleccionados?" - confirm_allow_account: "¿Estás seguro de que querés permitir las cuentas seleccionadas?" - confirm_disallow: "¿Estás seguro de que no querés permitir los estados seleccionados?" - confirm_disallow_account: "¿Estás seguro de que no querés permitir las cuentas seleccionadas?" + confirm_allow: "¿De verdad querés permitir los estados seleccionados?" + confirm_allow_account: "¿De verdad querés permitir las cuentas seleccionadas?" + confirm_disallow: "¿De verdad no querés permitir los estados seleccionados?" + confirm_disallow_account: "¿De verdad no querés permitir las cuentas seleccionadas?" description_html: Estos son mensajes que tu servidor detecta que están siendo compartidos y marcados como favoritos muchas veces en este momento. Esto puede ayudar a tus usuarios nuevos y retornantes a encontrar más cuentas para seguir. No hay mensajes que se muestren públicamente hasta que aprobés al autor, y el autor permita que su cuenta sea sugerida a otros. También podés permitir o rechazar mensajes individuales. disallow: Rechazar mensaje disallow_account: Rechazar autor @@ -1279,6 +1281,7 @@ es-AR: progress: confirm: Confirmar correo electrónico details: Tus detalles + list: Proceso de registro review: Nuestra reseña rules: Aceptar reglas providers: @@ -1294,6 +1297,7 @@ es-AR: invited_by: 'Podés unirte a %{domain} gracias a la invitación que recibiste de:' preamble: Estas reglas son establecidas y aplicadas por los moderadores de %{domain}. preamble_invited: Antes de continuar, por favor, tené en cuenta las reglas básicas establecidas por los moderadores de %{domain}. + read_more: Leé más title: Algunas reglas básicas. title_invited: Te invitaron. security: Seguridad @@ -1632,7 +1636,7 @@ es-AR: author_html: Por %{name} potentially_sensitive_content: action: Clic para mostrar - confirm_visit: "¿Está seguro de que querés abrir este enlace?" + confirm_visit: "¿De verdad querés abrir este enlace?" hide_button: Ocultar label: Contenido potencialmente sensible lists: @@ -1820,9 +1824,9 @@ es-AR: title: Estás dejando %{instance}. relationships: activity: Actividad de la cuenta - confirm_follow_selected_followers: "¿Estás seguro que querés seguir a los seguidores seleccionados?" - confirm_remove_selected_followers: "¿Estás seguro que querés quitar a los seguidores seleccionados?" - confirm_remove_selected_follows: "¿Estás seguro que querés quitar a las cuentas seguidas seleccionadas?" + confirm_follow_selected_followers: "¿De verdad querés seguir a los seguidores seleccionados?" + confirm_remove_selected_followers: "¿De verdad querés quitar a los seguidores seleccionados?" + confirm_remove_selected_follows: "¿De verdad querés quitar a las cuentas seguidas seleccionadas?" dormant: Inactivas follow_failure: No se pudieron seguir algunas de las cuentas seleccionadas. follow_selected_followers: Seguir a los seguidores seleccionados @@ -1965,7 +1969,7 @@ es-AR: quoted_user_not_mentioned: No se puede citar a un usuario no mencionado en un mensaje de mención privada. over_character_limit: se excedió el límite de %{max} caracteres pin_errors: - direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados + direct: Los mensajes que solo son visibles para los usuarios mencionados no pueden ser fijados limit: Ya fijaste el número máximo de mensajes ownership: No se puede fijar el mensaje de otra cuenta reblog: No se puede fijar una adhesión @@ -2118,7 +2122,7 @@ es-AR: disable: Ya no podés usar tu cuenta, pero tu perfil y el resto de datos permanecen intactos. Podés solicitar una copia de seguridad de tus datos, cambiar la configuración de tu cuenta, o eliminarla. mark_statuses_as_sensitive: Algunos de tus mensajes fueron marcados como sensibles por los moderadores de %{instance}. Esto significa que la gente tendrá que hacer clic o darle un toque a los medios en los mensajes antes de que se muestre una vista previa. Podés marcar los medios como sensibles vos mismo cuando publiqués en el futuro. sensitive: A partir de ahora, todos tus archivos subidos serán marcados como sensibles y ocultos tras una advertencia en la que habrá que hacer clic. - silence: Todavía podés usar tu cuenta, pero sólo las personas que te están siguiendo verán tus publicaciones en este servidor, y podrías ser excluido de varias funciones de descubrimiento. Sin embargo, otras cuentas podrán seguirte manualmente. + silence: Todavía podés usar tu cuenta, pero solo las personas que te están siguiendo verán tus publicaciones en este servidor, y podrías ser excluido de varias funciones de descubrimiento. Sin embargo, otras cuentas podrán seguirte manualmente. suspend: Ya no podés usar tu cuenta, y tu perfil y el resto de datos ya no son accesibles. Todavía podés iniciar sesión para solicitar una copia de seguridad de tus datos, hasta que estos sean eliminados por completo en unos 30 días, aunque conservaremos algunos datos básicos para impedir que esquivés la suspensión. reason: 'Motivo:' statuses: 'Mensajes citados:' @@ -2200,7 +2204,7 @@ es-AR: error: Hubo un problema al agregar tu llave de seguridad. Por favor, intentá de nuevo. success: Se agregó exitosamente tu llave de seguridad. delete: Eliminar - delete_confirmation: "¿Estás seguro que querés eliminar esta llave de seguridad?" + delete_confirmation: "¿De verdad querés eliminar esta llave de seguridad?" description_html: Si habilitás la autenticación de llave de seguridad, entonces en el inicio de sesión se te pedirá que usés una de tus llaves de seguridad. destroy: error: Hubo un problema al eliminar tu llave de seguridad. Por favor, intentá de nuevo. diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index bf05a6d4053ebd..44677285a7f383 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -361,7 +361,7 @@ es-MX: copy_failed_msg: No se pudo realizar una copia local de ese emoji create_new_category: Crear una nueva categoría created_msg: "¡Emoji creado con éxito!" - delete: Borrar + delete: Eliminar destroyed_msg: "¡Emojo destruido con éxito!" disable: Deshabilitar disabled: Desactivado @@ -473,7 +473,7 @@ es-MX: one: "%{count} intentos durante la última semana" other: "%{count} intentos de registro en la última semana" created_msg: Dominio de correo bloqueado con éxito - delete: Borrar + delete: Eliminar dns: types: mx: Registro MX @@ -610,7 +610,7 @@ es-MX: private_comment: Comentario privado public_comment: Comentario público purge: Purgar - purge_description_html: Si crees que este dominio está desconectado, puedes borrar todos los registros de cuentas y los datos asociados de este dominio de tu almacenamiento. Esto puede llevar un tiempo. + purge_description_html: Si crees que este dominio ya no está activo, puedes eliminar de tu almacenamiento todos los registros de la cuenta y los datos asociados a este dominio. Esto puede tardar un rato. title: Instancias conocidas total_blocked_by_us: Bloqueado por nosotros total_followed_by_them: Seguidos por ellos @@ -646,7 +646,7 @@ es-MX: title: Relaciones de %{acct} relays: add_new: Añadir nuevo relé - delete: Borrar + delete: Eliminar description_html: Un relés de federación es un servidor intermedio que intercambia grandes volúmenes de publicaciones públicas entre servidores que se suscriben y publican en él. Puede ayudar a servidores pequeños y medianos a descubrir contenido del fediverso, que de otra manera requeriría que los usuarios locales siguiesen manualmente a personas de servidores remotos. disable: Deshabilitar disabled: Deshabilitado @@ -776,8 +776,10 @@ es-MX: privileges: administrator: Administrador administrator_description: Los usuarios con este permiso saltarán todos los permisos - delete_user_data: Borrar Datos de Usuario + delete_user_data: Eliminar datos de usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar a usuarios sin revisión + invite_bypass_approval_description: Permite que las personas invitadas al servidor por estos usuarios no tengan que pasar por el proceso de aprobación de la moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar Anuncios @@ -1146,7 +1148,7 @@ es-MX: updated_msg: Regla de nombre de usuario actualizada correctamente warning_presets: add_new: Añadir nuevo - delete: Borrar + delete: Eliminar edit_preset: Editar aviso predeterminado empty: Aún no has definido ningún preajuste de advertencia. title: Preajustes de advertencia @@ -1258,7 +1260,7 @@ es-MX: registration_complete: "¡Tu registro en %{domain} ha sido completado!" welcome_title: "¡Bienvenido, %{name}!" wrong_email_hint: Si esa dirección de correo electrónico no es correcta, puedes cambiarla en la configuración de la cuenta. - delete_account: Borrar cuenta + delete_account: Eliminar cuenta delete_account_html: Si deseas eliminar tu cuenta, puedes proceder aquí. Se te pedirá una confirmación. description: prefix_invited_by_user: "¡@%{name} te invita a unirte a este servidor de Mastodon!" @@ -1279,6 +1281,7 @@ es-MX: progress: confirm: Confirmar dirección de correo details: Tus detalles + list: Registrar el progreso review: Nuestra revisión rules: Aceptar reglas providers: @@ -1294,6 +1297,7 @@ es-MX: invited_by: 'Puedes unirte a %{domain} gracias a la invitación que has recibido de:' preamble: Estas son establecidas y aplicadas por los moderadores de %{domain}. preamble_invited: Antes de continuar, por favor, revisa las reglas básicas establecidas por los moderadores de %{domain}. + read_more: Leer más title: Algunas reglas básicas. title_invited: Has sido invitado. security: Cambiar contraseña @@ -1453,10 +1457,10 @@ es-MX: mutes: Tienes en silencio storage: Almacenamiento featured_tags: - add_new: Añadir nuevo + add_new: Añadir nueva errors: limit: Ya has alcanzado la cantidad máxima de etiquetas - hint_html: "¿Qué son las etiquetas destacadas? Se muestran de forma prominente en tu perfil público y permiten a los usuarios navegar por tus publicaciones públicas específicamente bajo esas etiquetas. Son una gran herramienta para hacer un seguimiento de trabajos creativos o proyectos a largo plazo." + hint_html: "Destaca tus etiquetas más importantes en tu perfil. Una herramienta fantástica para llevar un registro de tus trabajos creativos y proyectos a largo plazo; las etiquetas destacadas aparecen en un lugar visible de tu perfil y te permiten acceder rápidamente a tus propias publicaciones." filters: contexts: account: Perfiles @@ -1475,7 +1479,7 @@ es-MX: invalid_context: Se suminstró un contexto inválido o vacío index: contexts: Filtros en %{contexts} - delete: Borrar + delete: Eliminar empty: No tienes filtros. expires_in: Caduca en %{distance} expires_on: Expira el %{date} @@ -1909,7 +1913,7 @@ es-MX: appearance: Apariencia authorized_apps: Aplicaciones autorizadas back: Volver al inicio - delete: Borrar cuenta + delete: Eliminar cuenta development: Desarrollo edit_profile: Editar perfil export: Exportar @@ -1987,7 +1991,7 @@ es-MX: unlisted: Pública, pero silenciosa unlisted_long: Ocultado de los resultados de búsqueda, tendencias y cronologías públicas de Mastodon statuses_cleanup: - enabled: Borrar automáticamente publicaciones antiguas + enabled: Eliminar automáticamente las publicaciones antiguas enabled_hint: Elimina automáticamente tus publicaciones una vez que alcancen un umbral de tiempo especificado, a menos que coincidan con alguna de las excepciones detalladas debajo exceptions: Excepciones explanation: La eliminación automática se realiza con baja prioridad. Puede haber un retraso entre el momento en que se alcanza el límite de antigüedad y el momento en que se elimina. diff --git a/config/locales/es.yml b/config/locales/es.yml index b5014dba7e42cd..c7de4346734838 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -778,6 +778,8 @@ es: administrator_description: Los usuarios con este permiso saltarán todos los permisos delete_user_data: Borrar Datos de Usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar usuarios sin revisión + invite_bypass_approval_description: Permite registrarse a las personas invitadas al servidor por estos usuarios sin la aprobación de la moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar Anuncios @@ -1279,6 +1281,7 @@ es: progress: confirm: Confirmar dirección de correo details: Tus detalles + list: Progreso de registro review: Nuestra revisión rules: Aceptar reglas providers: @@ -1294,6 +1297,7 @@ es: invited_by: 'Puedes unirte a %{domain} gracias a la invitación que has recibido de:' preamble: Estas son establecidas y aplicadas por los moderadores de %{domain}. preamble_invited: Antes de continuar, por favor, revisa las reglas básicas establecidas por los moderadores de %{domain}. + read_more: Leer más title: Algunas reglas básicas. title_invited: Has sido invitado. security: Cambiar contraseña diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 94c4793bdc01c8..850e68c2181fb3 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -778,6 +778,7 @@ fi: administrator_description: Käyttäjät, joilla on tämä käyttöoikeus, ohittavat jokaisen käyttöoikeuden delete_user_data: Poistaa käyttäjän tiedot delete_user_data_description: Sallii käyttäjien poistaa muiden käyttäjien tiedot viipymättä + invite_bypass_approval: Kutsu käyttäjiä arvioimatta invite_users: Kutsua käyttäjiä invite_users_description: Sallii käyttäjien kutsua uusia käyttäjiä palvelimelle manage_announcements: Hallita tiedotteita @@ -1219,7 +1220,7 @@ fi: advanced_settings: Edistyneet asetukset animations_and_accessibility: Animaatiot ja saavutettavuus boosting_preferences: Tehostusasetukset - boosting_preferences_info_html: "Vihje: Asetuksista riippumatta Vaihto + napsautus %{icon} Tehosta-kuvakkeeseen tehostaa välittömästi." + boosting_preferences_info_html: "Vinkki: Asetuksista riippumatta Vaihto + napsautus %{icon} Tehosta-kuvakkeeseen tehostaa välittömästi." discovery: Löydettävyys localization: body: Mastodonin ovat kääntäneet vapaaehtoiset. @@ -1279,6 +1280,7 @@ fi: progress: confirm: Vahvista sähköpostiosoite details: Omat tietosi + list: Rekisteröitymisprosessi review: Arviomme rules: Hyväksy säännöt providers: @@ -1294,6 +1296,7 @@ fi: invited_by: 'Voit liittyä palvelimelle %{domain} kutsulla, jonka sait seuraavalta käyttäjältä:' preamble: Palvelimen %{domain} moderaattorit määrittävät ja valvovat sääntöjä. preamble_invited: Ennen kuin jatkat ota huomioon palvelimen %{domain} moderaattorien asettamat perussäännöt. + read_more: Lue lisää title: Joitakin perussääntöjä. title_invited: Sinut on kutsuttu. security: Turvallisuus @@ -1333,7 +1336,7 @@ fi: title: Tekijän nimeäminen challenge: confirm: Jatka - hint_html: "Vihje: Emme pyydä sinulta salasanaa uudelleen seuraavan tunnin aikana." + hint_html: "Vinkki: Emme pyydä sinulta salasanaa uudelleen seuraavan tunnin aikana." invalid_password: Väärä salasana prompt: Jatka vahvistalla salasanasi color_scheme: diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 5ccda09b4bf3f9..14e7e5a3d87940 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -781,6 +781,8 @@ fr-CA: administrator_description: Les utilisateur⋅rice⋅s ayant cette autorisation pourront contourner toutes les autorisations delete_user_data: Supprimer les données de l'utilisateur⋅rice delete_user_data_description: Permet aux utilisateur⋅rice⋅s de supprimer sans délai les données des autres utilisateur⋅rice⋅s + invite_bypass_approval: Inviter des utilisateurs sans vérifications + invite_bypass_approval_description: Permet aux personnes invitées sur le serveur par ces utilisateur·rice·s de s'inscrire sans être soumises à l'approbation de la modération invite_users: Inviter des utilisateur⋅rice⋅s invite_users_description: Permet aux utilisateur⋅rice⋅s d'inviter de nouvelles personnes sur le serveur manage_announcements: Gérer les annonces @@ -1282,6 +1284,7 @@ fr-CA: progress: confirm: Confirmation de l'adresse mail details: Vos infos + list: Progression de l'inscription review: Notre avis rules: Accepter les règles providers: @@ -1297,6 +1300,7 @@ fr-CA: invited_by: 'Vous pouvez rejoindre %{domain} grâve à l''invitation reçue de:' preamble: Celles-ci sont définies et appliqués par les modérateurs de %{domain}. preamble_invited: Avant de continuer, veuillez lire les règles de base définies par les modérateurs de %{domain}. + read_more: Lire plus title: Quelques règles de base. title_invited: Vous avez été invité·e. security: Sécurité diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f330733b082b5b..3407c4515b9a6f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -781,6 +781,8 @@ fr: administrator_description: Les utilisateur⋅rice⋅s ayant cette autorisation pourront contourner toutes les autorisations delete_user_data: Supprimer les données de l'utilisateur⋅rice delete_user_data_description: Permet aux utilisateur⋅rice⋅s de supprimer sans délai les données des autres utilisateur⋅rice⋅s + invite_bypass_approval: Inviter des utilisateurs sans vérifications + invite_bypass_approval_description: Permet aux personnes invitées sur le serveur par ces utilisateur·rice·s de s'inscrire sans être soumises à l'approbation de la modération invite_users: Inviter des utilisateur⋅rice⋅s invite_users_description: Permet aux utilisateur⋅rice⋅s d'inviter de nouvelles personnes sur le serveur manage_announcements: Gérer les annonces @@ -1282,6 +1284,7 @@ fr: progress: confirm: Confirmation de l'adresse mail details: Vos infos + list: Progression de l'inscription review: Notre avis rules: Accepter les règles providers: @@ -1297,6 +1300,7 @@ fr: invited_by: 'Vous pouvez rejoindre %{domain} grâce à l''invitation de :' preamble: Celles-ci sont définies et appliqués par les modérateurs de %{domain}. preamble_invited: Avant de continuer, veuillez lire les règles de base définies par les modérateurs de %{domain}. + read_more: Lire plus title: Quelques règles de base. title_invited: Vous avez été invité·e. security: Sécurité diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 1d6f2c68e483f7..1295a4efc52b1b 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -826,6 +826,8 @@ ga: administrator_description: Seachnóidh úsáideoirí a bhfuil an cead seo acu gach cead delete_user_data: Scrios Sonraí Úsáideora delete_user_data_description: Ligeann sé d'úsáideoirí sonraí úsáideoirí eile a scriosadh gan mhoill + invite_bypass_approval: Tabhair cuireadh d'úsáideoirí gan athbhreithniú + invite_bypass_approval_description: Ceadaíonn sé do dhaoine a bhfuil cuireadh tugtha dóibh chuig an bhfreastalaí ag na húsáideoirí seo ceadú modhnóireachta a sheachaint invite_users: Tabhair cuireadh d'Úsáideoirí invite_users_description: Ligeann sé d'úsáideoirí cuireadh a thabhairt do dhaoine nua chuig an bhfreastalaí manage_announcements: Bainistigh Fógraí @@ -1344,6 +1346,7 @@ ga: progress: confirm: Deimhnigh ríomhphost details: Do chuid sonraí + list: Dul chun cinn clárúcháin review: Ár léirmheas rules: Glac le rialacha providers: @@ -1359,6 +1362,7 @@ ga: invited_by: 'Is féidir leat páirt a ghlacadh i %{domain} a bhuíochas leis an gcuireadh a fuair tú ó:' preamble: Socraíonn agus cuireann na modhnóirí %{domain} iad seo i bhfeidhm. preamble_invited: Sula dtéann tú ar aghaidh, smaoinigh le do thoil ar na bunrialacha atá socraithe ag modhnóirí %{domain}. + read_more: Léigh tuilleadh title: Roinnt bunrialacha. title_invited: Tá cuireadh faighte agat. security: Slándáil diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 7c59072d8f98e3..469f7badd70c8a 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -778,6 +778,8 @@ gl: administrator_description: As usuarias con este permiso poderán superar calquera restrición delete_user_data: Eliminar datos de usuarias delete_user_data_description: Permite eliminar datos doutras usuarias sen demoras + invite_bypass_approval: Invitar sen precisar revisión + invite_bypass_approval_description: Permitir que as persoas invitadas ao servidor por estas usuarias non precisen aprobación invite_users: Convidar usuarias invite_users_description: Permite que outras usuarias conviden a xente ao servidor manage_announcements: Xestionar anuncios @@ -1279,6 +1281,7 @@ gl: progress: confirm: Confirmar correo details: Detalles + list: Progreso da creación da conta review: A nosa revisión rules: Aceptar regras providers: @@ -1294,6 +1297,7 @@ gl: invited_by: 'Podes unirte a %{domain} grazas ao convite que recibiches de parte de:' preamble: Son establecidas e aplicadas pola moderación de %{domain}. preamble_invited: Antes de continuar adica un minuto a ler as regras básicas establecidas para %{domain}. + read_more: Ler máis title: Algunhas regras básicas. title_invited: Convidáronte. security: Seguranza diff --git a/config/locales/he.yml b/config/locales/he.yml index 62bbecccd1a690..d18d44689da282 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -810,6 +810,8 @@ he: administrator_description: משתמשים עם הרשאה זו יוכלו לעקוף כל הרשאה delete_user_data: מחיקת כל נתוני המשתמש delete_user_data_description: מאפשר למשתמשים למחוק נתוני משתמשים אחרים ללא דיחוי + invite_bypass_approval: הזמנה להרשם ללא הליך בדיקה + invite_bypass_approval_description: להרשות למשתמשים אלו להזמין א.נשים להרשם לשרת תוך עקיפת מנגנון הפיקוח על ההרשמה invite_users: הזמנת משתמשים invite_users_description: מאפשר למשתמשים להזמין אנשים חדשים לשרת manage_announcements: ניהול הכרזות @@ -1321,6 +1323,7 @@ he: progress: confirm: אימות כתובת הדואל details: הפרטים שלך + list: התקדמות תהליך ההרשמה review: הבדיקה שלנו rules: הסכמה לתקנות providers: @@ -1336,6 +1339,7 @@ he: invited_by: 'ניתן להצטרף אל %{domain} הודות להזמנה מאת:' preamble: אלו נקבעים ונאכפים ע"י המנחים של %{domain}. preamble_invited: לפני ההמשך יש להתחשב בחוקי המקום כפי שקבעו מנהלי הדיון על %{domain}. + read_more: לקריאה נוספת title: כמה חוקים בסיסיים. title_invited: קיבלת הזמנה. security: אבטחה diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 27532f9f6806b2..8bce2ee5297803 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1279,6 +1279,7 @@ hu: progress: confirm: E-mail megerősítése details: Saját adatok + list: Regisztrációs folyamat review: A felülvizsgálatunk rules: Szabályok elfogadása providers: @@ -1294,6 +1295,7 @@ hu: invited_by: 'Csatlakozhatsz a %{domain} kiszolgálóhoz, köszönhetően a meghívónak, melyet tőle kaptál:' preamble: Ezeket a(z) %{domain} moderátorai adjak meg és tartatják be. preamble_invited: Mielőtt csatlakozol, kérlek vedd fontolóra a %{domain} moderátorai által állított szabályokat. + read_more: Bővebben title: Néhány alapszabály. title_invited: Meghívtak. security: Biztonság diff --git a/config/locales/is.yml b/config/locales/is.yml index d173cc31c78951..1ea18a39d4ba16 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -778,6 +778,8 @@ is: administrator_description: Notendur með þessa heimild fara framhjá öllum öðrum heimildum delete_user_data: Eyða gögnum notanda delete_user_data_description: Leyfir notendum að eyða gögnum annarra notenda án tafar + invite_bypass_approval: Bjóða notendum án samþykktar + invite_bypass_approval_description: Leyfir fólki sem boðið er á netþjóninn af þessum notendum að sleppa við samþykki umsjónaraðila invite_users: Bjóða notendum invite_users_description: Leyfir notendum að bjóða nýju fólki inn á netþjóninn manage_announcements: Sýsla með tilkynningar @@ -1283,6 +1285,7 @@ is: progress: confirm: Staðfesta tölvupóstfang details: Nánari upplýsingar þínar + list: Nýskráningarferli review: Yfirferð okkar rules: Samþykkja reglur providers: @@ -1298,6 +1301,7 @@ is: invited_by: 'Þú getur tekið þátt í %{domain} þökk sé boði sem þú fékkst frá:' preamble: Þær eru settar og þeim framfylgt af umsjónarmönnum %{domain}. preamble_invited: Áður en haldið er lengra, vinsamlegast kynntu þér reglurnar sem stjórnendur %{domain} hafa sett. + read_more: Lesa meira title: Nokkrar grunnreglur. title_invited: Þér hefur verið boðið. security: Öryggi diff --git a/config/locales/it.yml b/config/locales/it.yml index 09dfb6a3493397..e01bed8b978a2f 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -778,6 +778,8 @@ it: administrator_description: Gli utenti con questo permesso saranno esentati da ogni permesso delete_user_data: Cancella dati utente delete_user_data_description: Consente agli utenti di eliminare subito i dati degli altri utenti + invite_bypass_approval: Invita utenti senza revisione + invite_bypass_approval_description: Permette alle persone invitate al server da questi utenti, di aggirare l'approvazione della moderazione invite_users: Invita Utenti invite_users_description: Consente agli utenti di invitare nuove persone su questo server manage_announcements: Gestisci Annunci @@ -838,7 +840,7 @@ it: rules_hint: C'è un'area dedicata per le regole che i tuoi utenti dovrebbero rispettare. title: Info allow_referrer_origin: - desc: Quando i tuoi utenti cliccano su link a siti esterni, il loro browser potrebbe inviare l'indirizzo del tuo server Mastodon come referente. Disattiva questa opzione se ciò identificherebbe in modo univoco i tuoi utenti, ad esempio se si tratta di un server Mastodon personale. + desc: Quando i tuoi utenti cliccano su link a siti esterni, il loro browser potrebbe inviare l'indirizzo del tuo server Mastodon come referente. Disattiva questa opzione se ciò identificherebbe in modo univoco i tuoi utenti, per esempio, se si tratta di un server Mastodon personale. title: Consenti ai siti esterni di vedere il tuo server Mastodon come fonte di traffico appearance: preamble: Personalizza l'interfaccia web di Mastodon. @@ -1279,6 +1281,7 @@ it: progress: confirm: Conferma l'e-mail details: I tuoi dettagli + list: Stato della registrazione review: La nostra revisione rules: Accetta le regole providers: @@ -1294,6 +1297,7 @@ it: invited_by: 'Puoi unirti a %{domain} grazie all''invito che hai ricevuto da:' preamble: Questi sono impostati e applicati dai moderatori di %{domain}. preamble_invited: Prima di procedere, si prega di considerare le regole di base stabilite dai moderatori di %{domain}. + read_more: Scopri di più title: Alcune regole di base. title_invited: Sei stato/a invitato/a. security: Credenziali diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 40034e1cfed22f..b807451ece13e9 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -587,6 +587,7 @@ lt: manage_taxonomies_description: Leidžia naudotojams peržiūrėti tendencingą turinį ir atnaujinti grotažymių nustatymus settings: branding: + preamble: Jūsų serverio pavadinimas išskiria jį iš kitų tinklo serverių. Ši informacija gali būti rodoma įvairiose aplinkose, pavyzdžiui, „Mastodon“ žiniatinklio sąsajoje, programėlėse, nuorodų peržiūrose kitose svetainėse, žinučių programėlėse ir pan. Dėl to geriausia, kad ši informacija būtų aiški, trumpa ir glausta. title: Firminio ženklo kūrimas captcha_enabled: desc_html: Tai priklauso nuo hCaptcha išorinių skriptų, kurie gali kelti susirūpinimą dėl saugumo ir privatumo. Be to, dėl to registracijos procesas kai kuriems žmonėms (ypač neįgaliesiems) gali būti gerokai sunkiau prieinami. Dėl šių priežasčių apsvarstyk alternatyvias priemones, pavyzdžiui, patvirtinimu arba kvietimu grindžiamą registraciją. diff --git a/config/locales/nan-TW.yml b/config/locales/nan-TW.yml index 3b89a0270745c8..dc471a9c2c3467 100644 --- a/config/locales/nan-TW.yml +++ b/config/locales/nan-TW.yml @@ -762,6 +762,8 @@ nan-TW: administrator_description: 有tsit ê權限ê用者ē忽略所有ê權限。 delete_user_data: Thâi掉用者ê資料 delete_user_data_description: 允准用者liâm-mi thâi掉其他用者ê資料 + invite_bypass_approval: 邀請iáu bē審查ê用者 + invite_bypass_approval_description: 允准予tsiah ê用者邀請kàu tsit臺服侍器ê lâng làng過管理審查許可 invite_users: 邀請用者 invite_users_description: 允准用者邀請新lâng來tsit ê服侍器 manage_announcements: 管理公告 @@ -1238,7 +1240,7 @@ nan-TW: welcome_title: 歡迎 %{name}! wrong_email_hint: Nā是電子phue ê地址無正確,lí ē當tī口座設定kā改。 delete_account: Thâi掉口座 - delete_account_html: Nā lí behthâi掉lí ê口座,lí ē當ji̍h tsia繼續。Lí著確認動作。 + delete_account_html: Nā lí beh thâi掉lí ê口座,lí ē當ji̍h tsia繼續。Lí著確認動作。 description: prefix_invited_by_user: "@%{name} 邀請lí加入tsit ê Mastodon 服侍器!" prefix_sign_up: Tsit-má註冊Mastodon ê口座! @@ -1258,6 +1260,7 @@ nan-TW: progress: confirm: 確認電子phue details: Lí ê詳細 + list: 註冊流程 review: Lán ê審查 rules: 接受規則 providers: @@ -1273,6 +1276,7 @@ nan-TW: invited_by: Lí通用有tuì hia收著ê邀請加入 %{domain}: preamble: Tsiah-ê hōo %{domain} ê管理員設定kap實施。 preamble_invited: 佇lí繼續進前,請思考 %{domain} ê管理員設立ê基本規則。 + read_more: 讀詳細 title: Tsi̍t-kuá基本規定。 title_invited: Lí受邀請ah。 security: 安全 @@ -1832,6 +1836,7 @@ nan-TW: nokia: Nokia S40 Ovi 瀏覽器 opera: Opera otter: Otter + phantom_js: PhantomJS qq: QQ 瀏覽器 safari: Safari uc_browser: UC 瀏覽器 @@ -1884,9 +1889,57 @@ nan-TW: strikes: 管理ê警告 two_factor_authentication: 雙因素驗證 webauthn_authentication: 安全鎖匙 + severed_relationships: + download: 載落去(%{count} 份) + event_type: + account_suspension: 中止權限ê口座(%{target_name}) + domain_block: 中止權限ê服侍器(%{target_name}) + user_domain_block: Lí kā %{target_name} 封鎖ah + lost_followers: 失去ê 跟tuè lí ê + lost_follows: 失去ê lí 跟tuè ê + preamble: Lí封鎖域名,á是lí ê管理員決定beh kā別ê服侍器停止權限ê時,Lí可能會失去跟tuè lí ê,á是lí跟tuè ê。若發生,lí會當kā中止聯絡ê ê列單載落去,來檢視或者有可能佇別站kā輸入。 + purged: 關係tsit台服侍器有予lí ê服侍器管理員清掉ê資訊。 + type: 事件 statuses: + attached: + audio: + other: "%{count} 段聲音" + description: 附件: %{attached} + image: + other: "%{count} 幅圖" + video: + other: "%{count} 段影片" + boosted_from_html: 對 %{acct_link} 轉送 + content_warning: 內容警告: %{warning} + content_warnings: + hide: Am-khàm PO文 + show: 顯示其他ê內容 default_language: Kap界面ê語言sio kâng + disallowed_hashtags: + other: 包含bē用得ê標籤: %{tags} + edited_at_html: 佇 %{date} 編輯 + errors: + in_reply_not_found: Lí試應ê PO文假若無佇leh。 + quoted_status_not_found: Lí試引用ê PO文假若無佇leh。 + quoted_user_not_mentioned: Bē當佇私人ê提起引用無提起ê用者。 + over_character_limit: 超過 %{max} 字ê限制ah + pin_errors: + direct: Kan-ta受提起ê用者tsiah通看ê PO文bē當釘 + limit: Lí已經kā PO文釘kàu盡磅ah。 + ownership: Bē當釘別lâng êPO文 + reblog: 轉送ê PO文 bē當釘 + quote_error: + not_available: PO文bē當看 + pending_approval: PO文當leh等審查 + revoked: PO文已經hōo作者thâi掉 + quote_policies: + followers: Kan-ta hōo跟tuè ê lâng + nobody: Kan-ta我 + public: Ta̍k ê lâng + quote_post_author: 引用 %{acct} ê PO文ah + title: "%{name}:「%{quote}」" visibilities: + direct: 私人ê提起 private: Kan-ta hōo跟tuè ê lâng public: 公開ê public_long: 逐ê lâng(無論佇Mastodon以內á是以外) @@ -1902,6 +1955,38 @@ nan-TW: interaction_exceptions: Tshāi佇互動ê特例 interaction_exceptions_explanation: 超過收藏kap轉送ê底限ê PO文可能會受保留,就算in後來降落。 keep_direct: 保留私人ê短phue + keep_direct_hint: Bē thâi掉任何lí ê私人ê提起 + keep_media: 保留有媒體附件ê PO文 + keep_media_hint: Bē thâi掉lí ê任何有媒體附件ê PO文 + keep_pinned: 保留所釘ê PO文 + keep_pinned_hint: Bē thâi掉任何lí所釘ê PO文 + keep_polls: 保留投票 + keep_polls_hint: Bē thâi掉任何lí ê投票 + keep_self_bookmark: 保留lí加冊籤ê PO文 + keep_self_bookmark_hint: Bē thâi掉lí家kī有加冊籤ê PO文 + keep_self_fav: 保留lí收藏ê PO文 + keep_self_fav_hint: Bē thâi掉lí家kī有收藏ê PO文 + min_age: + '1209600': 2 禮拜 + '15778476': 6 個月 + '2629746': 1 個月 + '31556952': 1 年 + '5259492': 2 個月 + '604800': 1 禮拜 + '63113904': 2 年 + '7889238': 3 個月 + min_age_label: 保持PO文ê期間 + min_favs: 保留超過下kha ê收藏數ê PO文 + min_favs_hint: 若是lí ê PO文有kàu tsit ê收藏數,就bē受thâi掉。留空白表示毋管收藏數,PO文lóng thâi掉 + min_reblogs: 保留超過下kha ê轉送數ê PO文 + min_reblogs_hint: 若是lí ê PO文有kàu tsit ê轉送數,就bē受thâi掉。留空白表示毋管轉送數,PO文lóng thâi掉 + stream_entries: + sensitive_content: 敏感ê內容 + strikes: + errors: + too_late: Lí bē赴申訴tsit ê處份ah + tags: + does_not_match_previous_name: kap進前ê名無kâng terms_of_service: title: 服務規定 themes: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index ffa8a49459b6b1..69ff56febdc2bf 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -778,6 +778,8 @@ nl: administrator_description: Deze gebruikers hebben volledige rechten en kun dus overal bij delete_user_data: Gebruikersgegevens verwijderen delete_user_data_description: Staat gebruikers toe om de gegevens van andere gebruikers zonder vertraging te verwijderen + invite_bypass_approval: Gebruikers uitnodigen zonder beoordeling + invite_bypass_approval_description: Staat het toe dat mensen die door deze gebruikers voor deze server zijn uitgenodigd, niet door moderatoren beoordeeld hoeven te worden invite_users: Gebruikers uitnodigen invite_users_description: Staat gebruikers toe om nieuwe mensen voor de server uit te nodigen manage_announcements: Aankondigingen beheren @@ -1279,6 +1281,7 @@ nl: progress: confirm: E-mailadres bevestigen details: Jouw gegevens + list: Voortgang registratie review: Onze beoordeling rules: Regels accepteren providers: @@ -1294,6 +1297,7 @@ nl: invited_by: 'Je kunt je registreren op %{domain} dankzij de uitnodiging die je hebt ontvangen van:' preamble: Deze zijn vastgesteld en worden gehandhaafd door de moderatoren van %{domain}. preamble_invited: Voordat je verder gaat, moet je eerst kennisnemen van de basisregels die door de moderatoren van %{domain} zijn vastgesteld. + read_more: Meer lezen title: Enkele basisregels. title_invited: Je bent uitgenodigd. security: Beveiliging diff --git a/config/locales/nn.yml b/config/locales/nn.yml index a1054649620bcd..f59ba333346ef0 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1279,6 +1279,7 @@ nn: progress: confirm: Stadfest e-post details: Opplysingane dine + list: Innmeldingsprosess review: Vår gjennomgang rules: Godta reglane providers: @@ -1294,6 +1295,7 @@ nn: invited_by: 'Du kan bli med i %{domain} takka vere invitasjonen du har fått frå:' preamble: Disse angis og håndheves av %{domain}-moderatorene. preamble_invited: Før du held fram, ver snill og sjå gjennom reglane bestemt av moderatorane av %{domain}. + read_more: Les meir title: Nokre grunnreglar. title_invited: Du har blitt invitert. security: Tryggleik diff --git a/config/locales/no.yml b/config/locales/no.yml index a143940462a2a0..f6dd1e9c8de88e 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -44,9 +44,11 @@ title: Endre E-postadressen til %{username} change_role: changed_msg: Rollen ble endret! + edit_roles: Administrer brukerroller label: Endre rolle no_role: Ingen rolle title: Endre rolle for %{username} + collections: Samlinger confirm: Bekreft confirmed: Bekreftet confirming: Bekrefte @@ -128,6 +130,7 @@ resubscribe: Abonner på nytt role: Rolle search: Søk + search_same_email_domain: Andre brukere med det samme epost domenet search_same_ip: Andre brukere med den samme IP-en security: Sikkerhet security_measures: @@ -168,6 +171,7 @@ approve_appeal: Godkjenn anke approve_user: Godkjenn bruker assigned_to_self_report: Tilordne rapport + change_email_user: Endre brukerens e-postadresse change_role_user: Endre rolle for brukeren confirm_user: Bekreft brukeren create_account_warning: Opprett en advarsel @@ -275,6 +279,7 @@ filter_by_user: Sorter etter bruker title: Revisionslogg announcements: + back: Tilbake til kunngjøringer destroyed_msg: Kunngjøringen er slettet! edit: title: Rediger kunngjøring @@ -291,6 +296,9 @@ unpublish: Avpubliser unpublished_msg: Kunngjøring upublisert! updated_msg: Kunngjøringen er oppdatert! + collections: + accounts: Kontoer + collection_title: Samling av %{name} critical_update_pending: Kritisk oppdatering avventer custom_emojis: assign_category: Tilegn kategori diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index a52cee4a701d94..e8789d4236f20b 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -267,6 +267,7 @@ pt-BR: demote_user_html: "%{name} rebaixou o usuário %{target}" destroy_announcement_html: "%{name} excluiu o anúncio %{target}" destroy_canonical_email_block_html: "%{name} desbloqueou o endereço e-mail com o hash %{target}" + destroy_collection_html: "%{name} removeu coleção de %{target}" destroy_custom_emoji_html: "%{name} apagou o emoji %{target}" destroy_domain_allow_html: "%{name} bloqueou federação com domínio %{target}" destroy_domain_block_html: "%{name} desbloqueou o domínio %{target}" @@ -306,6 +307,7 @@ pt-BR: unsilence_account_html: "%{name} removeu a limitação da conta de %{target}" unsuspend_account_html: "%{name} removeu a suspenção da conta de %{target}" update_announcement_html: "%{name} atualizou o anúncio %{target}" + update_collection_html: "%{name} atualizou a coleção de %{target}" update_custom_emoji_html: "%{name} atualizou o emoji %{target}" update_domain_block_html: "%{name} atualizou o bloqueio de domínio de %{target}" update_ip_block_html: "%{name} alterou a regra para o IP %{target}" diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index b85b5fbd1829e8..c388a0a48e2e92 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -347,6 +347,9 @@ pt-PT: accounts: Contas collection_title: Colecionado por %{name} contents: Conteúdo + number_of_accounts: + one: 1 conta + other: "%{count} contas" open: Abrir view_publicly: Ver publicamente critical_update_pending: Atualização crítica pendente diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml index fc6cbcaf51d6ac..e4ee1062ba44ed 100644 --- a/config/locales/simple_form.be.yml +++ b/config/locales/simple_form.be.yml @@ -40,14 +40,14 @@ be: text: Вы можаце абскардзіць рашэнне толькі адзін раз defaults: autofollow: Людзі, якія зарэгістраваліся праз запрашэнне, аўтаматычна падпішуцца на вас - avatar: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions}} пікселяў + avatar: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions} пікселяў bot: Паведаміць іншым, што гэты ўліковы запіс у асноўным выконвае аўтаматычныя дзеянні і можа не кантралявацца context: Адзін ці некалькі кантэкстаў, да якіх трэба прымяніць фільтр current_password: У мэтах бяспекі, увядзіце пароль бягучага ўліковага запісу current_username: Каб пацвердзіць, увядзіце, калі ласка імя карыстальніка бягучага ўліковага запісу digest: Будзе даслана толькі пасля доўгага перыяду неактыўнасці і толькі, калі Вы атрымалі асабістыя паведамленні падчас Вашай адсутнасці email: Пацвярджэнне будзе выслана па электроннай пошце - header: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions}} пікселяў + header: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions} пікселяў inbox_url: Капіраваць URL са старонкі рэтранслятара, якім вы хочаце карыстацца irreversible: Адфільтраваныя пасты прападуць незваротна, нават калі фільтр потым будзе выдалены locale: Мова карыстальніцкага інтэрфейсу, электронных паведамленняў і апавяшчэнняў diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 1c85042bc016c6..589ad5da470368 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -90,7 +90,7 @@ de: backups_retention_period: Nutzer*innen haben die Möglichkeit, Archive ihrer Beiträge zu erstellen, die sie später herunterladen können. Wenn ein positiver Wert gesetzt ist, werden diese Archive nach der festgelegten Anzahl von Tagen automatisch aus deinem Speicher gelöscht. bootstrap_timeline_accounts: Diese Konten werden neu registrierten Profilen empfohlen und in der Empfehlungsliste oben angeheftet. Gib eine durch Kommata getrennte Liste von Konten an. closed_registrations_message: Wird angezeigt, wenn Registrierungen deaktiviert sind - content_cache_retention_period: Sämtliche Beiträge von anderen Servern (einschließlich geteilte Beiträge und Antworten) werden, unabhängig von der Interaktion der lokalen Nutzer*innen mit diesen Beiträgen, nach der festgelegten Anzahl von Tagen gelöscht. Das betrifft auch Beiträge, die von lokalen Nutzer*innen favorisiert oder als Lesezeichen gespeichert wurden. Private Erwähnungen zwischen Nutzer*innen von verschiedenen Servern werden ebenfalls verloren gehen und können nicht wiederhergestellt werden. Diese Option richtet sich ausschließlich an Server mit speziellen Zwecken und wird die allgemeine Nutzungserfahrung beeinträchtigen, wenn sie für den allgemeinen Gebrauch aktiviert ist. + content_cache_retention_period: Sämtliche Beiträge von anderen Servern (einschließlich geteilte Beiträge und Antworten) werden, unabhängig von der Interaktion der lokalen Nutzer*innen mit diesen Beiträgen, nach der festgelegten Anzahl von Tagen gelöscht. Davon sind auch Beiträge betroffen, die von lokalen Nutzer*innen favorisiert oder als Lesezeichen gespeichert wurden. Private Erwähnungen zwischen Nutzer*innen von verschiedenen Servern werden ebenfalls verloren gehen und können nicht wiederhergestellt werden. Diese Option richtet sich ausschließlich an Server mit speziellen Zwecken und wird die allgemeine Nutzungserfahrung beeinträchtigen, wenn sie für den allgemeinen Gebrauch aktiviert ist. custom_css: Du kannst eigene Stylesheets für das Webinterface von Mastodon verwenden. favicon: WebP, PNG, GIF oder JPG. Überschreibt das Standard-Mastodon-Favicon mit einem eigenen Favicon. landing_page: Legt fest, welchen Bereich (nicht angemeldete) Besucher*innen sehen, wenn sie deinen Server besuchen. Für „Trends“ müssen die Trends in den Entdecken-Einstellungen aktiviert sein. Für „Lokaler Feed“ muss „Zugriff auf Live-Feeds, die lokale Beiträge beinhalten“ in den Entdecken-Einstellungen auf „Alle“ gesetzt werden. @@ -137,7 +137,7 @@ de: indexable: Deine Profilseite kann in Suchergebnissen auf Google, Bing und anderen erscheinen. show_application: Du wirst immer sehen können, über welche App dein Beitrag veröffentlicht wurde. tag: - name: Du kannst nur die Groß- und Kleinschreibung der Buchstaben ändern, um es z. B. lesbarer zu machen + name: Du kannst nur die Groß- und Kleinschreibung der Buchstaben ändern, um z. B. die Lesbarkeit zu verbessern terms_of_service: changelog: Kann mit der Markdown-Syntax formatiert werden. effective_date: Ein angemessener Zeitraum liegt zwischen 10 und 30 Tagen, nachdem deine Nutzer*innen benachrichtigt wurden. diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index d787d2bbc37f33..1477f1e2b15572 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -41,7 +41,7 @@ el: defaults: autofollow: Όσοι εγγραφούν μέσω της πρόσκλησης θα σε ακολουθούν αυτόματα avatar: WEBP, PNG, GIF ή JPG. Το πολύ %{size}. Θα υποβαθμιστεί σε %{dimensions}px - bot: Ο λογαριασμός αυτός εκτελεί κυρίως αυτοματοποιημένες ενέργειες και ίσως να μην παρακολουθείται + bot: Υποδεικνύει σε άλλους χρήστες ότι ο λογαριασμός αυτός εκτελεί κυρίως αυτοματοποιημένες ενέργειες και ίσως να μην παρακολουθείται context: Ένα ή περισσότερα πλαίσια στα οποία μπορεί να εφαρμόζεται αυτό το φίλτρο current_password: Για λόγους ασφαλείας παρακαλώ γράψε τον κωδικό του τρέχοντος λογαριασμού current_username: Για επιβεβαίωση, παρακαλώ γράψε το όνομα χρήστη του τρέχοντος λογαριασμού @@ -91,7 +91,7 @@ el: bootstrap_timeline_accounts: Αυτοί οι λογαριασμοί θα καρφιτσωθούν στην κορυφή των προτεινόμενων ακολουθήσεων για νέους χρήστες. Παρέχετε μια λίστα λογαριασμών χωρισμένη με κόμμα. closed_registrations_message: Εμφανίζεται όταν κλείνουν οι εγγραφές content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές επισημάνσεις μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. - custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστοσελίδας του Mastodon. + custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστού του Mastodon. favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. landing_page: Επιλέγει ποια σελίδα βλέπουν οι νέοι επισκέπτες όταν φτάνουν για πρώτη φορά στο διακομιστή σας. Αν επιλέξετε "Τάσεις", τότε οι τάσεις πρέπει να είναι ενεργοποιημένες στις Ρυθμίσεις Ανακάλυψης. Αν επιλέξετε "Τοπική ροή", τότε το "Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις" πρέπει να οριστεί σε "Όλοι" στις Ρυθμίσεις Ανακάλυψης. mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού. @@ -236,7 +236,7 @@ el: otp_attempt: Κωδικός δυο παραγόντων password: Συνθηματικό phrase: Λέξη-κλειδί ή φράση - setting_advanced_layout: Ενεργοποίηση προηγμένης λειτουργίας χρήσης + setting_advanced_layout: Ενεργοποίηση προηγμένης διεπαφής ιστού setting_aggregate_reblogs: Ομαδοποίηση προωθήσεων στις ροές setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index aad88989241524..da17198daa388f 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -4,7 +4,7 @@ eo: hints: account: attribution_domains: Unu por linio. Protektas kontraŭ falsaj atribuoj. - discoverable: Viaj publikaj afiŝoj kaj profilo povas esti prezentitaj aŭ rekomenditaj en diversaj lokoj de Mastodon kaj via profilo povas esti proponita al aliaj uzantoj. + discoverable: Viaj publikaj afiŝoj kaj profilo povas esti elstarigitaj aŭ rekomenditaj en diversaj lokoj de Mastodon kaj via profilo povas esti proponita al aliaj uzantoj. display_name: Via plena nomo aŭ via kromnomo. fields: Via retpaĝo, pronomoj, aĝo, ĉio, kion vi volas. indexable: Viaj publikaj afiŝoj povas aperi en serĉrezultoj ĉe Mastodon. Homoj, kiuj interagis kun viaj afiŝoj, eble povos serĉi ilin sendepende. diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index 5a70754484b0f3..62961d7817d95b 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -31,13 +31,13 @@ es-AR: suspend: Evitá cualquier interacción desde o hacia esta cuenta y eliminá su contenido. Revertible en 30 días. Esto cierra todas las denuncias contra esta cuenta. warning_preset_id: Opcional. Todavía podés agregar texto personalizado al final del preajuste announcement: - all_day: Cuando esté seleccionado, sólo se mostrarán las fechas del rango de tiempo + all_day: Cuando esté seleccionado, solo se mostrarán las fechas del rango de tiempo ends_at: Opcional. El anuncio desaparecerá automáticamente en este momento scheduled_at: Dejar en blanco para publicar el anuncio inmediatamente starts_at: Opcional. En caso de que tu anuncio esté vinculado a un rango de tiempo específico text: Podés usar sintaxis de mensajes. Por favor, tené en cuenta el espacio que ocupará el anuncio en la pantalla del usuario appeal: - text: Sólo podés apelar un incumplimiento una vez + text: Solo podés apelar un incumplimiento una vez defaults: autofollow: Los usuarios que se registren mediante la invitación te seguirán automáticamente avatar: WEBP, PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px @@ -45,7 +45,7 @@ es-AR: context: Uno o múltiples contextos en los que debe aplicarse el filtro current_password: Por razones de seguridad, por favor, ingresá la contraseña de la cuenta actual current_username: Para confirmar, por favor, ingresá el nombre de usuario de la cuenta actual - digest: Sólo enviado tras un largo periodo de inactividad, y sólo si recibiste mensajes personales en tu ausencia + digest: Solo enviado tras un largo periodo de inactividad, y solo si recibiste mensajes personales en tu ausencia email: Se te enviará un correo electrónico de confirmación header: WEBP, PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px inbox_url: Copiá la dirección web desde la página principal del relé que querés usar @@ -55,7 +55,7 @@ es-AR: phrase: Se aplicará sin importar las mayúsculas o las advertencias de contenido de un mensaje scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionás el alcance de nivel más alto, no necesitás seleccionar las individuales. setting_advanced_layout: Mostrar Mastodon como una disposición de varias columnas, permitiéndote ver la línea temporal, las notificaciones y una tercera columna de tu elección. No recomendado para pantallas pequeñas. - setting_aggregate_reblogs: No mostrar nuevas adhesiones de los mensajes que fueron recientemente adheridos (sólo afecta a las adhesiones recibidas recientemente) + setting_aggregate_reblogs: No mostrar nuevas adhesiones de los mensajes que fueron recientemente adheridos (solo afecta a las adhesiones recibidas recientemente) setting_always_send_emails: Normalmente las notificaciones por correo electrónico no se enviarán cuando estés usando Mastodon activamente setting_boost_modal: Al estar activado, la adhesión abrirá primero un diálogo de confirmación en el que podés cambiar su visibilidad. setting_default_quote_policy_private: Los mensajes solo para seguidores redactados en Mastodon no pueden ser citados por otras cuentas. @@ -70,7 +70,7 @@ es-AR: setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles setting_use_pending_items: Ocultar actualizaciones de la línea temporal detrás de un clic en lugar de desplazar automáticamente el flujo username: Podés usar letras, números y subguiones ("_") - whole_word: Cuando la palabra clave o frase es sólo alfanumérica, sólo será aplicado si coincide con toda la palabra + whole_word: Cuando la palabra clave o frase sea solo alfanumérica, solamente será aplicado si coincide con toda la palabra domain_allow: domain: Este dominio podrá recolectar datos de este servidor, y los datos entrantes serán procesados y archivados email_domain_block: @@ -97,7 +97,7 @@ es-AR: mascot: Reemplaza la ilustración en la interface web avanzada. media_cache_retention_period: Los archivos de medios de mensajes publicados por usuarios remotos se almacenan en la memoria caché en tu servidor. Cuando se establece un valor positivo, los medios se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si es que el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlace consultan a sitios web de terceros, se recomienda establecer este valor a, al menos, 14 días, o las tarjetas de previsualización de enlaces no se actualizarán a pedido antes de ese momento. min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro - peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Acá no se incluye ningún dato sobre si federás con un servidor determinado, sólo que tu servidor lo conoce. Esto es usado por los servicios que recopilan estadísticas sobre la federación en un sentido general. + peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Acá no se incluye ningún dato sobre si federás con un servidor determinado, solo que tu servidor lo conoce. Esto es usado por los servicios que recopilan estadísticas sobre la federación en un sentido general. profile_directory: El directorio de perfiles lista a todos los usuarios que han optado a que su cuenta pueda ser descubierta. require_invite_text: Cuando registros aprobación manual, hacé que la solicitud de invitación "¿Por qué querés unirte?" sea obligatoria, en vez de opcional site_contact_email: Cómo la gente puede estar en contacto con vos para consultas legales o de ayuda. @@ -137,7 +137,7 @@ es-AR: indexable: Tu página de perfil podría aparecer en los resultados de búsqueda en Google, Bing y otros motores de búsqueda. show_application: Sin embargo, siempre podrás ver desde qué aplicación se envió tu mensaje. tag: - name: Sólo podés cambiar la capitalización de las letras, por ejemplo, para que sea más legible + name: Solo podés cambiar la capitalización de las letras, por ejemplo, para que sea más legible terms_of_service: changelog: Se puede estructurar con sintaxis Markdown. effective_date: Un plazo razonable puede oscilar entre 10 y 30 días a partir de la fecha de notificación a tus usuarios. @@ -153,7 +153,7 @@ es-AR: jurisdiction: Listá el país donde vive quien paga las facturas. Si es una empresa u otra entidad, enumerá el país donde está basada y la ciudad, región, territorio o provincia/estado, según corresponda. min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción. user: - chosen_languages: Cuando estén marcados, sólo se mostrarán los mensajes en los idiomas seleccionados en las líneas temporales públicas + chosen_languages: Cuando estén marcados, solo se mostrarán los mensajes en los idiomas seleccionados en las líneas temporales públicas date_of_birth: one: Tenemos que asegurarnos de que al menos tenés %{count} años de edad para usar %{domain}. No almacenaremos esta información. other: Tenemos que asegurarnos de que al menos tenés %{count} años de edad para usar %{domain}. No almacenaremos esta información. @@ -163,7 +163,7 @@ es-AR: highlighted: Esto hace que el rol sea públicamente visible name: Nombre público del rol, si el rol se establece para que se muestre como una insignia permissions_as_keys: Los usuarios con este rol tendrán acceso a… - position: Un rol más alto decide la resolución de conflictos en ciertas situaciones. Ciertas acciones sólo pueden llevarse a cabo en roles con prioridad inferior + position: Un rol más alto decide la resolución de conflictos en ciertas situaciones. Ciertas acciones solo pueden llevarse a cabo en roles con prioridad inferior require_2fa: Los usuarios con este rol serán requeridos para configurar la autenticación de dos factores para usar Mastodon username_block: allow_with_approval: En lugar de impedir el registro total, los registros coincidentes requerirán tu aprobación diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index 362949480c7acb..c110d76ee66c66 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -77,7 +77,7 @@ es-MX: domain: Este puede ser el nombre de dominio que se muestra en la dirección de correo o el registro MX que utiliza. Se comprobarán al registrarse. with_dns_records: Se hará un intento de resolver los registros DNS del dominio dado y los resultados serán también puestos en lista negra featured_tag: - name: 'Aquí están algunas de las etiquetas que más has usado recientemente:' + name: 'Estas son algunas de las etiquetas que más has utilizado recientemente:' filters: action: Elige qué acción realizar cuando una publicación coincida con el filtro actions: diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 9854ef31a77c82..1187ffdf0593bb 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -257,7 +257,7 @@ fi: setting_emoji_style: Emojityyli setting_expand_spoilers: Laajenna aina sisältövaroituksilla merkityt julkaisut setting_hide_network: Piilota verkostotietosi - setting_missing_alt_text_modal: Varoita ennen kuin julkaisen mediaa ilman vaihtoehtoista tekstiä + setting_missing_alt_text_modal: Varoita ennen kuin julkaisen mediaa ilman tekstivastinetta setting_quick_boosting: Ota nopea tehostus käyttöön setting_reduce_motion: Vähennä animaatioiden liikettä setting_system_font_ui: Käytä järjestelmän oletusfonttia diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 9daddea9c42c1d..caf915df780a4f 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -773,6 +773,8 @@ sq: administrator_description: Përdoruesit me këtë leje do të anashkalojnë çdo leje delete_user_data: Të Fshijë të Dhëna Përdoruesi delete_user_data_description: U lejon përdoruesve të fshijnë pa humbur kohë të dhëna përdoruesish të tjerë + invite_bypass_approval: Ftoni Përdorues pa shqyrtim + invite_bypass_approval_description: U lejon personave të ftuar te shërbyesi nga këta përdorues të anashkalojnë miratim nga moderimi invite_users: Të Ftojë Përdorues invite_users_description: U lejon përdoruesve të ftojë te shërbyesi persona të rinj manage_announcements: Të Administrojë Njoftime @@ -1268,6 +1270,7 @@ sq: progress: confirm: Ripohoni email-in details: Hollësitë tuaja + list: Ecuri regjistrimi review: Shqyrtimi ynë rules: Pranoni rregulla providers: @@ -1283,6 +1286,7 @@ sq: invited_by: 'Mund të bëheni pjesë e %{domain} falë ftesës që morët prej:' preamble: Këto vendosen dhe zbatimi i tyre është nën kujdesin e moderatorëve të %{domain}. preamble_invited: Para se të vazhdoni më tej, ju lutemi, shihni rregullat bazë të vendosura nga moderatorët e %{domain}. + read_more: Lexoni më tepër title: Disa rregulla bazë. title_invited: Jeni ftuar. security: Siguri diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 72a8b4cdbb6c8e..d1b61289dc2eea 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -775,6 +775,8 @@ sv: administrator_description: Användare med denna behörighet kommer att kringgå alla behörigheter delete_user_data: Ta bort användardata delete_user_data_description: Tillåter användare att omedelbart radera andra användares data + invite_bypass_approval: Bjud in användare utan granskning + invite_bypass_approval_description: Tillåter personer som är inbjudna till servern av dessa användare att kringgå moderationsgodkännande invite_users: Bjud in användare invite_users_description: Tillåter användare att bjuda in nya personer till servern manage_announcements: Hantera kungörelser @@ -1291,6 +1293,7 @@ sv: invited_by: 'Du kan gå med i %{domain} tack vare den inbjudan du har fått från:' preamble: Dessa bestäms och upprätthålls av moderatorerna för %{domain}. preamble_invited: Innan du fortsätter bör du överväga grundreglerna som fastställts av moderatorerna för %{domain}. + read_more: Läs mer title: Några grundregler. title_invited: Du har blivit inbjuden. security: Säkerhet diff --git a/config/locales/th.yml b/config/locales/th.yml index e91c7c8fce3b36..b3e24132355747 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -9,6 +9,8 @@ th: accounts: followers: other: ผู้ติดตาม + following: + other: กำลังติดตาม instance_actor_flash: บัญชีนี้เป็นตัวดำเนินการเสมือนที่ใช้เพื่อเป็นตัวแทนของเซิร์ฟเวอร์เองและไม่ใช่ผู้ใช้รายบุคคลใด ๆ มีการใช้บัญชีสำหรับวัตถุประสงค์ในการติดต่อกับภายนอกและไม่ควรได้รับการระงับ last_active: ใช้งานล่าสุด link_verified_on: ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ %{date} @@ -315,6 +317,8 @@ th: new: create: สร้างประกาศ title: ประกาศใหม่ + preview: + explanation_html: 'อีเมลจะถูกส่งไปยังผู้ใช้ %{display_count} คน. โดยจะมีข้อความต่อไปนี้อยู่ในอีเมล:' publish: เผยแพร่ published_msg: เผยแพร่ประกาศสำเร็จ! scheduled_for: จัดกำหนดการไว้สำหรับ %{time} @@ -950,6 +954,7 @@ th: live: สด notify_users: แจ้งเตือนผู้ใช้ preview: + explanation_html: 'อีเมลจะถูกส่งไปยังผู้ใช้ %{display_count} คน. ที่ลงทะเบียนก่อนวันที่ %{date} โดยจะมีข้อความต่อไปนี้อยู่ในอีเมล:' send_preview: ส่งตัวอย่างไปยัง %{email} send_to_all: other: ส่ง %{display_count} อีเมล diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 4744417181741f..d58aab2664e264 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -778,6 +778,8 @@ tr: administrator_description: Bu izne sahip kullanıcılar tüm diğer izinleri atlıyorlar delete_user_data: Kullanıcı Verilerini Silme delete_user_data_description: Kullanıcıların, diğer kullanıcıların verisini gecikme olmaksızın silmesine izin verir + invite_bypass_approval: Kullanıcıları incelemeden davet et + invite_bypass_approval_description: Bu kullanıcılar tarafından sunucuya davet edilen kişilerin moderasyon onayını atlamasına izin verir invite_users: Kullanıcıları Davet Etme invite_users_description: Kullanıcıların yeni kişileri sunucuya davet etmesine izin verir manage_announcements: Duyuruları Yönetme @@ -1279,6 +1281,7 @@ tr: progress: confirm: E-postanızı onaylayın details: Ayrıntılarınız + list: Kayıt ilerlemesi review: İncelememiz rules: Kabul kuralları providers: @@ -1294,6 +1297,7 @@ tr: invited_by: 'Aşağıdakinden aldığınız davet sayesinde %{domain} sunucusuna katılabilirsiniz:' preamble: Bunlar, %{domain} moderatörleri tarafından ayarlanmış ve uygulanmıştır. preamble_invited: Devam etmeden önce, %{domain} moderatörleri tarafından belirlenmiş temel kuralları gözden geçirin. + read_more: Devamını okuyun title: Bazı temel kurallar. title_invited: Davet edildiniz. security: Güvenlik diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 4c470d2e6c42df..f773dda729cbf4 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -762,6 +762,8 @@ vi: administrator_description: Người này có thể truy cập mọi quyền hạn delete_user_data: Xóa dữ liệu delete_user_data_description: Cho phép xóa dữ liệu của mọi người khác lập tức + invite_bypass_approval: Mời người dùng mà không xem lại + invite_bypass_approval_description: Cho phép những người được người dùng này mời vào máy chủ bỏ qua bước phê duyệt invite_users: Mời tham gia invite_users_description: Cho phép mời những người mới vào máy chủ manage_announcements: Quản lý thông báo @@ -1258,6 +1260,7 @@ vi: progress: confirm: Xác nhận email details: Điền thông tin + list: Quy trình đăng ký review: Đợi duyệt rules: Đọc nội quy providers: @@ -1273,6 +1276,7 @@ vi: invited_by: 'Bạn có thể tham gia %{domain} với thư mời từ:' preamble: Được ban hành và áp dụng bởi quản trị máy chủ %{domain}. preamble_invited: Trước khi tiếp tục, hãy đọc nội quy của %{domain}. + read_more: Đọc tiếp title: Nội quy máy chủ. title_invited: Bạn vừa được mời. security: Bảo mật diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 87ada1c7ad223c..c161b996819792 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -496,7 +496,7 @@ zh-CN: debug: callbacks: created_at: 创建于 - delete: 刪除 + delete: 删除 ip: IP 地址 request_body: 请求正文 title: 调试回调 @@ -504,7 +504,7 @@ zh-CN: active: 有效 base_url: 基础 URL callback: 回调 - delete: 刪除 + delete: 删除 edit: 编辑提供商 finish_registration: 完成注册 name: 名称 @@ -762,6 +762,8 @@ zh-CN: administrator_description: 拥有此权限的用户将绕过所有权限限制。 delete_user_data: 删除用户数据 delete_user_data_description: 允许用户立即删除其他用户的数据 + invite_bypass_approval: 邀请未经审核的用户 + invite_bypass_approval_description: 允许被这些用户邀请到此服务器的人们绕过管理审核批准 invite_users: 邀请用户 invite_users_description: 允许用户邀请新人加入站点 manage_announcements: 管理公告 @@ -930,7 +932,7 @@ zh-CN: with_media: 含有媒体文件 strikes: actions: - delete_statuses: "%{name} 刪除了 %{target} 的嘟文" + delete_statuses: "%{name} 删除了 %{target} 的嘟文" disable: "%{name} 冻结了用户 %{target}" mark_statuses_as_sensitive: "%{name} 已将 %{target} 的嘟文标记为敏感内容" none: "%{name} 向 %{target} 发送了警告" @@ -1258,6 +1260,7 @@ zh-CN: progress: confirm: 确认邮箱 details: 你的详细信息 + list: 注册流程 review: 我们的审核 rules: 接受规则 providers: @@ -1273,6 +1276,7 @@ zh-CN: invited_by: 欢迎加入%{domain},你是通过以下用户的邀请加入的: preamble: 以下规则由 %{domain} 的管理员设定并执行。 preamble_invited: 在继续操作前,请先阅读并同意 %{domain} 管理员设置的基本规则。 + read_more: 查看更多 title: 一些基本规则。 title_invited: 通过邀请加入 security: 账号安全 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index d7c00ec9dfce09..4585b729bc4091 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -762,6 +762,8 @@ zh-TW: administrator_description: 擁有此權限的使用者將會略過所有權限 delete_user_data: 刪除使用者資料 delete_user_data_description: 允許使用者立刻刪除其他使用者的資料 + invite_bypass_approval: 邀請未經審核的使用者 + invite_bypass_approval_description: 允許被這些使用者邀請至此伺服器的人們跳過管理審核許可 invite_users: 邀請使用者 invite_users_description: 允許使用者邀請新人加入伺服器 manage_announcements: 管理公告 @@ -1260,6 +1262,7 @@ zh-TW: progress: confirm: 驗證電子郵件地址 details: 您的個人資料 + list: 註冊流程 review: 我們的審核 rules: 接受規則 providers: @@ -1275,6 +1278,7 @@ zh-TW: invited_by: 您可以藉由來自此處之邀請而加入 %{domain} preamble: 這些被 %{domain} 的管管們制定以及實施。 preamble_invited: 在您繼續之前,請考慮由 %{domain} 管理員設立的伺服器規則。 + read_more: 閱讀更多 title: 一些基本守則。 title_invited: 我們誠摯地邀請您。 security: 登入資訊 diff --git a/config/routes.rb b/config/routes.rb index dbcca2331215e7..e43b715e33142e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true require 'sidekiq-scheduler/web' class RedirectWithVary < ActionDispatch::Routing::PathRedirect @@ -233,7 +232,7 @@ def redirect_with_vary(path) draw(:web_app) - get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web, defaults: { any: '' }, format: false + get '/web/(*any)', to: redirect(path: '/%{any}', status: 302), as: :web, defaults: { any: '' }, format: false get '/about', to: 'about#show' get '/about/more', to: redirect('/about') diff --git a/config/vite/plugin-mastodon-themes.ts b/config/vite/plugin-mastodon-themes.ts index a8d75b25f9565e..9789d9078a81d9 100644 --- a/config/vite/plugin-mastodon-themes.ts +++ b/config/vite/plugin-mastodon-themes.ts @@ -24,7 +24,7 @@ export function MastodonThemes(): Plugin { let entrypoints: Record = {}; - const existingInputs = userConfig.build?.rollupOptions?.input; + const existingInputs = userConfig.build?.rolldownOptions?.input; if (typeof existingInputs === 'string') { entrypoints[path.basename(existingInputs)] = existingInputs; @@ -46,7 +46,7 @@ export function MastodonThemes(): Plugin { return { build: { - rollupOptions: { + rolldownOptions: { input: entrypoints, }, }, diff --git a/db/migrate/20180608213548_reject_following_blocked_users.rb b/db/migrate/20180608213548_reject_following_blocked_users.rb index a82bff62b41975..3a765803f77c6c 100644 --- a/db/migrate/20180608213548_reject_following_blocked_users.rb +++ b/db/migrate/20180608213548_reject_following_blocked_users.rb @@ -28,7 +28,7 @@ def up next follow.destroy! if blocked_account.local? - reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account)) + reject_follow_json = JSON.generate(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account)) ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_account, blocked_account.inbox_url) diff --git a/db/migrate/20230215074423_move_user_settings.rb b/db/migrate/20230215074423_move_user_settings.rb index 20fd168434d041..9c6c6830120964 100644 --- a/db/migrate/20230215074423_move_user_settings.rb +++ b/db/migrate/20230215074423_move_user_settings.rb @@ -86,7 +86,7 @@ def up end end - user.update_column('settings', Oj.dump(user_settings)) + user.update_column('settings', JSON.generate(user_settings)) end end end diff --git a/db/migrate/20231023083359_convert_dtl_force_settings.rb b/db/migrate/20231023083359_convert_dtl_force_settings.rb index 165b73671e1ba8..db9c5e57fb2d75 100644 --- a/db/migrate/20231023083359_convert_dtl_force_settings.rb +++ b/db/migrate/20231023083359_convert_dtl_force_settings.rb @@ -14,13 +14,13 @@ def up User.transaction do User.find_in_batches do |users| users.filter { |user| user.settings.present? }.each do |user| - json = Oj.load(user.settings, symbol_keys: true) + json = JSON.parse(user.settings, symbol_keys: true) dtl_force_with_tag = json.delete(:dtl_force_with_tag) next if dtl_force_with_tag.blank? json[:dtl_force_visibility] = dtl_force_with_tag == 'full' ? 'unlisted' : 'unchange' json[:dtl_force_searchability] = dtl_force_with_tag == 'none' ? 'unchange' : 'public' - user.update(settings: Oj.dump(json)) + user.update(settings: JSON.generate(json)) end end end @@ -32,7 +32,7 @@ def down User.transaction do User.find_in_batches do |users| users.filter { |user| user.settings.present? }.each do |user| - json = Oj.load(user.settings, symbol_keys: true) + json = JSON.parse(user.settings, symbol_keys: true) dtl_force_visibility = json.delete(:dtl_force_visibility) dtl_force_searchability = json.delete(:dtl_force_searchability) next unless dtl_force_visibility.present? || dtl_force_searchability.present? @@ -43,7 +43,7 @@ def down else dtl_force_searchability == 'unchange' ? 'none' : 'searchability' end - user.update(settings: Oj.dump(json)) + user.update(settings: JSON.generate(json)) end end end diff --git a/db/migrate/20240304090449_migrate_interaction_settings_to_policy.rb b/db/migrate/20240304090449_migrate_interaction_settings_to_policy.rb index ea4cfccdf9368b..1736d1b17d69a8 100644 --- a/db/migrate/20240304090449_migrate_interaction_settings_to_policy.rb +++ b/db/migrate/20240304090449_migrate_interaction_settings_to_policy.rb @@ -21,7 +21,7 @@ def down; end private def policy_for_user(user) - deserialized_settings = Oj.load(user.attributes_before_type_cast['settings']) + deserialized_settings = JSON.parse(user.attributes_before_type_cast['settings']) return if deserialized_settings.nil? requires_new_policy = false diff --git a/db/migrate/20250911163952_fill_default_quote_policy_setting.rb b/db/migrate/20250911163952_fill_default_quote_policy_setting.rb index c3d08e68c9e0d4..d1aefaf758e46c 100644 --- a/db/migrate/20250911163952_fill_default_quote_policy_setting.rb +++ b/db/migrate/20250911163952_fill_default_quote_policy_setting.rb @@ -8,7 +8,7 @@ class User < ApplicationRecord; end def up User.where.not(settings: nil).find_each do |user| - settings = Oj.load(user.attributes_before_type_cast['settings']) + settings = JSON.parse(user.attributes_before_type_cast['settings']) next if settings.nil? should_update_settings = false @@ -26,7 +26,7 @@ def up should_update_settings = true end - user.update_column('settings', Oj.dump(settings)) if should_update_settings + user.update_column('settings', JSON.generate(settings)) if should_update_settings end end end diff --git a/db/migrate/20260209143308_migrate_user_theme.rb b/db/migrate/20260209143308_migrate_user_theme.rb index 93870055db7010..71f69705b9e1d2 100644 --- a/db/migrate/20260209143308_migrate_user_theme.rb +++ b/db/migrate/20260209143308_migrate_user_theme.rb @@ -8,7 +8,7 @@ class User < ApplicationRecord; end def up User.where.not(settings: nil).find_each do |user| - settings = Oj.load(user.attributes_before_type_cast['settings']) + settings = JSON.parse(user.attributes_before_type_cast['settings']) next if settings.nil? || settings['theme'].blank? || %w(system default mastodon-light contrast).exclude?(settings['theme']) case settings['theme'] @@ -25,7 +25,7 @@ def up settings['theme'] = 'default' - user.update_column('settings', Oj.dump(settings)) + user.update_column('settings', JSON.generate(settings)) end end end diff --git a/db/migrate/20260310095021_add_description_html_to_collections.rb b/db/migrate/20260310095021_add_description_html_to_collections.rb new file mode 100644 index 00000000000000..ef6a9aaecf3d17 --- /dev/null +++ b/db/migrate/20260310095021_add_description_html_to_collections.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddDescriptionHtmlToCollections < ActiveRecord::Migration[8.1] + def change + add_column :collections, :description_html, :text + + reversible do |direction| + direction.up { change_column :collections, :description, :text, null: true } + + direction.down { change_column :collections, :description, :text, null: false } + end + end +end diff --git a/db/migrate/20260311152331_add_collections_url_to_accounts.rb b/db/migrate/20260311152331_add_collections_url_to_accounts.rb new file mode 100644 index 00000000000000..7fc0bb29443a04 --- /dev/null +++ b/db/migrate/20260311152331_add_collections_url_to_accounts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddCollectionsURLToAccounts < ActiveRecord::Migration[8.1] + def change + add_column :accounts, :collections_url, :string + end +end diff --git a/db/migrate/20260318144837_add_invite_approval_bypass_permission.rb b/db/migrate/20260318144837_add_invite_approval_bypass_permission.rb new file mode 100644 index 00000000000000..c42105d5613a35 --- /dev/null +++ b/db/migrate/20260318144837_add_invite_approval_bypass_permission.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddInviteApprovalBypassPermission < ActiveRecord::Migration[8.1] + class UserRole < ApplicationRecord; end + + def up + UserRole.where('permissions & (1 << 16) = 1 << 16').update_all('permissions = permissions | (1 << 21)') + end + + def down; end +end diff --git a/db/migrate/20260319142348_create_tagged_objects.rb b/db/migrate/20260319142348_create_tagged_objects.rb new file mode 100644 index 00000000000000..ae0ebc49f834e8 --- /dev/null +++ b/db/migrate/20260319142348_create_tagged_objects.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateTaggedObjects < ActiveRecord::Migration[8.1] + def change + create_table :tagged_objects do |t| + t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: false + t.references :object, polymorphic: true, null: true + t.string :ap_type, null: false + t.string :uri + + t.timestamps + end + + add_index :tagged_objects, [:status_id, :object_type, :object_id], unique: true, where: 'object_type IS NOT NULL AND object_id IS NOT NULL' + add_index :tagged_objects, [:status_id, :uri], unique: true, where: 'uri IS NOT NULL' + end +end diff --git a/db/post_migrate/20230904134623_fix_kmr_locale_settings.rb b/db/post_migrate/20230904134623_fix_kmr_locale_settings.rb index 10e3f1da99328d..21c69c352129c1 100644 --- a/db/post_migrate/20230904134623_fix_kmr_locale_settings.rb +++ b/db/post_migrate/20230904134623_fix_kmr_locale_settings.rb @@ -11,11 +11,11 @@ def up MigrationUser.reset_column_information MigrationUser.where.not(settings: [nil, '{}']).find_each do |user| - user_settings = Oj.load(user.settings) + user_settings = JSON.parse(user.settings) next unless user_settings['default_language'] == 'kmr' user_settings['default_language'] = 'ku' - user.update!(settings: Oj.dump(user_settings)) + user.update!(settings: JSON.generate(user_settings)) end MigrationUser.where.not(chosen_languages: nil).where('chosen_languages && ?', '{kmr}').find_each do |user| diff --git a/db/post_migrate/20240321160706_migrate_interaction_settings_to_policy_again.rb b/db/post_migrate/20240321160706_migrate_interaction_settings_to_policy_again.rb index c789b639517ae0..92dbd90c795638 100644 --- a/db/post_migrate/20240321160706_migrate_interaction_settings_to_policy_again.rb +++ b/db/post_migrate/20240321160706_migrate_interaction_settings_to_policy_again.rb @@ -21,7 +21,7 @@ def down; end private def policy_for_user(user) - deserialized_settings = Oj.load(user.attributes_before_type_cast['settings']) + deserialized_settings = JSON.parse(user.attributes_before_type_cast['settings']) return if deserialized_settings.nil? return if user.notification_policy.present? diff --git a/db/schema.rb b/db/schema.rb index 1a2feb1f3f91ea..6495cdb62d5599 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.1].define(version: 2026_03_03_144409) do +ActiveRecord::Schema[8.1].define(version: 2026_03_19_142348) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -164,6 +164,7 @@ t.string "avatar_remote_url" t.integer "avatar_storage_schema_version" t.datetime "avatar_updated_at", precision: nil + t.string "collections_url" t.datetime "created_at", precision: nil, null: false t.boolean "discoverable" t.string "display_name", default: "", null: false @@ -508,7 +509,8 @@ create_table "collections", id: :bigint, default: -> { "timestamp_id('collections'::text)" }, force: :cascade do |t| t.bigint "account_id", null: false t.datetime "created_at", null: false - t.text "description", null: false + t.text "description" + t.text "description_html" t.boolean "discoverable", null: false t.integer "item_count", default: 0, null: false t.string "language" @@ -1606,6 +1608,19 @@ t.index ["tag_id", "language"], name: "index_tag_trends_on_tag_id_and_language", unique: true end + create_table "tagged_objects", force: :cascade do |t| + t.string "ap_type", null: false + t.datetime "created_at", null: false + t.bigint "object_id" + t.string "object_type" + t.bigint "status_id", null: false + t.datetime "updated_at", null: false + t.string "uri" + t.index ["object_type", "object_id"], name: "index_tagged_objects_on_object" + t.index ["status_id", "object_type", "object_id"], name: "idx_on_status_id_object_type_object_id_d6ebe374bd", unique: true, where: "((object_type IS NOT NULL) AND (object_id IS NOT NULL))" + t.index ["status_id", "uri"], name: "index_tagged_objects_on_status_id_and_uri", unique: true, where: "(uri IS NOT NULL)" + end + create_table "tags", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.string "display_name" @@ -1944,6 +1959,7 @@ add_foreign_key "tag_follows", "accounts", on_delete: :cascade add_foreign_key "tag_follows", "tags", on_delete: :cascade add_foreign_key "tag_trends", "tags", on_delete: :cascade + add_foreign_key "tagged_objects", "statuses", on_delete: :cascade add_foreign_key "tombstones", "accounts", on_delete: :cascade add_foreign_key "user_invite_requests", "users", on_delete: :cascade add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0425f8892b6438..57b992021d958b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3,7 +3,7 @@ ## Overview Before starting local development, read the [CONTRIBUTING] guide to understand -what changes are desirable and what general processes to use. +what changes are desirable and what general processes to use. You should also read the project's [AI Contribution Policy] to understand how we approach AI-assisted contributions. ## Environments @@ -103,3 +103,4 @@ development environment configured with the software needed for this project. [GitHub Codespaces]: https://docs.github.com/en/codespaces [Homebrew]: https://brew.sh [Mastodon docs]: https://docs.joinmastodon.org/dev/setup/#working-with-emails-in-development +[AI Contribution Policy]: https://github.com/mastodon/.github/blob/main/AI_POLICY.md diff --git a/lib/action_dispatch/remote_ip_extensions.rb b/lib/action_dispatch/remote_ip_extensions.rb index e5c48bf3c5b0ee..bf78c69439d600 100644 --- a/lib/action_dispatch/remote_ip_extensions.rb +++ b/lib/action_dispatch/remote_ip_extensions.rb @@ -17,11 +17,11 @@ class RemoteIp module GetIpExtensions def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from(@req.remote_addr).last + remote_addr = sanitize_ips(ips_from(@req.remote_addr)).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from(@req.client_ip).reverse! - forwarded_ips = ips_from(@req.x_forwarded_for).reverse! + client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse! + forwarded_ips = sanitize_ips(@req.forwarded_for || []).reverse! # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they # are both set, it means that either: diff --git a/lib/elasticsearch/client_extensions.rb b/lib/elasticsearch/client_extensions.rb index 700bfa4a1c6bca..1e1b441c6edcd4 100644 --- a/lib/elasticsearch/client_extensions.rb +++ b/lib/elasticsearch/client_extensions.rb @@ -2,7 +2,9 @@ module Elasticsearch module ClientExtensions - def verify_elasticsearch + def initialize(arguments = {}, &block) + super + @verified = true end end diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb index fc4606d236b7c5..5f0eea86d6f05a 100644 --- a/lib/mastodon/cli/accounts.rb +++ b/lib/mastodon/cli/accounts.rb @@ -17,7 +17,6 @@ class Accounts < Base LONG_DESC def rotate(username = nil) if options[:all] - processed = 0 delay = 0 scope = Account.local.without_suspended progress = create_progress_bar(scope.count) @@ -26,14 +25,13 @@ def rotate(username = nil) accounts.each do |account| rotate_keys_for_account(account, delay) progress.increment - processed += 1 end delay += 5.minutes end progress.finish - say("OK, rotated keys for #{processed} accounts", :green) + say("OK, rotated keys for #{progress.progress} accounts", :green) elsif username.present? rotate_keys_for_account(Account.find_local(username)) say('OK', :green) @@ -479,7 +477,6 @@ def reset_relationships(username) total += account.following.reorder(nil).count if options[:follows] total += account.followers.reorder(nil).count if options[:followers] progress = create_progress_bar(total) - processed = 0 if options[:follows] account.following.reorder(nil).find_each do |target_account| @@ -488,7 +485,6 @@ def reset_relationships(username) progress.log pastel.red("Error processing #{target_account.id}: #{e}") ensure progress.increment - processed += 1 end BootstrapTimelineWorker.perform_async(account.id) @@ -501,12 +497,11 @@ def reset_relationships(username) progress.log pastel.red("Error processing #{target_account.id}: #{e}") ensure progress.increment - processed += 1 end end progress.finish - say("Processed #{processed} relationships", :green, true) + say("Processed #{progress.progress} relationships", :green, true) end option :number, type: :numeric, aliases: [:n] diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb index c247463af52b13..2b719f3777b505 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -140,13 +140,13 @@ def crawl(start = nil) Request.new(:get, "https://#{domain}/api/v1/instance").perform do |res| next unless res.code == 200 - stats[domain] = Oj.load(res.to_s) + stats[domain] = JSON.parse(res.to_s) end Request.new(:get, "https://#{domain}/api/v1/instance/peers").perform do |res| next unless res.code == 200 - Oj.load(res.to_s).reject { |peer| stats.key?(peer) }.each do |peer| + JSON.parse(res.to_s).reject { |peer| stats.key?(peer) }.each do |peer| pool.post(peer, &work_unit) end end @@ -154,7 +154,7 @@ def crawl(start = nil) Request.new(:get, "https://#{domain}/api/v1/instance/activity").perform do |res| next unless res.code == 200 - stats[domain]['activity'] = Oj.load(res.to_s) + stats[domain]['activity'] = JSON.parse(res.to_s) end rescue failed.increment @@ -214,7 +214,7 @@ def stats_to_domains(stats) def stats_to_json(stats) stats.compact! - say(Oj.dump(stats)) + say(stats.to_json) end end end diff --git a/lib/mastodon/cli/email_domain_blocks.rb b/lib/mastodon/cli/email_domain_blocks.rb index a6093685b9bb35..818ca8d8f87325 100644 --- a/lib/mastodon/cli/email_domain_blocks.rb +++ b/lib/mastodon/cli/email_domain_blocks.rb @@ -5,7 +5,7 @@ module Mastodon::CLI class EmailDomainBlocks < Base - option :only_blocked, type: :boolean, defaut: false + option :only_blocked, type: :boolean, default: false option :only_with_approval, type: :boolean, default: false desc 'list', 'List blocked e-mail domains' long_desc <<-LONG_DESC @@ -43,7 +43,7 @@ def list end option :with_dns_records, type: :boolean - option :allow_with_approval, type: :boolean, defaut: false + option :allow_with_approval, type: :boolean, default: false desc 'add DOMAIN...', 'Block e-mail domain(s)' long_desc <<-LONG_DESC Blocking an e-mail domain prevents users from signing up diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index 32ee35c7c7a803..e5db9d6ed23aca 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -26,7 +26,6 @@ class AccountPin < ApplicationRecord; end class ListAccount < ApplicationRecord; end class PollVote < ApplicationRecord; end class Mention < ApplicationRecord; end - class Notification < ApplicationRecord; end class NotificationPermission < ApplicationRecord; end class NotificationRequest < ApplicationRecord; end class AccountDomainBlock < ApplicationRecord; end @@ -40,8 +39,6 @@ class FollowRecommendationSuppression < ApplicationRecord; end class CanonicalEmailBlock < ApplicationRecord; end class Appeal < ApplicationRecord; end class Webhook < ApplicationRecord; end - class BulkImport < ApplicationRecord; end - class SoftwareUpdate < ApplicationRecord; end class SeveredRelationship < ApplicationRecord; end class TagFollow < ApplicationRecord; end @@ -58,6 +55,18 @@ class MediaAttachment < ApplicationRecord self.inheritance_column = nil end + class Notification < ApplicationRecord + self.inheritance_column = nil + end + + class BulkImport < ApplicationRecord + self.inheritance_column = nil + end + + class SoftwareUpdate < ApplicationRecord + self.inheritance_column = nil + end + class AccountStat < ApplicationRecord belongs_to :account, inverse_of: :account_stat end diff --git a/lib/mastodon/cli/search.rb b/lib/mastodon/cli/search.rb index 7c9bc62f51c1ee..51fce07b43c090 100644 --- a/lib/mastodon/cli/search.rb +++ b/lib/mastodon/cli/search.rb @@ -118,7 +118,7 @@ def deploy progress.finish say("Indexed #{added} records, de-indexed #{removed}", :green, true) - rescue Elasticsearch::Transport::Transport::ServerError => e + rescue Elastic::Transport::Transport::ServerError => e fail_with_message <<~ERROR There was an issue connecting to the search server. Make sure the server is configured and running correctly, and that the environment diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 1c16b9af3e7945..84b8617b1be59d 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -35,7 +35,7 @@ def patch end def default_prerelease - 'alpha.5' + 'alpha.6' end def prerelease @@ -96,7 +96,7 @@ def dev? def api_versions { - mastodon: 8, + mastodon: 9, kmyblue: KMYBLUE_API_VERSION, } end diff --git a/lib/paperclip/transcoder.rb b/lib/paperclip/transcoder.rb index 47e3f9f547f632..c2017b8d3a2220 100644 --- a/lib/paperclip/transcoder.rb +++ b/lib/paperclip/transcoder.rb @@ -51,10 +51,7 @@ def make @output_options['maxrate'] = bitrate + 192_000 @output_options['bufsize'] = bitrate * 5 - if high_vfr?(metadata) - # TODO: change to `fps_mode` in the future, as `vsync` is being deprecated - @output_options['vsync'] = 'vfr' - end + @output_options['fps_mode'] = 'vfr' if high_vfr?(metadata) end end diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake index 053dfd83bd0063..902c3087399c11 100644 --- a/lib/tasks/emojis.rake +++ b/lib/tasks/emojis.rake @@ -99,7 +99,7 @@ namespace :emojis do map = map.sort { |a, b| a[0].size <=> b[0].size }.to_h - File.write(dest, Oj.dump(map)) + File.write(dest, JSON.dump(map)) puts "Wrote emojo to destination! (#{dest})" end @@ -109,7 +109,7 @@ namespace :emojis do emojis_light = '👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️🪽🪿' emojis_dark = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂‍♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂‍♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲🚲🪮🐦‍⬛' - map = Oj.load(File.read(src)) + map = JSON.parse(File.read(src)) emojis_light.each_grapheme_cluster do |emoji| gen_border map[emoji], 'black' @@ -193,7 +193,7 @@ namespace :emojis do require 'vips' src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_data.json') - sheet = Oj.load(File.read(src)) + sheet = JSON.load_file(src) max = 0 sheet['emojis'].each_value do |row| diff --git a/lib/tasks/repo.rake b/lib/tasks/repo.rake index 6c696a909714ca..85c7bc43f36d7a 100644 --- a/lib/tasks/repo.rake +++ b/lib/tasks/repo.rake @@ -22,7 +22,7 @@ namespace :repo do while url.present? response = HTTP.get(url) - contributors = Oj.load(response.body) + contributors = JSON.parse(response.body) contributors.each do |c| file << "* [#{c['login']}](#{c['html_url']})\n" if c['login'] @@ -68,7 +68,7 @@ namespace :repo do end end - pull_request = Oj.load(response.to_s) + pull_request = JSON.parse(response.to_s) pull_request['user']['login'] end diff --git a/lib/vite_ruby/sri_extensions.rb b/lib/vite_ruby/sri_extensions.rb index 31363272bfa3c3..edff6f808ef628 100644 --- a/lib/vite_ruby/sri_extensions.rb +++ b/lib/vite_ruby/sri_extensions.rb @@ -15,7 +15,7 @@ def load_manifest end def load_name_lookup_cache - Oj.load(config.build_output_dir.join('.vite/manifest-lookup.json').read) + JSON.load_file(config.build_output_dir.join('.vite/manifest-lookup.json')) end # Upstream's `virtual` type is a hack, re-implement it with efficient exact name lookup diff --git a/package.json b/package.json index e7dbb73fafcceb..5f5b68e7f51b64 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/mastodon", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.12.0", + "packageManager": "yarn@4.13.0", "engines": { "node": ">=20" }, @@ -55,8 +55,9 @@ "@optimize-lodash/rollup-plugin": "^6.0.0", "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.0.1", + "@rolldown/plugin-babel": "^0.2.2", "@use-gesture/react": "^10.3.1", - "@vitejs/plugin-legacy": "^7.2.1", + "@vitejs/plugin-legacy": "^8.0.0", "@vitejs/plugin-react": "^5.0.0", "arrow-key-navigation": "^1.2.0", "async-mutex": "^0.5.0", @@ -96,6 +97,7 @@ "punycode": "^2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.6", "react-helmet": "^6.1.0", "react-immutable-proptypes": "^2.2.0", "react-immutable-pure-component": "^2.2.2", @@ -112,8 +114,8 @@ "redux-immutable": "^4.0.0", "regenerator-runtime": "^0.14.0", "requestidlecallback": "^0.3.0", - "rollup-plugin-gzip": "^4.1.1", - "rollup-plugin-visualizer": "^6.0.3", + "rollup-plugin-gzip": "^4.2.0", + "rollup-plugin-visualizer": "^7.0.1", "sass": "^1.62.1", "scroll-behavior": "^0.11.0", "stacktrace-js": "^2.0.2", @@ -123,11 +125,10 @@ "tiny-queue": "^0.2.1", "twitter-text": "3.1.0", "use-debounce": "^10.0.0", - "vite": "^7.1.1", + "vite": "^8.0.0", "vite-plugin-manifest-sri": "^0.2.0", - "vite-plugin-pwa": "^1.0.2", - "vite-plugin-svgr": "^4.3.0", - "vite-tsconfig-paths": "^6.0.0", + "vite-plugin-pwa": "^1.2.0", + "vite-plugin-svgr": "^4.5.0", "wicg-inert": "^3.1.2", "workbox-expiration": "^7.3.0", "workbox-routing": "^7.3.0", @@ -137,10 +138,10 @@ "devDependencies": { "@eslint/js": "^9.39.2", "@formatjs/cli": "^6.1.1", - "@storybook/addon-a11y": "^10.0.6", - "@storybook/addon-docs": "^10.0.6", - "@storybook/addon-vitest": "^10.0.6", - "@storybook/react-vite": "^10.0.6", + "@storybook/addon-a11y": "^10.3.0", + "@storybook/addon-docs": "^10.3.0", + "@storybook/addon-vitest": "^10.3.0", + "@storybook/react-vite": "^10.3.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/debug": "^4", @@ -165,10 +166,10 @@ "@types/react-toggle": "^4.0.3", "@types/redux-immutable": "^4.0.3", "@types/requestidlecallback": "^0.3.5", - "@vitest/browser": "^4.0.5", - "@vitest/browser-playwright": "^4.0.5", - "@vitest/coverage-v8": "^4.0.5", - "@vitest/ui": "^4.0.5", + "@vitest/browser": "^4.1.0", + "@vitest/browser-playwright": "^4.1.0", + "@vitest/coverage-v8": "^4.1.0", + "@vitest/ui": "^4.1.0", "chromatic": "^13.3.3", "eslint": "^9.39.2", "eslint-import-resolver-typescript": "^4.2.5", @@ -189,13 +190,13 @@ "oxfmt": "^0.33.0", "playwright": "^1.57.0", "react-test-renderer": "^18.2.0", - "storybook": "^10.0.5", + "storybook": "^10.3.0", "stylelint": "^17.0.0", "stylelint-config-standard-scss": "^17.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.55.0", "typescript-plugin-css-modules": "^5.2.0", - "vitest": "^4.0.5" + "vitest": "^4.1.0" }, "resolutions": { "@types/react": "^18.2.7", diff --git a/spec/chewy/accounts_index_spec.rb b/spec/chewy/accounts_index_spec.rb index f7b5b2e249b6fa..3e5f38a408df02 100644 --- a/spec/chewy/accounts_index_spec.rb +++ b/spec/chewy/accounts_index_spec.rb @@ -3,6 +3,15 @@ require 'rails_helper' RSpec.describe AccountsIndex do + context 'when elasticsearch is enabled', :search do + describe 'indexing records' do + it 'indexes records from scope' do + expect { Fabricate :account } + .to change(described_class, :count).by(1) + end + end + end + describe 'Searching the index' do before do mock_elasticsearch_response(described_class, raw_response) diff --git a/spec/chewy/instances_index_spec.rb b/spec/chewy/instances_index_spec.rb new file mode 100644 index 00000000000000..afd5b70a69a3b6 --- /dev/null +++ b/spec/chewy/instances_index_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe InstancesIndex do + context 'when elasticsearch is enabled', :search do + describe 'indexing records' do + before do + Fabricate :account, domain: 'host.example' + Instance.refresh + end + + it 'indexes records from scope' do + expect { described_class.import } + .to change(described_class, :count).by(1) + end + end + end + + describe 'Searching the index' do + before do + mock_elasticsearch_response(described_class, raw_response) + end + + it 'returns results from a query' do + results = described_class.query(match: { name: 'account' }) + + expect(results).to eq [] + end + end + + def raw_response + { + took: 3, + hits: { + hits: [ + { + _id: '0', + _score: 1.6375021, + }, + ], + }, + } + end +end diff --git a/spec/chewy/public_statuses_index_spec.rb b/spec/chewy/public_statuses_index_spec.rb index 6bc08832f33992..f9786772605ffd 100644 --- a/spec/chewy/public_statuses_index_spec.rb +++ b/spec/chewy/public_statuses_index_spec.rb @@ -3,6 +3,15 @@ require 'rails_helper' RSpec.describe PublicStatusesIndex do + context 'when elasticsearch is enabled', :search do + describe 'indexing records' do + it 'indexes records from scope' do + expect { Fabricate :status, visibility: :public } + .to change(described_class, :count).by(1) + end + end + end + describe 'Searching the index' do before do mock_elasticsearch_response(described_class, raw_response) diff --git a/spec/chewy/statuses_index_spec.rb b/spec/chewy/statuses_index_spec.rb index e3899f3a1767a5..78c86b4387cb7e 100644 --- a/spec/chewy/statuses_index_spec.rb +++ b/spec/chewy/statuses_index_spec.rb @@ -3,6 +3,15 @@ require 'rails_helper' RSpec.describe StatusesIndex do + context 'when elasticsearch is enabled', :search do + describe 'indexing records' do + it 'indexes records from scope' do + expect { Fabricate :status } + .to change(described_class, :count).by(1) + end + end + end + describe 'Searching the index' do before do mock_elasticsearch_response(described_class, raw_response) diff --git a/spec/chewy/tags_index_spec.rb b/spec/chewy/tags_index_spec.rb index 6b57da65e4d737..fac5f2914402d2 100644 --- a/spec/chewy/tags_index_spec.rb +++ b/spec/chewy/tags_index_spec.rb @@ -3,6 +3,15 @@ require 'rails_helper' RSpec.describe TagsIndex do + context 'when elasticsearch is enabled', :search do + describe 'indexing records' do + it 'indexes records from scope' do + expect { Fabricate :tag, listable: true } + .to change(described_class, :count).by(1) + end + end + end + describe 'Searching the index' do before do mock_elasticsearch_response(described_class, raw_response) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 24b775a5ad8c2c..898edb3e233370 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -6,52 +6,21 @@ render_views controller do - def success - head 200 - end - - def routing_error - raise ActionController::RoutingError, '' - end - - def record_not_found - raise ActiveRecord::RecordNotFound, '' - end - - def invalid_authenticity_token - raise ActionController::InvalidAuthenticityToken, '' - end - end - - shared_examples 'error response' do |code| - it "returns http #{code} for http and renders template" do - subject - - expect(response) - .to have_http_status(code) - expect(response.parsed_body) - .to have_css('body[class=error]') - expect(response.parsed_body.css('h1').to_s) - .to include(error_content(code)) - end - - def error_content(code) - if code == 422 - I18n.t('errors.422.content') - else - I18n.t("errors.#{code}") - end - end + def success = head(200) end context 'with a forgery' do - subject do + before do ActionController::Base.allow_forgery_protection = true routes.draw { post 'success' => 'anonymous#success' } - post 'success' end - it_behaves_like 'error response', 422 + it 'responds with 422 and error page' do + post 'success' + + expect(response) + .to have_http_status(422) + end end describe 'helper_method :current_account' do @@ -85,33 +54,6 @@ def error_content(code) end end - context 'with ActionController::RoutingError' do - subject do - routes.draw { get 'routing_error' => 'anonymous#routing_error' } - get 'routing_error' - end - - it_behaves_like 'error response', 404 - end - - context 'with ActiveRecord::RecordNotFound' do - subject do - routes.draw { get 'record_not_found' => 'anonymous#record_not_found' } - get 'record_not_found' - end - - it_behaves_like 'error response', 404 - end - - context 'with ActionController::InvalidAuthenticityToken' do - subject do - routes.draw { get 'invalid_authenticity_token' => 'anonymous#invalid_authenticity_token' } - get 'invalid_authenticity_token' - end - - it_behaves_like 'error response', 422 - end - describe 'before_action :check_suspension' do before do routes.draw { get 'success' => 'anonymous#success' } @@ -141,64 +83,4 @@ def error_content(code) expect { controller.raise_not_found }.to raise_error(ActionController::RoutingError, 'No route matches unmatched') end end - - describe 'forbidden' do - controller do - def route_forbidden - forbidden - end - end - - subject do - routes.draw { get 'route_forbidden' => 'anonymous#route_forbidden' } - get 'route_forbidden' - end - - it_behaves_like 'error response', 403 - end - - describe 'not_found' do - controller do - def route_not_found - not_found - end - end - - subject do - routes.draw { get 'route_not_found' => 'anonymous#route_not_found' } - get 'route_not_found' - end - - it_behaves_like 'error response', 404 - end - - describe 'gone' do - controller do - def route_gone - gone - end - end - - subject do - routes.draw { get 'route_gone' => 'anonymous#route_gone' } - get 'route_gone' - end - - it_behaves_like 'error response', 410 - end - - describe 'unprocessable_content' do - controller do - def route_unprocessable_content - unprocessable_content - end - end - - subject do - routes.draw { get 'route_unprocessable_content' => 'anonymous#route_unprocessable_content' } - get 'route_unprocessable_content' - end - - it_behaves_like 'error response', 422 - end end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index c919ae59ecd76f..17bd5828a7c8cc 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -298,7 +298,6 @@ context 'with Approval-based registrations with valid invite and required invite text' do subject do - inviter = Fabricate(:user, confirmed_at: 2.days.ago) Setting.registrations_mode = 'approved' Setting.require_invite_text = true request.headers['Accept-Language'] = accept_language @@ -306,7 +305,9 @@ post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', invite_code: invite.code, agreement: 'true' } } end - it 'redirects to setup and creates user' do + let!(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) } + + it 'redirects to setup and creates user in a non-approved state' do subject expect(response).to redirect_to auth_setup_path @@ -315,9 +316,28 @@ .to be_present .and have_attributes( locale: eq(accept_language), - approved: be(true) + approved: be(false) ) end + + context 'when the inviting user has the permission to bypass approval' do + before do + inviter.role.update!(permissions: inviter.role.permissions | UserRole::FLAGS[:invite_bypass_approval]) + end + + it 'redirects to setup and creates user in an approved state' do + subject + + expect(response).to redirect_to auth_setup_path + + expect(User.find_by(email: 'test@example.com')) + .to be_present + .and have_attributes( + locale: eq(accept_language), + approved: be(true) + ) + end + end end context 'with an already taken username' do diff --git a/spec/controllers/concerns/error_responses_spec.rb b/spec/controllers/concerns/error_responses_spec.rb new file mode 100644 index 00000000000000..678d8765180f73 --- /dev/null +++ b/spec/controllers/concerns/error_responses_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ErrorResponses do + render_views + + shared_examples 'error response' do |code| + before { routes.draw { get 'show' => 'anonymous#show' } } + + it "returns http #{code} and renders error template" do + get 'show' + + expect(response) + .to have_http_status(code) + expect(response.parsed_body) + .to have_css('body[class=error]') + .and have_css('h1', text: error_content(code)) + end + + def error_content(code) + I18n.t("errors.#{code}") + .then { |value| I18n.t("errors.#{code}.content") if value.is_a?(Hash) } + end + end + + describe 'bad_request' do + controller(ApplicationController) do + def show = bad_request + end + + it_behaves_like 'error response', 400 + end + + describe 'forbidden' do + controller(ApplicationController) do + def show = forbidden + end + + it_behaves_like 'error response', 403 + end + + describe 'gone' do + controller(ApplicationController) do + def show = gone + end + + it_behaves_like 'error response', 410 + end + + describe 'internal_server_error' do + controller(ApplicationController) do + def show = internal_server_error + end + + it_behaves_like 'error response', 500 + end + + describe 'not_acceptable' do + controller(ApplicationController) do + def show = not_acceptable + end + + it_behaves_like 'error response', 406 + end + + describe 'not_found' do + controller(ApplicationController) do + def show = not_found + end + + it_behaves_like 'error response', 404 + end + + describe 'service_unavailable' do + controller(ApplicationController) do + def show = service_unavailable + end + + it_behaves_like 'error response', 503 + end + + describe 'too_many_requests' do + controller(ApplicationController) do + def show = too_many_requests + end + + it_behaves_like 'error response', 429 + end + + describe 'unprocessable_content' do + controller(ApplicationController) do + def show = unprocessable_content + end + + it_behaves_like 'error response', 422 + end + + context 'with ActionController::RoutingError' do + controller(ApplicationController) do + def show + raise ActionController::RoutingError, '' + end + end + + it_behaves_like 'error response', 404 + end + + context 'with ActiveRecord::RecordNotFound' do + controller(ApplicationController) do + def show + raise ActiveRecord::RecordNotFound, '' + end + end + + it_behaves_like 'error response', 404 + end + + context 'with ActionController::InvalidAuthenticityToken' do + controller(ApplicationController) do + def show + raise ActionController::InvalidAuthenticityToken, '' + end + end + + it_behaves_like 'error response', 422 + end +end diff --git a/spec/fabricators/collection_fabricator.rb b/spec/fabricators/collection_fabricator.rb index 7e0e14a765f64a..cbce4bd478d0b9 100644 --- a/spec/fabricators/collection_fabricator.rb +++ b/spec/fabricators/collection_fabricator.rb @@ -12,6 +12,8 @@ Fabricator(:remote_collection, from: :collection) do account { Fabricate.build(:remote_account) } local false + description nil + description_html '

    People to follow

    ' uri { sequence(:uri) { |i| "https://example.com/collections/#{i}" } } original_number_of_items 0 end diff --git a/spec/fabricators/collection_item_fabricator.rb b/spec/fabricators/collection_item_fabricator.rb index 7c4bcb3d85cf69..c3f62ca15a42ba 100644 --- a/spec/fabricators/collection_item_fabricator.rb +++ b/spec/fabricators/collection_item_fabricator.rb @@ -11,6 +11,5 @@ account nil state :pending object_uri { Fabricate.build(:remote_account).uri } - approval_uri { sequence(:uri) { |i| "https://example.com/authorizations/#{i}" } } uri { sequence(:uri) { |i| "https://example.com/collection_items/#{i}" } } end diff --git a/spec/fabricators/tagged_object_fabricator.rb b/spec/fabricators/tagged_object_fabricator.rb new file mode 100644 index 00000000000000..5c0b6b94b9b701 --- /dev/null +++ b/spec/fabricators/tagged_object_fabricator.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Fabricator(:tagged_object) do + status + object nil + ap_type 'FeaturedCollection' + uri { Faker::Internet.device_token } +end diff --git a/spec/helpers/json_ld_helper_spec.rb b/spec/helpers/json_ld_helper_spec.rb index f216588d978bdb..e26e9cd6378331 100644 --- a/spec/helpers/json_ld_helper_spec.rb +++ b/spec/helpers/json_ld_helper_spec.rb @@ -112,6 +112,11 @@ expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil end + it 'returns nil if the body is not parsable' do + stub_request(:get, 'https://host.test/').to_return(status: 200, body: 'XXX', headers: { 'Content-Type': 'application/activity+json' }) + expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil + end + it 'returns hash' do stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource_without_id_validation('https://host.test/')).to eq({}) diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 19a644f8276272..d6f54ba1b8f11e 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -171,6 +171,72 @@ end end end + + context 'with a FeatureRequest', feature: :collections_federation do + let(:collection) { Fabricate(:collection, account: recipient) } + let(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :pending) } + let(:object) { collection_item.activity_uri } + let(:approval_uri) { 'https://example.com/stamps/1' } + let(:json) do + { + 'id' => 'https://example.com/accepts/1', + 'type' => 'Accept', + 'actor' => sender.uri, + 'to' => ActivityPub::TagManager.instance.uri_for(recipient), + 'object' => object, + 'result' => approval_uri, + } + end + + context 'when activity is valid' do + it 'accepts the collection item, stores the authorization uri and federates an `Add` activity' do + subject.perform + + expect(collection_item.reload).to be_accepted + expect(collection_item.approval_uri).to eq 'https://example.com/stamps/1' + expect(ActivityPub::AccountRawDistributionWorker) + .to have_enqueued_sidekiq_job + end + end + + context 'when activity is invalid' do + shared_examples 'ignoring activity' do + it 'does not accept the item and does not send out an activity' do + subject.perform + + expect(collection_item.reload).to_not be_accepted + expect(collection_item.approval_uri).to be_nil + expect(ActivityPub::AccountRawDistributionWorker) + .to_not have_enqueued_sidekiq_job + end + end + + context 'when matching collection item cannot be found' do + let(:object) { 'https://localhost/feature_requests/1' } + + it_behaves_like 'ignoring activity' + end + + context 'when the sender is not the featured account' do + let(:other_account) { Fabricate(:remote_account) } + let(:collection_item) { Fabricate(:collection_item, collection:, account: other_account, state: :pending) } + + it_behaves_like 'ignoring activity' + end + + context "when approval_uri does not match the sender's uri" do + let(:approval_uri) { 'https://other.localhost/authorizations/1' } + + it_behaves_like 'ignoring activity' + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it_behaves_like 'ignoring activity' + end + end + end end context 'when sender is from friend server' do diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index c0abd9f39376d4..d0bdfbe2185979 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -3,78 +3,160 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Add do - let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured', domain: 'example.com') } - let(:status) { Fabricate(:status, account: sender, visibility: :private) } - - let(:json) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Add', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: ActivityPub::TagManager.instance.uri_for(status), - target: sender.featured_collection_url, - }.with_indifferent_access - end - - describe '#perform' do - subject { described_class.new(json, sender) } + context 'when the target is the featured collection' do + let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured', domain: 'example.com') } + let(:status) { Fabricate(:status, account: sender, visibility: :private) } - it 'creates a pin' do - subject.perform - expect(sender.pinned?(status)).to be true + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Add', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: ActivityPub::TagManager.instance.uri_for(status), + target: sender.featured_collection_url, + }.with_indifferent_access end - context 'when status was not known before' do - let(:service_stub) { instance_double(ActivityPub::FetchRemoteStatusService) } - - let(:json) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Add', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: 'https://example.com/unknown', - target: sender.featured_collection_url, - }.with_indifferent_access - end + describe '#perform' do + subject { described_class.new(json, sender) } - before do - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_stub) + it 'creates a pin' do + subject.perform + expect(sender.pinned?(status)).to be true end - context 'when there is a local follower' do + context 'when status was not known before' do + let(:service_stub) { instance_double(ActivityPub::FetchRemoteStatusService) } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Add', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://example.com/unknown', + target: sender.featured_collection_url, + }.with_indifferent_access + end + before do - account = Fabricate(:account) - account.follow!(sender) + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_stub) end - it 'fetches the status and pins it' do - allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **| - expect(uri).to eq 'https://example.com/unknown' - expect(id).to be true - expect(on_behalf_of&.following?(sender)).to be true - status + context 'when there is a local follower' do + before do + account = Fabricate(:account) + account.follow!(sender) + end + + it 'fetches the status and pins it' do + allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **| + expect(uri).to eq 'https://example.com/unknown' + expect(id).to be true + expect(on_behalf_of&.following?(sender)).to be true + status + end + subject.perform + expect(service_stub).to have_received(:call) + expect(sender.pinned?(status)).to be true end - subject.perform - expect(service_stub).to have_received(:call) - expect(sender.pinned?(status)).to be true end - end - context 'when there is no local follower' do - it 'tries to fetch the status' do - allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **| - expect(uri).to eq 'https://example.com/unknown' - expect(id).to be true - expect(on_behalf_of).to be_nil - nil + context 'when there is no local follower' do + it 'tries to fetch the status' do + allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **| + expect(uri).to eq 'https://example.com/unknown' + expect(id).to be true + expect(on_behalf_of).to be_nil + nil + end + subject.perform + expect(service_stub).to have_received(:call) + expect(sender.pinned?(status)).to be false end - subject.perform - expect(service_stub).to have_received(:call) - expect(sender.pinned?(status)).to be false end end end end + + context 'when the target is the `featuredCollections` collection', feature: :collections_federation do + subject { described_class.new(activity_json, account) } + + let(:account) { Fabricate(:remote_account, collections_url: 'https://example.com/actor/1/featured_collections') } + let(:featured_collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://other.example.com/featured_item/1', + 'type' => 'FeaturedCollection', + 'attributedTo' => account.uri, + 'name' => 'Cool people', + 'summary' => 'People you should follow.', + 'totalItems' => 0, + 'sensitive' => false, + 'discoverable' => true, + 'published' => '2026-03-09T15:19:25Z', + } + end + let(:activity_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Add', + 'actor' => account.uri, + 'target' => 'https://example.com/actor/1/featured_collections', + 'object' => featured_collection_json, + } + end + let(:stubbed_service) do + instance_double(ActivityPub::ProcessFeaturedCollectionService, call: true) + end + + before do + allow(ActivityPub::ProcessFeaturedCollectionService).to receive(:new).and_return(stubbed_service) + end + + it 'calls the service' do + subject.perform + + expect(stubbed_service).to have_received(:call).with(account, featured_collection_json) + end + end + + context 'when the target is a collection', feature: :collections_federation do + subject { described_class.new(activity_json, collection.account) } + + let(:collection) { Fabricate(:remote_collection) } + let(:featured_item_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://other.example.com/featured_item/1', + 'type' => 'FeaturedItem', + 'featuredObject' => 'https://example.com/actor/1', + 'featuredObjectType' => 'Person', + 'featureAuthorization' => 'https://example.com/auth/1', + } + end + let(:activity_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Add', + 'actor' => collection.account.uri, + 'target' => collection.uri, + 'object' => featured_item_json, + } + end + let(:stubbed_service) do + instance_double(ActivityPub::ProcessFeaturedItemService, call: true) + end + + before do + allow(ActivityPub::ProcessFeaturedItemService).to receive(:new).and_return(stubbed_service) + end + + it 'determines the correct collection and calls the service' do + subject.perform + + expect(stubbed_service).to have_received(:call).with(collection, featured_item_json) + end + end end diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 45059645745051..51e4dacaf53bb8 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -35,7 +35,7 @@ context 'when sender is followed by a local account' do before do Fabricate(:account).follow!(sender) - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: unknown_object_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) subject.perform end @@ -152,7 +152,7 @@ let(:object_json) { 'https://example.com/actor/hello-world' } before do - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: unknown_object_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) end context 'when the relay is enabled' do diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index c6360854084ff0..7799cac22a42a2 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -38,7 +38,7 @@ stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' }) - stub_request(:get, 'http://example.com/conversation').to_return(body: Oj.dump(conversation_hash), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'http://example.com/conversation').to_return(body: conversation_hash.to_json, headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'http://example.com/invalid-conversation').to_return(status: 404) end @@ -1233,8 +1233,8 @@ def activity_for_object(json) end before do - stub_request(:get, 'https://foo.test').to_return(status: 200, body: Oj.dump(actor_json), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://foo.test/.well-known/webfinger?resource=acct:actor@foo.test').to_return(status: 200, body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://foo.test').to_return(status: 200, body: actor_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://foo.test/.well-known/webfinger?resource=acct:actor@foo.test').to_return(status: 200, body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:post, 'https://foo.test/inbox').to_return(status: 200) stub_request(:get, 'https://foo.test/.well-known/nodeinfo').to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }) end @@ -1399,6 +1399,30 @@ def activity_for_object(json) end end + context 'with tagged Featured Collections' do + let(:featured_collection) { Fabricate(:collection) } + + let(:object_json) do + build_object( + tag: [ + { + type: 'FeaturedCollection', + id: ActivityPub::TagManager.instance.uri_for(featured_collection), + }, + ] + ) + end + + it 'creates the status with appropriate tagged objects' do + expect { subject.perform } + .to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + + expect(status.tagged_objects.map(&:object)).to contain_exactly(featured_collection) + end + end + context 'with hashtags' do let(:object_json) do build_object( @@ -1839,8 +1863,8 @@ def activity_for_object(json) ) end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1865,7 +1889,11 @@ def activity_for_object(json) attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: object_json[:id], interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'creates a status with a verified quote' do @@ -1917,8 +1945,8 @@ def activity_for_object(json) ) end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1943,7 +1971,11 @@ def activity_for_object(json) attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: object_json[:id], interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'creates a status without the verified quote' do @@ -2715,7 +2747,7 @@ def activity_for_object(json) before do stub_request(:get, object_json[:id]) .with(headers: { Authorization: "Bearer #{token}" }) - .to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' }) + .to_return(body: object_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) subject.perform end diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 2719fad36a11ab..047138d3053971 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -119,6 +119,29 @@ .to change { quote.reload.state }.to('revoked') end end + + context 'with a FeatureAuthorization', feature: :collections_federation do + let(:recipient) { Fabricate(:account) } + let(:approval_uri) { 'https://example.com/authorizations/1' } + let(:collection) { Fabricate(:collection, account: recipient) } + let!(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :accepted, approval_uri:) } + let(:json) do + { + 'id' => 'https://example.com/accepts/1', + 'type' => 'Delete', + 'actor' => sender.uri, + 'to' => ActivityPub::TagManager.instance.uri_for(recipient), + 'object' => approval_uri, + } + end + + it 'revokes the collection item and federates a `Delete` activity' do + subject.perform + + expect(collection_item.reload).to be_revoked + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end + end end context 'when the status is limited post and has conversation' do diff --git a/spec/lib/activitypub/activity/feature_request_spec.rb b/spec/lib/activitypub/activity/feature_request_spec.rb new file mode 100644 index 00000000000000..ac3e42b27213d8 --- /dev/null +++ b/spec/lib/activitypub/activity/feature_request_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::Activity::FeatureRequest do + let(:sender) { Fabricate(:remote_account) } + let(:recipient) { Fabricate(:account, discoverable:) } + let(:collection) { Fabricate(:remote_collection, account: sender) } + + let(:json) do + { + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + ], + 'id' => 'https://example.com/feature_requests/1', + 'type' => 'FeatureRequest', + 'actor' => sender.uri, + 'object' => ActivityPub::TagManager.instance.uri_for(recipient), + 'instrument' => collection.uri, + } + end + + describe '#perform', feature: :collections_federation do + subject { described_class.new(json, sender) } + + context 'when recipient is discoverable' do + let(:discoverable) { true } + + it 'schedules a job to send an `Accept` activity' do + expect { subject.perform } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + response_json = JSON.parse(body) + response_json['type'] == 'Accept' && + response_json['to'] == sender.uri + end, recipient.id, sender.inbox_url) + end + end + + context 'when recipient is not discoverable' do + let(:discoverable) { false } + + it 'schedules a job to send a `Reject` activity' do + expect { subject.perform } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + response_json = JSON.parse(body) + response_json['type'] == 'Reject' && + response_json['to'] == sender.uri + end, recipient.id, sender.inbox_url) + end + end + end +end diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index 12c30feb28de6b..8f68be742526cc 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -273,6 +273,23 @@ end end + context 'when recipient blocks sender' do + before { Fabricate :block, account: recipient, target_account: sender } + + it 'sends a reject and does not follow' do + subject.perform + + expect(sender.requested?(recipient)) + .to be false + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job( + match_json_values(type: 'Reject', object: include(type: 'Follow')), + recipient.id, + anything + ) + end + end + context 'when a follow relationship already exists' do before do sender.active_relationships.create!(target_account: recipient, uri: 'bar') diff --git a/spec/lib/activitypub/activity/like_spec.rb b/spec/lib/activitypub/activity/like_spec.rb index 369be4caa26728..86d499148e7987 100644 --- a/spec/lib/activitypub/activity/like_spec.rb +++ b/spec/lib/activitypub/activity/like_spec.rb @@ -90,8 +90,8 @@ before do stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) stub_request(:get, 'http://foo.bar/emoji2.png').to_return(body: attachment_fixture('emojo.png')) - stub_request(:get, 'https://example.com/aaa').to_return(status: 200, body: Oj.dump(original_emoji), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/invalid').to_return(status: 200, body: Oj.dump(original_invalid_emoji), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/aaa').to_return(status: 200, body: original_emoji.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/invalid').to_return(status: 200, body: original_invalid_emoji.to_json, headers: { 'Content-Type': 'application/activity+json' }) end let(:json) do diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index db80448a80b6a4..84f931e618b8ce 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -65,7 +65,7 @@ expect { subject.perform } .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker) .with(satisfying do |body| - outgoing_json = Oj.load(body) + outgoing_json = JSON.parse(body) outgoing_json['type'] == 'Reject' && %w(type id actor object instrument).all? { |key| json[key] == outgoing_json['object'][key] } end, recipient.id, sender.inbox_url) end @@ -78,7 +78,7 @@ expect { subject.perform } .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker) .with(satisfying do |body| - outgoing_json = Oj.load(body) + outgoing_json = JSON.parse(body) outgoing_json['type'] == 'Reject' && json['instrument']['id'] == outgoing_json['object']['instrument'] && %w(type id actor object).all? { |key| json[key] == outgoing_json['object'][key] } end, recipient.id, sender.inbox_url) end @@ -86,7 +86,7 @@ context 'when trying to quote a quotable local status' do before do - stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) end @@ -95,7 +95,7 @@ .to change { quoted_post.reload.quotes.accepted.count }.by(1) .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker) .with(satisfying do |body| - outgoing_json = Oj.load(body) + outgoing_json = JSON.parse(body) outgoing_json['type'] == 'Accept' && %w(type id actor object instrument).all? { |key| json[key] == outgoing_json['object'][key] } end, recipient.id, sender.inbox_url) end @@ -113,7 +113,7 @@ .to change { quoted_post.reload.quotes.accepted.count }.by(1) .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker) .with(satisfying do |body| - outgoing_json = Oj.load(body) + outgoing_json = JSON.parse(body) outgoing_json['type'] == 'Accept' && json['instrument']['id'] == outgoing_json['object']['instrument'] && %w(type id actor object).all? { |key| json[key] == outgoing_json['object'][key] } end, recipient.id, sender.inbox_url) end diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb index 0c3f7f5f8db6fe..42974b251fc001 100644 --- a/spec/lib/activitypub/activity/reject_spec.rb +++ b/spec/lib/activitypub/activity/reject_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Reject do - let(:sender) { Fabricate(:account) } + let(:sender) { Fabricate(:remote_account) } let(:recipient) { Fabricate(:account) } let(:object_json) do @@ -138,12 +138,12 @@ context 'with a QuoteRequest' do let(:status) { Fabricate(:status, account: recipient) } let(:quoted_status) { Fabricate(:status, account: sender) } - let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status) } let(:approval_uri) { "https://#{sender.domain}/approvals/1" } let(:object_json) do { - id: 'https://abc-123/456', + id: quote.activity_uri, type: 'QuoteRequest', actor: ActivityPub::TagManager.instance.uri_for(recipient), object: ActivityPub::TagManager.instance.uri_for(quoted_status), @@ -157,6 +157,24 @@ end end + context 'with a FeatureRequest' do + let(:collection) { Fabricate(:collection, account: recipient) } + let!(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :pending) } + let(:json) do + { + 'id' => 'https://example.com/accepts/1', + 'type' => 'Accept', + 'actor' => sender.uri, + 'to' => ActivityPub::TagManager.instance.uri_for(recipient), + 'object' => collection_item.activity_uri, + } + end + + it 'deletes the collection item' do + expect { subject.perform }.to change(collection.collection_items, :count).by(-1) + end + end + context 'when rejecting a pending follow request by uri only' do let(:object_json) { 'bar' } diff --git a/spec/lib/activitypub/activity/remove_spec.rb b/spec/lib/activitypub/activity/remove_spec.rb index 937b938e4fccce..e31b40b9db0dd6 100644 --- a/spec/lib/activitypub/activity/remove_spec.rb +++ b/spec/lib/activitypub/activity/remove_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Remove do - let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } + let(:sender) do + Fabricate(:remote_account, + featured_collection_url: 'https://example.com/featured', + collections_url: 'https://example.com/actor/1/featured_collections') + end describe '#perform' do subject { described_class.new(json, sender) } @@ -59,5 +63,51 @@ .to change { sender.featured_tags.exists?(tag: tag) }.to(false) end end + + context 'when removing a featured collection' do + let(:collection) { Fabricate(:remote_collection, account: sender) } + let(:json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'foo', + 'type' => 'Remove', + 'actor' => ActivityPub::TagManager.instance.uri_for(sender), + 'object' => collection.uri, + 'target' => sender.collections_url, + } + end + + before do + Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/1') + end + + it 'deletes the collection' do + expect { subject.perform } + .to change(sender.collections, :count).by(-1) + .and change(CollectionItem, :count).by(-1) + end + end + + context 'when removing a featured item' do + let(:collection) { Fabricate(:remote_collection, account: sender) } + let(:collection_item) { Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/1') } + let(:json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'foo', + 'type' => 'Remove', + 'actor' => ActivityPub::TagManager.instance.uri_for(sender), + 'object' => collection_item.uri, + 'target' => collection.uri, + } + end + + before { json } + + it 'deletes the collection item' do + expect { subject.perform } + .to change(collection.collection_items, :count).by(-1) + end + end end end diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb index 443fc502cf97bb..5c74b3dde803e1 100644 --- a/spec/lib/activitypub/activity/update_spec.rb +++ b/spec/lib/activitypub/activity/update_spec.rb @@ -317,5 +317,45 @@ end end end + + context 'with a `FeaturedCollection` object', feature: :collections_federation do + let(:collection) { Fabricate(:remote_collection, account: sender, name: 'old name', discoverable: false) } + let(:featured_collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => collection.uri, + 'type' => 'FeaturedCollection', + 'attributedTo' => sender.uri, + 'name' => 'Cool people', + 'summary' => 'People you should follow.', + 'totalItems' => 0, + 'sensitive' => false, + 'discoverable' => true, + 'published' => '2026-03-09T15:19:25Z', + 'updated' => Time.zone.now.iso8601, + } + end + let(:json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Update', + 'actor' => sender.uri, + 'object' => featured_collection_json, + } + end + let(:stubbed_service) do + instance_double(ActivityPub::ProcessFeaturedCollectionService, call: true) + end + + before do + allow(ActivityPub::ProcessFeaturedCollectionService).to receive(:new).and_return(stubbed_service) + end + + it 'updates the collection' do + subject.perform + + expect(stubbed_service).to have_received(:call).with(sender, featured_collection_json) + end + end end end diff --git a/spec/lib/activitypub/activity_spec.rb b/spec/lib/activitypub/activity_spec.rb index d7d0700dc65c0b..d8236191df923e 100644 --- a/spec/lib/activitypub/activity_spec.rb +++ b/spec/lib/activitypub/activity_spec.rb @@ -89,7 +89,7 @@ before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump(approval_payload)) + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: approval_payload.to_json) end context 'when getting them in order' do diff --git a/spec/lib/activitypub/dereferencer_spec.rb b/spec/lib/activitypub/dereferencer_spec.rb index 11078de866e1e3..a482ecaced9859 100644 --- a/spec/lib/activitypub/dereferencer_spec.rb +++ b/spec/lib/activitypub/dereferencer_spec.rb @@ -12,7 +12,7 @@ let(:uri) { nil } before do - stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/foo').to_return(body: object.to_json, headers: { 'Content-Type' => 'application/activity+json' }) end context 'with a URI' do diff --git a/spec/lib/activitypub/forwarder_spec.rb b/spec/lib/activitypub/forwarder_spec.rb index f72e3342183e83..81175ba8da34ae 100644 --- a/spec/lib/activitypub/forwarder_spec.rb +++ b/spec/lib/activitypub/forwarder_spec.rb @@ -54,8 +54,8 @@ it 'correctly forwards to expected remote followers' do expect { subject.forward! } - .to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, eve.preferred_inbox_url) - .and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, mallory.preferred_inbox_url) + .to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(payload.to_json, anything, eve.preferred_inbox_url) + .and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(payload.to_json, anything, mallory.preferred_inbox_url) end end end diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index d14fe5231b8b27..746e102a45a37f 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -774,5 +774,15 @@ status = Fabricate(:status, uri: 'https://example.com/123') expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status end + + it 'returns the local featured collection' do + collection = Fabricate(:collection) + expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection + end + + it 'returns the remote featured collection' do + collection = Fabricate(:remote_collection) + expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection + end end end diff --git a/spec/lib/admin/system_check/elasticsearch_check_spec.rb b/spec/lib/admin/system_check/elasticsearch_check_spec.rb index 702c19cf09b37e..fe6585ff06a09b 100644 --- a/spec/lib/admin/system_check/elasticsearch_check_spec.rb +++ b/spec/lib/admin/system_check/elasticsearch_check_spec.rb @@ -131,7 +131,7 @@ def stub_elasticsearch_error client = instance_double(Elasticsearch::Client) - allow(client).to receive(:info).and_raise(Elasticsearch::Transport::Transport::Error) + allow(client).to receive(:info).and_raise(Elastic::Transport::Transport::Error) allow(Chewy).to receive(:client).and_return(client) end end diff --git a/spec/lib/elasticsearch/client_extensions_spec.rb b/spec/lib/elasticsearch/client_extensions_spec.rb new file mode 100644 index 00000000000000..1d97bb8e039946 --- /dev/null +++ b/spec/lib/elasticsearch/client_extensions_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Elasticsearch::ClientExtensions do + describe '#initialize' do + it 'marks the connection as verified on initialization' do + client = Elasticsearch::Client.new + + expect(client.instance_variable_get(:@verified)) + .to be(true) + end + end +end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index b0b7505c8ca056..addb24d082061b 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -594,7 +594,7 @@ allow(redis).to receive_messages(publish: nil) subject.unpush_from_home(receiver, status) - deletion = Oj.dump(event: :delete, payload: status.id.to_s) + deletion = { event: :delete, payload: status.id.to_s }.to_json expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion) end end diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb index d1c26546f04dd9..39203fbdfa3a3b 100644 --- a/spec/lib/mastodon/cli/domains_spec.rb +++ b/spec/lib/mastodon/cli/domains_spec.rb @@ -87,7 +87,7 @@ end def json_summary - Oj.dump('host.example': { activity: {} }) + { 'host.example': { activity: {} } }.to_json end end end diff --git a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb index 1662785f392a71..a564600eafd95f 100644 --- a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb +++ b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb @@ -101,7 +101,6 @@ context 'when no blocks exist' do let(:domain) { 'host.example' } let(:arguments) { [domain] } - let(:options) { { allow_with_approval: false } } it 'adds a new block' do expect { subject } @@ -113,7 +112,7 @@ context 'with --with-dns-records true' do let(:domain) { 'host.example' } let(:arguments) { [domain] } - let(:options) { { allow_with_approval: false, with_dns_records: true } } + let(:options) { { with_dns_records: true } } before do configure_mx(domain: domain, exchange: 'other.host') diff --git a/spec/lib/mastodon/cli/search_spec.rb b/spec/lib/mastodon/cli/search_spec.rb index 8a6c2492aa43d0..63913ae8ba01b8 100644 --- a/spec/lib/mastodon/cli/search_spec.rb +++ b/spec/lib/mastodon/cli/search_spec.rb @@ -36,7 +36,7 @@ context 'when server communication raises an error' do let(:options) { { reset_chewy: true } } - before { allow(Chewy::Stash::Specification).to receive(:reset!).and_raise(Elasticsearch::Transport::Transport::Errors::InternalServerError) } + before { allow(Chewy::Stash::Specification).to receive(:reset!).and_raise(Elastic::Transport::Transport::Errors::InternalServerError) } it 'Exits with error message' do expect { subject } diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb index 4797a3dc6380f9..a6a0ad15a29117 100644 --- a/spec/lib/translation_service/deepl_spec.rb +++ b/spec/lib/translation_service/deepl_spec.rb @@ -19,6 +19,19 @@ end describe '#translate' do + context 'with invalid body response' do + before do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html') + .to_return(body: 'XXX') + end + + it 'handles error and re-raises' do + expect { service.translate(['Hasta la vista'], 'es', 'en') } + .to raise_error(TranslationService::UnexpectedResponseError) + end + end + it 'returns translation with specified source language' do stub_request(:post, 'https://api.deepl.com/v2/translate') .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html') diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb index 90966a8ebf6449..4df2a2076cb11a 100644 --- a/spec/lib/translation_service/libre_translate_spec.rb +++ b/spec/lib/translation_service/libre_translate_spec.rb @@ -29,6 +29,19 @@ end describe '#translate' do + context 'with invalid body response' do + before do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":["Hasta la vista"],"source":"es","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: 'XXX') + end + + it 'handles error and re-raises' do + expect { service.translate(['Hasta la vista'], 'es', 'en') } + .to raise_error(TranslationService::UnexpectedResponseError) + end + end + it 'returns translation with specified source language' do stub_request(:post, 'https://libretranslate.example.com/translate') .with(body: '{"q":["Hasta la vista"],"source":"es","target":"en","format":"html","api_key":"my-api-key"}') diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb index e214a03536bf17..0bf682eca910db 100644 --- a/spec/lib/webfinger_spec.rb +++ b/spec/lib/webfinger_spec.rb @@ -3,35 +3,72 @@ require 'rails_helper' RSpec.describe Webfinger do - describe 'self link' do + describe '#initialize' do + subject { described_class.new(uri) } + + context 'when called with local account' do + let(:uri) { 'acct:alice' } + + it 'handles value and raises error' do + expect { subject }.to raise_error(ArgumentError, /for local account/) + end + end + + context 'when called with remote account' do + let(:uri) { 'acct:alice@host.example' } + + it 'handles value and sets attributes' do + expect { subject }.to_not raise_error + end + end + end + + describe '#perform' do subject { described_class.new('acct:alice@example.com').perform } context 'when self link is specified with type application/activity+json' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end + it 'correctly parses the response' do expect(subject.self_link_href).to eq 'https://example.com/alice' end end context 'when self link is specified with type application/ld+json' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } - it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end + it 'correctly parses the response' do expect(subject.self_link_href).to eq 'https://example.com/alice' end end context 'when self link is specified with incorrect type' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end it 'raises an error' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + expect { subject } + .to raise_error(Webfinger::Error) + end + end + context 'when response body is not parsable' do + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: 'XXX', headers: { 'Content-Type': 'application/jrd+json' }) + end + + it 'raises an error' do expect { subject } .to raise_error(Webfinger::Error) end @@ -53,7 +90,7 @@ before do stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(body: host_meta, headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'uses host meta details' do diff --git a/spec/lib/webhooks/payload_renderer_spec.rb b/spec/lib/webhooks/payload_renderer_spec.rb index 0623edd25475d4..7cd536fb058767 100644 --- a/spec/lib/webhooks/payload_renderer_spec.rb +++ b/spec/lib/webhooks/payload_renderer_spec.rb @@ -3,27 +3,34 @@ require 'rails_helper' RSpec.describe Webhooks::PayloadRenderer do - subject(:renderer) { described_class.new(json) } + subject(:renderer) { described_class.new(payload.to_json) } let(:event) { Webhooks::EventPresenter.new(type, object) } let(:payload) { ActiveModelSerializers::SerializableResource.new(event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json } - let(:json) { Oj.dump(payload) } describe '#render' do + subject { renderer.render(template) } + context 'when event is account.approved' do let(:type) { 'account.approved' } - let(:object) { Fabricate(:account, display_name: 'Foo"') } + let(:object) { Fabricate(:account, display_name: 'Foo"', username: 'foofoobarbar') } + + context 'with event-related variables' do + let(:template) { 'foo={{event}}' } - it 'renders event-related variables into template' do - expect(renderer.render('foo={{event}}')).to eq 'foo=account.approved' + it { is_expected.to eq('foo=account.approved') } end - it 'renders event-specific variables into template' do - expect(renderer.render('foo={{object.username}}')).to eq "foo=#{object.username}" + context 'with event-specific variables' do + let(:template) { 'foo={{object.username}}' } + + it { is_expected.to eq('foo=foofoobarbar') } end - it 'escapes values for use in JSON' do - expect(renderer.render('foo={{object.account.display_name}}')).to eq 'foo=Foo\\"' + context 'with values needing JSON escape' do + let(:template) { 'foo={{object.account.display_name}}' } + + it { is_expected.to eq('foo=Foo\\"') } end end end diff --git a/spec/support/examples/models/concerns/account_avatar.rb b/spec/models/account/avatar_spec.rb similarity index 76% rename from spec/support/examples/models/concerns/account_avatar.rb rename to spec/models/account/avatar_spec.rb index 01614578b3ab1f..bc37eda7e25d4c 100644 --- a/spec/support/examples/models/concerns/account_avatar.rb +++ b/spec/models/account/avatar_spec.rb @@ -1,24 +1,26 @@ # frozen_string_literal: true -RSpec.shared_examples 'AccountAvatar' do |fabricator| +require 'rails_helper' + +RSpec.describe Account::Avatar do describe 'static avatars', :attachment_processing do describe 'with a square GIF' do it 'creates a png static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif')) + account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) expect(account.avatar_static_url).to_not eq account.avatar_original_url end end describe 'with a higher-than-wide GIF' do it 'creates a png static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('avatar-high.gif')) + account = Fabricate(:account, avatar: attachment_fixture('avatar-high.gif')) expect(account.avatar_static_url).to_not eq account.avatar_original_url end end describe 'when non-GIF' do it 'does not create extra static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('attachment.jpg')) + account = Fabricate(:account, avatar: attachment_fixture('attachment.jpg')) expect(account.avatar_static_url).to eq account.avatar_original_url end end @@ -27,7 +29,7 @@ describe 'convertable avatars', :attachment_processing do describe 'with AVIF' do it 'creates a jpeg static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('avatar.avif')) + account = Fabricate(:account, avatar: attachment_fixture('avatar.avif')) expect(account.avatar_original_url.end_with?('.jpeg')).to be true end end @@ -35,7 +37,7 @@ describe 'base64-encoded files', :attachment_processing do let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" } - let(:account) { Fabricate(fabricator, avatar: base64_attachment) } + let(:account) { Fabricate(:account, avatar: base64_attachment) } it 'saves avatar' do expect(account.persisted?).to be true diff --git a/spec/support/examples/models/concerns/account_header.rb b/spec/models/account/header_spec.rb similarity index 84% rename from spec/support/examples/models/concerns/account_header.rb rename to spec/models/account/header_spec.rb index af8d22d633089e..79423e2d2543a1 100644 --- a/spec/support/examples/models/concerns/account_header.rb +++ b/spec/models/account/header_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -RSpec.shared_examples 'AccountHeader' do |fabricator| +require 'rails_helper' + +RSpec.describe Account::Header do describe 'base64-encoded files', :attachment_processing do let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" } - let(:account) { Fabricate(fabricator, header: base64_attachment) } + let(:account) { Fabricate(:account, header: base64_attachment) } it 'saves header' do expect(account.persisted?).to be true diff --git a/spec/support/examples/models/concerns/account/search.rb b/spec/models/account/search_spec.rb similarity index 77% rename from spec/support/examples/models/concerns/account/search.rb rename to spec/models/account/search_spec.rb index 4fcf4759c59b6d..1532b8bf208fa0 100644 --- a/spec/support/examples/models/concerns/account/search.rb +++ b/spec/models/account/search_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -RSpec.shared_examples 'Account::Search' do +require 'rails_helper' + +RSpec.describe Account::Search do describe '.search_for' do before do _missing = Fabricate( @@ -20,7 +22,7 @@ suspended: true ) - results = described_class.search_for('username') + results = Account.search_for('username') expect(results).to eq [] end @@ -33,7 +35,7 @@ match.user.update(approved: false) - results = described_class.search_for('username') + results = Account.search_for('username') expect(results).to eq [] end @@ -46,7 +48,7 @@ match.user.update(confirmed_at: nil) - results = described_class.search_for('username') + results = Account.search_for('username') expect(results).to eq [] end @@ -58,7 +60,7 @@ domain: 'example.com' ) - results = described_class.search_for('A?l\i:c e') + results = Account.search_for('A?l\i:c e') expect(results).to eq [match] end @@ -70,7 +72,7 @@ domain: 'example.com' ) - results = described_class.search_for('display') + results = Account.search_for('display') expect(results).to eq [match] end @@ -82,7 +84,7 @@ domain: 'example.com' ) - results = described_class.search_for('username') + results = Account.search_for('username') expect(results).to eq [match] end @@ -94,20 +96,20 @@ domain: 'example.com' ) - results = described_class.search_for('example') + results = Account.search_for('example') expect(results).to eq [match] end it 'limits via constant by default' do stub_const('Account::Search::DEFAULT_LIMIT', 1) 2.times.each { Fabricate(:account, display_name: 'Display Name') } - results = described_class.search_for('display') + results = Account.search_for('display') expect(results.size).to eq 1 end it 'accepts arbitrary limits' do 2.times.each { Fabricate(:account, display_name: 'Display Name') } - results = described_class.search_for('display', limit: 1) + results = Account.search_for('display', limit: 1) expect(results.size).to eq 1 end @@ -117,7 +119,7 @@ { display_name: 'Display Name', username: 'username', domain: 'example.com' }, ].map(&method(:Fabricate).curry(2).call(:account)) - results = described_class.search_for('username') + results = Account.search_for('username') expect(results).to eq matches end end @@ -135,7 +137,7 @@ ) account.follow!(match) - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) + results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) expect(results).to eq [match] end @@ -147,7 +149,7 @@ domain: 'example.com' ) - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) + results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) expect(results).to eq [] end @@ -160,7 +162,7 @@ suspended: true ) - results = described_class.advanced_search_for('username', account, limit: 10, following: true) + results = Account.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end @@ -173,7 +175,7 @@ match.user.update(approved: false) - results = described_class.advanced_search_for('username', account, limit: 10, following: true) + results = Account.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end @@ -186,7 +188,7 @@ match.user.update(confirmed_at: nil) - results = described_class.advanced_search_for('username', account, limit: 10, following: true) + results = Account.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end end @@ -201,7 +203,7 @@ ) match.follow!(account) - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, follower: true) + results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, follower: true) expect(results).to eq [match] end @@ -213,7 +215,7 @@ domain: 'example.com' ) - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, follower: true) + results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, follower: true) expect(results).to eq [] end end @@ -227,7 +229,7 @@ suspended: true ) - results = described_class.advanced_search_for('username', account) + results = Account.advanced_search_for('username', account) expect(results).to eq [] end @@ -240,7 +242,7 @@ match.user.update(approved: false) - results = described_class.advanced_search_for('username', account) + results = Account.advanced_search_for('username', account) expect(results).to eq [] end @@ -253,7 +255,7 @@ match.user.update(confirmed_at: nil) - results = described_class.advanced_search_for('username', account) + results = Account.advanced_search_for('username', account) expect(results).to eq [] end @@ -265,20 +267,20 @@ domain: 'example.com' ) - results = described_class.advanced_search_for('A?l\i:c e', account) + results = Account.advanced_search_for('A?l\i:c e', account) expect(results).to eq [match] end it 'limits result count by default value' do stub_const('Account::Search::DEFAULT_LIMIT', 1) 2.times { Fabricate(:account, display_name: 'Display Name') } - results = described_class.advanced_search_for('display', account) + results = Account.advanced_search_for('display', account) expect(results.size).to eq 1 end it 'accepts arbitrary limits' do 2.times { Fabricate(:account, display_name: 'Display Name') } - results = described_class.advanced_search_for('display', account, limit: 1) + results = Account.advanced_search_for('display', account, limit: 1) expect(results.size).to eq 1 end @@ -287,7 +289,7 @@ followed_match = Fabricate(:account, username: 'Matcher') Fabricate(:follow, account: account, target_account: followed_match) - results = described_class.advanced_search_for('match', account) + results = Account.advanced_search_for('match', account) expect(results).to eq [followed_match, match] expect(results.first.rank).to be > results.last.rank end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index b92771e8f5c737..1bb238f7ef8796 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -7,6 +7,10 @@ describe 'acct' do it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') } end + + describe 'current_username' do + it { is_expected.to normalize(:current_username).from(' @username ').to('username') } + end end describe 'Validations' do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 351c1b237c0c38..ca22e7a3773616 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -3,7 +3,6 @@ require 'rails_helper' RSpec.describe Account do - it_behaves_like 'Account::Search' it_behaves_like 'Reviewable' describe 'Associations' do @@ -959,9 +958,6 @@ def fields_empty_name end end - it_behaves_like 'AccountAvatar', :account - it_behaves_like 'AccountHeader', :account - describe '#increment_count!' do subject { Fabricate(:account) } diff --git a/spec/models/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb index 6032594850d346..f25ddd6fe4c63f 100644 --- a/spec/models/admin/account_action_spec.rb +++ b/spec/models/admin/account_action_spec.rb @@ -11,12 +11,14 @@ let(:account) { Fabricate(:admin_user).account } let(:target_account) { Fabricate(:account) } let(:type) { 'disable' } + let(:text) { nil } before do account_action.assign_attributes( type: type, current_account: account, - target_account: target_account + target_account: target_account, + text: ) end @@ -53,6 +55,20 @@ end end + context 'when type is `none`' do + let(:type) { 'none' } + + context 'when a custom text is given' do + let(:text) { 'custom' } + + it 'logs the action' do + expect { subject }.to change(Admin::ActionLog, :count).by(1) + + expect(Admin::ActionLog.last.target.text).to eq 'custom' + end + end + end + context 'when type is invalid' do let(:type) { 'whatever' } diff --git a/spec/models/collection_item_spec.rb b/spec/models/collection_item_spec.rb index e4905535cfc263..89ef2bc53483eb 100644 --- a/spec/models/collection_item_spec.rb +++ b/spec/models/collection_item_spec.rb @@ -17,13 +17,24 @@ end context 'when item is not local' do - subject { Fabricate.build(:collection_item, collection: remote_collection) } + subject { Fabricate.build(:collection_item, collection: remote_collection, account:) } + let(:account) { Fabricate.build(:remote_account) } let(:remote_collection) { Fabricate.build(:collection, local: false) } - it { is_expected.to validate_absence_of(:approval_uri) } - it { is_expected.to validate_presence_of(:uri) } + + context 'when account is not present' do + subject { Fabricate.build(:collection_item, collection: remote_collection, account: nil) } + + it { is_expected.to validate_presence_of(:approval_uri) } + end + + context 'when account is local' do + let(:account) { Fabricate.build(:account) } + + it { is_expected.to_not validate_presence_of(:uri) } + end end context 'when account is not present' do @@ -51,6 +62,12 @@ expect(custom_item.position).to eq 7 end + it 'automatically sets the position if excplicitly set to `nil`' do + item = collection.collection_items.create!(account:, position: nil) + + expect(item.position).to eq 1 + end + it 'automatically sets `activity_uri` when account is remote' do item = collection.collection_items.create(account: Fabricate(:remote_account)) diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index ba1819fa6cda2d..6937829ebbac50 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -8,8 +8,12 @@ it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_length_of(:name).is_at_most(40) } + it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_length_of(:description).is_at_most(100) } + it { is_expected.to_not allow_value(nil).for(:local) } it { is_expected.to_not allow_value(nil).for(:sensitive) } @@ -23,6 +27,14 @@ context 'when collection is remote' do subject { Fabricate.build :collection, local: false } + it { is_expected.to validate_length_of(:name).is_at_most(Collection::NAME_LENGTH_HARD_LIMIT) } + + it { is_expected.to_not validate_presence_of(:description) } + + it { is_expected.to validate_presence_of(:description_html) } + + it { is_expected.to validate_length_of(:description_html).is_at_most(Collection::DESCRIPTION_LENGTH_HARD_LIMIT) } + it { is_expected.to validate_presence_of(:uri) } it { is_expected.to validate_presence_of(:original_number_of_items) } diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index c3662b2d6cd83f..5dbc4a5aff5c69 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -56,16 +56,20 @@ end describe '.requires_approval?' do - subject { described_class.requires_approval?(input) } + subject { described_class.requires_approval?(input, attempt_ip: IPAddr.new('100.100.100.100')) } let(:input) { nil } context 'with a matching block requiring approval' do - before { Fabricate :email_domain_block, domain: input, allow_with_approval: true } + let!(:email_domain_block) { Fabricate :email_domain_block, domain: input, allow_with_approval: true } let(:input) { 'host.example' } - it { is_expected.to be true } + it 'returns true and records attempt' do + expect do + expect(subject).to be(true) + end.to change { email_domain_block.history.get(Date.current).accounts }.by(1) + end end context 'with a matching block not requiring approval' do diff --git a/spec/models/form/redirect_spec.rb b/spec/models/form/redirect_spec.rb index 4427a0bb8646bd..afd10c3ccb2647 100644 --- a/spec/models/form/redirect_spec.rb +++ b/spec/models/form/redirect_spec.rb @@ -29,4 +29,11 @@ end end end + + describe 'Normalizations' do + it { is_expected.to normalize(:acct).from(nil).to('') } + it { is_expected.to normalize(:acct).from(' @username ').to('username') } + + it { is_expected.to normalize(:current_username).from(' @username ').to('username') } + end end diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb index 03758ca6a8fecf..e1fed60b81b0cf 100644 --- a/spec/models/relay_spec.rb +++ b/spec/models/relay_spec.rb @@ -78,7 +78,7 @@ .to change { relay.reload.state }.to('idle') .and change { relay.reload.follow_activity_id }.to(be_nil) expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(match('Undo'), Account.representative.id, relay.inbox_url) + .to have_received(:perform_async).with(match_json_values(type: 'Undo'), Account.representative.id, relay.inbox_url) expect(DeliveryFailureTracker) .to have_received(:reset!).with(relay.inbox_url) end @@ -94,7 +94,7 @@ .to change { relay.reload.state }.to('pending') .and change { relay.reload.follow_activity_id }.to(be_present) expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(match('Follow'), Account.representative.id, relay.inbox_url) + .to have_received(:perform_async).with(match_json_values(type: 'Follow'), Account.representative.id, relay.inbox_url) expect(DeliveryFailureTracker) .to have_received(:reset!).with(relay.inbox_url) end diff --git a/spec/support/examples/models/concerns/status/visibility.rb b/spec/models/status/visibility_spec.rb similarity index 99% rename from spec/support/examples/models/concerns/status/visibility.rb rename to spec/models/status/visibility_spec.rb index e9b97dbf796abe..0b71f1c3012d37 100644 --- a/spec/support/examples/models/concerns/status/visibility.rb +++ b/spec/models/status/visibility_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.shared_examples 'Status::Visibility' do +RSpec.describe Status::Visibility do describe 'Validations' do context 'when status is a reblog' do subject { Fabricate.build :status, reblog: Fabricate(:status) } diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 03ebc5217b7192..afa85b1c3f0a8b 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -9,8 +9,6 @@ let(:bob) { Fabricate(:account, username: 'bob') } let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') } - it_behaves_like 'Status::Visibility' - describe '#local?' do it 'returns true when no remote URI is set' do expect(subject.local?).to be true diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 9fe723b3ba6296..61ef531fe188e2 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -103,6 +103,10 @@ def previous_name_error_message expect(subject.match('https://en.wikipedia.org/wiki/Google_LLC_v._Oracle_America,_Inc.#Decision')).to be_nil end + it 'matches a hashtag preceded by a non-break space' do + expect(subject.match('test #foo').to_s).to eq '#foo' + end + it 'matches #aesthetic' do expect(subject.match('this is #aesthetic').to_s).to eq '#aesthetic' end diff --git a/spec/models/terms_of_service_spec.rb b/spec/models/terms_of_service_spec.rb index 16cd5dd2ebfd17..99a7caa6131028 100644 --- a/spec/models/terms_of_service_spec.rb +++ b/spec/models/terms_of_service_spec.rb @@ -108,6 +108,24 @@ end end + describe '#usable_effective_date' do + subject { terms_of_service.usable_effective_date } + + let(:terms_of_service) { Fabricate.build(:terms_of_service, effective_date:) } + + context 'when effective_date value is set' do + let(:effective_date) { 5.days.ago } + + it { is_expected.to eq(effective_date.to_date) } + end + + context 'when effective_date value is not set' do + let(:effective_date) { nil } + + it { is_expected.to eq(Time.zone.today) } + end + end + describe '::current' do context 'when no terms exist' do it 'returns nil' do diff --git a/spec/support/examples/models/concerns/user/activity.rb b/spec/models/user/activity_spec.rb similarity index 89% rename from spec/support/examples/models/concerns/user/activity.rb rename to spec/models/user/activity_spec.rb index 7e647b694a9826..ca861507161640 100644 --- a/spec/support/examples/models/concerns/user/activity.rb +++ b/spec/models/user/activity_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.shared_examples 'User::Activity' do +RSpec.describe User::Activity do before { stub_const 'User::ACTIVE_DURATION', 7.days } describe 'Scopes' do @@ -11,14 +11,14 @@ describe '.signed_in_recently' do it 'returns users who have signed in during the recent period' do - expect(described_class.signed_in_recently) + expect(User.signed_in_recently) .to contain_exactly(recent_sign_in_user) end end describe '.not_signed_in_recently' do it 'returns users who have not signed in during the recent period' do - expect(described_class.not_signed_in_recently) + expect(User.not_signed_in_recently) .to contain_exactly(no_recent_sign_in_user) end end diff --git a/spec/support/examples/models/concerns/user/confirmation.rb b/spec/models/user/confirmation_spec.rb similarity index 97% rename from spec/support/examples/models/concerns/user/confirmation.rb rename to spec/models/user/confirmation_spec.rb index 19532254dc0e7f..6357c97c717cbc 100644 --- a/spec/support/examples/models/concerns/user/confirmation.rb +++ b/spec/models/user/confirmation_spec.rb @@ -2,21 +2,21 @@ require 'rails_helper' -RSpec.shared_examples 'User::Confirmation' do +RSpec.describe User::Confirmation do describe 'Scopes' do let!(:unconfirmed_user) { Fabricate :user, confirmed_at: nil } let!(:confirmed_user) { Fabricate :user, confirmed_at: Time.now.utc } describe '.confirmed' do it 'returns users who are confirmed' do - expect(described_class.confirmed) + expect(User.confirmed) .to contain_exactly(confirmed_user) end end describe '.unconfirmed' do it 'returns users who are not confirmed' do - expect(described_class.unconfirmed) + expect(User.unconfirmed) .to contain_exactly(unconfirmed_user) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4ea2e6a79ca649..35f0b987614475 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,8 +10,6 @@ let(:account) { Fabricate(:account, username: 'alice') } it_behaves_like 'two_factor_backupable' - it_behaves_like 'User::Activity' - it_behaves_like 'User::Confirmation' describe 'otp_secret' do it 'encrypts the saved value' do @@ -208,9 +206,13 @@ context 'with a new user' do let(:user) { Fabricate.build :user } + before { allow(ActivityTracker).to receive(:record) } + it 'does not persist the user' do expect { user.update_sign_in! } .to_not change(user, :persisted?).from(false) + expect(ActivityTracker) + .to_not have_received(:record).with('activity:logins', anything) end end end @@ -399,7 +401,32 @@ expect(user).to have_attributes(disabled: true) expect(redis) - .to have_received(:publish).with("timeline:system:#{user.account.id}", Oj.dump(event: :kill)).once + .to have_received(:publish).with("timeline:system:#{user.account.id}", { event: :kill }.to_json).once + end + end + + describe '#revoke_access!' do + subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) } + + let(:current_sign_in_at) { Time.zone.now } + + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } + + let(:redis_pipeline_stub) { instance_double(Redis::PipelinedConnection, publish: nil) } + + before do + allow(redis) + .to receive(:pipelined) + .and_yield(redis_pipeline_stub) + end + + it 'revokes tokens' do + user.revoke_access! + + expect(redis_pipeline_stub) + .to have_received(:publish).with("timeline:access_token:#{token.id}", { event: :kill }.to_json).once + + expect(token.reload.revoked?).to be true end end @@ -441,7 +468,7 @@ expect { web_push_subscription.reload } .to raise_error(ActiveRecord::RecordNotFound) expect(redis_pipeline_stub) - .to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once + .to have_received(:publish).with("timeline:access_token:#{access_token.id}", { event: :kill }.to_json).once end def remove_activated_sessions diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index 3c2cd3bac1b910..b1e93687b35e3a 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -93,4 +93,8 @@ end end end + + describe 'Delegations' do + it { is_expected.to delegate_method(:token).to(:access_token).with_prefix(:associated_access) } + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 48a3268bf04f4f..31487cc3ef2ede 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -138,12 +138,6 @@ def sign_in(resource, _deprecated = nil, scope: nil) example.run end - config.around(:each, type: :search) do |example| - Chewy.settings[:enabled] = true - example.run - Chewy.settings[:enabled] = false - end - config.before :each, type: :cli do stub_reset_connection_pools end diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb index e33afa53c9b2ee..fd013f38d67bd6 100644 --- a/spec/requests/activitypub/inboxes_spec.rb +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -156,6 +156,24 @@ def stub_followers_hash expect(response) .to have_http_status(401) end + + context 'when sending an unknown account' do + let(:unknown_actor) do + { + actor: 'https://unknown-actor.host', + object: 'https://unknown-actor.host', + type: 'Update', + } + end + let(:headers) { { 'CONTENT_TYPE' => 'application/json' } } + + it 'returns http accepted' do + post(inbox_path, params: unknown_actor.to_json, headers:) + + expect(response) + .to have_http_status(202) + end + end end end end diff --git a/spec/requests/activitypub/replies_spec.rb b/spec/requests/activitypub/replies_spec.rb index 02832c049a28f2..95bc1afe61dd1a 100644 --- a/spec/requests/activitypub/replies_spec.rb +++ b/spec/requests/activitypub/replies_spec.rb @@ -252,13 +252,13 @@ def inlined_replies response .parsed_body[:first][:items] - .select { |x| x.is_a?(Hash) } + .grep(Hash) end def remote_replies response .parsed_body[:first][:items] - .reject { |x| x.is_a?(Hash) } + .grep_v(Hash) end def parsed_uri_query_values(uri) diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index 75f30f5431af47..72cbe78272036b 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -185,7 +185,12 @@ expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body[:access_token]).to_not be_blank + expect(response.parsed_body) + .to include( + access_token: be_present, + created_at: be_a(Integer), + token_type: 'Bearer' + ) user = User.find_by(email: 'hello@world.tld') expect(user).to_not be_nil diff --git a/spec/requests/api/v1/donation_campaigns_spec.rb b/spec/requests/api/v1/donation_campaigns_spec.rb index 2ab3fb8e8a68e8..a9bc0c319046a1 100644 --- a/spec/requests/api/v1/donation_campaigns_spec.rb +++ b/spec/requests/api/v1/donation_campaigns_spec.rb @@ -52,6 +52,19 @@ end end + context 'when the donation campaign returns bad response' do + before do + stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: 'Cats & Dogs', status: 200) + end + + it 'handles the error and returns http empty' do + get '/api/v1/donation_campaigns', headers: headers + + expect(response) + .to have_http_status(204) + end + end + context 'when the donation campaign API returns a campaign' do let(:campaign_json) do { @@ -78,7 +91,7 @@ end before do - stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: Oj.dump(campaign_json), status: 200) + stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: campaign_json.to_json, status: 200) end it 'returns the expected campaign' do @@ -96,7 +109,7 @@ expect(Rails.cache.read("donation_campaign_request:#{seed}:en", raw: true)) .to eq 'campaign-1:en' - expect(Oj.load(Rails.cache.read('donation_campaign:campaign-1:en', raw: true))) + expect(JSON.parse(Rails.cache.read('donation_campaign:campaign-1:en', raw: true))) .to match(campaign_json) end end diff --git a/spec/requests/api/v1/markers_spec.rb b/spec/requests/api/v1/markers_spec.rb index 0e6ecc56855b23..17f19872f73670 100644 --- a/spec/requests/api/v1/markers_spec.rb +++ b/spec/requests/api/v1/markers_spec.rb @@ -7,8 +7,10 @@ describe 'GET /api/v1/markers' do before do - Fabricate(:marker, timeline: 'home', last_read_id: 123, user: user) - Fabricate(:marker, timeline: 'notifications', last_read_id: 456, user: user) + travel_to DateTime.parse('2026-03-15T12:34:56.789Z'), with_usec: true do + Fabricate(:marker, timeline: 'home', last_read_id: 123, user: user) + Fabricate(:marker, timeline: 'notifications', last_read_id: 456, user: user) + end get '/api/v1/markers', headers: headers, params: { timeline: %w(home notifications) } end @@ -23,6 +25,11 @@ notifications: include(last_read_id: '456') ) end + + it 'uses a specific style of IS08601 timestamps' do + expect(response.parsed_body) + .to include(home: include(updated_at: eq('2026-03-15T12:34:56.789Z'))) + end end describe 'POST /api/v1/markers' do diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb index b2c74b0191954d..7d0d1a3622465d 100644 --- a/spec/requests/api/v1/profiles_spec.rb +++ b/spec/requests/api/v1/profiles_spec.rb @@ -62,6 +62,7 @@ let(:params) do { avatar: fixture_file_upload('avatar.gif', 'image/gif'), + avatar_description: 'animated walking round cat', discoverable: true, display_name: "Alice Isn't Dead", header: fixture_file_upload('attachment.jpg', 'image/jpeg'), @@ -110,6 +111,7 @@ display_name: eq("Alice Isn't Dead"), note: 'Hello!', avatar: exist, + avatar_description: 'animated walking round cat', header: exist, attribution_domains: ['example.com'], fields: contain_exactly( diff --git a/spec/requests/api/v1/statuses/pins_spec.rb b/spec/requests/api/v1/statuses/pins_spec.rb index 66ed1510a48727..26e939cd516599 100644 --- a/spec/requests/api/v1/statuses/pins_spec.rb +++ b/spec/requests/api/v1/statuses/pins_spec.rb @@ -29,6 +29,8 @@ expect(response.parsed_body).to match( a_hash_including(id: status.id.to_s, pinned: true) ) + expect(ActivityPub::RawDistributionWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Add'), user.account.id) end end @@ -118,6 +120,8 @@ expect(response.parsed_body).to match( a_hash_including(id: status.id.to_s, pinned: false) ) + expect(ActivityPub::RawDistributionWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Remove'), user.account.id) end end diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb index a65291e9129c24..8876db657ddbbc 100644 --- a/spec/requests/api/v2/filters_spec.rb +++ b/spec/requests/api/v2/filters_spec.rb @@ -251,7 +251,7 @@ expect(keyword.reload.keyword).to eq 'updated' - expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", Oj.dump(event: :filters_changed)).once + expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", { event: :filters_changed }.to_json).once end end diff --git a/spec/requests/statuses/embed_spec.rb b/spec/requests/statuses/embed_spec.rb index 33c7ea192c80a3..7fc1b0125c8199 100644 --- a/spec/requests/statuses/embed_spec.rb +++ b/spec/requests/statuses/embed_spec.rb @@ -41,6 +41,8 @@ .to have_http_status(200) expect(response.parsed_body.at('body.embed')) .to be_present + expect(response.parsed_body.at('#mastodon-status')['data-props']) + .to eq({ locale: 'en', id: status.id.to_s }.to_json) expect(response.headers).to include( 'Vary' => 'Accept, Accept-Language, Cookie', 'Cache-Control' => include('public'), diff --git a/spec/requests/username_rewrites_spec.rb b/spec/requests/username_rewrites_spec.rb new file mode 100644 index 00000000000000..9e025a5f0dbafe --- /dev/null +++ b/spec/requests/username_rewrites_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Username URL rewrites' do + describe 'GET /users/:username' do + it 'redirects to at-username page variation' do + get '/users/username' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/following' do + it 'redirects to at-username page variation' do + get '/users/username/following' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/following') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/followers' do + it 'redirects to at-username page variation' do + get '/users/username/followers' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/followers') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end + + describe 'GET /users/:username/statuses/:id' do + it 'redirects to at-username page variation' do + get '/users/username/statuses/123456' + + expect(response) + .to have_http_status(301) + .and redirect_to('/@username/123456') + expect(response.headers) + .to include('Vary' => 'Origin, Accept') + end + end +end diff --git a/spec/search/models/concerns/account/search_spec.rb b/spec/search/models/concerns/account/search_spec.rb index de12161ef9f3a6..7b1fc695dbaf74 100644 --- a/spec/search/models/concerns/account/search_spec.rb +++ b/spec/search/models/concerns/account/search_spec.rb @@ -3,48 +3,48 @@ require 'rails_helper' RSpec.describe Account::Search do - describe 'a non-discoverable account becoming discoverable' do - let(:account) { Account.find_by(username: 'search_test_account_1') } - - context 'when picking a non-discoverable account' do - it 'its bio is not in the AccountsIndex' do - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to be_nil + describe 'Callbacks for discoverable changes' do + let(:results) { AccountsIndex.filter(term: { username: account.username }) } + + context 'with a non-discoverable account' do + let(:account) { Fabricate :account, discoverable: false, note: 'Account note' } + + context 'when looking for the non discoverable account' do + it 'is missing account bio in the AccountsIndex' do + expect(results.count) + .to eq(1) + expect(results.first.text) + .to be_nil + end end - end - - context 'when the non-discoverable account becomes discoverable' do - it 'its bio is added to the AccountsIndex' do - account.discoverable = true - account.save! - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to eq(account.note) + context 'when the account becomes discoverable' do + it 'has an account bio in the AccountsIndex' do + expect { account.update! discoverable: true } + .to change { results.first.text }.from(be_blank).to(account.note) + .and not_change(results, :count).from(1) + end end end - end - describe 'a discoverable account becoming non-discoverable' do - let(:account) { Account.find_by(username: 'search_test_account_0') } + describe 'with a discoverable account' do + let(:account) { Fabricate :account, discoverable: true } - context 'when picking an discoverable account' do - it 'has its bio in the AccountsIndex' do - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to eq(account.note) + context 'when looking for the account' do + it 'is present in the AccountsIndex' do + expect(results.count) + .to eq(1) + expect(results.first.text) + .to eq(account.note) + end end - end - - context 'when the discoverable account becomes non-discoverable' do - it 'its bio is removed from the AccountsIndex' do - account.discoverable = false - account.save! - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to be_nil + context 'when the account becomes non-discoverable' do + it 'is missing from the AccountsIndex' do + expect { account.update! discoverable: false } + .to change { results.first.text }.from(account.note).to(be_blank) + .and not_change(results, :count).from(1) + end end end end diff --git a/spec/search/models/concerns/account/statuses_search_spec.rb b/spec/search/models/concerns/account/statuses_search_spec.rb index bce1aecd7505f3..716d1b28ffebde 100644 --- a/spec/search/models/concerns/account/statuses_search_spec.rb +++ b/spec/search/models/concerns/account/statuses_search_spec.rb @@ -3,50 +3,55 @@ require 'rails_helper' RSpec.describe Account::StatusesSearch, :inline_jobs do - describe 'a non-indexable account becoming indexable' do - let(:account) { Account.find_by(username: 'search_test_account_1') } + describe 'Callbacks for indexable changes' do + let(:account) { Fabricate :account, indexable: } + let(:public_statuses_results) { PublicStatusesIndex.filter(term: { account_id: account.id }) } + let(:statuses_results) { StatusesIndex.filter(term: { account_id: account.id }) } + + before do + Fabricate :status, account:, visibility: :private + Fabricate :status, account:, visibility: :public + end - context 'when picking a non-indexable account' do - it 'has no statuses in the PublicStatusesIndex' do - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(0) - end + context 'with a non-indexable account' do + let(:indexable) { false } - it 'has statuses in the StatusesIndex' do - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) + context 'when looking for statuses from the account' do + it 'does not have public index statuses' do + expect(public_statuses_results.count) + .to eq(0) + expect(statuses_results.count) + .to eq(account.statuses.count) + end end - end - context 'when the non-indexable account becomes indexable' do - it 'adds the public statuses to the PublicStatusesIndex' do - account.indexable = true - account.save! - - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count) - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) + context 'when the non-indexable account becomes indexable' do + it 'does have public index statuses' do + expect { account.update! indexable: true } + .to change(public_statuses_results, :count).to(account.statuses.public_visibility.count) + .and not_change(statuses_results, :count).from(account.statuses.count) + end end end - end - describe 'an indexable account becoming non-indexable' do - let(:account) { Account.find_by(username: 'search_test_account_0') } + describe 'with an indexable account' do + let(:indexable) { true } - context 'when picking an indexable account' do - it 'has statuses in the PublicStatusesIndex' do - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count) + context 'when picking an indexable account' do + it 'does have public index statuses' do + expect(public_statuses_results.count) + .to eq(account.statuses.public_visibility.count) + expect(statuses_results.count) + .to eq(account.statuses.count) + end end - it 'has statuses in the StatusesIndex' do - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) - end - end - - context 'when the indexable account becomes non-indexable' do - it 'removes the statuses from the PublicStatusesIndex' do - account.indexable = false - account.save! - - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(0) - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) + context 'when the indexable account becomes non-indexable' do + it 'does not have public index statuses' do + expect { account.update! indexable: false } + .to change(public_statuses_results, :count).to(0) + .and not_change(statuses_results, :count).from(account.statuses.count) + end end end end diff --git a/spec/serializers/activitypub/accept_feature_request_serializer_spec.rb b/spec/serializers/activitypub/accept_feature_request_serializer_spec.rb new file mode 100644 index 00000000000000..6fc277f2734564 --- /dev/null +++ b/spec/serializers/activitypub/accept_feature_request_serializer_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::AcceptFeatureRequestSerializer do + include RoutingHelper + + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:collection) { Fabricate(:remote_collection) } + let(:record) do + Fabricate(:collection_item, + collection:, + uri: 'https://example.com/featured_items/1', + activity_uri: 'https://example.com/feature_requests/1', + state: :accepted) + end + let(:tag_manager) { ActivityPub::TagManager.instance } + + it 'returns expected attributes' do + expect(subject) + .to include( + 'id' => match("#accepts/feature_requests/#{record.id}"), + 'type' => 'Accept', + 'actor' => tag_manager.uri_for(record.account), + 'to' => tag_manager.uri_for(collection.account), + 'object' => 'https://example.com/feature_requests/1', + 'result' => ap_account_feature_authorization_url(record.account_id, record) + ) + end + end +end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index a987ae5a349f87..7f7f684134204a 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -98,6 +98,26 @@ def reply_items end end + context 'with tagged featured collections' do + let(:collection) { Fabricate(:collection) } + + before do + parent.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection)) + end + + it 'has the expected shape' do + expect(subject).to include({ + 'type' => 'Note', + 'tag' => include( + a_hash_including({ + 'type' => 'FeaturedCollection', + 'id' => ActivityPub::TagManager.instance.uri_for(collection), + }) + ), + }) + end + end + context 'with a quote' do let(:quoted_status) { Fabricate(:status) } let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, state: :accepted) } diff --git a/spec/serializers/activitypub/reject_feature_request_serializer_spec.rb b/spec/serializers/activitypub/reject_feature_request_serializer_spec.rb new file mode 100644 index 00000000000000..1e7aa0bcded9de --- /dev/null +++ b/spec/serializers/activitypub/reject_feature_request_serializer_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::RejectFeatureRequestSerializer do + include RoutingHelper + + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:collection) { Fabricate(:remote_collection) } + let(:record) do + Fabricate(:collection_item, + collection:, + uri: 'https://example.com/featured_items/1', + activity_uri: 'https://example.com/feature_requests/1', + state: :rejected) + end + let(:tag_manager) { ActivityPub::TagManager.instance } + + it 'returns expected attributes' do + expect(subject) + .to include( + 'id' => match("#rejects/feature_requests/#{record.id}"), + 'type' => 'Reject', + 'actor' => tag_manager.uri_for(record.account), + 'to' => tag_manager.uri_for(collection.account), + 'object' => 'https://example.com/feature_requests/1' + ) + end + end +end diff --git a/spec/serializers/rest/collection_serializer_spec.rb b/spec/serializers/rest/collection_serializer_spec.rb index 0fbe955b2ebf45..67ff464d187fa5 100644 --- a/spec/serializers/rest/collection_serializer_spec.rb +++ b/spec/serializers/rest/collection_serializer_spec.rb @@ -43,4 +43,22 @@ 'items' => [] ) end + + context 'when the collection is remote' do + let(:collection) { Fabricate(:remote_collection, description_html: '

    remote

    ') } + + it 'includes the html description' do + expect(subject) + .to include('description' => '

    remote

    ') + end + + context 'when the description contains unwanted HTML' do + let(:description_html) { '

    Nice people

    ' } + let(:collection) { Fabricate(:remote_collection, description_html:) } + + it 'scrubs the HTML' do + expect(subject).to include('description' => '

    Nice people

    ') + end + end + end end diff --git a/spec/serializers/rest/collection_with_accounts_serializer_spec.rb b/spec/serializers/rest/collection_with_accounts_serializer_spec.rb index 6a2b8683977500..205674737d8815 100644 --- a/spec/serializers/rest/collection_with_accounts_serializer_spec.rb +++ b/spec/serializers/rest/collection_with_accounts_serializer_spec.rb @@ -26,11 +26,14 @@ discoverable: false, tag:) end - - before do - accounts[1..2].each do |account| + let(:collection_items) do + accounts[1..2].map do |account| Fabricate(:collection_item, collection:, account:) end + end + + before do + collection_items collection.reload end @@ -56,4 +59,14 @@ ) expect(subject['accounts'].size).to eq 3 end + + context 'when collection includes pending items without account' do + let(:collection_items) do + [Fabricate(:collection_item, collection:, account: nil, object_uri: 'https://example.com/actor/1', state: :pending)] + end + + it 'renders successfully' do + expect(subject).to be_a Hash + end + end end diff --git a/spec/serializers/rest/status_serializer_spec.rb b/spec/serializers/rest/status_serializer_spec.rb index 510328c7fbc13e..0ad48417911952 100644 --- a/spec/serializers/rest/status_serializer_spec.rb +++ b/spec/serializers/rest/status_serializer_spec.rb @@ -92,5 +92,22 @@ ) end end + + context 'with a tagged collection' do + let(:collection) { Fabricate(:collection) } + + before do + status.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection)) + end + + it 'contains the tagged collection' do + expect(subject) + .to include( + 'tagged_collections' => [a_hash_including( + 'id' => collection.id.to_s + )] + ) + end + end end end diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb index 7d251641ee45db..6295d2f8ffffbe 100644 --- a/spec/services/account_search_service_spec.rb +++ b/spec/services/account_search_service_spec.rb @@ -86,4 +86,16 @@ expect(results).to eq [] end end + + context 'when elasticsearch is enabled', :search do + it 'returns matching accounts' do + account = Fabricate(:account, username: 'matchingusername') + + AccountsIndex.import! + + results = subject.call('match', nil, limit: 5) + + expect(results).to eq [account] + end + end end diff --git a/spec/services/activate_remote_statuses_service_spec.rb b/spec/services/activate_remote_statuses_service_spec.rb index e42e1a73461bfe..a36bed5c49517a 100644 --- a/spec/services/activate_remote_statuses_service_spec.rb +++ b/spec/services/activate_remote_statuses_service_spec.rb @@ -25,7 +25,7 @@ ], } end - let(:json) { Oj.dump(payload) } + let(:json) { payload.to_json } before do stub_request(:get, 'https://example.com/note').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' }) diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb index f0002bc388a9c7..71462e65e00a82 100644 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -75,11 +75,11 @@ shared_examples 'sets pinned posts' do before do - stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: status_json_pinned_known.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: status_json_pinned_unknown_inlined.to_json, headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: status_json_pinned_unknown_reachable.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: featured_with_null.to_json, headers: { 'Content-Type': 'application/activity+json' }) subject end @@ -101,7 +101,7 @@ let(:collection_or_uri) { actor.featured_collection_url } before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -122,7 +122,7 @@ context 'when the endpoint is a Collection' do before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -139,7 +139,7 @@ end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -148,11 +148,12 @@ let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - subject + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: status_json_pinned_unknown_reachable.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'sets expected posts as pinned posts' do + subject + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( 'https://example.com/account/pinned/unknown-reachable' ) @@ -175,7 +176,7 @@ end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -184,11 +185,12 @@ let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - subject + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: status_json_pinned_unknown_reachable.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'sets expected posts as pinned posts' do + subject + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( 'https://example.com/account/pinned/unknown-reachable' ) diff --git a/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb new file mode 100644 index 00000000000000..37a78dbf41b88c --- /dev/null +++ b/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FetchFeaturedCollectionsCollectionService do + subject { described_class.new } + + let(:account) { Fabricate(:remote_account, collections_url: 'https://example.com/account/featured_collections') } + let(:featured_collection_one) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/featured_collections/1', + 'type' => 'FeaturedCollection', + 'name' => 'Incredible people', + 'summary' => 'These are really amazing', + 'attributedTo' => account.uri, + 'sensitive' => false, + 'discoverable' => true, + 'totalItems' => 0, + } + end + let(:featured_collection_two) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/featured_collections/2', + 'type' => 'FeaturedCollection', + 'name' => 'Even cooler people', + 'summary' => 'These are just as amazing', + 'attributedTo' => account.uri, + 'sensitive' => false, + 'discoverable' => true, + 'totalItems' => 0, + } + end + let(:items) { [featured_collection_one, featured_collection_two] } + let(:collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Collection', + 'id' => account.collections_url, + 'items' => items, + } + end + + describe '#call' do + subject { described_class.new.call(account) } + + before do + stub_request(:get, account.collections_url) + .to_return_json(status: 200, body: collection_json, headers: { 'Content-Type': 'application/activity+json' }) + end + + shared_examples 'collection creation' do + it 'creates the expected collections' do + expect { subject }.to change(account.collections, :count).by(2) + expect(account.collections.pluck(:name)).to contain_exactly('Incredible people', 'Even cooler people') + end + end + + context 'when the endpoint is not paginated' do + context 'when all items are inlined' do + it_behaves_like 'collection creation' + end + + context 'when items are URIs' do + let(:items) { [featured_collection_one['id'], featured_collection_two['id']] } + + before do + [featured_collection_one, featured_collection_two].each do |featured_collection| + stub_request(:get, featured_collection['id']) + .to_return_json(status: 200, body: featured_collection, headers: { 'Content-Type': 'application/activity+json' }) + end + end + + it_behaves_like 'collection creation' + end + end + + context 'when the endpoint is a paginated Collection' do + let(:first_page) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'partOf' => account.collections_url, + 'id' => 'https://example.com/featured_collections/1/1', + 'items' => [featured_collection_one], + 'next' => second_page['id'], + } + end + let(:second_page) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'partOf' => account.collections_url, + 'id' => 'https://example.com/featured_collections/1/2', + 'items' => [featured_collection_two], + } + end + let(:collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Collection', + 'id' => account.collections_url, + 'first' => first_page['id'], + } + end + + before do + [first_page, second_page].each do |page| + stub_request(:get, page['id']) + .to_return_json(status: 200, body: page, headers: { 'Content-Type': 'application/activity+json' }) + end + end + + it_behaves_like 'collection creation' + end + end +end diff --git a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb index 59367b1e32c5e8..e8ffea546d66a8 100644 --- a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb @@ -38,7 +38,7 @@ describe '#call' do context 'when the endpoint is a Collection' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -46,7 +46,7 @@ context 'when the account already has featured tags' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) actor.featured_tags.create!(name: 'FoO') actor.featured_tags.create!(name: 'baz') @@ -67,7 +67,7 @@ end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -88,7 +88,7 @@ end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' diff --git a/spec/services/activitypub/fetch_references_service_spec.rb b/spec/services/activitypub/fetch_references_service_spec.rb index a53076306e1b36..19b5f475253a97 100644 --- a/spec/services/activitypub/fetch_references_service_spec.rb +++ b/spec/services/activitypub/fetch_references_service_spec.rb @@ -54,7 +54,7 @@ subject { described_class.new.call(status, collection_uri) } before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'first 8 items are returned' do @@ -83,7 +83,7 @@ subject { described_class.new.call(status, collection_uri) } before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'first 8 items are returned' do @@ -116,7 +116,7 @@ subject { described_class.new.call(status, collection_uri) } before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'first 8 items are returned' do diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index 653e30be34b598..4317b6d55c1014 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -38,8 +38,8 @@ before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -55,8 +55,8 @@ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -77,9 +77,9 @@ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}') end @@ -101,8 +101,8 @@ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -118,9 +118,9 @@ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}') end @@ -135,7 +135,7 @@ context 'with wrong id' do it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil + expect(subject.call('https://fake.address/@foo', prefetched_body: actor.to_json)).to be_nil end end end diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index 88c2e8331d76e9..3c1dec9a6d8060 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -38,8 +38,8 @@ before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -55,8 +55,8 @@ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -77,9 +77,9 @@ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}') end @@ -101,8 +101,8 @@ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -118,9 +118,9 @@ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}') end @@ -135,7 +135,7 @@ context 'with wrong id' do it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil + expect(subject.call('https://fake.address/@foo', prefetched_body: actor.to_json)).to be_nil end end end diff --git a/spec/services/activitypub/fetch_remote_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_remote_featured_collection_service_spec.rb new file mode 100644 index 00000000000000..f5cb9194b2e974 --- /dev/null +++ b/spec/services/activitypub/fetch_remote_featured_collection_service_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FetchRemoteFeaturedCollectionService do + subject { described_class.new } + + let(:account) { Fabricate(:remote_account) } + let(:uri) { 'https://example.com/featured_collections/1' } + let(:status) { 200 } + let(:response) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => uri, + 'type' => 'FeaturedCollection', + 'name' => 'Incredible people', + 'summary' => 'These are really amazing', + 'attributedTo' => account.uri, + 'sensitive' => false, + 'discoverable' => true, + 'totalItems' => 0, + } + end + + before do + stub_request(:get, uri) + .to_return_json( + status: status, + body: response, + headers: { 'Content-Type' => 'application/activity+json' } + ) + end + + context 'when collection does not exist' do + it 'creates a new collection' do + collection = nil + expect { collection = subject.call(uri) }.to change(Collection, :count).by(1) + + expect(collection.uri).to eq uri + expect(collection.name).to eq 'Incredible people' + end + end + + context 'when collection already exists' do + let!(:collection) do + Fabricate(:remote_collection, account:, uri:, name: 'temp') + end + + it 'returns the existing collection' do + expect do + expect(subject.call(uri)).to eq collection + end.to_not change(Collection, :count) + end + end + + context 'when the URI can not be fetched' do + let(:response) { nil } + let(:status) { 404 } + + it 'returns `nil`' do + expect(subject.call(uri)).to be_nil + end + end +end diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index bd414be76d47a7..74378ea5fa6754 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -50,8 +50,8 @@ end before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -60,7 +60,7 @@ context 'when the key is a sub-object from the actor' do before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -72,7 +72,7 @@ let(:public_key_id) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: key_json.merge({ '@context': ['https://w3id.org/security/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -85,7 +85,7 @@ let(:actor_public_key) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the nil' do diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 6afee5f25ef263..d7196d8dab84f0 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -11,7 +11,7 @@ let(:follower) { Fabricate(:account, username: 'alice') } let(:follow) { nil } - let(:response) { { body: Oj.dump(object), headers: { 'content-type': 'application/activity+json' } } } + let(:response) { { body: object.to_json, headers: { 'content-type': 'application/activity+json' } } } let(:existing_status) { nil } let(:note) do @@ -369,7 +369,7 @@ end it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } + expect { subject.call(object[:id], prefetched_body: object.to_json) } .to change { sender.statuses.count }.by_at_least(2) .and change { sender.statuses.count }.by_at_most(3) end @@ -419,7 +419,7 @@ end it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } + expect { subject.call(object[:id], prefetched_body: object.to_json) } .to change { sender.statuses.count }.by_at_least(2) .and change { sender.statuses.count }.by_at_most(3) end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index 36159309f1de6a..f84a44f93126d9 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -58,7 +58,7 @@ context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -93,7 +93,7 @@ context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -132,7 +132,7 @@ context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index f090b357d14eae..2cc7c733a58f6c 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -454,6 +454,37 @@ end end + context 'with collection URIs', feature: :collections_federation do + let(:payload) do + { + 'id' => 'https://foo.test', + 'type' => 'Actor', + 'inbox' => 'https://foo.test/inbox', + 'featured' => 'https://foo.test/featured', + 'followers' => 'https://foo.test/followers', + 'following' => 'https://foo.test/following', + 'featuredCollections' => 'https://foo.test/featured_collections', + } + end + + before do + stub_request(:get, %r{^https://foo\.test/follow}) + .to_return(status: 200, body: '', headers: {}) + end + + it 'parses and sets the URIs, queues jobs to synchronize' do + account = subject.call('alice', 'example.com', payload) + + expect(account.featured_collection_url).to eq 'https://foo.test/featured' + expect(account.followers_url).to eq 'https://foo.test/followers' + expect(account.following_url).to eq 'https://foo.test/following' + expect(account.collections_url).to eq 'https://foo.test/featured_collections' + + expect(ActivityPub::SynchronizeFeaturedCollectionWorker).to have_enqueued_sidekiq_job + expect(ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker).to have_enqueued_sidekiq_job + end + end + context 'with attribution domains' do let(:payload) do { @@ -474,6 +505,30 @@ end end + context 'with profile settings' do + let(:payload) do + { + id: 'https://foo.test', + type: 'Actor', + inbox: 'https://foo.test/inbox', + showMedia: true, + showRepliesInMedia: false, + showFeatured: false, + }.with_indifferent_access + end + + it 'sets the profile settings as expected' do + account = subject.call('alice', 'example.com', payload) + + expect(account) + .to have_attributes( + show_media: true, + show_media_replies: false, + show_featured: false + ) + end + end + context 'with inlined feature collection' do let(:payload) do { diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index 74df0f91063f53..c5fa38c641fdc1 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -21,7 +21,7 @@ } end - let(:json) { Oj.dump(payload) } + let(:json) { payload.to_json } describe '#call' do context 'when actor is suspended' do diff --git a/spec/services/activitypub/process_featured_collection_service_spec.rb b/spec/services/activitypub/process_featured_collection_service_spec.rb new file mode 100644 index 00000000000000..c68120bf0dfc0b --- /dev/null +++ b/spec/services/activitypub/process_featured_collection_service_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::ProcessFeaturedCollectionService do + subject { described_class.new } + + let(:account) { Fabricate(:remote_account) } + let(:summary) { '

    A list of remote actors you should follow.

    ' } + let(:base_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/featured_collections/1', + 'type' => 'FeaturedCollection', + 'attributedTo' => account.uri, + 'name' => 'Good people from other servers', + 'sensitive' => false, + 'discoverable' => true, + 'topic' => { + 'type' => 'Hashtag', + 'name' => '#people', + }, + 'published' => '2026-03-09T15:19:25Z', + 'totalItems' => 2, + 'orderedItems' => [ + 'https://example.com/featured_items/1', + 'https://example.com/featured_items/2', + ], + } + end + let(:featured_collection_json) { base_json.merge('summary' => summary) } + + context "when the collection's URI does not match the account's" do + let(:non_matching_account) { Fabricate(:remote_account, domain: 'other.example.com') } + + it 'does not create a collection and returns `nil`' do + expect do + expect(subject.call(non_matching_account, featured_collection_json)).to be_nil + end.to_not change(Collection, :count) + end + end + + context 'when URIs match up' do + it 'creates a collection and queues jobs to handle its items' do + expect { subject.call(account, featured_collection_json) }.to change(account.collections, :count).by(1) + + new_collection = account.collections.last + expect(new_collection.uri).to eq 'https://example.com/featured_collections/1' + expect(new_collection.name).to eq 'Good people from other servers' + expect(new_collection.description_html).to eq '

    A list of remote actors you should follow.

    ' + expect(new_collection.sensitive).to be false + expect(new_collection.discoverable).to be true + expect(new_collection.tag.formatted_name).to eq '#people' + + expect(ActivityPub::ProcessFeaturedItemWorker).to have_enqueued_sidekiq_job.exactly(2).times + end + end + + context 'when the json includes a summary map' do + let(:featured_collection_json) do + base_json.merge({ + 'summaryMap' => { + 'en' => summary, + }, + }) + end + + it 'sets language and summary correctly' do + expect { subject.call(account, featured_collection_json) }.to change(account.collections, :count).by(1) + + new_collection = account.collections.last + expect(new_collection.language).to eq 'en' + expect(new_collection.description_html).to eq '

    A list of remote actors you should follow.

    ' + end + end + + context 'when the collection already exists' do + let(:collection) { Fabricate(:remote_collection, account:, uri: base_json['id'], name: 'placeholder') } + + before do + Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/1') + Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/3') + end + + it 'updates the existing collection, removes the item that no longer exists and queues a jobs to fetch the other items' do + expect { subject.call(account, featured_collection_json) } + .to change(collection.collection_items, :count).by(-1) + + expect(collection.reload.name).to eq 'Good people from other servers' + expect(ActivityPub::ProcessFeaturedItemWorker).to have_enqueued_sidekiq_job.exactly(2).times + end + + context 'when the updated collection no longer contains any items' do + let(:featured_collection_json) do + base_json.merge({ + 'summary' => summary, + 'totalItems' => 0, + 'orderedItems' => nil, + }) + end + + it 'removes all items' do + expect { subject.call(account, featured_collection_json) } + .to change(collection.collection_items, :count).by(-2) + end + end + end +end diff --git a/spec/services/activitypub/process_featured_item_service_spec.rb b/spec/services/activitypub/process_featured_item_service_spec.rb new file mode 100644 index 00000000000000..bfcea2a5da4a54 --- /dev/null +++ b/spec/services/activitypub/process_featured_item_service_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::ProcessFeaturedItemService do + include RoutingHelper + + subject { described_class.new } + + let(:collection) { Fabricate(:remote_collection, uri: 'https://other.example.com/collection/1') } + let(:position) { 3 } + let(:featured_object_uri) { 'https://example.com/actor/1' } + let(:feature_authorization_uri) { 'https://example.com/auth/1' } + let(:featured_item_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://other.example.com/featured_item/1', + 'type' => 'FeaturedItem', + 'featuredObject' => featured_object_uri, + 'featuredObjectType' => 'Person', + 'featureAuthorization' => feature_authorization_uri, + } + end + let(:stubbed_service) do + instance_double(ActivityPub::VerifyFeaturedItemService, call: true) + end + + before do + allow(ActivityPub::VerifyFeaturedItemService).to receive(:new).and_return(stubbed_service) + end + + shared_examples 'non-matching URIs' do + context "when the item's URI does not match the collection's" do + let(:collection) { Fabricate(:remote_collection) } + + it 'does not create a collection item and returns `nil`' do + expect do + expect(subject.call(collection, object, position:)).to be_nil + end.to_not change(CollectionItem, :count) + end + end + end + + context 'when the collection item is inlined' do + let(:object) { featured_item_json } + + it_behaves_like 'non-matching URIs' + + context 'when item does not yet exist' do + context 'when a position is given' do + it 'creates and verifies the item' do + expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1) + + expect(stubbed_service).to have_received(:call) + + new_item = collection.collection_items.last + expect(new_item.object_uri).to eq 'https://example.com/actor/1' + expect(new_item.approval_uri).to be_nil + expect(new_item.position).to eq 3 + end + end + + context 'when no position is given' do + it 'creates the item' do + expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1) + new_item = collection.collection_items.last + + expect(new_item.position).to eq 1 + end + end + end + + context 'when item exists at a different position' do + let!(:collection_item) do + Fabricate(:collection_item, collection:, uri: featured_item_json['id'], position: 2) + end + + it 'updates the position' do + expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count) + + expect(collection_item.reload.position).to eq 3 + end + end + + context 'when an item exists for a local featured account' do + let!(:collection_item) do + Fabricate(:collection_item, collection:, state: :accepted) + end + let(:featured_object_uri) { ActivityPub::TagManager.instance.uri_for(collection_item.account) } + let(:feature_authorization_uri) { ap_account_feature_authorization_url(collection_item.account_id, collection_item) } + + it 'updates the URI of the existing record' do + expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count) + expect(collection_item.reload.uri).to eq 'https://other.example.com/featured_item/1' + end + end + end + + context 'when only the id of the collection item is given' do + let(:object) { featured_item_json['id'] } + let(:featured_item_request) do + stub_request(:get, object) + .to_return_json( + status: 200, + body: featured_item_json, + headers: { 'Content-Type' => 'application/activity+json' } + ) + end + + before do + featured_item_request + end + + it_behaves_like 'non-matching URIs' + + it 'fetches the collection item' do + expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1) + + expect(featured_item_request).to have_been_requested + + new_item = collection.collection_items.last + expect(new_item.object_uri).to eq 'https://example.com/actor/1' + expect(new_item.approval_uri).to be_nil + end + end +end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 7be68f00ec794d..1fd542b2499880 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -14,6 +14,7 @@ { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, { type: 'Mention', href: bogus_mention }, + { type: 'FeaturedCollection', id: ActivityPub::TagManager.instance.uri_for(featured_collection) }, ] end let(:content) { 'Hello universe' } @@ -29,10 +30,11 @@ } end let(:payload_override) { {} } - let(:json) { Oj.load(Oj.dump(payload.merge(payload_override))) } + let(:json) { JSON.parse(payload.merge(payload_override).to_json) } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } + let(:featured_collection) { Fabricate(:collection) } let(:mentions) { [] } let(:tags) { [] } @@ -282,6 +284,16 @@ end end + context 'when originally without tagged objects' do + before do + subject.call(status, json, json) + end + + it 'updates tags' do + expect(status.tagged_objects.reload.map(&:object)).to contain_exactly(featured_collection) + end + end + context 'when originally without tags' do before do subject.call(status, json, json) @@ -863,8 +875,8 @@ } end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -889,7 +901,11 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'updates the approval URI and verifies the quote' do @@ -928,8 +944,8 @@ } end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -954,7 +970,11 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'updates the approval URI and verifies the quote' do @@ -1138,8 +1158,8 @@ } end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1164,7 +1184,11 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'updates the approval URI and verifies the quote' do @@ -1203,8 +1227,8 @@ } end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1229,13 +1253,17 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } end - it 'updates the approval URI but does not verify the quote' do + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) + end + + it 'does not update the approval URI and does not verify the quote' do expect { subject.call(status, json, json) } .to change(status, :quote).from(nil) - expect(status.quote.approval_uri).to eq approval_uri + expect(status.quote.approval_uri).to be_nil expect(status.quote.state).to_not eq 'accepted' expect(status.quote.quoted_status).to be_nil end @@ -1482,8 +1510,8 @@ } end - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1508,7 +1536,11 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'updates the URI and unverifies the quote' do @@ -1590,8 +1622,8 @@ } end - before do - stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + let(:quote_authorization_json) do + { '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1616,7 +1648,11 @@ attributedTo: ActivityPub::TagManager.instance.uri_for(second_quoted_status.account), interactingObject: ActivityPub::TagManager.instance.uri_for(status), interactionTarget: ActivityPub::TagManager.instance.uri_for(second_quoted_status), - })) + } + end + + before do + stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: quote_authorization_json.to_json) end it 'updates the URI and unverifies the quote' do diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index 813658d149b889..5d577bd0069d45 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -55,7 +55,7 @@ context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -72,7 +72,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -93,7 +93,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -102,31 +102,31 @@ context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do before do stub_request(:get, 'https://example.com/partial-followers') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: 'https://example.com/partial-followers', first: 'https://example.com/partial-followers/1', - })) + }.to_json) stub_request(:get, 'https://example.com/partial-followers/1') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/1', partOf: 'https://example.com/partial-followers', next: 'https://example.com/partial-followers/2', items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, - })) + }.to_json) stub_request(:get, 'https://example.com/partial-followers/2') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/2', partOf: 'https://example.com/partial-followers', items: ActivityPub::TagManager.instance.uri_for(mallory), - })) + }.to_json) end it_behaves_like 'synchronizes followers' @@ -135,22 +135,22 @@ context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do before do stub_request(:get, 'https://example.com/partial-followers') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: 'https://example.com/partial-followers', first: 'https://example.com/partial-followers/1', - })) + }.to_json) stub_request(:get, 'https://example.com/partial-followers/1') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/1', partOf: 'https://example.com/partial-followers', next: 'https://example.com/partial-followers/2', items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, - })) + }.to_json) stub_request(:get, 'https://example.com/partial-followers/2') .to_return(status: 404) @@ -185,7 +185,7 @@ before do stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1) - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'confirms pending follow request but does not remove extra followers' do @@ -213,7 +213,7 @@ context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -230,7 +230,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -251,7 +251,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -263,7 +263,7 @@ context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do @@ -286,7 +286,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do @@ -313,7 +313,7 @@ end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: payload.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do diff --git a/spec/services/activitypub/verify_featured_item_service_spec.rb b/spec/services/activitypub/verify_featured_item_service_spec.rb new file mode 100644 index 00000000000000..f0f1661b6dccc4 --- /dev/null +++ b/spec/services/activitypub/verify_featured_item_service_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::VerifyFeaturedItemService do + subject { described_class.new } + + let(:collection) { Fabricate(:remote_collection) } + let(:collection_item) do + Fabricate(:collection_item, + collection:, + account: nil, + state: :pending, + uri: 'https://other.example.com/items/1', + object_uri: 'https://example.com/actor/1') + end + let(:approval_uri) { 'https://example.com/auth/1' } + let(:verification_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'FeatureAuthorization', + 'id' => approval_uri, + 'interactionTarget' => 'https://example.com/actor/1', + 'interactingObject' => collection.uri, + } + end + let(:verification_request) do + stub_request(:get, 'https://example.com/auth/1') + .to_return_json( + status: 200, + body: verification_json, + headers: { 'Content-Type' => 'application/activity+json' } + ) + end + let(:featured_account) { Fabricate(:remote_account, uri: 'https://example.com/actor/1') } + + before { verification_request } + + context 'when the authorization can be verified' do + context 'when the featured account is known' do + before { featured_account } + + it 'verifies and creates the item' do + subject.call(collection_item, approval_uri) + + expect(verification_request).to have_been_requested + + expect(collection_item.account_id).to eq featured_account.id + expect(collection_item).to be_accepted + expect(collection_item.approval_uri).to eq approval_uri + end + end + + context 'when the featured account is not known' do + let(:stubbed_service) { instance_double(ActivityPub::FetchRemoteAccountService) } + + before do + allow(stubbed_service).to receive(:call).with('https://example.com/actor/1', request_id: nil) { featured_account } + allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(stubbed_service) + end + + it 'fetches the actor and creates the item' do + subject.call(collection_item, approval_uri) + + expect(stubbed_service).to have_received(:call) + expect(verification_request).to have_been_requested + + expect(collection_item.account_id).to eq featured_account.id + expect(collection_item).to be_accepted + expect(collection_item.approval_uri).to eq approval_uri + end + end + end + + context 'when the authorization cannot be verified' do + let(:verification_request) do + stub_request(:get, 'https://example.com/auth/1') + .to_return(status: 404) + end + + it 'creates item without attached account and in proper state' do + subject.call(collection_item, approval_uri) + + expect(collection_item.account_id).to be_nil + expect(collection_item).to be_rejected + end + end +end diff --git a/spec/services/activitypub/verify_quote_service_spec.rb b/spec/services/activitypub/verify_quote_service_spec.rb index 94b9e33ed3b1a0..3ec00bad076207 100644 --- a/spec/services/activitypub/verify_quote_service_spec.rb +++ b/spec/services/activitypub/verify_quote_service_spec.rb @@ -9,268 +9,284 @@ let(:quoted_account) { Fabricate(:account, domain: 'b.example.com') } let(:quoted_status) { Fabricate(:status, account: quoted_account) } let(:status) { Fabricate(:status, account: account) } - let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri_record) } - context 'with an unfetchable approval URI' do - let(:approval_uri) { 'https://b.example.com/approvals/1234' } + shared_examples 'common behavior' do + context 'with an unfetchable approval URI' do + let(:approval_uri) { 'https://b.example.com/approvals/1234' } - before do - stub_request(:get, approval_uri) - .to_return(status: 404) - end - - context 'with an already-fetched post' do - it 'does not update the status' do - expect { subject.call(quote) } - .to change(quote, :state).to('rejected') + before do + stub_request(:get, approval_uri) + .to_return(status: 404) end - end - - context 'with an already-verified quote' do - let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } - it 'rejects the quote' do - expect { subject.call(quote) } - .to change(quote, :state).to('revoked') + context 'with an already-fetched post' do + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to change(quote, :state).to('rejected') + end end - end - end - - context 'with an approval URI' do - let(:approval_uri) { 'https://b.example.com/approvals/1234' } - let(:approval_type) { 'QuoteAuthorization' } - let(:approval_id) { approval_uri } - let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(quoted_account) } - let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(status) } - let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(quoted_status) } + context 'with an already-verified quote' do + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri_record, state: :accepted) } - let(:json) do - { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', - gts: 'https://gotosocial.org/ns#', - interactionPolicy: { - '@id': 'gts:interactionPolicy', - '@type': '@id', - }, - interactingObject: { - '@id': 'gts:interactingObject', - '@type': '@id', - }, - interactionTarget: { - '@id': 'gts:interactionTarget', - '@type': '@id', - }, - }, - ], - type: approval_type, - id: approval_id, - attributedTo: approval_attributed_to, - interactingObject: approval_interacting_object, - interactionTarget: approval_interaction_target, - }.with_indifferent_access - end - - before do - stub_request(:get, approval_uri) - .to_return(status: 200, body: Oj.dump(json), headers: { 'Content-Type': 'application/activity+json' }) - end - - context 'with a valid activity for already-fetched posts' do - it 'updates the status' do - expect { subject.call(quote) } - .to change(quote, :state).to('accepted') - - expect(a_request(:get, approval_uri)) - .to have_been_made.once + it 'rejects the quote' do + expect { subject.call(quote, approval_uri_arg) } + .to change(quote, :state).to('revoked') + end end end - context 'with a valid activity for a post that cannot be fetched but is passed as fetched_quoted_object' do - let(:quoted_status) { nil } + context 'with an approval URI' do + let(:approval_uri) { 'https://b.example.com/approvals/1234' } - let(:approval_interaction_target) { 'https://b.example.com/unknown-quoted' } - let(:prefetched_object) do + let(:approval_type) { 'QuoteAuthorization' } + let(:approval_id) { approval_uri } + let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(quoted_account) } + let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(status) } + let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(quoted_status) } + + let(:json) do { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: 'https://b.example.com/unknown-quoted', - to: 'https://www.w3.org/ns/activitystreams#Public', - attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account), - content: 'previously unknown post', + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: approval_type, + id: approval_id, + attributedTo: approval_attributed_to, + interactingObject: approval_interacting_object, + interactionTarget: approval_interaction_target, }.with_indifferent_access end before do - stub_request(:get, 'https://b.example.com/unknown-quoted') - .to_return(status: 404) + stub_request(:get, approval_uri) + .to_return(status: 200, body: json.to_json, headers: { 'Content-Type': 'application/activity+json' }) end - it 'updates the status' do - expect { subject.call(quote, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted', prefetched_quoted_object: prefetched_object) } - .to change(quote, :state).to('accepted') + context 'with a valid activity for already-fetched posts' do + it 'updates the status' do + expect { subject.call(quote, approval_uri_arg) } + .to change(quote, :state).to('accepted') - expect(a_request(:get, approval_uri)) - .to have_been_made.once - - expect(quote.reload.quoted_status.content).to eq 'previously unknown post' + expect(a_request(:get, approval_uri)) + .to have_been_made.once + end end - end - - context 'with a valid activity for a post that cannot be fetched but is inlined' do - let(:quoted_status) { nil } - let(:approval_interaction_target) do - { - type: 'Note', - id: 'https://b.example.com/unknown-quoted', - to: 'https://www.w3.org/ns/activitystreams#Public', - attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account), - content: 'previously unknown post', - } - end + context 'with a valid activity for a post that cannot be fetched but is passed as fetched_quoted_object' do + let(:quoted_status) { nil } - before do - stub_request(:get, 'https://b.example.com/unknown-quoted') - .to_return(status: 404) + let(:approval_interaction_target) { 'https://b.example.com/unknown-quoted' } + let(:prefetched_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: 'https://b.example.com/unknown-quoted', + to: 'https://www.w3.org/ns/activitystreams#Public', + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account), + content: 'previously unknown post', + }.with_indifferent_access + end + + before do + stub_request(:get, 'https://b.example.com/unknown-quoted') + .to_return(status: 404) + end + + it 'updates the status' do + expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted', prefetched_quoted_object: prefetched_object) } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + + expect(quote.reload.quoted_status.content).to eq 'previously unknown post' + end end - it 'updates the status' do - expect { subject.call(quote, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted') } - .to change(quote, :state).to('accepted') + context 'with a valid activity for a post that cannot be fetched but is inlined' do + let(:quoted_status) { nil } - expect(a_request(:get, approval_uri)) - .to have_been_made.once - - expect(quote.reload.quoted_status.content).to eq 'previously unknown post' + let(:approval_interaction_target) do + { + type: 'Note', + id: 'https://b.example.com/unknown-quoted', + to: 'https://www.w3.org/ns/activitystreams#Public', + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account), + content: 'previously unknown post', + } + end + + before do + stub_request(:get, 'https://b.example.com/unknown-quoted') + .to_return(status: 404) + end + + it 'updates the status' do + expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted') } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + + expect(quote.reload.quoted_status.content).to eq 'previously unknown post' + end end - end - context 'with a valid activity for a post that cannot be fetched and is inlined from an untrusted source' do - let(:quoted_status) { nil } + context 'with a valid activity for a post that cannot be fetched and is inlined from an untrusted source' do + let(:quoted_status) { nil } - let(:approval_interaction_target) do - { - type: 'Note', - id: 'https://example.com/unknown-quoted', - to: 'https://www.w3.org/ns/activitystreams#Public', - attributedTo: ActivityPub::TagManager.instance.uri_for(account), - content: 'previously unknown post', - } + let(:approval_interaction_target) do + { + type: 'Note', + id: 'https://example.com/unknown-quoted', + to: 'https://www.w3.org/ns/activitystreams#Public', + attributedTo: ActivityPub::TagManager.instance.uri_for(account), + content: 'previously unknown post', + } + end + + before do + stub_request(:get, 'https://example.com/unknown-quoted') + .to_return(status: 404) + end + + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://example.com/unknown-quoted') } + .to not_change(quote, :state) + .and not_change(quote, :quoted_status) + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + end end - before do - stub_request(:get, 'https://example.com/unknown-quoted') - .to_return(status: 404) + context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do + it 'updates the status without fetching the activity' do + expect { subject.call(quote, approval_uri_arg, prefetched_approval: JSON.generate(json)) } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to_not have_been_made + end end - it 'does not update the status' do - expect { subject.call(quote, fetchable_quoted_uri: 'https://example.com/unknown-quoted') } - .to not_change(quote, :state) - .and not_change(quote, :quoted_status) + context 'with an unverifiable approval' do + let(:approval_uri) { 'https://evil.com/approvals/1234' } - expect(a_request(:get, approval_uri)) - .to have_been_made.once + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end - end - context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do - it 'updates the status without fetching the activity' do - expect { subject.call(quote, prefetched_approval: Oj.dump(json)) } - .to change(quote, :state).to('accepted') + context 'with an invalid approval document because of a mismatched ID' do + let(:approval_id) { 'https://evil.com/approvals/1234' } - expect(a_request(:get, approval_uri)) - .to_not have_been_made + it 'does not accept the quote' do + # NOTE: maybe we want to skip that instead of rejecting it? + expect { subject.call(quote, approval_uri_arg) } + .to change(quote, :state).to('rejected') + end end - end - context 'with an unverifiable approval' do - let(:approval_uri) { 'https://evil.com/approvals/1234' } + context 'with an approval from the wrong account' do + let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(Fabricate(:account, domain: 'b.example.com')) } - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end - end - context 'with an invalid approval document because of a mismatched ID' do - let(:approval_id) { 'https://evil.com/approvals/1234' } + context 'with an approval for the wrong quoted post' do + let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: quoted_account)) } - it 'does not accept the quote' do - # NOTE: maybe we want to skip that instead of rejecting it? - expect { subject.call(quote) } - .to change(quote, :state).to('rejected') + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end - end - context 'with an approval from the wrong account' do - let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(Fabricate(:account, domain: 'b.example.com')) } + context 'with an approval for the wrong quote post' do + let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: account)) } - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end - end - context 'with an approval for the wrong quoted post' do - let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: quoted_account)) } + context 'with an approval of the wrong type' do + let(:approval_type) { 'ReplyAuthorization' } - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end end - context 'with an approval for the wrong quote post' do - let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: account)) } + context 'with fast-track authorizations' do + let(:approval_uri) { nil } - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + context 'without any fast-track condition' do + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state) + end end - end - context 'with an approval of the wrong type' do - let(:approval_type) { 'ReplyAuthorization' } + context 'when the account and the quoted account are the same' do + let(:quoted_account) { account } - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + it 'updates the status' do + expect { subject.call(quote, approval_uri_arg) } + .to change(quote, :state).to('accepted') + end end - end - end - context 'with fast-track authorizations' do - let(:approval_uri) { nil } + context 'when the account is mentioned by the quoted post' do + before do + quoted_status.mentions << Mention.new(account: account) + end - context 'without any fast-track condition' do - it 'does not update the status' do - expect { subject.call(quote) } - .to_not change(quote, :state) + it 'does not update the status' do + expect { subject.call(quote, approval_uri_arg) } + .to_not change(quote, :state).from('pending') + end end end + end - context 'when the account and the quoted account are the same' do - let(:quoted_account) { account } + context 'when approval URI is passed as argument' do + let(:approval_uri_arg) { approval_uri } + let(:approval_uri_record) { nil } - it 'updates the status' do - expect { subject.call(quote) } - .to change(quote, :state).to('accepted') - end - end + it_behaves_like 'common behavior' + end - context 'when the account is mentioned by the quoted post' do - before do - quoted_status.mentions << Mention.new(account: account) - end + context 'when approval URI is stored in the record (legacy)' do + let(:approval_uri_arg) { nil } + let(:approval_uri_record) { approval_uri } - it 'does not the status' do - expect { subject.call(quote) } - .to_not change(quote, :state).from('pending') - end - end + it_behaves_like 'common behavior' end end diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 1434df49db2451..19873c1ae53829 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -12,7 +12,9 @@ let!(:limited_status) { Fabricate(:status, account: user.account, text: 'sec mutual', visibility: :limited, limited_scope: :mutual) } let!(:reblog_status) { Fabricate(:status, account: user.account, reblog_of_id: Fabricate(:status).id) } let!(:favourite) { Fabricate(:favourite, account: user.account) } + let!(:more_favourite) { Fabricate(:favourite, account: user.account) } let!(:bookmark) { Fabricate(:bookmark, account: user.account) } + let!(:more_bookmark) { Fabricate(:bookmark, account: user.account) } let!(:backup) { Fabricate(:backup, user: user) } def read_zip_file(backup, filename) @@ -58,7 +60,7 @@ def process_backup def expect_outbox_export body = export_json_raw(:outbox) - json = Oj.load(body) + json = JSON.parse(body) aggregate_failures do expect(body.scan('@context').count).to eq 1 @@ -75,21 +77,27 @@ def expect_outbox_export end def expect_likes_export - json = export_json(:likes) - - aggregate_failures do - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)] - end + expect(export_json(:likes).deep_symbolize_keys) + .to include( + id: 'likes.json', + type: 'OrderedCollection', + orderedItems: contain_exactly( + ActivityPub::TagManager.instance.uri_for(favourite.status), + ActivityPub::TagManager.instance.uri_for(more_favourite.status) + ) + ) end def expect_bookmarks_export - json = export_json(:bookmarks) - - aggregate_failures do - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)] - end + expect(export_json(:bookmarks).deep_symbolize_keys) + .to include( + id: 'bookmarks.json', + type: 'OrderedCollection', + orderedItems: contain_exactly( + ActivityPub::TagManager.instance.uri_for(bookmark.status), + ActivityPub::TagManager.instance.uri_for(more_bookmark.status) + ) + ) end def export_json_raw(type) @@ -97,7 +105,7 @@ def export_json_raw(type) end def export_json(type) - Oj.load(export_json_raw(type)) + JSON.parse(export_json_raw(type)) end def include_create_item(status) diff --git a/spec/services/delete_collection_item_service_spec.rb b/spec/services/delete_collection_item_service_spec.rb index 099671e8fc5ee1..bdd37ad4a82c5e 100644 --- a/spec/services/delete_collection_item_service_spec.rb +++ b/spec/services/delete_collection_item_service_spec.rb @@ -18,5 +18,13 @@ expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job end + + context 'when `revoke` is set to true' do + it 'revokes the collection item' do + subject.call(collection_item, revoke: true) + + expect(collection_item.reload).to be_revoked + end + end end end diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index 3a9c6ab018bf67..0af7c837547621 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -113,11 +113,18 @@ def antenna_with_options(owner, **options) .and be_in(home_feed_of(tom)) .and be_in(home_feed_of(tagf)) - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything) - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge:local', anything) - expect(redis).to have_received(:publish).with('timeline:public', anything) - expect(redis).to have_received(:publish).with('timeline:public:local', anything) - expect(redis).to have_received(:publish).with('timeline:public:media', anything) + expected_payload = { event: 'update', payload: include(id: status.id.to_s, created_at: status.created_at.iso8601(3), content: /

    Hello/) } + + expect(redis) + .to have_received(:publish).with('timeline:hashtag:hoge', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:hashtag:hoge:local', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public:local', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public:media', match_json_values(expected_payload)) end end diff --git a/spec/services/fetch_remote_status_service_spec.rb b/spec/services/fetch_remote_status_service_spec.rb index a9c61e7b4e54e5..3dcd3b885d459d 100644 --- a/spec/services/fetch_remote_status_service_spec.rb +++ b/spec/services/fetch_remote_status_service_spec.rb @@ -19,17 +19,15 @@ context 'when protocol is :activitypub' do subject { described_class.new.call(note[:id], prefetched_body: prefetched_body) } - let(:prefetched_body) { Oj.dump(note) } - - before do - subject - end + let(:prefetched_body) { note.to_json } it 'creates status' do - status = account.statuses.first + expect { subject } + .to change(Status, :count).by(1) - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' + expect(account.statuses.first) + .to be_present + .and have_attributes(text: 'Lorem ipsum') end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index a99edefb81cef3..3793a491a793d1 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -224,7 +224,7 @@ status = subject.call(account, text: 'test status update') expect(ProcessMentionsService).to have_received(:new) - expect(mention_service).to have_received(:call).with(status, limited_type: '', circle: nil, save_records: false) + expect(mention_service).to have_received(:call).with(status, limited_type: '', circle: nil) end it 'self-banned visibility is set' do @@ -566,6 +566,16 @@ expect(status.quote.quoted_status_id).to eq target_status.id end + it 'processes tagged objects' do + account = Fabricate(:account) + collection = Fabricate(:collection) + + status = subject.call(account, text: "test #{ActivityPub::TagManager.instance.uri_for(collection)} #{ActivityPub::TagManager.instance.uri_for(collection)}") + + expect(status.tagged_objects.map(&:object)) + .to contain_exactly(collection) + end + it 'gets distributed' do allow(DistributionWorker).to receive(:perform_async) allow(ActivityPub::DistributionWorker).to receive(:perform_async) diff --git a/spec/services/process_links_service_spec.rb b/spec/services/process_links_service_spec.rb new file mode 100644 index 00000000000000..367eaa01774efb --- /dev/null +++ b/spec/services/process_links_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ProcessLinksService do + subject { described_class.new } + + let(:account) { Fabricate(:account, username: 'alice') } + + context 'when status mentions known collections' do + let!(:collection) { Fabricate(:collection) } + let(:status) { Fabricate(:status, account: account, text: "Hello check out this collection! #{ActivityPub::TagManager.instance.uri_for(collection)}", visibility: :public) } + + it 'creates a tagged object' do + expect { subject.call(status) } + .to change { status.tagged_objects.count }.by(1) + end + end + + context 'when status has a generic link' do + let(:status) { Fabricate(:status, account: account, text: 'Hello check out my personal web page: https://example.com/test', visibility: :public) } + + it 'skips the link and does not create a tagged object' do + expect { expect { subject.call(status) }.to_not raise_error } + .to not_change { status.tagged_objects.count }.from(0) + end + end +end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index cc0e7e4fd4333e..c54a6b55b76aa6 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -54,10 +54,10 @@ def skip_mention_for_domain_blocked context 'when mentioning a user several times when not saving records' do let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct} @#{remote_user.acct} @#{remote_user.acct}", visibility: :public) } + let(:status) { Fabricate.build(:status, account: account, text: "Hello @#{remote_user.acct} @#{remote_user.acct} @#{remote_user.acct}", visibility: :public) } it 'creates exactly one mention' do - subject.call(status, save_records: false) + subject.call(status) expect(status.mentions.size).to eq 1 end diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb index f7dfc25d6184c8..ce7a3326b4b93a 100644 --- a/spec/services/process_references_service_spec.rb +++ b/spec/services/process_references_service_spec.rb @@ -144,7 +144,7 @@ def notify?(target_status_id = nil) let(:text) { 'BT:https://example.com/test_post' } before do - stub_request(:get, 'https://example.com/test_post').to_return(status: 200, body: Oj.dump(object_json), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/test_post').to_return(status: 200, body: object_json.to_json, headers: { 'Content-Type' => 'application/activity+json' }) stub_request(:get, 'https://example.com/not_found').to_return(status: 404) end @@ -220,7 +220,7 @@ def notify?(target_status_id = nil) end before do - stub_request(:get, 'https://example.com/test_post').to_return(status: 200, body: Oj.dump(object_json), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/test_post').to_return(status: 200, body: object_json.to_json, headers: { 'Content-Type' => 'application/activity+json' }) end it_behaves_like 'reference once', 'https://example.com/test_post', 'https://example.com/test_post' diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 97b591b475e643..1f7e502d28883b 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -42,7 +42,7 @@ .to_not include(status.id) expect(redis) - .to have_received(:publish).with('timeline:public:media', Oj.dump(event: :delete, payload: status.id.to_s)) + .to have_received(:publish).with('timeline:public:media', { event: :delete, payload: status.id.to_s }.to_json) expect(delete_delivery(hank, status)) .to have_been_made.once diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index a3fe2e8729ea48..b42723123dbab1 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -105,7 +105,7 @@ def webfinger_discovery_request context 'with a legitimate webfinger redirection' do before do webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -123,7 +123,7 @@ def webfinger_discovery_request context 'with a misconfigured redirection' do before do webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -141,10 +141,10 @@ def webfinger_discovery_request context 'with too many webfinger redirections' do before do webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://redirected.example.com/.well-known/nodeinfo').to_return(body: '{}') webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: webfinger2.to_json, headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://evil.example.com/.well-known/nodeinfo').to_return(body: '{}') end @@ -154,7 +154,7 @@ def webfinger_discovery_request end context 'with webfinger response subject missing a host value' do - let(:body) { Oj.dump({ subject: 'user@' }) } + let(:body) { { subject: 'user@' }.to_json } let(:url) { 'https://host.example/.well-known/webfinger?resource=acct:user@host.example' } before do diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb index c6473b5b13088b..1e0edf57adf31a 100644 --- a/spec/services/software_update_check_service_spec.rb +++ b/spec/services/software_update_check_service_spec.rb @@ -55,6 +55,16 @@ end end + context 'when the update server returns invalid response body' do + before do + stub_request(:get, full_update_check_url).to_return(status: 200, body: 'XXX') + end + + it 'handles the error and returns' do + expect(subject.call).to be_nil + end + end + context 'when the server returns new versions' do let(:server_json) do { @@ -82,7 +92,7 @@ end before do - stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json)) + stub_request(:get, full_update_check_url).to_return(body: server_json.to_json) end it 'updates the list of known updates' do diff --git a/spec/services/statuses_search_service_spec.rb b/spec/services/statuses_search_service_spec.rb new file mode 100644 index 00000000000000..982b67ac870f6b --- /dev/null +++ b/spec/services/statuses_search_service_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe StatusesSearchService do + describe '#call' do + let!(:status) { Fabricate(:status, text: 'status number one') } + let(:results) { subject.call('one', status.account, limit: 5) } + + before { Fabricate(:status, text: 'status number two') } + + context 'when elasticsearch is enabled', :search do + it 'runs a search for statuses' do + expect(results) + .to have_attributes( + size: 1, + first: eq(status) + ) + end + end + end +end diff --git a/spec/services/tag_search_service_spec.rb b/spec/services/tag_search_service_spec.rb index de42e54071e823..4339424a1ace36 100644 --- a/spec/services/tag_search_service_spec.rb +++ b/spec/services/tag_search_service_spec.rb @@ -5,17 +5,28 @@ RSpec.describe TagSearchService do describe '#call' do let!(:one) { Fabricate(:tag, name: 'one') } + let(:results) { subject.call('#one', limit: 5) } before { Fabricate(:tag, name: 'two') } - it 'runs a search for tags' do - results = subject.call('#one', limit: 5) + context 'with postgres search' do + it 'runs a search for tags' do + expect(results) + .to have_attributes( + size: 1, + first: eq(one) + ) + end + end - expect(results) - .to have_attributes( - size: 1, - first: eq(one) - ) + context 'when elasticsearch is enabled', :search do + it 'runs a search for tags' do + expect(results) + .to have_attributes( + size: 1, + first: eq(one) + ) + end end end end diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb index 2a2bbd68b65e5a..ce111ad8d0fee4 100644 --- a/spec/services/update_status_service_spec.rb +++ b/spec/services/update_status_service_spec.rb @@ -243,6 +243,19 @@ def match_update_request(req, type) end end + context 'when tagged objects in text change' do + let!(:old_collection) { Fabricate(:collection) } + let!(:new_collection) { Fabricate(:collection) } + + let!(:account) { Fabricate(:account) } + let!(:status) { PostStatusService.new.call(account, text: "Check out #{ActivityPub::TagManager.instance.uri_for(old_collection)}") } + + it 'changes tagged objects' do + expect { subject.call(status, status.account_id, text: "Check out #{ActivityPub::TagManager.instance.uri_for(new_collection)} #{ActivityPub::TagManager.instance.uri_for(new_collection)}") } + .to change { status.reload.tagged_objects.map(&:object) }.from([old_collection]).to([new_collection]) + end + end + context 'when hashtags in text change' do let!(:account) { Fabricate(:account) } let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') } diff --git a/spec/services/webhook_service_spec.rb b/spec/services/webhook_service_spec.rb index 22a60db9f59efa..8c515423668320 100644 --- a/spec/services/webhook_service_spec.rb +++ b/spec/services/webhook_service_spec.rb @@ -8,12 +8,14 @@ let!(:report) { Fabricate(:report) } let!(:webhook) { Fabricate(:webhook, events: ['report.created']) } + before { freeze_time Time.current } + it 'finds and delivers webhook payloads' do expect { subject.call('report.created', report) } .to enqueue_sidekiq_job(Webhooks::DeliveryWorker) .with( webhook.id, - anything + match_json_values(event: 'report.created', created_at: Time.current.iso8601(3)) ) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 13683e404e0e8f..90e7e40578ddfb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,12 +44,3 @@ def serialized_record_json(record, serializer, adapter: nil, options: {}) ).to_json ) end - -def expect_push_bulk_to_match(klass, matcher) - allow(Sidekiq::Client).to receive(:push_bulk) - yield - expect(Sidekiq::Client).to have_received(:push_bulk).with(hash_including({ - 'class' => klass, - 'args' => matcher, - })) -end diff --git a/spec/support/search.rb b/spec/support/search.rb new file mode 100644 index 00000000000000..159068c1c55fbe --- /dev/null +++ b/spec/support/search.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before :suite do + if search_examples_present? + Chewy.settings[:enabled] = true + # Configure chewy to use `urgent` strategy to index documents immediately + Chewy.strategy(:urgent) + else + Chewy.settings[:enabled] = false + end + end + + config.after :each, :search do + search_indices.each(&:delete) + end + + private + + def search_indices + [ + AccountsIndex, + InstancesIndex, + PublicStatusesIndex, + StatusesIndex, + TagsIndex, + ] + end + + def search_examples_present? + RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:search] == true } + end +end diff --git a/spec/support/search_data_manager.rb b/spec/support/search_data_manager.rb deleted file mode 100644 index d521bceeed5738..00000000000000 --- a/spec/support/search_data_manager.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -class SearchDataManager - def prepare_test_data - 4.times do |i| - username = "search_test_account_#{i}" - account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.") - 2.times do |j| - Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private) - end - end - - 3.times do |i| - Fabricate.create(:tag, name: "search_test_tag_#{i}") - end - end - - def indexes - [ - AccountsIndex, - PublicStatusesIndex, - StatusesIndex, - TagsIndex, - ] - end - - def populate_indexes - indexes.each do |index_class| - index_class.purge! - index_class.import! - end - end - - def remove_indexes - indexes.each(&:delete!) - end - - def cleanup_test_data - Status.destroy_all - Account.destroy_all - Tag.destroy_all - end -end - -RSpec.configure do |config| - config.before :suite do - if search_examples_present? - Chewy.settings[:enabled] = true - # Configure chewy to use `urgent` strategy to index documents - Chewy.strategy(:urgent) - - # Create search data - search_data_manager.prepare_test_data - else - Chewy.settings[:enabled] = false - end - end - - config.after :suite do - if search_examples_present? - # Clean up after search data - search_data_manager.cleanup_test_data - end - end - - config.around :each, :search do |example| - search_data_manager.populate_indexes - example.run - search_data_manager.remove_indexes - end - - private - - def search_data_manager - @search_data_manager ||= SearchDataManager.new - end - - def search_examples_present? - RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:search] == true } - end -end diff --git a/spec/support/streaming_client.rb b/spec/support/streaming_client.rb index 02186e781c7d3e..005ddd922ed8f5 100644 --- a/spec/support/streaming_client.rb +++ b/spec/support/streaming_client.rb @@ -152,7 +152,7 @@ def connect end def subscribe(channel, **params) - send(Oj.dump({ type: 'subscribe', stream: channel }.merge(params))) + send(JSON.generate({ type: 'subscribe', stream: channel }.merge(params))) end def wait_for(event = nil) @@ -161,8 +161,8 @@ def wait_for(event = nil) def wait_for_message message = @connection.wait_for_event(:message) - event = Oj.load(message) - event['payload'] = Oj.load(event['payload']) if event['payload'] + event = JSON.parse(message) + event['payload'] = JSON.parse(event['payload']) if event['payload'] event.deep_symbolize_keys end diff --git a/spec/system/admin/fasp/debug/callbacks_spec.rb b/spec/system/admin/fasp/debug/callbacks_spec.rb index 0e47aac6777b0f..3b5c9d0453dd7b 100644 --- a/spec/system/admin/fasp/debug/callbacks_spec.rb +++ b/spec/system/admin/fasp/debug/callbacks_spec.rb @@ -15,14 +15,14 @@ it 'displays callbacks and allows to delete them' do visit admin_fasp_debug_callbacks_path - expect(page).to have_css('h2', text: I18n.t('admin.fasp.debug.callbacks.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.debug.callbacks.title')) expect(page).to have_css('td', text: 'debug prov') expect(page).to have_css('code', text: 'called back') expect do click_on I18n.t('admin.fasp.debug.callbacks.delete') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.debug.callbacks.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.debug.callbacks.title')) end.to change(Fasp::DebugCallback, :count).by(-1) end end diff --git a/spec/system/admin/fasp/debug_calls_spec.rb b/spec/system/admin/fasp/debug_calls_spec.rb index d2f6a3a08b5953..55a608a3339c96 100644 --- a/spec/system/admin/fasp/debug_calls_spec.rb +++ b/spec/system/admin/fasp/debug_calls_spec.rb @@ -19,14 +19,14 @@ it 'makes a debug call to the provider' do visit admin_fasp_providers_path - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(page).to have_css('td', text: provider.name) within 'table#providers' do click_on I18n.t('admin.fasp.providers.callback') end - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(debug_call).to have_been_requested end end diff --git a/spec/system/admin/fasp/providers_spec.rb b/spec/system/admin/fasp/providers_spec.rb index 03837ad5d987fe..04750a1516d3d2 100644 --- a/spec/system/admin/fasp/providers_spec.rb +++ b/spec/system/admin/fasp/providers_spec.rb @@ -34,30 +34,30 @@ it 'allows enabling and disabling of capabilities' do visit admin_fasp_providers_path - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(page).to have_css('td', text: provider.name) click_on I18n.t('admin.fasp.providers.edit') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.edit')) check 'callback' click_on I18n.t('admin.fasp.providers.save') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(provider.reload).to be_capability_enabled('callback') expect(enable_call).to have_been_requested click_on I18n.t('admin.fasp.providers.edit') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.edit')) uncheck 'callback' click_on I18n.t('admin.fasp.providers.save') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(provider.reload).to_not be_capability_enabled('callback') expect(disable_call).to have_been_requested end @@ -69,12 +69,12 @@ it 'allows to completely remove a provider' do visit admin_fasp_providers_path - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(page).to have_css('td', text: provider.name) click_on I18n.t('admin.fasp.providers.delete') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.title')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.title')) expect(page).to have_no_css('td', text: provider.name) end end diff --git a/spec/system/admin/fasp/registrations_spec.rb b/spec/system/admin/fasp/registrations_spec.rb index 3da6f01915168f..1068d94065a306 100644 --- a/spec/system/admin/fasp/registrations_spec.rb +++ b/spec/system/admin/fasp/registrations_spec.rb @@ -31,7 +31,7 @@ click_on I18n.t('admin.fasp.providers.registrations.confirm') - expect(page).to have_css('h2', text: I18n.t('admin.fasp.providers.edit')) + expect(page).to have_css('h1', text: I18n.t('admin.fasp.providers.edit')) expect(provider.reload).to be_confirmed end diff --git a/spec/system/auth/passwords_spec.rb b/spec/system/auth/passwords_spec.rb index 83853d68fa9ef6..55f9c689384cf6 100644 --- a/spec/system/auth/passwords_spec.rb +++ b/spec/system/auth/passwords_spec.rb @@ -11,7 +11,14 @@ describe 'Resetting a password', :inline_jobs do let(:new_password) { 'New.Pass.123' } - before { allow(Devise).to receive(:pam_authentication).and_return(false) } # Avoid the "seamless external" path + before do + allow(Devise).to receive(:pam_authentication).and_return(false) # Avoid the "seamless external" path + + # Disable wrapstodon to avoid redis calls that we don't want to stub + Setting.wrapstodon = false + + allow(redis).to receive(:publish) + end it 'initiates reset, sends link, resets password from form, clears data' do visit new_user_password_path @@ -31,6 +38,10 @@ .to be_present .and be_valid_password(new_password) + # Disables the token associated with the session + expect(redis) + .to have_received(:publish).with("timeline:access_token:#{session_activation.access_token.id}", { event: :kill }.to_json).once + # Deactivate session expect(user_session_count) .to eq(0) diff --git a/spec/system/home_spec.rb b/spec/system/home_spec.rb index aafa9323c0b07e..e839ae160be430 100644 --- a/spec/system/home_spec.rb +++ b/spec/system/home_spec.rb @@ -12,6 +12,8 @@ expect(page) .to have_css('noscript', text: /Mastodon/) .and have_css('body', class: 'app-body') + expect(find('.app-holder#mastodon')['data-props']) + .to eq('{"locale":"en"}') end end diff --git a/spec/system/settings/migration/redirects_spec.rb b/spec/system/settings/migration/redirects_spec.rb index b59be5ac1f910d..11882c571dc44f 100644 --- a/spec/system/settings/migration/redirects_spec.rb +++ b/spec/system/settings/migration/redirects_spec.rb @@ -32,6 +32,19 @@ .to have_content(I18n.t('migrations.cancelled_msg')) end + context 'when user has blank encrypted password' do + before { user.update! encrypted_password: '' } + + it 'saves a redirect via username confirmation' do + visit new_settings_migration_redirect_path + + fill_in 'form_redirect_acct', with: 'new@example.host' + fill_in 'form_redirect_current_username', with: " @#{user.account.username} " + expect { click_on I18n.t('migrations.set_redirect') } + .to(change { user.reload.account.moved_to_account_id }.from(nil)) + end + end + private def stub_resolver diff --git a/spec/system/settings/migrations_spec.rb b/spec/system/settings/migrations_spec.rb index fecde36f35fd0c..d95636a6091f23 100644 --- a/spec/system/settings/migrations_spec.rb +++ b/spec/system/settings/migrations_spec.rb @@ -33,20 +33,36 @@ end describe 'Creating migrations' do - let(:user) { Fabricate(:user, password: '12345678') } + let(:user) { Fabricate(:user, password:) } + let(:password) { '12345678' } before { sign_in(user) } context 'when migration account is changed' do let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } - it 'updates moved to account' do - visit settings_migration_path + context 'when user has encrypted password' do + it 'updates moved to account' do + visit settings_migration_path - expect { fill_in_and_submit } - .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) - expect(page) - .to have_content(I18n.t('settings.migrate')) + expect { fill_in_and_submit } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end + end + + context 'when user has blank encrypted password value' do + before { user.update! encrypted_password: '' } + + it 'updates moved to account using at-username value' do + visit settings_migration_path + + expect { fill_in_and_submit_via_username("@#{user.account.username}") } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end end end @@ -92,8 +108,18 @@ def fill_in_and_submit fill_in 'account_migration_acct', with: acct.username - fill_in 'account_migration_current_password', with: '12345678' + if block_given? + yield + else + fill_in 'account_migration_current_password', with: password + end click_on I18n.t('migrations.proceed_with_move') end + + def fill_in_and_submit_via_username(username) + fill_in_and_submit do + fill_in 'account_migration_current_username', with: username + end + end end end diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb index 0f07d96efebd78..8f91d28a12d6e3 100644 --- a/spec/system/share_entrypoint_spec.rb +++ b/spec/system/share_entrypoint_spec.rb @@ -19,6 +19,8 @@ .to have_css('.modal-layout__mastodon') .and have_css('div#mastodon-compose') .and have_css('.compose-form__submit') + expect(find_by_id('mastodon-compose')['data-props']) + .to eq('{"locale":"en"}') fill_in_form diff --git a/spec/system/streaming/streaming_spec.rb b/spec/system/streaming/streaming_spec.rb index f5d3ba114265ae..53aa6f21f718f9 100644 --- a/spec/system/streaming/streaming_spec.rb +++ b/spec/system/streaming/streaming_spec.rb @@ -75,6 +75,23 @@ end end + context 'when destroying a session activation tied to the used token' do + let(:session_activation) { Fabricate(:session_activation, user: user) } + let(:token) { session_activation.access_token } + + it 'disconnects the client' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + + session_activation.destroy! + + expect(streaming_client.wait_for(:closed).code).to be(1000) + expect(streaming_client.open?).to be(false) + end + end + context 'with a disabled user account' do before do user.disable! diff --git a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb index 9ff4731f96aa3c..d04b2331a61348 100644 --- a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb +++ b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb @@ -16,9 +16,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com']]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Update'), account.id, 'http://example.com') end end end diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb index de82d660cee162..485c9a03cf48ad 100644 --- a/spec/workers/activitypub/distribution_worker_spec.rb +++ b/spec/workers/activitypub/distribution_worker_spec.rb @@ -19,9 +19,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything) end end @@ -31,9 +32,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything) end end @@ -43,9 +45,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything) end end @@ -57,9 +60,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com/follower/inbox', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com/follower/inbox', anything) end end @@ -89,9 +93,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com/no_follower/inbox', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com/no_follower/inbox', anything) end end @@ -104,9 +109,10 @@ end it 'delivers to mentioned accounts' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'https://foo.bar/inbox', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'https://foo.bar/inbox', anything) end end @@ -125,9 +131,10 @@ object: ActivityPub::TagManager.instance.uri_for(status), } - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(expected_json), reblog.account.id, 'http://example.com', anything]]) do - subject.perform(reblog.id) - end + subject.perform(reblog.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(expected_json), reblog.account.id, 'http://example.com', anything) end end @@ -144,9 +151,10 @@ }), } - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(expected_json), reblog.account.id, 'http://example.com', anything]]) do - subject.perform(reblog.id) - end + subject.perform(reblog.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(expected_json), reblog.account.id, 'http://example.com', anything) end end end diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb index 9795c4619a1d67..bd166c4c7f4314 100644 --- a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb @@ -126,11 +126,11 @@ all_items.each do |item| next if [top_note_uri, reply_note_uri].include? item - stub_request(:get, item).to_return(status: 200, body: Oj.dump(empty_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, item).to_return(status: 200, body: empty_object.to_json, headers: { 'Content-Type': 'application/activity+json' }) end - stub_request(:get, top_note_uri).to_return(status: 200, body: Oj.dump(top_object), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_note_uri).to_return(status: 200, body: Oj.dump(reply_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_note_uri).to_return(status: 200, body: top_object.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_note_uri).to_return(status: 200, body: reply_object.to_json, headers: { 'Content-Type': 'application/activity+json' }) end shared_examples 'fetches all replies' do @@ -180,8 +180,8 @@ end before do - stub_request(:get, top_collection_uri).to_return(status: 200, body: Oj.dump(replies_top), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_collection_uri).to_return(status: 200, body: Oj.dump(replies_nested), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_collection_uri).to_return(status: 200, body: replies_top.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_collection_uri).to_return(status: 200, body: replies_nested.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'fetches all replies' @@ -254,8 +254,8 @@ end before do - stub_request(:get, top_page_2_uri).to_return(status: 200, body: Oj.dump(top_page_two), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_page_2_uri).to_return(status: 200, body: Oj.dump(reply_page_two), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_page_2_uri).to_return(status: 200, body: top_page_two.to_json, headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_page_2_uri).to_return(status: 200, body: reply_page_two.to_json, headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'fetches all replies' diff --git a/spec/workers/activitypub/fetch_instance_info_worker_spec.rb b/spec/workers/activitypub/fetch_instance_info_worker_spec.rb index 4ce1fdf51906ce..f5b85d1dfcc418 100644 --- a/spec/workers/activitypub/fetch_instance_info_worker_spec.rb +++ b/spec/workers/activitypub/fetch_instance_info_worker_spec.rb @@ -27,8 +27,8 @@ } end - let(:wellknown_nodeinfo_json) { Oj.dump(wellknown_nodeinfo) } - let(:nodeinfo_json) { Oj.dump(nodeinfo) } + let(:wellknown_nodeinfo_json) { wellknown_nodeinfo.to_json } + let(:nodeinfo_json) { nodeinfo.to_json } context 'when success' do before do @@ -59,7 +59,7 @@ protocols: ['activitypub'], } end - let(:new_nodeinfo_json) { Oj.dump(new_nodeinfo) } + let(:new_nodeinfo_json) { new_nodeinfo.to_json } before do stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 200, body: wellknown_nodeinfo_json) diff --git a/spec/workers/activitypub/fetch_remote_status_worker_spec.rb b/spec/workers/activitypub/fetch_remote_status_worker_spec.rb index a5ab71a2c33046..a15793e8e4eff9 100644 --- a/spec/workers/activitypub/fetch_remote_status_worker_spec.rb +++ b/spec/workers/activitypub/fetch_remote_status_worker_spec.rb @@ -22,7 +22,7 @@ ], } end - let(:json) { Oj.dump(payload) } + let(:json) { payload.to_json } before do stub_request(:get, 'https://example.com/note').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' }) diff --git a/spec/workers/activitypub/fetch_replies_worker_spec.rb b/spec/workers/activitypub/fetch_replies_worker_spec.rb index 56d19705a4d034..fd791871a19ac4 100644 --- a/spec/workers/activitypub/fetch_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_replies_worker_spec.rb @@ -17,7 +17,7 @@ } end - let(:json) { Oj.dump(payload) } + let(:json) { payload.to_json } describe 'perform' do it 'performs a request if the collection URI is from the same host' do diff --git a/spec/workers/activitypub/move_distribution_worker_spec.rb b/spec/workers/activitypub/move_distribution_worker_spec.rb index 63396834de01db..bde0449186f830 100644 --- a/spec/workers/activitypub/move_distribution_worker_spec.rb +++ b/spec/workers/activitypub/move_distribution_worker_spec.rb @@ -16,16 +16,11 @@ end it 'delivers to followers and known blockers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, expected_migration_deliveries) do - subject.perform(migration.id) - end - end + subject.perform(migration.id) - def expected_migration_deliveries - [ - [match_json_values(type: 'Move'), migration.account.id, 'http://example.com'], - [match_json_values(type: 'Move'), migration.account.id, 'http://example2.com'], - ] + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Move'), migration.account.id, 'http://example.com') + .and have_enqueued_sidekiq_job(match_json_values(type: 'Move'), migration.account.id, 'http://example2.com') end end end diff --git a/spec/workers/activitypub/process_featured_item_worker_spec.rb b/spec/workers/activitypub/process_featured_item_worker_spec.rb new file mode 100644 index 00000000000000..531b85407f9db8 --- /dev/null +++ b/spec/workers/activitypub/process_featured_item_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::ProcessFeaturedItemWorker do + subject { described_class.new } + + let(:collection) { Fabricate(:remote_collection) } + let(:object) { 'https://example.com/featured_items/1' } + let(:stubbed_service) do + instance_double(ActivityPub::ProcessFeaturedItemService, call: true) + end + + before do + allow(ActivityPub::ProcessFeaturedItemService).to receive(:new).and_return(stubbed_service) + end + + describe 'perform' do + it 'calls the service to process the item' do + subject.perform(collection.id, object) + + expect(stubbed_service).to have_received(:call).with(collection, object, position: nil, request_id: nil) + end + end +end diff --git a/spec/workers/activitypub/quote_refresh_worker_spec.rb b/spec/workers/activitypub/quote_refresh_worker_spec.rb index bcdcc0b7468b0d..dbb72a09df4df6 100644 --- a/spec/workers/activitypub/quote_refresh_worker_spec.rb +++ b/spec/workers/activitypub/quote_refresh_worker_spec.rb @@ -20,7 +20,7 @@ expect { worker.perform(quote.id) } .to(change { quote.reload.updated_at }) - expect(service).to have_received(:call).with(quote) + expect(service).to have_received(:call).with(quote, quote.approval_uri) end end @@ -31,7 +31,7 @@ expect { worker.perform(quote.id) } .to_not(change { quote.reload.updated_at }) - expect(service).to_not have_received(:call).with(quote) + expect(service).to_not have_received(:call).with(quote, quote.approval_uri) end end end diff --git a/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb b/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb index a925709885e589..3b3ed29489b250 100644 --- a/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb +++ b/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb @@ -13,11 +13,20 @@ let(:status) { Fabricate(:status, account: account) } let(:quote) { Fabricate(:quote, status: status, quoted_status: nil) } let(:url) { 'https://example.com/quoted-status' } + let(:approval_uri) { 'https://example.com/approval-uri' } it 'sends the status to the service' do - worker.perform(quote.id, url) + worker.perform(quote.id, url, { 'approval_uri' => approval_uri }) - expect(service).to have_received(:call).with(quote, fetchable_quoted_uri: url, request_id: anything) + expect(service).to have_received(:call).with(quote, approval_uri, fetchable_quoted_uri: url, request_id: anything) + end + + context 'with the old format' do + it 'sends the status to the service' do + worker.perform(quote.id, url) + + expect(service).to have_received(:call).with(quote, nil, fetchable_quoted_uri: url, request_id: anything) + end end it 'returns nil for non-existent record' do diff --git a/spec/workers/activitypub/status_update_distribution_worker_spec.rb b/spec/workers/activitypub/status_update_distribution_worker_spec.rb index f9d2574d9369e8..2f4bf108b2d91b 100644 --- a/spec/workers/activitypub/status_update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/status_update_distribution_worker_spec.rb @@ -61,9 +61,8 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + expect { subject.perform(status.id) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) end end diff --git a/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb b/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb new file mode 100644 index 00000000000000..6fcb9d02b23ad3 --- /dev/null +++ b/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker do + let(:worker) { described_class.new } + let(:service) { instance_double(ActivityPub::FetchFeaturedCollectionsCollectionService, call: true) } + + describe '#perform' do + before do + allow(ActivityPub::FetchFeaturedCollectionsCollectionService).to receive(:new).and_return(service) + end + + let(:account) { Fabricate(:account) } + + it 'sends the account to the service' do + worker.perform(account.id) + + expect(service).to have_received(:call).with(account, request_id: nil) + end + + it 'returns true for non-existent record' do + result = worker.perform(123_123_123) + + expect(result).to be(true) + end + end +end diff --git a/spec/workers/activitypub/update_distribution_worker_spec.rb b/spec/workers/activitypub/update_distribution_worker_spec.rb index 7d786063988b59..b156c329e84291 100644 --- a/spec/workers/activitypub/update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/update_distribution_worker_spec.rb @@ -14,9 +14,10 @@ end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com', anything]]) do - subject.perform(account.id) - end + subject.perform(account.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Update'), account.id, 'http://example.com', anything) end end end diff --git a/spec/workers/activitypub/verify_featured_item_worker_spec.rb b/spec/workers/activitypub/verify_featured_item_worker_spec.rb new file mode 100644 index 00000000000000..d7d31b3510e3e4 --- /dev/null +++ b/spec/workers/activitypub/verify_featured_item_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::VerifyFeaturedItemWorker do + let(:worker) { described_class.new } + let(:service) { instance_double(ActivityPub::VerifyFeaturedItemService, call: true) } + + describe '#perform' do + let(:collection_item) { Fabricate(:unverified_remote_collection_item) } + + before { stub_service } + + it 'sends the status to the service' do + worker.perform(collection_item.id, 'https://example.com/authorizations/1') + + expect(service).to have_received(:call).with(collection_item, 'https://example.com/authorizations/1', request_id: nil) + end + + it 'returns nil for non-existent record' do + result = worker.perform(123_123_123, 'https://example.com/authorizations/1') + + expect(result).to be_nil + end + end + + def stub_service + allow(ActivityPub::VerifyFeaturedItemService) + .to receive(:new) + .and_return(service) + end +end diff --git a/spec/workers/push_update_worker_spec.rb b/spec/workers/push_update_worker_spec.rb index f3e0a128df55b8..a423031fb75aac 100644 --- a/spec/workers/push_update_worker_spec.rb +++ b/spec/workers/push_update_worker_spec.rb @@ -15,7 +15,7 @@ context 'with valid records' do let(:account) { Fabricate :account } - let(:status) { Fabricate :status } + let(:status) { Fabricate :status, text: 'Test Post' } before { allow(redis).to receive(:publish) } @@ -25,7 +25,16 @@ expect(redis) .to have_received(:publish) - .with(redis_key, anything) + .with( + redis_key, + match_json_values( + event: 'update', + payload: include( + created_at: status.created_at.iso8601(3), + content: eq('

    Test Post

    ') + ) + ) + ) end def redis_key diff --git a/spec/workers/scheduler/instance_refresh_scheduler_spec.rb b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb index 37682ebb8f1a4f..8ddeffa12eb902 100644 --- a/spec/workers/scheduler/instance_refresh_scheduler_spec.rb +++ b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb @@ -7,7 +7,17 @@ describe 'perform' do it 'runs without error' do - expect { worker.perform }.to_not raise_error + expect { worker.perform } + .to_not raise_error + end + end + + context 'with elasticsearch enabled', :search do + before { Fabricate :remote_account } + + it 'updates search indexes' do + expect { worker.perform } + .to change(InstancesIndex, :count).by(1) end end end diff --git a/spec/workers/scheduler/self_destruct_scheduler_spec.rb b/spec/workers/scheduler/self_destruct_scheduler_spec.rb index a79559efddd571..0ff0faf05a72ac 100644 --- a/spec/workers/scheduler/self_destruct_scheduler_spec.rb +++ b/spec/workers/scheduler/self_destruct_scheduler_spec.rb @@ -39,6 +39,8 @@ end context 'when sidekiq is operational' do + let!(:other_account) { Fabricate :account, inbox_url: 'https://host.example/inbox', domain: 'host.example', protocol: :activitypub } + it 'suspends local non-suspended accounts' do worker.perform @@ -51,6 +53,9 @@ worker.perform + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Delete', signature: be_present), account.id, other_account.inbox_url) + expect(account.reload.suspended_at).to be > 1.day.ago expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound) end diff --git a/streaming/package.json b/streaming/package.json index 4f3d4a9a8edcc8..3989917b08710a 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/streaming", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.12.0", + "packageManager": "yarn@4.13.0", "engines": { "node": ">=20" }, @@ -21,7 +21,7 @@ "dotenv": "^17.0.0", "express": "^5.1.0", "ioredis": "^5.3.2", - "jsdom": "^28.0.0", + "jsdom": "^29.0.0", "pg": "^8.5.0", "pg-connection-string": "^2.6.0", "pino": "^10.0.0", diff --git a/vite.config.mts b/vite.config.mts index 1b264e3446a406..cb8188e5ea37c6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -2,6 +2,7 @@ import { readdir } from 'node:fs/promises'; import path from 'node:path'; import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin'; +import babel from '@rolldown/plugin-babel'; import legacy from '@vitejs/plugin-legacy'; import react from '@vitejs/plugin-react'; import postcssPresetEnv from 'postcss-preset-env'; @@ -16,7 +17,6 @@ import { import manifestSRI from 'vite-plugin-manifest-sri'; import { VitePWA } from 'vite-plugin-pwa'; import svgr from 'vite-plugin-svgr'; -import tsconfigPaths from 'vite-tsconfig-paths'; import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest'; import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed'; @@ -44,6 +44,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { base: `/${outDirName}/`, envDir: __dirname, resolve: { + tsconfigPaths: true, alias: { '~/': `${jsRoot}/`, '@/': `${jsRoot}/`, @@ -122,7 +123,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { assetsDir: 'assets', assetsInlineLimit: (filePath, _) => /\.woff2?$/.exec(filePath) ? false : undefined, - rollupOptions: { + rolldownOptions: { input: await findEntrypoints(), output: { chunkFileNames({ facadeModuleId, name }) { @@ -168,11 +169,9 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { format: 'es', }, plugins: [ - tsconfigPaths({ projects: [path.resolve(__dirname, 'tsconfig.json')] }), - react({ - babel: { - plugins: ['formatjs', 'transform-react-remove-prop-types'], - }, + react(), + babel({ + plugins: ['formatjs', 'transform-react-remove-prop-types'], }), MastodonThemes(), MastodonAssetsManifest(), diff --git a/vitest.config.mts b/vitest.config.mts index 16c1ba2e9ef3a1..3c1cb91de76411 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -27,7 +27,6 @@ const storybookTests: TestProjectInlineConfiguration = { provider: playwright(), instances: [{ browser: 'chromium' }], }, - setupFiles: [resolve(__dirname, '.storybook/vitest.setup.ts')], }, }; diff --git a/yarn.lock b/yarn.lock index 03baee48365023..9a0f058e1ec428 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,13 +12,6 @@ __metadata: languageName: node linkType: hard -"@acemir/cssom@npm:^0.9.31": - version: 0.9.31 - resolution: "@acemir/cssom@npm:0.9.31" - checksum: 10c0/cbfff98812642104ec3b37de1ad3a53f216ddc437e7b9276a23f46f2453844ea3c3f46c200bc4656a2f747fb26567560b3cc5183d549d119a758926551b5f566 - languageName: node - linkType: hard - "@adobe/css-tools@npm:^4.4.0": version: 4.4.3 resolution: "@adobe/css-tools@npm:4.4.3" @@ -46,29 +39,29 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^4.1.1": - version: 4.1.1 - resolution: "@asamuzakjp/css-color@npm:4.1.1" +"@asamuzakjp/css-color@npm:^5.0.1": + version: 5.0.1 + resolution: "@asamuzakjp/css-color@npm:5.0.1" dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - lru-cache: "npm:^11.2.4" - checksum: 10c0/2948ae9cd4c2f326ab5470d6ac7d415bb8062150ef254f830d774b6a77d6dccfbdb4b84ed4ef5c86c5643d42c52d77204b8d94d0d90f2e2cea9ec9b6cbb9c336 + "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + lru-cache: "npm:^11.2.6" + checksum: 10c0/3e8d74a3b7f3005a325cb8e7f3da1aa32aeac4cd9ce387826dc25b16eaab4dc0e4a6faded8ccc1895959141f4a4a70e8bc38723347b89667b7b224990d16683c languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^6.7.6": - version: 6.7.6 - resolution: "@asamuzakjp/dom-selector@npm:6.7.6" +"@asamuzakjp/dom-selector@npm:^7.0.3": + version: 7.0.3 + resolution: "@asamuzakjp/dom-selector@npm:7.0.3" dependencies: "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" - css-tree: "npm:^3.1.0" + css-tree: "npm:^3.2.1" is-potential-custom-element-name: "npm:^1.0.1" - lru-cache: "npm:^11.2.4" - checksum: 10c0/1715faae0787f0c8430b3a0ff3db8576a5b9a4f964408d0808fc2060ab01e0c2f5d8e26409de54b8641433c891dab8b561b196e58798811146084c561a4954ce + lru-cache: "npm:^11.2.7" + checksum: 10c0/c64b06a23479970ded4f38bec34069e98f4062b4ecb798b81b1fc37b5472ec6110d5bd9d8a267bfc431503f6ee3080c2e94eb3d99e30aaa1b6d9f83fbd2744fa languageName: node linkType: hard @@ -101,14 +94,14 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.0, @babel/compat-data@npm:^7.28.6": +"@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.0": version: 7.29.0 resolution: "@babel/compat-data@npm:7.29.0" checksum: 10c0/08f348554989d23aa801bf1405aa34b15e841c0d52d79da7e524285c77a5f9d298e70e11d91cc578d8e2c9542efc586d50c5f5cf8e1915b254a9dcf786913a94 languageName: node linkType: hard -"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0": +"@babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0, @babel/core@npm:^7.29.0": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -202,48 +195,48 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-create-class-features-plugin@npm:7.27.1" +"@babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-member-expression-to-functions": "npm:^7.27.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" "@babel/helper-optimise-call-expression": "npm:^7.27.1" - "@babel/helper-replace-supers": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/4ee199671d6b9bdd4988aa2eea4bdced9a73abfc831d81b00c7634f49a8fc271b3ceda01c067af58018eb720c6151322015d463abea7072a368ee13f35adbb4c + checksum: 10c0/0b62b46717891f4366006b88c9b7f277980d4f578c4c3789b7a4f5a2e09e121de4cda9a414ab403986745cd3ad1af3fe2d948c9f78ab80d4dc085afc9602af50 languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.27.1" +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1, @babel/helper-create-regexp-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - regexpu-core: "npm:^6.2.0" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + regexpu-core: "npm:^6.3.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/591fe8bd3bb39679cc49588889b83bd628d8c4b99c55bafa81e80b1e605a348b64da955e3fd891c4ba3f36fd015367ba2eadea22af6a7de1610fbb5bcc2d3df0 + checksum: 10c0/7af3d604cadecdb2b0d2cedd696507f02a53a58be0523281c2d6766211443b55161dde1e6c0d96ab16ddfd82a2607a2f792390caa24797e9733631f8aa86859f languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.5": - version: 0.6.5 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.5" +"@babel/helper-define-polyfill-provider@npm:^0.6.8": + version: 0.6.8 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.8" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - debug: "npm:^4.4.1" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + debug: "npm:^4.4.3" lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.22.10" + resolve: "npm:^1.22.11" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/4886a068d9ca1e70af395340656a9dda33c50502c67eed39ff6451785f370bdfc6e57095b90cb92678adcd4a111ca60909af53d3a741120719c5604346ae409e + checksum: 10c0/306a169f2cb285f368578219ef18ea9702860d3d02d64334f8d45ea38648be0b9e1edad8c8f732fa34bb4206ccbb9883c395570fd57ab7bbcf293bc5964c5b3a languageName: node linkType: hard @@ -254,13 +247,13 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" +"@babel/helper-member-expression-to-functions@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" dependencies: - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/5762ad009b6a3d8b0e6e79ff6011b3b8fdda0fefad56cfa8bfbe6aa02d5a8a8a9680a45748fe3ac47e735a03d2d88c0a676e3f9f59f20ae9fadcc8d51ccd5a53 + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814 languageName: node linkType: hard @@ -309,10 +302,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-plugin-utils@npm:7.27.1" - checksum: 10c0/94cf22c81a0c11a09b197b41ab488d416ff62254ce13c57e62912c85700dc2e99e555225787a4099ff6bae7a1812d622c80fbaeda824b79baa10a6c5ac4cf69b +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: 10c0/3f5f8acc152fdbb69a84b8624145ff4f9b9f6e776cb989f9f968f8606eb7185c5c3cfcf3ba08534e37e1e0e1c118ac67080610333f56baa4f7376c99b5f1143d languageName: node linkType: hard @@ -329,16 +322,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-replace-supers@npm:7.27.1" +"@babel/helper-replace-supers@npm:^7.27.1, @babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" dependencies: - "@babel/helper-member-expression-to-functions": "npm:^7.27.1" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" "@babel/helper-optimise-call-expression": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/4f2eaaf5fcc196580221a7ccd0f8873447b5d52745ad4096418f6101a1d2e712e9f93722c9a32bc9769a1dc197e001f60d6f5438d4dfde4b9c6a9e4df719354c + checksum: 10c0/04663c6389551b99b8c3e7ba4e2638b8ca2a156418c26771516124c53083aa8e74b6a45abe5dd46360af79709a0e9c6b72c076d0eab9efecdd5aaf836e79d8d5 languageName: node linkType: hard @@ -426,15 +419,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.27.1" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/7dfffa978ae1cd179641a7c4b4ad688c6828c2c58ec96b118c2fb10bc3715223de6b88bff1ebff67056bb5fccc568ae773e3b83c592a1b843423319f80c99ebd + checksum: 10c0/844b7c7e9eec6d858262b2f3d5af75d3a6bbd9d3ecc740d95271fbdd84985731674536f5d8ac98f2dc0e8872698b516e406636e4d0cb04b50afe471172095a53 languageName: node linkType: hard @@ -473,15 +466,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.27.1" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/b94e6c3fc019e988b1499490829c327a1067b4ddea8ad402f6d0554793c9124148c2125338c723661b6dff040951abc1f092afbf3f2d234319cd580b68e52445 + checksum: 10c0/f1a9194e8d1742081def7af748e9249eb5082c25d0ced292720a1f054895f99041c764a05f45af669a2c8898aeb79266058aedb0d3e1038963ad49be8288918a languageName: node linkType: hard @@ -494,25 +487,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.27.1" +"@babel/plugin-syntax-import-assertions@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/06a954ee672f7a7c44d52b6e55598da43a7064e80df219765c51c37a0692641277e90411028f7cae4f4d1dedeed084f0c453576fa421c35a81f1603c5e3e0146 + checksum: 10c0/f3b8bdccb9b4d3e3b9226684ca518e055399d05579da97dfe0160a38d65198cfe7dce809e73179d6463a863a040f980de32425a876d88efe4eda933d0d95982c languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.27.1" +"@babel/plugin-syntax-import-attributes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e66f7a761b8360419bbb93ab67d87c8a97465ef4637a985ff682ce7ba6918b34b29d81190204cf908d0933058ee7b42737423cd8a999546c21b3aabad4affa9a + checksum: 10c0/1be160e2c426faa74e5be2e30e39e8d0d8c543063bd5d06cd804f8751b8fbcb82ce824ca7f9ce4b09c003693f6c06a11ce503b7e34d85e1a259631e4c3f72ad2 languageName: node linkType: hard @@ -550,29 +543,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.28.0" +"@babel/plugin-transform-async-generator-functions@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/739d577e649d7d7b9845dc309e132964327ab3eaea43ad04d04a7dcb977c63f9aa9a423d1ca39baf10939128d02f52e6fda39c834fb9f1753785b1497e72c4dc + checksum: 10c0/4080fc5e7dad7761bfebbb4fbe06bdfeb3a8bf0c027bcb4373e59e6b3dc7c5002eca7cbb1afba801d6439df8f92f7bcb3fb862e8fbbe43a9e59bb5653dcc0568 languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.27.1" +"@babel/plugin-transform-async-to-generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e76b1f6f9c3bbf72e17d7639406d47f09481806de4db99a8de375a0bb40957ea309b20aa705f0c25ab1d7c845e3f365af67eafa368034521151a0e352a03ef2f + checksum: 10c0/2eb0826248587df6e50038f36194a138771a7df22581020451c7779edeaf9ef39bf47c5b7a20ae2645af6416e8c896feeca273317329652e84abd79a4ab920ad languageName: node linkType: hard @@ -587,90 +580,90 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.0" +"@babel/plugin-transform-block-scoping@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/787d85e72a92917e735aa54e23062fa777031f8a07046e67f5026eff3d91e64eb535575dd1df917b0011bee014ae51287478af14c1d4ba60bc81e326bc044cfc + checksum: 10c0/2e3e09e1f9770b56cef4dcbffddf262508fd03416072f815ac66b2b224a3a12cd285cfec12fc067f1add414e7db5ce6dafb5164a6e0fb1a728e6a97d0c6f6e9d languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" +"@babel/plugin-transform-class-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-properties@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/cc0662633c0fe6df95819fef223506ddf26c369c8d64ab21a728d9007ec866bf9436a253909819216c24a82186b6ccbc1ec94d7aaf3f82df227c7c02fa6a704b + checksum: 10c0/c4327fcd730c239d9f173f9b695b57b801729e273b4848aef1f75818069dfd31d985d75175db188d947b9b1bbe5353dae298849042026a5e4fcf07582ff3f9f1 languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-class-static-block@npm:7.27.1" +"@babel/plugin-transform-class-static-block@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10c0/396997dd81fc1cf242b921e337d25089d6b9dc3596e81322ff11a6359326dc44f2f8b82dcc279c2e514cafaf8964dc7ed39e9fab4b8af1308b57387d111f6a20 + checksum: 10c0/dbe9b1fd302ae41b73186e17ac8d8ecf625ebc2416a91f2dc8013977a1bdf21e6ea288a83f084752b412242f3866e789d4fddeb428af323fe35b60e0fae4f98c languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-classes@npm:7.28.0" +"@babel/plugin-transform-classes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-classes@npm:7.28.6" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.27.3" - "@babel/helper-compilation-targets": "npm:^7.27.2" + "@babel/helper-compilation-targets": "npm:^7.28.6" "@babel/helper-globals": "npm:^7.28.0" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-replace-supers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/3b213b43104fe99dd7e79401a86d09e545836e057a70ffe77e8196a87bf67ae167e502ae90afdf0d1a2be683be5652514aaeda743bd984e583523dd8ecfef887 + checksum: 10c0/dc22f1f6eadab17305128fbf9cc5f30e87a51a77dd0a6d5498097994e8a9b9a90ab298c11edf2342acbeaac9edc9c601cad72eedcf4b592cd465a787d7f41490 languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-computed-properties@npm:7.27.1" +"@babel/plugin-transform-computed-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/template": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/template": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e09a12f8c8ae0e6a6144c102956947b4ec05f6c844169121d0ec4529c2d30ad1dc59fee67736193b87a402f44552c888a519a680a31853bdb4d34788c28af3b0 + checksum: 10c0/1e9893503ae6d651125701cc29450e87c0b873c8febebff19da75da9c40cfb7968c52c28bf948244e461110aeb7b3591f2cc199b7406ff74a24c50c7a5729f39 languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-destructuring@npm:7.28.0" +"@babel/plugin-transform-destructuring@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/cc7ccafa952b3ff7888544d5688cfafaba78c69ce1e2f04f3233f4f78c9de5e46e9695f5ea42c085b0c0cfa39b10f366d362a2be245b6d35b66d3eb1d427ccb2 + checksum: 10c0/288207f488412b23bb206c7c01ba143714e2506b72a9ec09e993f28366cc8188d121bde714659b3437984a86d2881d9b1b06de3089d5582823ccf2f3b3eaa2c4 languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.27.1" +"@babel/plugin-transform-dotall-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f9caddfad9a551b4dabe0dcb7c040f458fbaaa7bbb44200c20198b32c8259be8e050e58d2c853fdac901a4cfe490b86aa857036d8d461b192dd010d0e242dedb + checksum: 10c0/e2fb76b7ae99087cf4212013a3ca9dee07048f90f98fd6264855080fb6c3f169be11c9b8c9d8b26cf9a407e4d0a5fa6e103f7cef433a542b75cf7127c99d4f97 languageName: node linkType: hard @@ -685,15 +678,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.27.1" +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/121502a252b3206913e1e990a47fea34397b4cbf7804d4cd872d45961bc45b603423f60ca87f3a3023a62528f5feb475ac1c9ec76096899ec182fcb135eba375 + checksum: 10c0/6f03d9e5e31a05b28555541be6e283407e08447a36be6ddf8068b3efa970411d832e04b1282e2b894baf89a3864ff7e7f1e36346652a8d983170c6d548555167 languageName: node linkType: hard @@ -708,26 +701,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-explicit-resource-management@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.0" +"@babel/plugin-transform-explicit-resource-management@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/3baa706af3112adf2ae0c7ec0dc61b63dd02695eb5582f3c3a2b2d05399c6aa7756f55e7bbbd5412e613a6ba1dd6b6736904074b4d7ebd6b45a1e3f9145e4094 + checksum: 10c0/e6ea28c26e058fe61ada3e70b0def1992dd5a44f5fc14d8e2c6a3a512fb4d4c6dc96a3e1d0b466d83db32a9101e0b02df94051e48d3140da115b8ea9f8a31f37 languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.27.1" +"@babel/plugin-transform-exponentiation-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/953d21e01fed76da8e08fb5094cade7bf8927c1bb79301916bec2db0593b41dbcfbca1024ad5db886b72208a93ada8f57a219525aad048cf15814eeb65cf760d + checksum: 10c0/4572d955a50dbc9a652a19431b4bb822cb479ee6045f4e6df72659c499c13036da0a2adf650b07ca995f2781e80aa868943bea1e7bff1de3169ec3f0a73a902e languageName: node linkType: hard @@ -767,14 +760,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-json-strings@npm:7.27.1" +"@babel/plugin-transform-json-strings@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-json-strings@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/2379714aca025516452a7c1afa1ca42a22b9b51a5050a653cc6198a51665ab82bdecf36106d32d731512706a1e373c5637f5ff635737319aa42f3827da2326d6 + checksum: 10c0/ab1091798c58e6c0bb8a864ee2b727c400924592c6ed69797a26b4c205f850a935de77ad516570be0419c279a3d9f7740c2aa448762eb8364ea77a6a357a9653 languageName: node linkType: hard @@ -789,14 +782,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.27.1" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/5b0abc7c0d09d562bf555c646dce63a30288e5db46fd2ce809a61d064415da6efc3b2b3c59b8e4fe98accd072c89a2f7c3765b400e4bf488651735d314d9feeb + checksum: 10c0/4632a35453d2131f0be466681d0a33e3db44d868ff51ec46cd87e0ebd1e47c6a39b894f7d1c9b06f931addf6efa9d30e60c4cdedeb4f69d426f683e11f8490cf languageName: node linkType: hard @@ -823,29 +816,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1" +"@babel/plugin-transform-modules-commonjs@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" dependencies: - "@babel/helper-module-transforms": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/4def972dcd23375a266ea1189115a4ff61744b2c9366fc1de648b3fab2c650faf1a94092de93a33ff18858d2e6c4dddeeee5384cb42ba0129baeab01a5cdf1e2 + checksum: 10c0/7c45992797c6150644c8552feff4a016ba7bd6d59ff2b039ed969a9c5b20a6804cd9d21db5045fc8cca8ca7f08262497e354e93f8f2be6a1cdf3fbfa8c31a9b6 languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.27.1" +"@babel/plugin-transform-modules-systemjs@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.0" dependencies: - "@babel/helper-module-transforms": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f16fca62d144d9cbf558e7b5f83e13bb6d0f21fdeff3024b0cecd42ffdec0b4151461da42bd0963512783ece31aafa5ffe03446b4869220ddd095b24d414e2b5 + checksum: 10c0/44ea502f2c990398b7d9adc5b44d9e1810a0a5e86eebc05c92d039458f0b3994fe243efa9353b90f8a648d8a91b79845fb353d8679d7324cc9de0162d732771d languageName: node linkType: hard @@ -861,15 +854,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.27.1" +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/8eaa8c9aee00a00f3bd8bd8b561d3f569644d98cb2cfe3026d7398aabf9b29afd62f24f142b4112fa1f572d9b0e1928291b099cde59f56d6b59f4d565e58abf2 + checksum: 10c0/1904db22da7f2bc3e380cd2c0786bda330ee1b1b3efa3f5203d980708c4bfeb5daa4dff48d01692193040bcc5f275dbdc0c2eadc8b1eb1b6dfe363564ad6e898 languageName: node linkType: hard @@ -884,40 +877,40 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a435fc03aaa65c6ef8e99b2d61af0994eb5cdd4a28562d78c3b0b0228ca7e501aa255e1dff091a6996d7d3ea808eb5a65fd50ecd28dfb10687a8a1095dcadc7a + checksum: 10c0/6607f2201d66ccb688f0b1db09475ef995837df19f14705da41f693b669f834c206147a854864ab107913d7b4f4748878b0cd9fe9ca8bfd1bee0c206fc027b49 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.27.1" +"@babel/plugin-transform-numeric-separator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/b72cbebbfe46fcf319504edc1cf59f3f41c992dd6840db766367f6a1d232cd2c52143c5eaf57e0316710bee251cae94be97c6d646b5022fcd9274ccb131b470c + checksum: 10c0/191097d8d2753cdd16d1acca65a945d1645ab20b65655c2f5b030a9e38967a52e093dcb21ebf391e342222705c6ffe5dea15dafd6257f7b51b77fb64a830b637 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.0" +"@babel/plugin-transform-object-rest-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/360dc6fd5285ee5e1d3be8a1fb0decd120b2a1726800317b4ab48b7c91616247030239b7fa06ceaa1a8a586fde1e143c24d45f8d41956876099d97d664f8ef1e + checksum: 10c0/f55334352d4fcde385f2e8a58836687e71ff668c9b6e4c34d52575bf2789cdde92d9d3116edba13647ac0bc3e51fb2a6d1e8fb822dce7e8123334b82600bc4c3 languageName: node linkType: hard @@ -933,26 +926,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.27.1" +"@babel/plugin-transform-optional-catch-binding@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/807a4330f1fac08e2682d57bc82e714868fc651c8876f9a8b3a3fd8f53c129e87371f8243e712ac7dae11e090b737a2219a02fe1b6459a29e664fa073c3277bb + checksum: 10c0/36e8face000ee65e478a55febf687ce9be7513ad498c60dfe585851555565e0c28e7cb891b3c59709318539ce46f7697d5f42130eb18f385cd47e47cfa297446 languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" +"@babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/5b18ff5124e503f0a25d6b195be7351a028b3992d6f2a91fb4037e2a2c386400d66bc1df8f6df0a94c708524f318729e81a95c41906e5a7919a06a43e573a525 + checksum: 10c0/c159cc74115c2266be21791f192dd079e2aeb65c8731157e53b80fcefa41e8e28ad370021d4dfbdb31f25e5afa0322669a8eb2d032cd96e65ac37e020324c763 languageName: node linkType: hard @@ -967,28 +960,28 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-methods@npm:7.27.1" +"@babel/plugin-transform-private-methods@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/232bedfe9d28df215fb03cc7623bdde468b1246bdd6dc24465ff4bf9cc5f5a256ae33daea1fafa6cc59705e4d29da9024bb79baccaa5cd92811ac5db9b9244f2 + checksum: 10c0/fb504e2bfdcf3f734d2a90ab20d61427c58385f57f950d3de6ff4e6d12dd4aa7d552147312d218367e129b7920dccfc3230ba554de861986cda38921bad84067 languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.27.1" +"@babel/plugin-transform-private-property-in-object@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a8c4536273ca716dcc98e74ea25ca76431528554922f184392be3ddaf1761d4aa0e06f1311577755bd1613f7054fb51d29de2ada1130f743d329170a1aa1fe56 + checksum: 10c0/0f6bbc6ec3f93b556d3de7d56bf49335255fc4c43488e51a5025d6ee0286183fd3cf950ffcac1bbeed8a45777f860a49996455c8d3b4a04c3b1a5f28e697fe31 languageName: node linkType: hard @@ -1025,26 +1018,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.28.0": - version: 7.28.1 - resolution: "@babel/plugin-transform-regenerator@npm:7.28.1" +"@babel/plugin-transform-regenerator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/6c9e6eb80ce9c0bde0876c80979e078fbc85dc802272cba4ee72b5b1c858472e38167c418917e4f0d4384ce888706d95544a8d266880c0e199e167e078168b67 + checksum: 10c0/86c7db9b97f85ee47c0fae0528802cbc06e5775e61580ee905335c16bb971270086764a3859873d9adcd7d0f913a5b93eb0dc271aec8fb9e93e090e4ac95e29e languageName: node linkType: hard -"@babel/plugin-transform-regexp-modifiers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.27.1" +"@babel/plugin-transform-regexp-modifiers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/31ae596ab56751cf43468a6c0a9d6bc3521d306d2bee9c6957cdb64bea53812ce24bd13a32f766150d62b737bca5b0650b2c62db379382fff0dccbf076055c33 + checksum: 10c0/97e36b086800f71694fa406abc00192e3833662f2bdd5f51c018bd0c95eef247c4ae187417c207d03a9c5374342eac0bb65a39112c431a9b23b09b1eda1562e5 languageName: node linkType: hard @@ -1070,15 +1063,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-spread@npm:7.27.1" +"@babel/plugin-transform-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-spread@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/b34fc58b33bd35b47d67416655c2cbc8578fbb3948b4592bc15eb6d8b4046986e25c06e3b9929460fa4ab08e9653582415e7ef8b87d265e1239251bdf5a4c162 + checksum: 10c0/bcac50e558d6f0c501cbce19ec197af558cef51fe3b3a6eba27276e323e57a5be28109b4264a5425ac12a67bf95d6af9c2a42b05e79c522ce913fb9529259d76 languageName: node linkType: hard @@ -1126,15 +1119,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.27.1" +"@babel/plugin-transform-unicode-property-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a332bc3cb3eeea67c47502bc52d13a0f8abae5a7bfcb08b93a8300ddaff8d9e1238f912969494c1b494c1898c6f19687054440706700b6d12cb0b90d88beb4d0 + checksum: 10c0/b25f8cde643f4f47e0fa4f7b5c552e2dfbb6ad0ce07cf40f7e8ae40daa9855ad855d76d4d6d010153b74e48c8794685955c92ca637c0da152ce5f0fa9e7c90fa languageName: node linkType: hard @@ -1150,95 +1143,95 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.27.1" +"@babel/plugin-transform-unicode-sets-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/236645f4d0a1fba7c18dc8ffe3975933af93e478f2665650c2d91cf528cfa1587cde5cfe277e0e501fc03b5bf57638369575d6539cef478632fb93bd7d7d7178 + checksum: 10c0/c03c8818736b138db73d1f7a96fbfa22d1994639164d743f0f00e6383d3b7b3144d333de960ff4afad0bddd0baaac257295e3316969eba995b1b6a1b4dec933e languageName: node linkType: hard -"@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/preset-env@npm:7.28.0" +"@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.29.0": + version: 7.29.2 + resolution: "@babel/preset-env@npm:7.29.2" dependencies: - "@babel/compat-data": "npm:^7.28.0" - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/compat-data": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-validator-option": "npm:^7.27.1" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.27.1" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.28.6" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions": "npm:^7.27.1" - "@babel/plugin-syntax-import-attributes": "npm:^7.27.1" + "@babel/plugin-syntax-import-assertions": "npm:^7.28.6" + "@babel/plugin-syntax-import-attributes": "npm:^7.28.6" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" - "@babel/plugin-transform-async-generator-functions": "npm:^7.28.0" - "@babel/plugin-transform-async-to-generator": "npm:^7.27.1" + "@babel/plugin-transform-async-generator-functions": "npm:^7.29.0" + "@babel/plugin-transform-async-to-generator": "npm:^7.28.6" "@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1" - "@babel/plugin-transform-block-scoping": "npm:^7.28.0" - "@babel/plugin-transform-class-properties": "npm:^7.27.1" - "@babel/plugin-transform-class-static-block": "npm:^7.27.1" - "@babel/plugin-transform-classes": "npm:^7.28.0" - "@babel/plugin-transform-computed-properties": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" - "@babel/plugin-transform-dotall-regex": "npm:^7.27.1" + "@babel/plugin-transform-block-scoping": "npm:^7.28.6" + "@babel/plugin-transform-class-properties": "npm:^7.28.6" + "@babel/plugin-transform-class-static-block": "npm:^7.28.6" + "@babel/plugin-transform-classes": "npm:^7.28.6" + "@babel/plugin-transform-computed-properties": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" + "@babel/plugin-transform-dotall-regex": "npm:^7.28.6" "@babel/plugin-transform-duplicate-keys": "npm:^7.27.1" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" - "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.0" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.27.1" + "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.6" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.6" "@babel/plugin-transform-export-namespace-from": "npm:^7.27.1" "@babel/plugin-transform-for-of": "npm:^7.27.1" "@babel/plugin-transform-function-name": "npm:^7.27.1" - "@babel/plugin-transform-json-strings": "npm:^7.27.1" + "@babel/plugin-transform-json-strings": "npm:^7.28.6" "@babel/plugin-transform-literals": "npm:^7.27.1" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.27.1" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.6" "@babel/plugin-transform-member-expression-literals": "npm:^7.27.1" "@babel/plugin-transform-modules-amd": "npm:^7.27.1" - "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1" - "@babel/plugin-transform-modules-systemjs": "npm:^7.27.1" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" + "@babel/plugin-transform-modules-systemjs": "npm:^7.29.0" "@babel/plugin-transform-modules-umd": "npm:^7.27.1" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-new-target": "npm:^7.27.1" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.27.1" - "@babel/plugin-transform-numeric-separator": "npm:^7.27.1" - "@babel/plugin-transform-object-rest-spread": "npm:^7.28.0" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" + "@babel/plugin-transform-numeric-separator": "npm:^7.28.6" + "@babel/plugin-transform-object-rest-spread": "npm:^7.28.6" "@babel/plugin-transform-object-super": "npm:^7.27.1" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.27.1" - "@babel/plugin-transform-optional-chaining": "npm:^7.27.1" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.28.6" + "@babel/plugin-transform-optional-chaining": "npm:^7.28.6" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/plugin-transform-private-methods": "npm:^7.27.1" - "@babel/plugin-transform-private-property-in-object": "npm:^7.27.1" + "@babel/plugin-transform-private-methods": "npm:^7.28.6" + "@babel/plugin-transform-private-property-in-object": "npm:^7.28.6" "@babel/plugin-transform-property-literals": "npm:^7.27.1" - "@babel/plugin-transform-regenerator": "npm:^7.28.0" - "@babel/plugin-transform-regexp-modifiers": "npm:^7.27.1" + "@babel/plugin-transform-regenerator": "npm:^7.29.0" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.28.6" "@babel/plugin-transform-reserved-words": "npm:^7.27.1" "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" - "@babel/plugin-transform-spread": "npm:^7.27.1" + "@babel/plugin-transform-spread": "npm:^7.28.6" "@babel/plugin-transform-sticky-regex": "npm:^7.27.1" "@babel/plugin-transform-template-literals": "npm:^7.27.1" "@babel/plugin-transform-typeof-symbol": "npm:^7.27.1" "@babel/plugin-transform-unicode-escapes": "npm:^7.27.1" - "@babel/plugin-transform-unicode-property-regex": "npm:^7.27.1" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.28.6" "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" - "@babel/plugin-transform-unicode-sets-regex": "npm:^7.27.1" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.28.6" "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2: "npm:^0.4.14" - babel-plugin-polyfill-corejs3: "npm:^0.13.0" - babel-plugin-polyfill-regenerator: "npm:^0.6.5" - core-js-compat: "npm:^3.43.0" + babel-plugin-polyfill-corejs2: "npm:^0.4.15" + babel-plugin-polyfill-corejs3: "npm:^0.14.0" + babel-plugin-polyfill-regenerator: "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f343103b8f0e8da5be4ae031aff8bf35da4764997af4af78ae9506f421b785dd45da1bc09f845b1fc308c8b7d134aead4a1f89e7fb6e213cd2f9fe1d2aa78bc9 + checksum: 10c0/d49cb005f2dbc3f2293ab6d80ee8f1380e6215af5518fe26b087c8961c1ea8ebaa554dfce589abe1fbebac25ad7c2515d943dec3859ea2d4981a3f8f4711c580 languageName: node linkType: hard @@ -1286,7 +1279,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": +"@babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": version: 7.29.0 resolution: "@babel/traverse@npm:7.29.0" dependencies: @@ -1316,7 +1309,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.4.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.4.4": version: 7.29.0 resolution: "@babel/types@npm:7.29.0" dependencies: @@ -1343,6 +1336,24 @@ __metadata: languageName: node linkType: hard +"@blazediff/core@npm:1.9.1": + version: 1.9.1 + resolution: "@blazediff/core@npm:1.9.1" + checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 + languageName: node + linkType: hard + +"@bramus/specificity@npm:^2.4.2": + version: 2.4.2 + resolution: "@bramus/specificity@npm:2.4.2" + dependencies: + css-tree: "npm:^3.0.0" + bin: + specificity: bin/cli.js + checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72 + languageName: node + linkType: hard + "@cacheable/memory@npm:^2.0.5": version: 2.0.5 resolution: "@cacheable/memory@npm:2.0.5" @@ -1375,13 +1386,6 @@ __metadata: languageName: node linkType: hard -"@csstools/color-helpers@npm:^5.1.0": - version: 5.1.0 - resolution: "@csstools/color-helpers@npm:5.1.0" - checksum: 10c0/b7f99d2e455cf1c9b41a67a5327d5d02888cd5c8802a68b1887dffef537d9d4bc66b3c10c1e62b40bbed638b6c1d60b85a232f904ed7b39809c4029cb36567db - languageName: node - linkType: hard - "@csstools/color-helpers@npm:^6.0.0": version: 6.0.0 resolution: "@csstools/color-helpers@npm:6.0.0" @@ -1389,17 +1393,14 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^2.1.4": - version: 2.1.4 - resolution: "@csstools/css-calc@npm:2.1.4" - peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/42ce5793e55ec4d772083808a11e9fb2dfe36db3ec168713069a276b4c3882205b3507c4680224c28a5d35fe0bc2d308c77f8f2c39c7c09aad8747708eb8ddd8 +"@csstools/color-helpers@npm:^6.0.2": + version: 6.0.2 + resolution: "@csstools/color-helpers@npm:6.0.2" + checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789 languageName: node linkType: hard -"@csstools/css-calc@npm:^3.0.0": +"@csstools/css-calc@npm:^3.0.0, @csstools/css-calc@npm:^3.1.1": version: 3.1.1 resolution: "@csstools/css-calc@npm:3.1.1" peerDependencies: @@ -1409,19 +1410,6 @@ __metadata: 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" - dependencies: - "@csstools/color-helpers": "npm:^5.1.0" - "@csstools/css-calc": "npm:^2.1.4" - peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/0e0c670ad54ec8ec4d9b07568b80defd83b9482191f5e8ca84ab546b7be6db5d7cc2ba7ac9fae54488b129a4be235d6183d3aab4416fec5e89351f73af4222c5 - 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" @@ -1435,12 +1423,16 @@ __metadata: 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" +"@csstools/css-color-parser@npm:^4.0.2": + version: 4.0.2 + resolution: "@csstools/css-color-parser@npm:4.0.2" + dependencies: + "@csstools/color-helpers": "npm:^6.0.2" + "@csstools/css-calc": "npm:^3.1.1" peerDependencies: - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/d9a1c888bd43849ae3437ca39251d5c95d2c8fd6b5ccdb7c45491dfd2c1cbdc3075645e80901d120e4d2c1993db9a5b2d83793b779dbbabcfb132adb142eb7f7 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/487cf507ef4630f74bd67d84298294ed269900b206ade015a968d20047e07ff46f235b72e26fe0c6b949a03f8f9f00a22c363da49c1b06ca60b32d0188e546be languageName: node linkType: hard @@ -1453,24 +1445,15 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:^1.0.21": - version: 1.0.26 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.26" - checksum: 10c0/65ac2a9e3a6908ce503ae1ba354c59ffdda04fef1f4c6e2b64f083c2896fe24fd775861e693e0eb2f84a1e7e0d333eab0fa26b5873268945833b62d7fae97404 - languageName: node - linkType: hard - -"@csstools/css-syntax-patches-for-csstree@npm:^1.0.25": - version: 1.0.27 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.27" - checksum: 10c0/ef3f2a639109758c0f3c04520465800ca4c830174bd6f7979795083877c82ace51ab8353857b06a818cb6c0de6d4dc88f84a86fc3b021be47f11a0f1c4b74e7e - languageName: node - linkType: hard - -"@csstools/css-tokenizer@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/css-tokenizer@npm:3.0.4" - checksum: 10c0/3b589f8e9942075a642213b389bab75a2d50d05d203727fcdac6827648a5572674caff07907eff3f9a2389d86a4ee47308fafe4f8588f4a77b7167c588d2559f +"@csstools/css-syntax-patches-for-csstree@npm:^1.0.25, @csstools/css-syntax-patches-for-csstree@npm:^1.1.1": + version: 1.1.1 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.1" + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + checksum: 10c0/947f82e9e8af0512f1d6600f68da1bbe8d15112fa73435169608a68dcf20262ae517c799202c86a6c3bc889d0e9fab724ad5661a3aa98432390f8f9765b86ddc languageName: node linkType: hard @@ -2102,31 +2085,31 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.3": - version: 1.4.5 - resolution: "@emnapi/core@npm:1.4.5" +"@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.7.1": + version: 1.9.0 + resolution: "@emnapi/core@npm:1.9.0" dependencies: - "@emnapi/wasi-threads": "npm:1.0.4" + "@emnapi/wasi-threads": "npm:1.2.0" tslib: "npm:^2.4.0" - checksum: 10c0/da4a57f65f325d720d0e0d1a9c6618b90c4c43a5027834a110476984e1d47c95ebaed4d316b5dddb9c0ed9a493ffeb97d1934f9677035f336d8a36c1f3b2818f + checksum: 10c0/defbfa5861aa5ff1346dbc6a19df50d727ae76ae276a31a97b178db8eecae0c5179976878087b43ac2441750e40e6c50e465280383256deb16dd2fb167dd515c languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.3": - version: 1.4.5 - resolution: "@emnapi/runtime@npm:1.4.5" +"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.7.1": + version: 1.9.0 + resolution: "@emnapi/runtime@npm:1.9.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/37a0278be5ac81e918efe36f1449875cbafba947039c53c65a1f8fc238001b866446fc66041513b286baaff5d6f9bec667f5164b3ca481373a8d9cb65bfc984b + checksum: 10c0/f825e53b2d3f9d31fd880e669197d006bb5158c3a52ab25f0546f3d52ac58eb539a4bd1dcc378af6c10d202956fa064b28ab7b572a76de58972c0b8656a692ef languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.0.4": - version: 1.0.4 - resolution: "@emnapi/wasi-threads@npm:1.0.4" +"@emnapi/wasi-threads@npm:1.2.0": + version: 1.2.0 + resolution: "@emnapi/wasi-threads@npm:1.2.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/2c91a53e62f875800baf035c4d42c9c0d18e5afd9a31ca2aac8b435aeaeaeaac386b5b3d0d0e70aa7a5a9852bbe05106b1f680cd82cce03145c703b423d41313 + checksum: 10c0/1e3724b5814b06c14782fda87eee9b9aa68af01576c81ffeaefdf621ddb74386e419d5b3b1027b6a8172397729d95a92f814fc4b8d3c224376428faa07a6a01a languageName: node linkType: hard @@ -2274,177 +2257,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/aix-ppc64@npm:0.25.5" +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm64@npm:0.25.5" +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm@npm:0.25.5" +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-x64@npm:0.25.5" +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-arm64@npm:0.25.5" +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-x64@npm:0.25.5" +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-arm64@npm:0.25.5" +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-x64@npm:0.25.5" +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm64@npm:0.25.5" +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm@npm:0.25.5" +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ia32@npm:0.25.5" +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-loong64@npm:0.25.5" +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-mips64el@npm:0.25.5" +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ppc64@npm:0.25.5" +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-riscv64@npm:0.25.5" +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-s390x@npm:0.25.5" +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-x64@npm:0.25.5" +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-arm64@npm:0.25.5" +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-x64@npm:0.25.5" +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-arm64@npm:0.25.5" +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-x64@npm:0.25.5" +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/sunos-x64@npm:0.25.5" +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-arm64@npm:0.25.5" +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-ia32@npm:0.25.5" +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-x64@npm:0.25.5" +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2549,6 +2539,18 @@ __metadata: languageName: node linkType: hard +"@exodus/bytes@npm:^1.15.0": + version: 1.15.0 + resolution: "@exodus/bytes@npm:1.15.0" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/b48aad9729653385d6ed055c28cfcf0b1b1481cf5d83f4375c12abd7988f1d20f69c80b5f95d4a1cc24d9abe32b9efc352a812d53884c26efea172aca8b6356d + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.4.2": version: 1.5.0 resolution: "@floating-ui/core@npm:1.5.0" @@ -2823,22 +2825,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/balanced-match@npm:^4.0.1": - version: 4.0.1 - resolution: "@isaacs/balanced-match@npm:4.0.1" - checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420 - languageName: node - linkType: hard - -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" - dependencies: - "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977 - languageName: node - linkType: hard - "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -2862,12 +2848,11 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1": - version: 0.6.1 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:^0.6.4": + version: 0.6.4 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.4" dependencies: - glob: "npm:^10.0.0" - magic-string: "npm:^0.30.0" + glob: "npm:^13.0.1" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" @@ -2875,7 +2860,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/0bcc2adbb49158018102bd9d84cd8572c770daee3d46733157933ef0330953bd5b9e102c26f2338ee7dfb8f21a7bb937134d23f8a7935d5dc88525a253557467 + checksum: 10c0/73149b2d41d5b8eff7dfe4d037a6903fe4123ae46f3928d88535020539f44159c4ea1b342e6a77d4c14219f2f743fea0ef96e81279cce8b6d247dc4d582e27ed languageName: node linkType: hard @@ -2923,7 +2908,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31": +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -2969,10 +2954,11 @@ __metadata: "@optimize-lodash/rollup-plugin": "npm:^6.0.0" "@react-spring/web": "npm:^9.7.5" "@reduxjs/toolkit": "npm:^2.0.1" - "@storybook/addon-a11y": "npm:^10.0.6" - "@storybook/addon-docs": "npm:^10.0.6" - "@storybook/addon-vitest": "npm:^10.0.6" - "@storybook/react-vite": "npm:^10.0.6" + "@rolldown/plugin-babel": "npm:^0.2.2" + "@storybook/addon-a11y": "npm:^10.3.0" + "@storybook/addon-docs": "npm:^10.3.0" + "@storybook/addon-vitest": "npm:^10.3.0" + "@storybook/react-vite": "npm:^10.3.0" "@testing-library/dom": "npm:^10.4.1" "@testing-library/react": "npm:^16.3.0" "@types/debug": "npm:^4" @@ -2998,12 +2984,12 @@ __metadata: "@types/redux-immutable": "npm:^4.0.3" "@types/requestidlecallback": "npm:^0.3.5" "@use-gesture/react": "npm:^10.3.1" - "@vitejs/plugin-legacy": "npm:^7.2.1" + "@vitejs/plugin-legacy": "npm:^8.0.0" "@vitejs/plugin-react": "npm:^5.0.0" - "@vitest/browser": "npm:^4.0.5" - "@vitest/browser-playwright": "npm:^4.0.5" - "@vitest/coverage-v8": "npm:^4.0.5" - "@vitest/ui": "npm:^4.0.5" + "@vitest/browser": "npm:^4.1.0" + "@vitest/browser-playwright": "npm:^4.1.0" + "@vitest/coverage-v8": "npm:^4.1.0" + "@vitest/ui": "npm:^4.1.0" arrow-key-navigation: "npm:^1.2.0" async-mutex: "npm:^0.5.0" axios: "npm:^1.4.0" @@ -3061,6 +3047,7 @@ __metadata: punycode: "npm:^2.3.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" + react-easy-crop: "npm:^5.5.6" react-helmet: "npm:^6.1.0" react-immutable-proptypes: "npm:^2.2.0" react-immutable-pure-component: "npm:^2.2.2" @@ -3078,12 +3065,12 @@ __metadata: redux-immutable: "npm:^4.0.0" regenerator-runtime: "npm:^0.14.0" requestidlecallback: "npm:^0.3.0" - rollup-plugin-gzip: "npm:^4.1.1" - rollup-plugin-visualizer: "npm:^6.0.3" + rollup-plugin-gzip: "npm:^4.2.0" + rollup-plugin-visualizer: "npm:^7.0.1" sass: "npm:^1.62.1" scroll-behavior: "npm:^0.11.0" stacktrace-js: "npm:^2.0.2" - storybook: "npm:^10.0.5" + storybook: "npm:^10.3.0" stringz: "npm:^2.1.0" stylelint: "npm:^17.0.0" stylelint-config-standard-scss: "npm:^17.0.0" @@ -3095,12 +3082,11 @@ __metadata: typescript-eslint: "npm:^8.55.0" typescript-plugin-css-modules: "npm:^5.2.0" use-debounce: "npm:^10.0.0" - vite: "npm:^7.1.1" + vite: "npm:^8.0.0" vite-plugin-manifest-sri: "npm:^0.2.0" - vite-plugin-pwa: "npm:^1.0.2" - vite-plugin-svgr: "npm:^4.3.0" - vite-tsconfig-paths: "npm:^6.0.0" - vitest: "npm:^4.0.5" + vite-plugin-pwa: "npm:^1.2.0" + vite-plugin-svgr: "npm:^4.5.0" + vitest: "npm:^4.1.0" wicg-inert: "npm:^3.1.2" workbox-expiration: "npm:^7.3.0" workbox-routing: "npm:^7.3.0" @@ -3131,7 +3117,7 @@ __metadata: express: "npm:^5.1.0" globals: "npm:^17.3.0" ioredis: "npm:^5.3.2" - jsdom: "npm:^28.0.0" + jsdom: "npm:^29.0.0" pg: "npm:^8.5.0" pg-connection-string: "npm:^2.6.0" pino: "npm:^10.0.0" @@ -3188,6 +3174,17 @@ __metadata: languageName: node linkType: hard +"@napi-rs/wasm-runtime@npm:^1.1.1": + version: 1.1.1 + resolution: "@napi-rs/wasm-runtime@npm:1.1.1" + dependencies: + "@emnapi/core": "npm:^1.7.1" + "@emnapi/runtime": "npm:^1.7.1" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10c0/04d57b67e80736e41fe44674a011878db0a8ad893f4d44abb9d3608debb7c174224cba2796ed5b0c1d367368159f3ca6be45f1c59222f70e32ddc880f803d447 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3290,6 +3287,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.120.0": + version: 0.120.0 + resolution: "@oxc-project/types@npm:0.120.0" + checksum: 10c0/3090ca95ed1467ae790a79cf7aa49d1ea4ac390dbfccb7afb914c138034d01e72115e2e137a3cc76f409ba424e4d2b160a599fe137c88033ad68ba2df1e40b29 + languageName: node + linkType: hard + "@oxfmt/binding-android-arm-eabi@npm:0.33.0": version: 0.33.0 resolution: "@oxfmt/binding-android-arm-eabi@npm:0.33.0" @@ -3694,6 +3698,135 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.10" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.10" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.10" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.10" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.10" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.10" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.10" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.10" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.10" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.10" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.10" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.10" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.10" + dependencies: + "@napi-rs/wasm-runtime": "npm:^1.1.1" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.10" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.10" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/plugin-babel@npm:^0.2.2": + version: 0.2.2 + resolution: "@rolldown/plugin-babel@npm:0.2.2" + dependencies: + picomatch: "npm:^4.0.3" + peerDependencies: + "@babel/core": ^7.29.0 || ^8.0.0-rc.1 + "@babel/plugin-transform-runtime": ^7.29.0 || ^8.0.0-rc.1 + "@babel/runtime": ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + "@babel/plugin-transform-runtime": + optional: true + "@babel/runtime": + optional: true + vite: + optional: true + checksum: 10c0/d00d6afd831c1efa5eac8bbe9eb4c78abfe731c744ffac99d2246e78a94ac8546ee26d0a304541143427e59a846c96d3d5728da540293d02c37bcbe3972428b3 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.47": version: 1.0.0-beta.47 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47" @@ -3701,6 +3834,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.10" + checksum: 10c0/7478f982d2705fef5f844e714aa264571d30368ef90883642fdc9eb869613c0c3060e8a8f69255e37a6fb600cbe4be35ce273d1f808fa6fe2a4b4e72116caf29 + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.0": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -3793,146 +3933,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.4" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@rollup/rollup-android-arm64@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-android-arm64@npm:4.46.4" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-darwin-arm64@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-darwin-arm64@npm:4.46.4" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-darwin-x64@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-darwin-x64@npm:4.46.4" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-freebsd-arm64@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.4" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-freebsd-x64@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-freebsd-x64@npm:4.46.4" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.4" - conditions: os=linux & cpu=arm & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm-musleabihf@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.4" - conditions: os=linux & cpu=arm & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm64-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.4" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm64-musl@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.4" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.4" - conditions: os=linux & cpu=loong64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-ppc64-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.4" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-riscv64-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.4" - conditions: os=linux & cpu=riscv64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-riscv64-musl@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.4" - conditions: os=linux & cpu=riscv64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-s390x-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.4" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-x64-gnu@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.4" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-x64-musl@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.4" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-win32-arm64-msvc@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.4" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-win32-ia32-msvc@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.4" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@rollup/rollup-win32-x64-msvc@npm:4.46.4": - version: 4.46.4 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.4" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -3954,10 +3954,10 @@ __metadata: languageName: node linkType: hard -"@standard-schema/spec@npm:^1.0.0": - version: 1.0.0 - resolution: "@standard-schema/spec@npm:1.0.0" - checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f +"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 languageName: node linkType: hard @@ -3968,48 +3968,46 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:^10.0.6": - version: 10.0.6 - resolution: "@storybook/addon-a11y@npm:10.0.6" +"@storybook/addon-a11y@npm:^10.3.0": + version: 10.3.0 + resolution: "@storybook/addon-a11y@npm:10.3.0" dependencies: "@storybook/global": "npm:^5.0.0" axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^10.0.6 - checksum: 10c0/44ab785b48476a9de44686be2d60c3720ed59c0d95d4ca7e5693ec2acb0cc2b062e4abde0a04f6bc0aeb54156e00a65803bb85c2a21f69c8329139096316b299 + storybook: ^10.3.0 + checksum: 10c0/3efcaeeaadf028427ed7af929a4f9e8d5ae1cfba6b0f0c20832009ecc6088e3df998c567ba7a2ee695cc5da4d17fe404f2094b80119a1a34a8c1c0a900d19ebe languageName: node linkType: hard -"@storybook/addon-docs@npm:^10.0.6": - version: 10.0.6 - resolution: "@storybook/addon-docs@npm:10.0.6" +"@storybook/addon-docs@npm:^10.3.0": + version: 10.3.0 + resolution: "@storybook/addon-docs@npm:10.3.0" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/csf-plugin": "npm:10.0.6" - "@storybook/icons": "npm:^1.6.0" - "@storybook/react-dom-shim": "npm:10.0.6" + "@storybook/csf-plugin": "npm:10.3.0" + "@storybook/icons": "npm:^2.0.1" + "@storybook/react-dom-shim": "npm:10.3.0" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.0.6 - checksum: 10c0/4b1a59416cf54853a09a156e1e1016c13f4d477f73562be960b12d86eeb86d1f1d2da5111255cc9da1cd232955d0ea68b1c42e4e87671042682dcb7eb2a059a0 + storybook: ^10.3.0 + checksum: 10c0/f70ce1bb5bd31ace42386c8000b5e1b0a9b509c574b9b1dca4f1e55ebaa081b93db45a5661e3dfe0133581abfcb70a7acbd4bd45ee0f846cbc01f631ced54ffa languageName: node linkType: hard -"@storybook/addon-vitest@npm:^10.0.6": - version: 10.0.6 - resolution: "@storybook/addon-vitest@npm:10.0.6" +"@storybook/addon-vitest@npm:^10.3.0": + version: 10.3.0 + resolution: "@storybook/addon-vitest@npm:10.3.0" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.6.0" - prompts: "npm:^2.4.0" - ts-dedent: "npm:^2.2.0" + "@storybook/icons": "npm:^2.0.1" peerDependencies: "@vitest/browser": ^3.0.0 || ^4.0.0 "@vitest/browser-playwright": ^4.0.0 "@vitest/runner": ^3.0.0 || ^4.0.0 - storybook: ^10.0.6 + storybook: ^10.3.0 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: "@vitest/browser": @@ -4020,32 +4018,32 @@ __metadata: optional: true vitest: optional: true - checksum: 10c0/6377cfbac4c2f9f9b43006131e0273c2d2f8b76b0ce01fdcb42f2935ac245a2236fab60b2543507a69852dba1ac4e3b770b0a96f5595123e0f9599609d0a57e2 + checksum: 10c0/d6c1c688af591155b9dcdc68be55ab04cb864b569b86c91fafc4407e5c1eec30bacc2d23258837eda0e069ae86f3f95d134b8e6cc7b1d7f4b73f7a00f7470244 languageName: node linkType: hard -"@storybook/builder-vite@npm:10.0.6": - version: 10.0.6 - resolution: "@storybook/builder-vite@npm:10.0.6" +"@storybook/builder-vite@npm:10.3.0": + version: 10.3.0 + resolution: "@storybook/builder-vite@npm:10.3.0" dependencies: - "@storybook/csf-plugin": "npm:10.0.6" + "@storybook/csf-plugin": "npm:10.3.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.0.6 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10c0/1e5f163f2abd62f99292ee48cde10f68d8db1ba6f4613c20cb2af679d44c3b548c7a2209338d24b4ffda2a245ae68bfcfc57af9f76de7f4a251253635c4179d8 + storybook: ^10.3.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/62dc1ead65b5a8a17187862ac13a47867dd6f1fc9ff43e6d0c8e1a82d6c4cc8a6bb219cd4adcbce664654cc1c0bf1cb82fd56b706923b3b7220290eef3011e3f languageName: node linkType: hard -"@storybook/csf-plugin@npm:10.0.6": - version: 10.0.6 - resolution: "@storybook/csf-plugin@npm:10.0.6" +"@storybook/csf-plugin@npm:10.3.0": + version: 10.3.0 + resolution: "@storybook/csf-plugin@npm:10.3.0" dependencies: unplugin: "npm:^2.3.5" peerDependencies: esbuild: "*" rollup: "*" - storybook: ^10.0.6 + storybook: ^10.3.0 vite: "*" webpack: "*" peerDependenciesMeta: @@ -4057,7 +4055,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10c0/422286e7d2ef3f64ea2a71bdd1cad0bc3e850b31574f048529616eeb3cd0b1216b5d680c8b36bd300e31d141ad7781586cc7d57763babf993c31430b854491c4 + checksum: 10c0/59c1784257d313fb7298a34c079462ba0ad518f82369441e6432da1abdc47ecb16afc40bbb02fd1331183be10914d383ed1f68a6b431ea25019fd50b63453e7f languageName: node linkType: hard @@ -4068,35 +4066,35 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.6.0": - version: 1.6.0 - resolution: "@storybook/icons@npm:1.6.0" +"@storybook/icons@npm:^2.0.1": + version: 2.0.1 + resolution: "@storybook/icons@npm:2.0.1" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10c0/bbec9201a78a730195f9cf377b15856dc414a54d04e30d16c379d062425cc617bfd0d6586ba1716012cfbdab461f0c9693a6a52920f9bd09c7b4291fb116f59c + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/df2bbf1a5b50f12ab1bf78cae6de4dbf7c49df0e3a5f845553b51b20adbe8386a09fd172ea60342379f9284bb528cba2d0e2659cae6eb8d015cf92c8b32f1222 languageName: node linkType: hard -"@storybook/react-dom-shim@npm:10.0.6": - version: 10.0.6 - resolution: "@storybook/react-dom-shim@npm:10.0.6" +"@storybook/react-dom-shim@npm:10.3.0": + version: 10.3.0 + resolution: "@storybook/react-dom-shim@npm:10.3.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.6 - checksum: 10c0/067b86aeadc96d0fedccd3e047c4b506484e7b2af21796b995e97cd3f4f31d77f8a2674fb29f77b45245353cc714a4511f49fa618a7386ba12706c663701da08 + storybook: ^10.3.0 + checksum: 10c0/59690a34b39299853ae689e891b88b01dd3019b1b9da56aa9cfc318ccd3880ec3736ec2f36b08c924df6952e510ea62d2810c32d2fda5bb6db71d133b3c94984 languageName: node linkType: hard -"@storybook/react-vite@npm:^10.0.6": - version: 10.0.6 - resolution: "@storybook/react-vite@npm:10.0.6" +"@storybook/react-vite@npm:^10.3.0": + version: 10.3.0 + resolution: "@storybook/react-vite@npm:10.3.0" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.6.1" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:^0.6.4" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:10.0.6" - "@storybook/react": "npm:10.0.6" + "@storybook/builder-vite": "npm:10.3.0" + "@storybook/react": "npm:10.3.0" empathic: "npm:^2.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^8.0.0" @@ -4105,27 +4103,29 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.6 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10c0/1689b7d866650912a8fe5ff2ec1c35292d408085d2d819f3a13055805f72d67b5c0fbeabe359de9a65f6f129500e20b935e832b280559afe5050b10432ccf6f2 + storybook: ^10.3.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/ab000cb70698e971965d5970dd547579dc14428238055826b6d0d7069bb56fc5e6ce7dc22a0ee0f4c4b9c3f0c505a6c246367d5bedac0bc70720fee666a24d88 languageName: node linkType: hard -"@storybook/react@npm:10.0.6": - version: 10.0.6 - resolution: "@storybook/react@npm:10.0.6" +"@storybook/react@npm:10.3.0": + version: 10.3.0 + resolution: "@storybook/react@npm:10.3.0" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/react-dom-shim": "npm:10.0.6" + "@storybook/react-dom-shim": "npm:10.3.0" + react-docgen: "npm:^8.0.2" + react-docgen-typescript: "npm:^2.2.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.6 + storybook: ^10.3.0 typescript: ">= 4.9.x" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/7545a3f84a64ccfacc84f41d4f4b744fb71861f1a413ba48e4440694fb89f591273182771324097c420fca65253d738bde852e14a776e5b1df30a0d4beb100b0 + checksum: 10c0/604414597b2093877b2ef5c9fc1d04d2b231a5cafdb1acc303b888fbf3c701c014642b20fa1f40048e3517bddd5b2e05f51427909f4c5475f1cecae768d352ab languageName: node linkType: hard @@ -4284,18 +4284,17 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.6.3": - version: 6.6.3 - resolution: "@testing-library/jest-dom@npm:6.6.3" +"@testing-library/jest-dom@npm:^6.9.1": + version: 6.9.1 + resolution: "@testing-library/jest-dom@npm:6.9.1" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" + picocolors: "npm:^1.1.1" redent: "npm:^3.0.0" - checksum: 10c0/5566b6c0b7b0709bc244aec3aa3dc9e5f4663e8fb2b99d8cd456fc07279e59db6076cbf798f9d3099a98fca7ef4cd50e4e1f4c4dec5a60a8fad8d24a638a5bf6 + checksum: 10c0/4291ebd2f0f38d14cefac142c56c337941775a5807e2a3d6f1a14c2fbd6be76a18e498ed189e95bedc97d9e8cf1738049bc76c85b5bc5e23fae7c9e10f7b3a12 languageName: node linkType: hard @@ -4328,12 +4327,12 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.10.0": - version: 0.10.0 - resolution: "@tybys/wasm-util@npm:0.10.0" +"@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/044feba55c1e2af703aa4946139969badb183ce1a659a75ed60bc195a90e73a3f3fc53bcd643497c9954597763ddb051fec62f80962b2ca6fc716ba897dc696e + checksum: 10c0/b255094f293794c6d2289300c5fbcafbb5532a3aed3a5ffd2f8dc1828e639b88d75f6a376dd8f94347a44813fd7a7149d8463477a9a49525c8b2dcaa38c2d1e8 languageName: node linkType: hard @@ -4344,7 +4343,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:*, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.5": +"@types/babel__core@npm:*, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -4385,12 +4384,12 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.18.0, @types/babel__traverse@npm:^7.20.6": - version: 7.20.7 - resolution: "@types/babel__traverse@npm:7.20.7" +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.20.6, @types/babel__traverse@npm:^7.20.7": + version: 7.28.0 + resolution: "@types/babel__traverse@npm:7.28.0" dependencies: - "@babel/types": "npm:^7.20.7" - checksum: 10c0/5386f0af44f8746b063b87418f06129a814e16bb2686965a575e9d7376b360b088b89177778d8c426012abc43dd1a2d8ec3218bfc382280c898682746ce2ffbd + "@babel/types": "npm:^7.28.2" + checksum: 10c0/b52d7d4e8fc6a9018fe7361c4062c1c190f5778cf2466817cb9ed19d69fbbb54f9a85ffedeb748ed8062d2cf7d4cc088ee739848f47c57740de1c48cbf0d0994 languageName: node linkType: hard @@ -4480,7 +4479,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": +"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -5172,26 +5171,26 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-legacy@npm:^7.2.1": - version: 7.2.1 - resolution: "@vitejs/plugin-legacy@npm:7.2.1" +"@vitejs/plugin-legacy@npm:^8.0.0": + version: 8.0.0 + resolution: "@vitejs/plugin-legacy@npm:8.0.0" dependencies: - "@babel/core": "npm:^7.28.0" + "@babel/core": "npm:^7.29.0" "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" - "@babel/plugin-transform-modules-systemjs": "npm:^7.27.1" - "@babel/preset-env": "npm:^7.28.0" - babel-plugin-polyfill-corejs3: "npm:^0.13.0" - babel-plugin-polyfill-regenerator: "npm:^0.6.5" - browserslist: "npm:^4.25.1" + "@babel/plugin-transform-modules-systemjs": "npm:^7.29.0" + "@babel/preset-env": "npm:^7.29.0" + babel-plugin-polyfill-corejs3: "npm:^0.14.1" + babel-plugin-polyfill-regenerator: "npm:^0.6.7" + browserslist: "npm:^4.28.1" browserslist-to-esbuild: "npm:^2.1.1" - core-js: "npm:^3.45.0" - magic-string: "npm:^0.30.17" + core-js: "npm:^3.48.0" + magic-string: "npm:^0.30.21" regenerator-runtime: "npm:^0.14.1" systemjs: "npm:^6.15.1" peerDependencies: terser: ^5.16.0 - vite: ^7.0.0 - checksum: 10c0/bb9c14793c304ab84202a27218df4a71472ef8998c984121dcbc268b7dad8141d3c153f899794d5d62fed0e422ca756d24780e7da3f053e0ff87a37429808737 + vite: ^8.0.0 + checksum: 10c0/5b88d81ac1eb82aa607d9809f3df1354ee065095f5941b63b51d9aea2a73ba9cee8d08d7af944971b651dedf12fea931fec99baf9673fde54e2331abc8d709c9 languageName: node linkType: hard @@ -5211,165 +5210,108 @@ __metadata: languageName: node linkType: hard -"@vitest/browser-playwright@npm:^4.0.5": - version: 4.0.13 - resolution: "@vitest/browser-playwright@npm:4.0.13" +"@vitest/browser-playwright@npm:^4.1.0": + version: 4.1.0 + resolution: "@vitest/browser-playwright@npm:4.1.0" dependencies: - "@vitest/browser": "npm:4.0.13" - "@vitest/mocker": "npm:4.0.13" + "@vitest/browser": "npm:4.1.0" + "@vitest/mocker": "npm:4.1.0" tinyrainbow: "npm:^3.0.3" peerDependencies: playwright: "*" - vitest: 4.0.13 + vitest: 4.1.0 peerDependenciesMeta: playwright: optional: false - checksum: 10c0/5a387eb02534736a25cfff442e66e8c41ef97f0db744ffe8360e484af61d66db793cb44ba8681471b8c21ba509db1775f1ba688bc7f50325eee76918773848cb + checksum: 10c0/af2f6fc36eb56e3c1ac6e31b0ab2a2f4ca0bda86a306d0991b2f01047213fb191339b35775103af11ce1ef323ec72432eebe4bfeccd744d5e7c658716f1b985a languageName: node linkType: hard -"@vitest/browser@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/browser@npm:4.0.13" - dependencies: - "@vitest/mocker": "npm:4.0.13" - "@vitest/utils": "npm:4.0.13" - magic-string: "npm:^0.30.21" - pixelmatch: "npm:7.1.0" - pngjs: "npm:^7.0.0" - sirv: "npm:^3.0.2" - tinyrainbow: "npm:^3.0.3" - ws: "npm:^8.18.3" - peerDependencies: - vitest: 4.0.13 - checksum: 10c0/22c9297888a7288717cad706ca08159b3af05337a2f9b8da98fe74e683d534c8d816e40fece96f218d223a54c06762c5aa2a5db23ce8565c174ab9a70aade7f0 - languageName: node - linkType: hard - -"@vitest/browser@npm:^4.0.5": - version: 4.0.15 - resolution: "@vitest/browser@npm:4.0.15" +"@vitest/browser@npm:4.1.0, @vitest/browser@npm:^4.1.0": + version: 4.1.0 + resolution: "@vitest/browser@npm:4.1.0" dependencies: - "@vitest/mocker": "npm:4.0.15" - "@vitest/utils": "npm:4.0.15" + "@blazediff/core": "npm:1.9.1" + "@vitest/mocker": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" magic-string: "npm:^0.30.21" - pixelmatch: "npm:7.1.0" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" tinyrainbow: "npm:^3.0.3" - ws: "npm:^8.18.3" + ws: "npm:^8.19.0" peerDependencies: - vitest: 4.0.15 - checksum: 10c0/b74c1ab5b03a494b1a91e270417a794e616d3d9d5002de816b6a9913073fdf5939ca63b30a37e4e865cb9402b8682254facaf4b854d002b65b6ea85fccf38253 + vitest: 4.1.0 + checksum: 10c0/33b35cea63f392b6afafb6636bebe7ff0d234b1c120ec74a97462c7a7cbdbc67f415a5f0f95651f4074d53bfe12d4ff3ae8f16ba79045226df6365c77f950e18 languageName: node linkType: hard -"@vitest/coverage-v8@npm:^4.0.5": - version: 4.0.13 - resolution: "@vitest/coverage-v8@npm:4.0.13" +"@vitest/coverage-v8@npm:^4.1.0": + version: 4.1.0 + resolution: "@vitest/coverage-v8@npm:4.1.0" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.0.13" - ast-v8-to-istanbul: "npm:^0.3.8" - debug: "npm:^4.4.3" + "@vitest/utils": "npm:4.1.0" + ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" - istanbul-lib-source-maps: "npm:^5.0.6" istanbul-reports: "npm:^3.2.0" - magicast: "npm:^0.5.1" - std-env: "npm:^3.10.0" + magicast: "npm:^0.5.2" + obug: "npm:^2.1.1" + std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.0.3" peerDependencies: - "@vitest/browser": 4.0.13 - vitest: 4.0.13 + "@vitest/browser": 4.1.0 + vitest: 4.1.0 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/dd462b13605fca62d20cb5a4f9d7cfda2bfa5e77aedc16ad5a633d8dabb24f68e96382ac4d16c2fdcddb45e7c4717e558f5ac51a70c64857f5e89d12d8700823 - languageName: node - linkType: hard - -"@vitest/expect@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/expect@npm:3.2.4" - dependencies: - "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:3.2.4" - "@vitest/utils": "npm:3.2.4" - chai: "npm:^5.2.0" - tinyrainbow: "npm:^2.0.0" - checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db - languageName: node - linkType: hard - -"@vitest/expect@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/expect@npm:4.0.13" - dependencies: - "@standard-schema/spec": "npm:^1.0.0" - "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.13" - "@vitest/utils": "npm:4.0.13" - chai: "npm:^6.2.1" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/1cd7dc02cb650d024826f2e20260d23c2b9ab6733341045ffb59be7af73402eecd2422198d7e4eac609710730b6d11f0faf22af0c074d65445ab88d9da7f6556 + checksum: 10c0/0bcbc9d20dd4c998ff76b82a721d6000f1300346b93cfc441f9012797a34be65bb73dc99451275d7f7dcb06b98856b4e5dc30b2c483051ec2320e9a89af14179 languageName: node linkType: hard -"@vitest/mocker@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/mocker@npm:3.2.4" - dependencies: - "@vitest/spy": "npm:3.2.4" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.17" - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db languageName: node linkType: hard -"@vitest/mocker@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/mocker@npm:4.0.13" +"@vitest/expect@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/expect@npm:4.1.0" dependencies: - "@vitest/spy": "npm:4.0.13" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/667ec4fbb77a28ede1b055b9d962beed92c2dd2d83b7bab1ed22239578a7b399180a978e26ef136301c0bc7c57c75ca178cda55ec94081856437e3b4be4a3e19 + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/91cd7bb036401df5dfd9204f3de9a0afdb21dea6ee154622e5ed849e87a0df68b74258d490559c7046d3c03bc7aa634e9b0c166942a21d5e475c86c971486091 languageName: node linkType: hard -"@vitest/mocker@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/mocker@npm:4.0.15" +"@vitest/mocker@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/mocker@npm:4.1.0" dependencies: - "@vitest/spy": "npm:4.0.15" + "@vitest/spy": "npm:4.1.0" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10c0/7a164aa25daab3e92013ec95aab5c5778e805b1513e266ce6c00e0647eb9f7b281e33fcaf0d9d2aed88321079183b60c1aeab90961f618c24e2e3a5143308850 + checksum: 10c0/f61d3df6461008eb1e62ba465172207b29bd0d9866ff6bc88cd40fc99cd5d215ad89e2894ba6de87068e33f75de903b28a65ccc6074edf3de1fbead6a4a369cc languageName: node linkType: hard @@ -5382,42 +5324,34 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/pretty-format@npm:4.0.13" - dependencies: - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/c32ebd3457fd4b92fa89800b0ddaa2ca7de84df75be3c64f87ace006f3a3ec546a6ffd4c06f88e3161e80f9e10c83dfee61150e682eaa5a1871240d98c7ef0eb - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/pretty-format@npm:4.0.15" +"@vitest/pretty-format@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/pretty-format@npm:4.1.0" dependencies: tinyrainbow: "npm:^3.0.3" - checksum: 10c0/d863f3818627b198f9c66515f8faa200e76a1c30c7f2b25ac805e253204ae51abbfa6b6211c58b2d75e1a273a2e6925e3a8fa480ebfa9c479d75a19815e1cbea + checksum: 10c0/638077f53b5f24ff2d4bc062e69931fa718141db28ddafe435de98a402586b82e8c3cadfc580c0ad233d7f0203aa22d866ac2adca98b83038dbd5423c3d7fe27 languageName: node linkType: hard -"@vitest/runner@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/runner@npm:4.0.13" +"@vitest/runner@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/runner@npm:4.1.0" dependencies: - "@vitest/utils": "npm:4.0.13" + "@vitest/utils": "npm:4.1.0" pathe: "npm:^2.0.3" - checksum: 10c0/e9f95b8a413f875123e5c32322dd92bd523d6e3ba25b054f0e865f42e01f82666b847535fe5ea2ff3238faa2df16cefc7e5845d3d5ccfecb3a96ab924d31e760 + checksum: 10c0/9e09ca1b9070d3fe26c9bd48443d21b9fe2cb9abb2f694300bd9e5065f4e904f7322c07cd4bafadfed6fb11adfb50e4d1535f327ac6d24b6c373e92be90510bc languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/snapshot@npm:4.0.13" +"@vitest/snapshot@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/snapshot@npm:4.1.0" dependencies: - "@vitest/pretty-format": "npm:4.0.13" + "@vitest/pretty-format": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/ad3fbe9ff30bc294811556f958e0014cb03888ea06ac7c05ab41e20c582fe8e27d8f4176aaf8a8e230fc6377124af30f5622173fb459b70a30ff9dd622664be2 + checksum: 10c0/582c22988c47a99d93dd17ef660427fefe101f67ae4394b64fe58ec103ddc55fc5993626b4a2b556e0a38d40552abaca78196907455e794805ba197b3d56860f languageName: node linkType: hard @@ -5430,34 +5364,27 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/spy@npm:4.0.13" - checksum: 10c0/64dc4c496eb9aacd3137beedccdb3265c895f8cd2626b3f76d7324ad944be5b1567ede2652eee407991796879270a63abdec4453c73185e637a1d7ff9cd1a009 - languageName: node - linkType: hard - -"@vitest/spy@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/spy@npm:4.0.15" - checksum: 10c0/22319cead44964882d9e7bd197a9cd317c945ff75a4a9baefe06c32c5719d4cb887e86b4560d79716765f288a93a6c76e78e3eeab0000f24b2236dab678b7c34 +"@vitest/spy@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/spy@npm:4.1.0" + checksum: 10c0/363776bbffda45af76ff500deacb9b1a35ad8b889462c1be9ebe5f29578ce1dd2c4bd7858c8188614a7db9699a5c802b7beb72e5a18ab5130a70326817961446 languageName: node linkType: hard -"@vitest/ui@npm:^4.0.5": - version: 4.0.13 - resolution: "@vitest/ui@npm:4.0.13" +"@vitest/ui@npm:^4.1.0": + version: 4.1.0 + resolution: "@vitest/ui@npm:4.1.0" dependencies: - "@vitest/utils": "npm:4.0.13" + "@vitest/utils": "npm:4.1.0" fflate: "npm:^0.8.2" - flatted: "npm:^3.3.3" + flatted: "npm:3.4.0" pathe: "npm:^2.0.3" sirv: "npm:^3.0.2" tinyglobby: "npm:^0.2.15" tinyrainbow: "npm:^3.0.3" peerDependencies: - vitest: 4.0.13 - checksum: 10c0/7656762bc6a9c99850639d0809ada53ad4b842e4d9a8c7b82987b60bcf1675c98c077516a3777fce9580255538d0d050c92cb1e6f6296af6365f2387d7a972b9 + vitest: 4.1.0 + checksum: 10c0/3629aadc120b992c80a18c32879358a40d936245ab987f64bd76cf6b13abb319b2ef9a029be69be7f6ea7f7ae9182e54f6d631fd57df32ba31060d6ae488048e languageName: node linkType: hard @@ -5472,23 +5399,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.0.13": - version: 4.0.13 - resolution: "@vitest/utils@npm:4.0.13" - dependencies: - "@vitest/pretty-format": "npm:4.0.13" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/1b64872e82a652f11bfd813c0140eaae9b6e4ece39fc0e460ab2b3111b925892f1128f3b27f3a280471cfc404bb9c9289c59f8ca5387950ab35d024d154e9ec1 - languageName: node - linkType: hard - -"@vitest/utils@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/utils@npm:4.0.15" +"@vitest/utils@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/utils@npm:4.1.0" dependencies: - "@vitest/pretty-format": "npm:4.0.15" + "@vitest/pretty-format": "npm:4.1.0" + convert-source-map: "npm:^2.0.0" tinyrainbow: "npm:^3.0.3" - checksum: 10c0/2ef661c2c2359ae956087f0b728b6a0f7555cd10524a7def27909f320f6b8ba00560ee1bd856bf68d4debc01020cea21b200203a5d2af36c44e94528c5587aee + checksum: 10c0/222afbdef4f680a554bb6c3d946a4a879a441ebfb8597295cb7554d295e0e2624f3d4c2920b5767bbb8961a9f8a16756270ffc84032f5ea432cdce080ccab050 languageName: node linkType: hard @@ -5789,14 +5707,14 @@ __metadata: languageName: node linkType: hard -"ast-v8-to-istanbul@npm:^0.3.8": - version: 0.3.8 - resolution: "ast-v8-to-istanbul@npm:0.3.8" +"ast-v8-to-istanbul@npm:^1.0.0": + version: 1.0.0 + resolution: "ast-v8-to-istanbul@npm:1.0.0" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" estree-walker: "npm:^3.0.3" - js-tokens: "npm:^9.0.1" - checksum: 10c0/6f7d74fc36011699af6d4ad88ecd8efc7d74bd90b8e8dbb1c69d43c8f4bec0ed361fb62a5b5bd98bbee02ee87c62cd8bcc25a39634964e45476bf5489dfa327f + js-tokens: "npm:^10.0.0" + checksum: 10c0/35e57b754ba63287358094d4f7ae8de2de27286fb4e76a1fbf28b2e67e3b670b59c3f511882473d0fd2cdbaa260062e3cd4f216b724c70032e2b09e5cebbd618 languageName: node linkType: hard @@ -5925,39 +5843,39 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.14": - version: 0.4.14 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.14" +"babel-plugin-polyfill-corejs2@npm:^0.4.15": + version: 0.4.17 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.17" dependencies: - "@babel/compat-data": "npm:^7.27.7" - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-define-polyfill-provider": "npm:^0.6.8" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/d74cba0600a6508e86d220bde7164eb528755d91be58020e5ea92ea7fbb12c9d8d2c29246525485adfe7f68ae02618ec428f9a589cac6cbedf53cc3972ad7fbe + checksum: 10c0/1284960ea403c63b0dd598f338666c4b17d489aefee30b4da6a7313eff1d91edffb0ccf26341a6e5d94231684b74e016eade66b3921ea112f8b0e4980fa08a5c languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.13.0": - version: 0.13.0 - resolution: "babel-plugin-polyfill-corejs3@npm:0.13.0" +"babel-plugin-polyfill-corejs3@npm:^0.14.0, babel-plugin-polyfill-corejs3@npm:^0.14.1": + version: 0.14.2 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.2" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" - core-js-compat: "npm:^3.43.0" + "@babel/helper-define-polyfill-provider": "npm:^0.6.8" + core-js-compat: "npm:^3.48.0" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/5d8e228da425edc040d8c868486fd01ba10b0440f841156a30d9f8986f330f723e2ee61553c180929519563ef5b64acce2caac36a5a847f095d708dda5d8206d + checksum: 10c0/32f70442f142d0f5607f4b57c121c573b106e09da8659c0f03108a85bf1d09ba5bdc89595a82b34ff76c19f1faf3d1c831b56166f03babf69c024f36da77c3bf languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.6.5": - version: 0.6.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.5" +"babel-plugin-polyfill-regenerator@npm:^0.6.6, babel-plugin-polyfill-regenerator@npm:^0.6.7": + version: 0.6.8 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.8" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/helper-define-polyfill-provider": "npm:^0.6.8" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/63aa8ed716df6a9277c6ab42b887858fa9f57a70cc1d0ae2b91bdf081e45d4502848cba306fb60b02f59f99b32fd02ff4753b373cac48ccdac9b7d19dd56f06d + checksum: 10c0/7c8b2497c29fa880e0acdc8e7b93e29b81b154179b83beb0476eb2c4e7a78b6b42fc35c2554ca250c9bd6d39941eaf75416254b8592ce50979f9a12e1d51c049 languageName: node linkType: hard @@ -5982,6 +5900,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + "baseline-browser-mapping@npm:^2.8.30": version: 2.8.30 resolution: "baseline-browser-mapping@npm:2.8.30" @@ -6066,6 +5991,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a + languageName: node + linkType: hard + "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -6088,7 +6022,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.28.1": +"browserslist@npm:^4.24.0, browserslist@npm:^4.28.1": version: 4.28.1 resolution: "browserslist@npm:4.28.1" dependencies: @@ -6120,6 +6054,15 @@ __metadata: languageName: node linkType: hard +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: "npm:^7.0.0" + checksum: 10c0/8e575981e79c2bcf14d8b1c027a3775c095d362d1382312f444a7c861b0e21513c0bd8db5bd2b16e50ba0709fa622d4eab6b53192d222120305e68359daece29 + languageName: node + linkType: hard + "bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -6233,20 +6176,10 @@ __metadata: languageName: node linkType: hard -"chai@npm:^6.2.1": - version: 6.2.1 - resolution: "chai@npm:6.2.1" - checksum: 10c0/0c2d84392d7c6d44ca5d14d94204f1760e22af68b83d1f4278b5c4d301dabfc0242da70954dd86b1eda01e438f42950de6cf9d569df2103678538e4014abe50b - languageName: node - linkType: hard - -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10c0/ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2 +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 languageName: node linkType: hard @@ -6353,6 +6286,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^9.0.1": + version: 9.0.1 + resolution: "cliui@npm:9.0.1" + dependencies: + string-width: "npm:^7.2.0" + strip-ansi: "npm:^7.1.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10c0/13441832e9efe7c7a76bd2b8e683555c478d461a9f249dc5db9b17fe8d4b47fa9277b503914b90bd00e4a151abb6b9b02b2288972ffe2e5e3ca40bcb1c2330d3 + languageName: node + linkType: hard + "cluster-key-slot@npm:^1.1.0": version: 1.1.2 resolution: "cluster-key-slot@npm:1.1.2" @@ -6508,12 +6452,12 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.43.0": - version: 3.44.0 - resolution: "core-js-compat@npm:3.44.0" +"core-js-compat@npm:^3.48.0": + version: 3.49.0 + resolution: "core-js-compat@npm:3.49.0" dependencies: - browserslist: "npm:^4.25.1" - checksum: 10c0/5de4b042b8bb232b8390be3079030de5c7354610f136ed3eb91310a44455a78df02cfcf49b2fd05d5a5aa2695460620abf1b400784715f7482ed4770d40a68b2 + browserslist: "npm:^4.28.1" + checksum: 10c0/546e64b7ce45f724825bc13c1347f35c0459a6e71c0dcccff3ec21fbff463f5b0b97fc1220e6d90302153863489301793276fe2bf96f46001ff555ead4140308 languageName: node linkType: hard @@ -6524,10 +6468,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.30.2, core-js@npm:^3.45.0": - version: 3.47.0 - resolution: "core-js@npm:3.47.0" - checksum: 10c0/9b1a7088b7c660c7b8f1d4c90bb1816a8d5352ebdcb7bc742e3a0e4eb803316b5aa17bacb8769522342196351a5430178f46914644f2bfdb94ce0ced3c7fd523 +"core-js@npm:^3.30.2, core-js@npm:^3.48.0": + version: 3.49.0 + resolution: "core-js@npm:3.49.0" + checksum: 10c0/2e42edb47eda38fd5368380131623c8aa5d4a6b42164125b17744bdc08fa5ebbbdd06b4b4aa6ca3663470a560b0f2fba48e18f142dfe264b0039df85bc625694 languageName: node linkType: hard @@ -6659,6 +6603,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + "css-tree@npm:^3.0.1, css-tree@npm:^3.1.0": version: 3.1.0 resolution: "css-tree@npm:3.1.0" @@ -6692,18 +6646,6 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^5.3.7": - version: 5.3.7 - resolution: "cssstyle@npm:5.3.7" - dependencies: - "@asamuzakjp/css-color": "npm:^4.1.1" - "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.21" - css-tree: "npm:^3.1.0" - lru-cache: "npm:^11.2.4" - checksum: 10c0/9330f014f4209df06305264b92b8e963dfef636fdc2ae7d13f24ea7da6468aba1dc5eb13082621258bdd22cbd7fb7cb291894e188a3cdf660e8b79cd2c5e5e0e - languageName: node - linkType: hard - "csstype@npm:^3.0.2, csstype@npm:^3.2.2": version: 3.2.3 resolution: "csstype@npm:3.2.3" @@ -6768,7 +6710,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -6817,6 +6759,23 @@ __metadata: languageName: node linkType: hard +"default-browser-id@npm:^5.0.0": + version: 5.0.1 + resolution: "default-browser-id@npm:5.0.1" + checksum: 10c0/5288b3094c740ef3a86df9b999b04ff5ba4dee6b64e7b355c0fff5217752c8c86908d67f32f6cba9bb4f9b7b61a1b640c0a4f9e34c57e0ff3493559a625245ee + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1, default-browser@npm:^5.4.0": + version: 5.5.0 + resolution: "default-browser@npm:5.5.0" + dependencies: + bundle-name: "npm:^4.1.0" + default-browser-id: "npm:^5.0.0" + checksum: 10c0/576593b617b17a7223014b4571bfe1c06a2581a4eb8b130985d90d253afa3f40999caec70eb0e5776e80d4af6a41cce91018cd3f86e57ad578bf59e46fb19abe + languageName: node + linkType: hard + "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" @@ -6828,10 +6787,10 @@ __metadata: languageName: node linkType: hard -"define-lazy-prop@npm:^2.0.0": - version: 2.0.0 - resolution: "define-lazy-prop@npm:2.0.0" - checksum: 10c0/db6c63864a9d3b7dc9def55d52764968a5af296de87c1b2cc71d8be8142e445208071953649e0386a8cc37cfcf9a2067a47207f1eb9ff250c2a269658fdae422 +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 10c0/5ab0b2bf3fa58b3a443140bbd4cd3db1f91b985cc8a246d330b9ac3fc0b6a325a6d82bddc0b055123d745b3f9931afeea74a5ec545439a1630b9c8512b0eeb49 languageName: node linkType: hard @@ -6899,6 +6858,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "detect-passive-events@npm:^2.0.3": version: 2.0.3 resolution: "detect-passive-events@npm:2.0.3" @@ -7270,10 +7236,10 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.7.0": - version: 1.7.0 - resolution: "es-module-lexer@npm:1.7.0" - checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b +"es-module-lexer@npm:^2.0.0": + version: 2.0.0 + resolution: "es-module-lexer@npm:2.0.0" + checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817 languageName: node linkType: hard @@ -7318,35 +7284,36 @@ __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, 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" +"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": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -7390,6 +7357,8 @@ __metadata: optional: true "@esbuild/openbsd-x64": optional: true + "@esbuild/openharmony-arm64": + optional: true "@esbuild/sunos-x64": optional: true "@esbuild/win32-arm64": @@ -7400,7 +7369,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10c0/aba8cbc11927fa77562722ed5e95541ce2853f67ad7bdc40382b558abc2e0ec57d92ffb820f082ba2047b4ef9f3bc3da068cdebe30dfd3850cfa3827a78d604e + checksum: 10c0/cf83f626f55500f521d5fe7f4bc5871bec240d3deb2a01fbd379edc43b3664d1167428738a5aad8794b35d1cca985c44c375b1cd38a2ca613c77ced2c83aafcd languageName: node linkType: hard @@ -7831,10 +7800,10 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.2.2": - version: 1.2.2 - resolution: "expect-type@npm:1.2.2" - checksum: 10c0/6019019566063bbc7a690d9281d920b1a91284a4a093c2d55d71ffade5ac890cf37a51e1da4602546c4b56569d2ad2fc175a2ccee77d1ae06cb3af91ef84f44b +"expect-type@npm:^1.3.0": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd languageName: node linkType: hard @@ -8066,6 +8035,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:3.4.0": + version: 3.4.0 + resolution: "flatted@npm:3.4.0" + checksum: 10c0/033b0d28dc7c11c20cdddfef160647d37ee6f49cac265e6315d7c172a8a518a971316938d49c72cce3e20bddd40f1bae1455a5cba29f9741fcfb0af4d3491fa4 + languageName: node + linkType: hard + "flatted@npm:^3.2.9, flatted@npm:^3.3.3": version: 3.3.3 resolution: "flatted@npm:3.3.3" @@ -8331,7 +8307,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2": +"glob@npm:^10.2.2": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -8363,6 +8339,17 @@ __metadata: languageName: node linkType: hard +"glob@npm:^13.0.1": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10c0/269c236f11a9b50357fe7a8c6aadac667e01deb5242b19c84975628f05f4438d8ee1354bb62c5d6c10f37fd59911b54d7799730633a2786660d8c69f1d18120a + languageName: node + linkType: hard + "glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -8442,13 +8429,6 @@ __metadata: languageName: node linkType: hard -"globrex@npm:^0.1.2": - version: 0.1.2 - resolution: "globrex@npm:0.1.2" - checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 - languageName: node - linkType: hard - "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -8660,7 +8640,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": +"http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -8670,7 +8650,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -8975,7 +8955,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -9005,12 +8985,12 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" bin: is-docker: cli.js - checksum: 10c0/e828365958d155f90c409cdbe958f64051d99e8aedc2c8c4cd7c89dcf35329daed42f7b99346f7828df013e27deb8f721cf9408ba878c76eb9e8290235fbcdcc + checksum: 10c0/d2c4f8e6d3e34df75a5defd44991b6068afad4835bb783b902fa12d13ebdb8f41b2a199dcb0b5ed2cb78bfee9e4c0bbdb69c2d9646f4106464674d3e697a5856 languageName: node linkType: hard @@ -9064,6 +9044,24 @@ __metadata: languageName: node linkType: hard +"is-in-ssh@npm:^1.0.0": + version: 1.0.0 + resolution: "is-in-ssh@npm:1.0.0" + checksum: 10c0/fbb4c25d85c543df09997fbe7aeca410ae0c839c5825bba2d4c672df765e9ce0e7558e781b371c0a21d6ef9bbac39b31875617a68eaaea5504438d07db9a2ffa + languageName: node + linkType: hard + +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10c0/a8efb0e84f6197e6ff5c64c52890fa9acb49b7b74fed4da7c95383965da6f0fa592b4dbd5e38a79f87fc108196937acdbcd758fcefc9b140e479b39ce1fcd1cd + languageName: node + linkType: hard + "is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" @@ -9256,12 +9254,12 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" dependencies: - is-docker: "npm:^2.0.0" - checksum: 10c0/a6fa2d370d21be487c0165c7a440d567274fbba1a817f2f0bfa41cc5e3af25041d84267baa22df66696956038a43973e72fca117918c91431920bdef490fa25e + is-inside-container: "npm:^1.0.0" + checksum: 10c0/d3317c11995690a32c362100225e22ba793678fe8732660c6de511ae71a0ff05b06980cf21f98a6bf40d7be0e9e9506f859abe00a1118287d63e53d0a3d06947 languageName: node linkType: hard @@ -9311,17 +9309,6 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^5.0.6": - version: 5.0.6 - resolution: "istanbul-lib-source-maps@npm:5.0.6" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.23" - debug: "npm:^4.1.1" - istanbul-lib-coverage: "npm:^3.0.0" - checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f - languageName: node - linkType: hard - "istanbul-reports@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-reports@npm:3.2.0" @@ -9389,6 +9376,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -9396,13 +9390,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^9.0.1": - version: 9.0.1 - resolution: "js-tokens@npm:9.0.1" - checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e - languageName: node - linkType: hard - "js-yaml@npm:^4.1.0": version: 4.1.1 resolution: "js-yaml@npm:4.1.1" @@ -9421,45 +9408,46 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:^28.0.0": - version: 28.0.0 - resolution: "jsdom@npm:28.0.0" +"jsdom@npm:^29.0.0": + version: 29.0.1 + resolution: "jsdom@npm:29.0.1" dependencies: - "@acemir/cssom": "npm:^0.9.31" - "@asamuzakjp/dom-selector": "npm:^6.7.6" - "@exodus/bytes": "npm:^1.11.0" - cssstyle: "npm:^5.3.7" + "@asamuzakjp/css-color": "npm:^5.0.1" + "@asamuzakjp/dom-selector": "npm:^7.0.3" + "@bramus/specificity": "npm:^2.4.2" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1" + "@exodus/bytes": "npm:^1.15.0" + css-tree: "npm:^3.2.1" data-urls: "npm:^7.0.0" decimal.js: "npm:^10.6.0" html-encoding-sniffer: "npm:^6.0.0" - http-proxy-agent: "npm:^7.0.2" - https-proxy-agent: "npm:^7.0.6" is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.2.7" parse5: "npm:^8.0.0" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^6.0.0" - undici: "npm:^7.20.0" + tough-cookie: "npm:^6.0.1" + undici: "npm:^7.24.5" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^8.0.1" whatwg-mimetype: "npm:^5.0.0" - whatwg-url: "npm:^16.0.0" + whatwg-url: "npm:^16.0.1" xml-name-validator: "npm:^5.0.0" peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true - checksum: 10c0/6aa2419506f912f40c5f1c6ca52c6dfdfde5970cfbaf105ebfc55ab975dda2d2492b6f8dc4c62b94e46501c4f77dfd2a60ea229ee67f924d59fe6c51bf653043 + checksum: 10c0/f8eeadc9bb45fb5736501f855b5f8247c9eadcd7f52ef2e11677c3a2197284051b4623004889543eb9613ecdfb47ddb5405b822d9623b0524edd901288cc361d languageName: node linkType: hard -"jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" +"jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" bin: jsesc: bin/jsesc - checksum: 10c0/ef22148f9e793180b14d8a145ee6f9f60f301abf443288117b4b6c53d0ecd58354898dc506ccbb553a5f7827965cd38bc5fb726575aae93c5e8915e2de8290e1 + checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 languageName: node linkType: hard @@ -9602,13 +9590,6 @@ __metadata: languageName: node linkType: hard -"kleur@npm:^3.0.3": - version: 3.0.3 - resolution: "kleur@npm:3.0.3" - checksum: 10c0/cd3a0b8878e7d6d3799e54340efe3591ca787d9f95f109f28129bdd2915e37807bf8918bb295ab86afb8c82196beec5a1adcaf29042ce3f2bd932b038fe3aa4b - languageName: node - linkType: hard - "known-css-properties@npm:^0.37.0": version: 0.37.0 resolution: "known-css-properties@npm:0.37.0" @@ -9625,71 +9606,191 @@ __metadata: languageName: node linkType: hard -"language-subtag-registry@npm:^0.3.20": - version: 0.3.22 - resolution: "language-subtag-registry@npm:0.3.22" - checksum: 10c0/d1e09971260a7cd3b9fdeb190d33af0b6e99c8697013537d9aaa15f7856d9d83aee128ba8078e219df0a7cf4b8dd18d1a0c188f6543b500d92a2689d2d114b70 +"language-subtag-registry@npm:^0.3.20": + version: 0.3.22 + resolution: "language-subtag-registry@npm:0.3.22" + checksum: 10c0/d1e09971260a7cd3b9fdeb190d33af0b6e99c8697013537d9aaa15f7856d9d83aee128ba8078e219df0a7cf4b8dd18d1a0c188f6543b500d92a2689d2d114b70 + languageName: node + linkType: hard + +"language-tags@npm:^1.0.9": + version: 1.0.9 + resolution: "language-tags@npm:1.0.9" + dependencies: + language-subtag-registry: "npm:^0.3.20" + checksum: 10c0/9ab911213c4bd8bd583c850201c17794e52cb0660d1ab6e32558aadc8324abebf6844e46f92b80a5d600d0fbba7eface2c207bfaf270a1c7fd539e4c3a880bff + languageName: node + linkType: hard + +"less@npm:^4.2.0": + version: 4.4.2 + resolution: "less@npm:4.4.2" + dependencies: + copy-anything: "npm:^2.0.1" + errno: "npm:^0.1.1" + graceful-fs: "npm:^4.1.2" + image-size: "npm:~0.5.0" + make-dir: "npm:^2.1.0" + mime: "npm:^1.4.1" + needle: "npm:^3.1.0" + parse-node-version: "npm:^1.0.1" + source-map: "npm:~0.6.0" + tslib: "npm:^2.3.0" + dependenciesMeta: + errno: + optional: true + graceful-fs: + optional: true + image-size: + optional: true + make-dir: + optional: true + mime: + optional: true + needle: + optional: true + source-map: + optional: true + bin: + lessc: bin/lessc + checksum: 10c0/f8b796e45ef171adc390b5250f3018922cd046c256181dd9d4cbcbbdc5d6de7cb88c8327741c10eff7ff76421cd826fd95a664ea1b88fbf6f31742428d4a2dab + languageName: node + linkType: hard + +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 10c0/cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + languageName: node + linkType: hard + +"lightningcss-android-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-android-arm64@npm:1.32.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-arm64@npm:1.32.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-x64@npm:1.32.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-freebsd-x64@npm:1.32.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-musl@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"language-tags@npm:^1.0.9": - version: 1.0.9 - resolution: "language-tags@npm:1.0.9" - dependencies: - language-subtag-registry: "npm:^0.3.20" - checksum: 10c0/9ab911213c4bd8bd583c850201c17794e52cb0660d1ab6e32558aadc8324abebf6844e46f92b80a5d600d0fbba7eface2c207bfaf270a1c7fd539e4c3a880bff +"lightningcss-win32-x64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"less@npm:^4.2.0": - version: 4.4.2 - resolution: "less@npm:4.4.2" +"lightningcss@npm:^1.32.0": + version: 1.32.0 + resolution: "lightningcss@npm:1.32.0" dependencies: - copy-anything: "npm:^2.0.1" - errno: "npm:^0.1.1" - graceful-fs: "npm:^4.1.2" - image-size: "npm:~0.5.0" - make-dir: "npm:^2.1.0" - mime: "npm:^1.4.1" - needle: "npm:^3.1.0" - parse-node-version: "npm:^1.0.1" - source-map: "npm:~0.6.0" - tslib: "npm:^2.3.0" + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.32.0" + lightningcss-darwin-arm64: "npm:1.32.0" + lightningcss-darwin-x64: "npm:1.32.0" + lightningcss-freebsd-x64: "npm:1.32.0" + lightningcss-linux-arm-gnueabihf: "npm:1.32.0" + lightningcss-linux-arm64-gnu: "npm:1.32.0" + lightningcss-linux-arm64-musl: "npm:1.32.0" + lightningcss-linux-x64-gnu: "npm:1.32.0" + lightningcss-linux-x64-musl: "npm:1.32.0" + lightningcss-win32-arm64-msvc: "npm:1.32.0" + lightningcss-win32-x64-msvc: "npm:1.32.0" dependenciesMeta: - errno: + lightningcss-android-arm64: optional: true - graceful-fs: + lightningcss-darwin-arm64: optional: true - image-size: + lightningcss-darwin-x64: optional: true - make-dir: + lightningcss-freebsd-x64: optional: true - mime: + lightningcss-linux-arm-gnueabihf: optional: true - needle: + lightningcss-linux-arm64-gnu: optional: true - source-map: + lightningcss-linux-arm64-musl: optional: true - bin: - lessc: bin/lessc - checksum: 10c0/f8b796e45ef171adc390b5250f3018922cd046c256181dd9d4cbcbbdc5d6de7cb88c8327741c10eff7ff76421cd826fd95a664ea1b88fbf6f31742428d4a2dab - languageName: node - linkType: hard - -"leven@npm:^3.1.0": - version: 3.1.0 - resolution: "leven@npm:3.1.0" - checksum: 10c0/cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df - languageName: node - linkType: hard - -"levn@npm:^0.4.1": - version: 0.4.1 - resolution: "levn@npm:0.4.1" - dependencies: - prelude-ls: "npm:^1.2.1" - type-check: "npm:~0.4.0" - checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 languageName: node linkType: hard @@ -9850,17 +9951,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0": - version: 11.2.6 - resolution: "lru-cache@npm:11.2.6" - checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd - languageName: node - linkType: hard - -"lru-cache@npm:^11.2.4": - version: 11.2.4 - resolution: "lru-cache@npm:11.2.4" - checksum: 10c0/4a24f9b17537619f9144d7b8e42cd5a225efdfd7076ebe7b5e7dc02b860a818455201e67fbf000765233fe7e339d3c8229fc815e9b58ee6ede511e07608c19b2 +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.6, lru-cache@npm:^11.2.7": + version: 11.2.7 + resolution: "lru-cache@npm:11.2.7" + checksum: 10c0/549cdb59488baa617135fc12159cafb1a97f91079f35093bb3bcad72e849fc64ace636d244212c181dfdf1a99bbfa90757ff303f98561958ee4d0f885d9bd5f7 languageName: node linkType: hard @@ -9891,7 +9985,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0, magic-string@npm:^0.30.17, magic-string@npm:^0.30.21, magic-string@npm:~0.30.11": +"magic-string@npm:^0.30.0, magic-string@npm:^0.30.21, magic-string@npm:~0.30.11": version: 0.30.21 resolution: "magic-string@npm:0.30.21" dependencies: @@ -9900,14 +9994,14 @@ __metadata: languageName: node linkType: hard -"magicast@npm:^0.5.1": - version: 0.5.1 - resolution: "magicast@npm:0.5.1" +"magicast@npm:^0.5.2": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" dependencies: - "@babel/parser": "npm:^7.28.5" - "@babel/types": "npm:^7.28.5" + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" source-map-js: "npm:^1.2.1" - checksum: 10c0/a00bbf3688b9b3e83c10b3bfe3f106cc2ccbf20c4f2dc1c9020a10556dfe0a6a6605a445ee8e86a6e2b484ec519a657b5e405532684f72678c62e4c0d32f962c + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 languageName: node linkType: hard @@ -9977,6 +10071,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "mdn-data@npm:^2.25.0": version: 2.26.0 resolution: "mdn-data@npm:2.26.0" @@ -10091,12 +10192,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.1.1, minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10c0/c85d44821c71973d636091fddbfbffe62370f5ee3caf0241c5b60c18cd289e916200acb2361b7e987558cd06896d153e25d505db9fc1e43e6b4b6752e2702902 + brace-expansion: "npm:^5.0.2" + checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 languageName: node linkType: hard @@ -10194,10 +10295,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb languageName: node linkType: hard @@ -10424,6 +10525,13 @@ __metadata: languageName: node linkType: hard +"normalize-wheel@npm:^1.0.1": + version: 1.0.1 + resolution: "normalize-wheel@npm:1.0.1" + checksum: 10c0/5daf4c97e39f36658a5263a6499bbc148676ae2bd85f12c8d03c46ffe7bc3c68d44564c00413d88d0457ac0d94450559bb1c24c2ce7ae0c107031f82d093ac06 + languageName: node + linkType: hard + "object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -10513,6 +10621,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.1.1": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -10547,14 +10662,29 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.0": - version: 8.4.2 - resolution: "open@npm:8.4.2" +"open@npm:^10.2.0": + version: 10.2.0 + resolution: "open@npm:10.2.0" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + wsl-utils: "npm:^0.1.0" + checksum: 10c0/5a36d0c1fd2f74ce553beb427ca8b8494b623fc22c6132d0c1688f246a375e24584ea0b44c67133d9ab774fa69be8e12fbe1ff12504b1142bd960fb09671948f + languageName: node + linkType: hard + +"open@npm:^11.0.0": + version: 11.0.0 + resolution: "open@npm:11.0.0" dependencies: - define-lazy-prop: "npm:^2.0.0" - is-docker: "npm:^2.1.1" - is-wsl: "npm:^2.2.0" - checksum: 10c0/bb6b3a58401dacdb0aad14360626faf3fb7fba4b77816b373495988b724fb48941cad80c1b65d62bb31a17609b2cd91c41a181602caea597ca80dfbcc27e84c9 + default-browser: "npm:^5.4.0" + define-lazy-prop: "npm:^3.0.0" + is-in-ssh: "npm:^1.0.0" + is-inside-container: "npm:^1.0.0" + powershell-utils: "npm:^0.1.0" + wsl-utils: "npm:^0.3.0" + checksum: 10c0/7aeeda4131268ed90f90e7728dda5c46bb0c6205b27a4be3e86ea33593e30dd393423e20e31c00802a8e635ef59becaee33ef9749a8ceb027567cd253e9e7b1e languageName: node linkType: hard @@ -10812,13 +10942,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0": - version: 2.0.1 - resolution: "path-scurry@npm:2.0.1" +"path-scurry@npm:^2.0.0, path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" dependencies: lru-cache: "npm:^11.0.0" minipass: "npm:^7.1.2" - checksum: 10c0/2a16ed0e81fbc43513e245aa5763354e25e787dab0d539581a6c3f0f967461a159ed6236b2559de23aa5b88e7dc32b469b6c47568833dd142a4b24b4f5cd2620 + checksum: 10c0/b35ad37cf6557a87fd057121ce2be7695380c9138d93e87ae928609da259ea0a170fac6f3ef1eb3ece8a068e8b7f2f3adf5bb2374cf4d4a57fe484954fcc9482 languageName: node linkType: hard @@ -10897,9 +11027,9 @@ __metadata: linkType: hard "pg-protocol@npm:*": - version: 1.12.0 - resolution: "pg-protocol@npm:1.12.0" - checksum: 10c0/577f33c756f6503682d9ac17fd813f9edbe4a1716e497f17d36b6edaf9bf8383accaf8cd7422c49e2fbe4eb28ef275bc52fbd8287e154d4510f50b9ccefe4165 + version: 1.13.0 + resolution: "pg-protocol@npm:1.13.0" + checksum: 10c0/a4e851e6bb8ff404ca19d561cf49b6b0caf45163bd3f289889edaf6c4e9fb25b08fb57f50d37a8cc86007efcf2cbb3dd2372c97a353a546f45eb49ddebc84fa9 languageName: node linkType: hard @@ -11063,17 +11193,6 @@ __metadata: languageName: node linkType: hard -"pixelmatch@npm:7.1.0": - version: 7.1.0 - resolution: "pixelmatch@npm:7.1.0" - dependencies: - pngjs: "npm:^7.0.0" - bin: - pixelmatch: bin/pixelmatch - checksum: 10c0/ff069f92edaa841ac9b58b0ab74e1afa1f3b5e770eea0218c96bac1da4e752f5f6b79a0f9c4ba6b02afb955d39b8c78bcc3cc884f8122b67a1f2efbbccbe1a73 - languageName: node - linkType: hard - "playwright-core@npm:1.57.0": version: 1.57.0 resolution: "playwright-core@npm:1.57.0" @@ -11584,14 +11703,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.6": - version: 8.5.6 - resolution: "postcss@npm:8.5.6" +"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.6, postcss@npm:^8.5.8": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c languageName: node linkType: hard @@ -11625,6 +11744,13 @@ __metadata: languageName: node linkType: hard +"powershell-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "powershell-utils@npm:0.1.0" + checksum: 10c0/a64713cf3583259c9ed6be211c06b4b19e8608bcb0f7f6287ffac0a95b8c7582b6b662eea0e201fd659492c8e9f9c5fd0bfc4579645c5add9c1a600075621c95 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -11691,16 +11817,6 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.4.0": - version: 2.4.2 - resolution: "prompts@npm:2.4.2" - dependencies: - kleur: "npm:^3.0.3" - sisteransi: "npm:^1.0.5" - checksum: 10c0/16f1ac2977b19fe2cf53f8411cc98db7a3c8b115c479b2ca5c82b5527cd937aa405fa04f9a5960abeb9daef53191b53b4d13e35c1f5d50e8718c76917c5f1ea4 - languageName: node - linkType: hard - "prop-types@npm:^15.5.10, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" @@ -11829,21 +11945,21 @@ __metadata: languageName: node linkType: hard -"react-docgen@npm:^8.0.0": - version: 8.0.0 - resolution: "react-docgen@npm:8.0.0" +"react-docgen@npm:^8.0.0, react-docgen@npm:^8.0.2": + version: 8.0.3 + resolution: "react-docgen@npm:8.0.3" dependencies: - "@babel/core": "npm:^7.18.9" - "@babel/traverse": "npm:^7.18.9" - "@babel/types": "npm:^7.18.9" - "@types/babel__core": "npm:^7.18.0" - "@types/babel__traverse": "npm:^7.18.0" + "@babel/core": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" + "@types/babel__core": "npm:^7.20.5" + "@types/babel__traverse": "npm:^7.20.7" "@types/doctrine": "npm:^0.0.9" "@types/resolve": "npm:^1.20.2" doctrine: "npm:^3.0.0" resolve: "npm:^1.22.1" strip-indent: "npm:^4.0.0" - checksum: 10c0/2e3c187bed074895ac3420910129f23b30fe8f7faf984cbf6e210dd3914fa03a910583c5a4c4564edbef7461c37dfd6cd967c3bfc5d83c6f8c02cacedda38014 + checksum: 10c0/0231fb9177bc7c633f3d1f228eebb0ee90a2f0feac50b1869ef70b0a3683b400d7875547a2d5168f2619b63d4cc29d7c45ae33d3f621fc67a7fa6790ac2049f6 languageName: node linkType: hard @@ -11870,6 +11986,19 @@ __metadata: languageName: node linkType: hard +"react-easy-crop@npm:^5.5.6": + version: 5.5.6 + resolution: "react-easy-crop@npm:5.5.6" + dependencies: + normalize-wheel: "npm:^1.0.1" + tslib: "npm:^2.0.1" + peerDependencies: + react: ">=16.4.0" + react-dom: ">=16.4.0" + checksum: 10c0/ce623791d31559fc46f210ece7b22c0f659710d5de219ef9fb05650940f50445d5e6573ed229b66fad06dfda9651ae458c0f5efb8e1cabdf01511dc32942cdc8 + languageName: node + linkType: hard + "react-fast-compare@npm:^3.1.1": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" @@ -12285,12 +12414,12 @@ __metadata: languageName: node linkType: hard -"regenerate-unicode-properties@npm:^10.2.0": - version: 10.2.0 - resolution: "regenerate-unicode-properties@npm:10.2.0" +"regenerate-unicode-properties@npm:^10.2.2": + version: 10.2.2 + resolution: "regenerate-unicode-properties@npm:10.2.2" dependencies: regenerate: "npm:^1.4.2" - checksum: 10c0/5510785eeaf56bbfdf4e663d6753f125c08d2a372d4107bc1b756b7bf142e2ed80c2733a8b54e68fb309ba37690e66a0362699b0e21d5c1f0255dea1b00e6460 + checksum: 10c0/66a1d6a1dbacdfc49afd88f20b2319a4c33cee56d245163e4d8f5f283e0f45d1085a78f7f7406dd19ea3a5dd7a7799cd020cd817c97464a7507f9d10fbdce87c languageName: node linkType: hard @@ -12329,17 +12458,17 @@ __metadata: languageName: node linkType: hard -"regexpu-core@npm:^6.2.0": - version: 6.2.0 - resolution: "regexpu-core@npm:6.2.0" +"regexpu-core@npm:^6.3.1": + version: 6.4.0 + resolution: "regexpu-core@npm:6.4.0" dependencies: regenerate: "npm:^1.4.2" - regenerate-unicode-properties: "npm:^10.2.0" + regenerate-unicode-properties: "npm:^10.2.2" regjsgen: "npm:^0.8.0" - regjsparser: "npm:^0.12.0" + regjsparser: "npm:^0.13.0" unicode-match-property-ecmascript: "npm:^2.0.0" - unicode-match-property-value-ecmascript: "npm:^2.1.0" - checksum: 10c0/bbcb83a854bf96ce4005ee4e4618b71c889cda72674ce6092432f0039b47890c2d0dfeb9057d08d440999d9ea03879ebbb7f26ca005ccf94390e55c348859b98 + unicode-match-property-value-ecmascript: "npm:^2.2.1" + checksum: 10c0/1eed9783c023dd06fb1f3ce4b6e3fdf0bc1e30cb036f30aeb2019b351e5e0b74355b40462282ea5db092c79a79331c374c7e9897e44a5ca4509e9f0b570263de languageName: node linkType: hard @@ -12350,14 +12479,14 @@ __metadata: languageName: node linkType: hard -"regjsparser@npm:^0.12.0": - version: 0.12.0 - resolution: "regjsparser@npm:0.12.0" +"regjsparser@npm:^0.13.0": + version: 0.13.0 + resolution: "regjsparser@npm:0.13.0" dependencies: - jsesc: "npm:~3.0.2" + jsesc: "npm:~3.1.0" bin: regjsparser: bin/parser - checksum: 10c0/99d3e4e10c8c7732eb7aa843b8da2fd8b647fe144d3711b480e4647dc3bff4b1e96691ccf17f3ace24aa866a50b064236177cb25e6e4fbbb18285d99edaed83b + checksum: 10c0/4702f85cda09f67747c1b2fb673a0f0e5d1ba39d55f177632265a0be471ba59e3f320623f411649141f752b126b8126eac3ff4c62d317921e430b0472bfc6071 languageName: node linkType: hard @@ -12424,16 +12553,16 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.10, resolve@npm:^1.22.4, resolve@npm:^1.22.8": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" +"resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.11, resolve@npm:^1.22.4, resolve@npm:^1.22.8": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 + checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409 languageName: node linkType: hard @@ -12450,16 +12579,16 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.10#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" +"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 + checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63 languageName: node linkType: hard @@ -12514,25 +12643,86 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-gzip@npm:^4.1.1": - version: 4.1.1 - resolution: "rollup-plugin-gzip@npm:4.1.1" +"rolldown@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "rolldown@npm:1.0.0-rc.10" + dependencies: + "@oxc-project/types": "npm:=0.120.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.10" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.10" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.10" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.10" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.10" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.10" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.10" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.10" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.10" + "@rolldown/pluginutils": "npm:1.0.0-rc.10" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: bin/cli.mjs + checksum: 10c0/3d7970ce31bb4b267c3240a1c03f275483f8523484b1218b75a4cc3ddffa188e58f73b9b3e0bec850544db3839754015959fdea87278c9ccf93ab76b4fb8672a + languageName: node + linkType: hard + +"rollup-plugin-gzip@npm:^4.2.0": + version: 4.2.0 + resolution: "rollup-plugin-gzip@npm:4.2.0" peerDependencies: rollup: ">=2.0.0" - checksum: 10c0/0ad79a6eb84bb8d88db15a184ca661f44aa6fb3412c98d6a97f1dec365db37945a84c3a2d0bf709ae605ae305a40a0021b2e6d5494c537b029759f3695d9ac96 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/05aecd5cb96f5b40ef128d87ea15d86c07e416a1fb98eb5712911254ce1a09ddf60c0128edb7fc8dc7478cbca1f94d04ab07cf4437947f5ce330ce627f80bada languageName: node linkType: hard -"rollup-plugin-visualizer@npm:^6.0.3": - version: 6.0.5 - resolution: "rollup-plugin-visualizer@npm:6.0.5" +"rollup-plugin-visualizer@npm:^7.0.1": + version: 7.0.1 + resolution: "rollup-plugin-visualizer@npm:7.0.1" dependencies: - open: "npm:^8.0.0" + open: "npm:^11.0.0" picomatch: "npm:^4.0.2" source-map: "npm:^0.7.4" - yargs: "npm:^17.5.1" + yargs: "npm:^18.0.0" peerDependencies: - rolldown: 1.x || ^1.0.0-beta + rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc rollup: 2.x || 3.x || 4.x peerDependenciesMeta: rolldown: @@ -12541,7 +12731,7 @@ __metadata: optional: true bin: rollup-plugin-visualizer: dist/bin/cli.js - checksum: 10c0/3824626e97d5033fbb3aa1bbe93c8c17a8569bc47e33c941bde6b90404f2cae70b26fec1b623bd393c3e076338014196c91726ed2c96218edc67e1f21676f7ef + checksum: 10c0/8ca591a465554d7a4a348538b35acd8eb796156fe24fb7252457640fa49a5aa399962e2cabb542ae8590a391918ae2927ed492081e5598977f3a69b91d0042f4 languageName: node linkType: hard @@ -12559,81 +12749,6 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.43.0": - version: 4.46.4 - resolution: "rollup@npm:4.46.4" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.46.4" - "@rollup/rollup-android-arm64": "npm:4.46.4" - "@rollup/rollup-darwin-arm64": "npm:4.46.4" - "@rollup/rollup-darwin-x64": "npm:4.46.4" - "@rollup/rollup-freebsd-arm64": "npm:4.46.4" - "@rollup/rollup-freebsd-x64": "npm:4.46.4" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.4" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.4" - "@rollup/rollup-linux-arm64-gnu": "npm:4.46.4" - "@rollup/rollup-linux-arm64-musl": "npm:4.46.4" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.4" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.4" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.4" - "@rollup/rollup-linux-riscv64-musl": "npm:4.46.4" - "@rollup/rollup-linux-s390x-gnu": "npm:4.46.4" - "@rollup/rollup-linux-x64-gnu": "npm:4.46.4" - "@rollup/rollup-linux-x64-musl": "npm:4.46.4" - "@rollup/rollup-win32-arm64-msvc": "npm:4.46.4" - "@rollup/rollup-win32-ia32-msvc": "npm:4.46.4" - "@rollup/rollup-win32-x64-msvc": "npm:4.46.4" - "@types/estree": "npm:1.0.8" - fsevents: "npm:~2.3.2" - dependenciesMeta: - "@rollup/rollup-android-arm-eabi": - optional: true - "@rollup/rollup-android-arm64": - optional: true - "@rollup/rollup-darwin-arm64": - optional: true - "@rollup/rollup-darwin-x64": - optional: true - "@rollup/rollup-freebsd-arm64": - optional: true - "@rollup/rollup-freebsd-x64": - optional: true - "@rollup/rollup-linux-arm-gnueabihf": - optional: true - "@rollup/rollup-linux-arm-musleabihf": - optional: true - "@rollup/rollup-linux-arm64-gnu": - optional: true - "@rollup/rollup-linux-arm64-musl": - optional: true - "@rollup/rollup-linux-loongarch64-gnu": - optional: true - "@rollup/rollup-linux-ppc64-gnu": - optional: true - "@rollup/rollup-linux-riscv64-gnu": - optional: true - "@rollup/rollup-linux-riscv64-musl": - optional: true - "@rollup/rollup-linux-s390x-gnu": - optional: true - "@rollup/rollup-linux-x64-gnu": - optional: true - "@rollup/rollup-linux-x64-musl": - optional: true - "@rollup/rollup-win32-arm64-msvc": - optional: true - "@rollup/rollup-win32-ia32-msvc": - optional: true - "@rollup/rollup-win32-x64-msvc": - optional: true - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: 10c0/17871534544bd19ec9b5bc1d82a8509addbdb7ee0dd865f20352a8b5695e7b9288af842cd50187bed9fece61b0f1d7b7ff43cf070265d3a2e7d8348497e3ba1e - languageName: node - linkType: hard - "router@npm:^2.2.0": version: 2.2.0 resolution: "router@npm:2.2.0" @@ -12647,6 +12762,13 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^7.0.0": + version: 7.1.0 + resolution: "run-applescript@npm:7.1.0" + checksum: 10c0/ab826c57c20f244b2ee807704b1ef4ba7f566aa766481ae5922aac785e2570809e297c69afcccc3593095b538a8a77d26f2b2e9a1d9dffee24e0e039502d1a03 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -12819,15 +12941,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.6.2": - version: 7.7.3 - resolution: "semver@npm:7.7.3" - bin: - semver: bin/semver.js - checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e - languageName: node - linkType: hard - "send@npm:^1.1.0, send@npm:^1.2.0": version: 1.2.0 resolution: "send@npm:1.2.0" @@ -13001,13 +13114,6 @@ __metadata: languageName: node linkType: hard -"sisteransi@npm:^1.0.5": - version: 1.0.5 - resolution: "sisteransi@npm:1.0.5" - checksum: 10c0/230ac975cca485b7f6fe2b96a711aa62a6a26ead3e6fb8ba17c5a00d61b8bed0d7adc21f5626b70d7c33c62ff4e63933017a6462942c719d1980bb0b1207ad46 - languageName: node - linkType: hard - "slash@npm:^5.1.0": version: 5.1.0 resolution: "slash@npm:5.1.0" @@ -13256,10 +13362,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.10.0": - version: 3.10.0 - resolution: "std-env@npm:3.10.0" - checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f +"std-env@npm:^4.0.0-rc.1": + version: 4.0.0 + resolution: "std-env@npm:4.0.0" + checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c languageName: node linkType: hard @@ -13273,20 +13379,21 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^10.0.5": - version: 10.0.5 - resolution: "storybook@npm:10.0.5" +"storybook@npm:^10.3.0": + version: 10.3.0 + resolution: "storybook@npm:10.3.0" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.6.0" - "@testing-library/jest-dom": "npm:^6.6.3" + "@storybook/icons": "npm:^2.0.1" + "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" - "@vitest/mocker": "npm:3.2.4" "@vitest/spy": "npm:3.2.4" - 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" + 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" + open: "npm:^10.2.0" recast: "npm:^0.23.5" - semver: "npm:^7.6.2" + semver: "npm:^7.7.3" + use-sync-external-store: "npm:^1.5.0" ws: "npm:^8.18.0" peerDependencies: prettier: ^2 || ^3 @@ -13295,7 +13402,7 @@ __metadata: optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10c0/ea4bcdbc8d793f53970fe2e72de805bfd5b0872d3640f7526bdf42fbe0114f225c09f3683ab011ac08b5240450fd7726f17c5210d929c6f261dadc851ee09eec + checksum: 10c0/aaab242fb36948c122b8a9bd6b45120418e25af105e96c303f565ef19d08001adb17aaa9810c9f44b4d494fab999a53e58e4789c6202e0880d1e701e93f10e21 languageName: node linkType: hard @@ -13335,14 +13442,14 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^7.0.0": - version: 7.0.0 - resolution: "string-width@npm:7.0.0" +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" dependencies: emoji-regex: "npm:^10.3.0" get-east-asian-width: "npm:^1.0.0" strip-ansi: "npm:^7.1.0" - checksum: 10c0/8ffaeeccf4a56ccce5b6235d0b99ee3a581e3e3e5d453708efe7aa8e264fa3a858b4fe2244310cb71c6a20d8c05921cedc8b2ccd88cbaad9f5c92051ff68edc6 + checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 languageName: node linkType: hard @@ -13859,10 +13966,10 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.2": - version: 0.3.2 - resolution: "tinyexec@npm:0.3.2" - checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10c0/1261a8e34c9b539a9aae3b7f0bb5372045ff28ee1eba035a2a059e532198fe1a182ec61ac60fa0b4a4129f0c4c4b1d2d57355b5cb9aa2d17ac9454ecace502ee languageName: node linkType: hard @@ -13955,12 +14062,12 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^6.0.0": - version: 6.0.0 - resolution: "tough-cookie@npm:6.0.0" +"tough-cookie@npm:^6.0.0, tough-cookie@npm:^6.0.1": + version: 6.0.1 + resolution: "tough-cookie@npm:6.0.1" dependencies: tldts: "npm:^7.0.5" - checksum: 10c0/7b17a461e9c2ac0d0bea13ab57b93b4346d0b8c00db174c963af1e46e4ea8d04148d2a55f2358fc857db0c0c65208a98e319d0c60693e32e0c559a9d9cf20cb5 + checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d languageName: node linkType: hard @@ -14005,27 +14112,13 @@ __metadata: languageName: node linkType: hard -"ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": +"ts-dedent@npm:^2.0.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" checksum: 10c0/175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 languageName: node linkType: hard -"tsconfck@npm:^3.0.3": - version: 3.1.5 - resolution: "tsconfck@npm:3.1.5" - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - bin: - tsconfck: bin/tsconfck.js - checksum: 10c0/9b62cd85d5702aa23ea50ea578d7124f3d59cc4518fcc7eacc04f4f9c9c481f720738ff8351bd4472247c0723a17dfd01af95a5b60ad623cdb8727fbe4881847 - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.15.0": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0" @@ -14266,10 +14359,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:^7.20.0": - version: 7.20.0 - resolution: "undici@npm:7.20.0" - checksum: 10c0/99054958a07b4105e1461bf5f38550746a15e01d6807e7a2b0849f18e1bc3f481c1ad080ea87b255a39264cec5d80ebf2b3bc82c3e732d81e6a0cc3c920c05c6 +"undici@npm:^7.24.5": + version: 7.24.5 + resolution: "undici@npm:7.24.5" + checksum: 10c0/2a836f1f6ab078fde3eeb4cc8fd5b34eeaf52cfbdf16a9bab61b7223f43f7847bcd2125d1da7c4e3f5996c528bf9f7940015d39909bab80cfbd71b855470cf21 languageName: node linkType: hard @@ -14299,10 +14392,10 @@ __metadata: languageName: node linkType: hard -"unicode-match-property-value-ecmascript@npm:^2.1.0": - version: 2.2.0 - resolution: "unicode-match-property-value-ecmascript@npm:2.2.0" - checksum: 10c0/1d0a2deefd97974ddff5b7cb84f9884177f4489928dfcebb4b2b091d6124f2739df51fc6ea15958e1b5637ac2a24cff9bf21ea81e45335086ac52c0b4c717d6d +"unicode-match-property-value-ecmascript@npm:^2.2.1": + version: 2.2.1 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.1" + checksum: 10c0/93acd1ad9496b600e5379d1aaca154cf551c5d6d4a0aefaf0984fc2e6288e99220adbeb82c935cde461457fb6af0264a1774b8dfd4d9a9e31548df3352a4194d languageName: node linkType: hard @@ -14521,7 +14614,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": version: 1.6.0 resolution: "use-sync-external-store@npm:1.6.0" peerDependencies: @@ -14577,7 +14670,7 @@ __metadata: languageName: node linkType: hard -"vite-plugin-pwa@npm:^1.0.2": +"vite-plugin-pwa@npm:^1.2.0": version: 1.2.0 resolution: "vite-plugin-pwa@npm:1.2.0" dependencies: @@ -14598,7 +14691,7 @@ __metadata: languageName: node linkType: hard -"vite-plugin-svgr@npm:^4.3.0": +"vite-plugin-svgr@npm:^4.5.0": version: 4.5.0 resolution: "vite-plugin-svgr@npm:4.5.0" dependencies: @@ -14611,38 +14704,22 @@ __metadata: languageName: node linkType: hard -"vite-tsconfig-paths@npm:^6.0.0": - version: 6.0.1 - resolution: "vite-tsconfig-paths@npm:6.0.1" - dependencies: - debug: "npm:^4.1.1" - globrex: "npm:^0.1.2" - tsconfck: "npm:^3.0.3" - peerDependencies: - vite: "*" - peerDependenciesMeta: - vite: - optional: true - checksum: 10c0/c0702f1d2b9d2e3e6ebb44d8e9c27b17b1102e86946ab54b6bbd290419b134e84df4e451b55db973bc97d9de5689df6f67e479633df20244aa0c62ffd0b16e43 - languageName: node - linkType: hard - -"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.1.1": - version: 7.2.6 - resolution: "vite@npm:7.2.6" +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0-0, vite@npm:^8.0.0": + version: 8.0.1 + resolution: "vite@npm:8.0.1" dependencies: - esbuild: "npm:^0.25.0" - fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.3" - postcss: "npm:^8.5.6" - rollup: "npm:^4.43.0" + postcss: "npm:^8.5.8" + rolldown: "npm:1.0.0-rc.10" tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.1.0 + esbuild: ^0.27.0 jiti: ">=1.21.0" less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: ">=0.54.8" @@ -14656,12 +14733,14 @@ __metadata: peerDependenciesMeta: "@types/node": optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -14678,52 +14757,50 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/d444a159ab8f0f854d596d1938f201b449d59ed4d336e587be9dc89005467214d85848c212c2495f76a8421372ffe4d061d023d659600f1aaa3ba5ac13e804f7 + checksum: 10c0/f1379726cfd50f3f12d172cf6f61b7b067521bd92955176d0bc6e6e9dd538fe76c87e7f7102d5815e4f83f6795e8ba95502fd442507dc8574ba13bcb7230b2c3 languageName: node linkType: hard -"vitest@npm:^4.0.5": - version: 4.0.13 - resolution: "vitest@npm:4.0.13" - dependencies: - "@vitest/expect": "npm:4.0.13" - "@vitest/mocker": "npm:4.0.13" - "@vitest/pretty-format": "npm:4.0.13" - "@vitest/runner": "npm:4.0.13" - "@vitest/snapshot": "npm:4.0.13" - "@vitest/spy": "npm:4.0.13" - "@vitest/utils": "npm:4.0.13" - debug: "npm:^4.4.3" - es-module-lexer: "npm:^1.7.0" - expect-type: "npm:^1.2.2" +"vitest@npm:^4.1.0": + version: 4.1.0 + resolution: "vitest@npm:4.1.0" + dependencies: + "@vitest/expect": "npm:4.1.0" + "@vitest/mocker": "npm:4.1.0" + "@vitest/pretty-format": "npm:4.1.0" + "@vitest/runner": "npm:4.1.0" + "@vitest/snapshot": "npm:4.1.0" + "@vitest/spy": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" pathe: "npm:^2.0.3" picomatch: "npm:^4.0.3" - std-env: "npm:^3.10.0" + std-env: "npm:^4.0.0-rc.1" tinybench: "npm:^2.9.0" - tinyexec: "npm:^0.3.2" + tinyexec: "npm:^1.0.2" tinyglobby: "npm:^0.2.15" tinyrainbow: "npm:^3.0.3" - vite: "npm:^6.0.0 || ^7.0.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0-0" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 - "@types/debug": ^4.1.12 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.13 - "@vitest/browser-preview": 4.0.13 - "@vitest/browser-webdriverio": 4.0.13 - "@vitest/ui": 4.0.13 + "@vitest/browser-playwright": 4.1.0 + "@vitest/browser-preview": 4.1.0 + "@vitest/browser-webdriverio": 4.1.0 + "@vitest/ui": 4.1.0 happy-dom: "*" jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: "@edge-runtime/vm": optional: true "@opentelemetry/api": optional: true - "@types/debug": - optional: true "@types/node": optional: true "@vitest/browser-playwright": @@ -14738,9 +14815,11 @@ __metadata: optional: true jsdom: optional: true + vite: + optional: false bin: vitest: vitest.mjs - checksum: 10c0/8582ab1848d5d7dbbac0b3a5eae2625f44d0db887f73da2ee8f588fb13c66fe8ea26dac05c26ebb43673b735bc246764f52969f7c7e25455dfb7c6274659ae2c + checksum: 10c0/48048e4391e4e8190aa12b1c868bef4ad8d346214631b4506e0dc1f3241ecb8bcb24f296c38a7d98eae712a042375ae209da4b35165db38f9a9bc79a3a9e2a04 languageName: node linkType: hard @@ -14804,14 +14883,14 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^16.0.0": - version: 16.0.0 - resolution: "whatwg-url@npm:16.0.0" +"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1": + version: 16.0.1 + resolution: "whatwg-url@npm:16.0.1" dependencies: "@exodus/bytes": "npm:^1.11.0" tr46: "npm:^6.0.0" webidl-conversions: "npm:^8.0.1" - checksum: 10c0/9b8cb392be244d0e9687ffe543f9ea63b7aa051a98547ea362a38d182d89bfbd96e13e7ed3f40df1f7566bb7c3581f6c081ddea950cf5382532716ce33000ff4 + checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2 languageName: node linkType: hard @@ -15200,9 +15279,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.12.1, ws@npm:^8.18.0, ws@npm:^8.18.3": - version: 8.18.3 - resolution: "ws@npm:8.18.3" +"ws@npm:^8.12.1, ws@npm:^8.18.0, ws@npm:^8.19.0": + version: 8.19.0 + resolution: "ws@npm:8.19.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -15211,7 +15290,26 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + checksum: 10c0/4741d9b9bc3f9c791880882414f96e36b8b254e34d4b503279d6400d9a4b87a033834856dbdd94ee4b637944df17ea8afc4bce0ff4a1560d2166be8855da5b04 + languageName: node + linkType: hard + +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10c0/44318f3585eb97be994fc21a20ddab2649feaf1fbe893f1f866d936eea3d5f8c743bec6dc02e49fbdd3c0e69e9b36f449d90a0b165a4f47dd089747af4cf2377 + languageName: node + linkType: hard + +"wsl-utils@npm:^0.3.0": + version: 0.3.1 + resolution: "wsl-utils@npm:0.3.1" + dependencies: + is-wsl: "npm:^3.1.0" + powershell-utils: "npm:^0.1.0" + checksum: 10c0/b3ba99cc6b71f66457eef598d529beeb8cb57a72646877fe25993894b808c60b82f6d47df5463f0b6e54632272f62f5eaea105c12784fd09b06f500f3f53aa2e languageName: node linkType: hard @@ -15287,7 +15385,14 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.5.1, yargs@npm:^17.7.2": +"yargs-parser@npm:^22.0.0": + version: 22.0.0 + resolution: "yargs-parser@npm:22.0.0" + checksum: 10c0/cb7ef81759c4271cb1d96b9351dbbc9a9ce35d3e1122d2b739bf6c432603824fa02c67cc12dcef6ea80283379d63495686e8f41cc7b06c6576e792aba4d33e1c + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -15302,6 +15407,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^18.0.0": + version: 18.0.0 + resolution: "yargs@npm:18.0.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^7.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"