diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml new file mode 100644 index 000000000..de21e2e58 --- /dev/null +++ b/.github/workflows/crowdin-download-stable.yml @@ -0,0 +1,69 @@ +name: Crowdin / Download translations (stable branches) +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + download-translations-stable: + runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Increase Git http.postBuffer + # This is needed due to a bug in Ubuntu's cURL version? + # See https://github.com/orgs/community/discussions/55820 + run: | + git config --global http.version HTTP/1.1 + git config --global http.postBuffer 157286400 + + # Download the translation files from Crowdin + - name: crowdin action + uses: crowdin/github-action@v2 + with: + upload_sources: false + upload_translations: false + download_translations: true + crowdin_branch_name: ${{ github.base_ref || github.ref_name }} + push_translations: false + create_pull_request: false + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + # As the files are extracted from a Docker container, they belong to root:root + # We need to fix this before the next steps + - name: Fix file permissions + run: sudo chown -R runner:docker . + + # This is needed to run the normalize step + - name: Set up Ruby environment + uses: ./.github/actions/setup-ruby + + - name: Run i18n normalize task + run: bundle exec i18n-tasks normalize + + # Create or update the pull request + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7.0.5 + with: + commit-message: 'New Crowdin translations' + title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' + author: 'GitHub Actions ' + body: | + New Crowdin translations, automated with GitHub Actions + + See `.github/workflows/crowdin-download.yml` + + This PR will be updated every day with new translations. + + Due to a limitation in GitHub Actions, checks are not running on this PR without manual action. + If you want to run the checks, then close and re-open it. + branch: i18n/crowdin/translations-${{ github.base_ref || github.ref_name }} + base: ${{ github.base_ref || github.ref_name }} + labels: i18n diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index f1817b3e9..900899dd5 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -52,7 +52,7 @@ jobs: # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.1 + uses: peter-evans/create-pull-request@v7.0.5 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations (automated)' diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml index b7a0a2b81..4f4d917d1 100644 --- a/.github/workflows/crowdin-upload.yml +++ b/.github/workflows/crowdin-upload.yml @@ -1,7 +1,6 @@ name: Crowdin / Upload translations on: - merge_group: push: branches: - 'main' @@ -31,7 +30,7 @@ jobs: upload_sources: true upload_translations: false download_translations: false - crowdin_branch_name: main + crowdin_branch_name: ${{ github.base_ref || github.ref_name }} env: CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1467a4e..a3a28c888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,204 @@ All notable changes to this project will be documented in this file. -## [4.2.13] - 2024-09-30 +## [4.3.0] - 2024-10-08 + +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 @mjankowski. ### Security - Fix ReDoS vulnerability on some Ruby versions ([GHSA-jpxp-r43f-rhvx](https://github.com/mastodon/mastodon/security/advisories/GHSA-jpxp-r43f-rhvx)) +- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 and #32241 by @ClearlyClaire) - Update dependencies ### Added +- **Add server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610, #31929, #32089, #32085, #32243, #32179 and #32254 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\ + Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\ + This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\ + As part of this, the visual design of the entire notifications feature has been revamped.\ + This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features” section of the notifications column settings.\ + The API is not final yet, but it consists of: + - a new `group_key` attribute to `Notification` entities + - `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped + - `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group + - `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts + - `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group + - `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count +- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\ + The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\ + You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\ + Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\ + This adds the following REST API endpoints: + + - `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy + - `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications + - `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests + - `GET /api/v1/notifications/requests/:id`: https://docs.joinmastodon.org/methods/notifications/#get-one-request + - `POST /api/v1/notifications/requests/:id/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-request + - `POST /api/v1/notifications/requests/:id/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-request + - `POST /api/v1/notifications/requests/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests + - `POST /api/v1/notifications/requests/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests + - `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged + + In addition, accepting one or more notification requests generates a new streaming event: + + - `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed + +- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ + Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\ + Note that this does not notify remote users.\ + This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). +- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\ + Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\ + This can be disabled in the “Animations and accessibility” section of the preferences. +- **Add "system" theme setting (light/dark theme depending on user system preference)** (#29748, #29553, #29795, #29918, #30839, and #30861 by @nshki, @ErikUden, @mjankowski, @renchap, and @vmstan)\ + Add a “system” theme that automatically switch between default dark and light themes depending on the user's system preferences.\ + Also changes the default server theme to this new “system” theme so that automatic theme selection happens even when logged out. +- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\ + You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\ + This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link +- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, #31900 and #32188 by @Gargron, @mjankowski and @oneiros)\ + This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\ + Articles hosted outside the fediverse can indicate a fediverse author with a meta tag: + ```html + + ``` + On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\ + Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\ + This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1 +- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\ + In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\ + This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning). +- **Add domain information to profiles in web UI** (#29602 by @Gargron)\ + Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation. +- **Add support for Redis sentinel** (#31694, #31623, #31744, #31767, and #31768 by @ThisIsMissEm and @oneiros)\ + See https://docs.joinmastodon.org/admin/scaling/#redis-sentinel +- **Add ability to reorder uploaded media before posting in web UI** (#28456 and #32093 by @Gargron) - Add “A Mastodon update is available.” message on admin dashboard for non-bugfix updates (#32106 by @ClearlyClaire) +- Add ability to view alt text by clicking the ALT badge in web UI (#32058 by @Gargron) +- Add preview of followers removed in domain block modal in web UI (#32032 and #32105 by @ClearlyClaire and @Gargron) +- Add reblogs and favourites counts to statuses in ActivityPub (#32007 by @Gargron) +- Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm) +- Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\ + This is also exposed through the REST API: https://docs.joinmastodon.org/entities/Instance/#icon +- Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\ + Add API version number to make it easier for clients to detect compatible features going forward.\ + See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions +- Add quick links to Administration and Moderation Reports from Web UI (#24838 by @ThisIsMissEm) +- Add link to `/admin/roles` in moderation interface when changing someone's role (#31791 by @ClearlyClaire) +- Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm) +- Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron) +- Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron) +- Add optional hints for server rules (#29539 and #29758 by @ClearlyClaire and @Gargron)\ + Server rules can now be broken into a short rule name and a longer explanation of the rule.\ + This adds a new [`hint` attribute](https://docs.joinmastodon.org/entities/Rule/#hint) to `Rule` entities in the REST API. +- Add support for PKCE in OAuth flow (#31129 by @ThisIsMissEm) +- Add CDN cache busting on media deletion (#31353 and #31414 by @ClearlyClaire and @tribela) +- Add the OAuth application used in local reports (#30539 by @ThisIsMissEm) +- Add hint to user that other remote statuses may be missing (#26910, #31387, and #31516 by @Gargron, @audiodude, and @renchap) +- Add lang attribute on preview card title (#31303 by @c960657) +- Add check for `Content-Length` in `ResponseWithLimitAdapter` (#31285 by @c960657) +- Add `Accept-Language` header to fetch preview cards in the server's default language (#31232 by @c960657) +- Add support for PKCE Extension in OmniAuth OIDC through the `OIDC_USE_PKCE` environment variable (#31131 by @ThisIsMissEm) +- Add API endpoints for unread notifications count (#31191 by @ClearlyClaire)\ + This adds the following REST API endpoints: + - `GET /api/v1/notifications/unread_count`: https://docs.joinmastodon.org/methods/notifications/#unread-count +- Add `/` keyboard shortcut to focus the search field (#29921 by @ClearlyClaire) +- Add button to view the Hashtag on the instance from Hashtags in Moderation UI (#31533 by @ThisIsMissEm) +- Add list of pending releases directly in mail notifications for version updates (#29436 and #30035 by @ClearlyClaire) +- Add “Appeals” link under “Moderation” navigation category in moderation interface (#31071 by @ThisIsMissEm) +- Add badge on account card in report moderation interface when account is already suspended (#29592 by @ClearlyClaire) +- Add admin comments directly to the `admin/instances` page (#29240 by @tribela) +- Add ability to require approval when users sign up using specific email domains (#28468, #28732, #28607, and #28608 by @ClearlyClaire) +- Add banner for forwarded reports made by remote users about remote content (#27549 by @ClearlyClaire) +- Add support HTML ruby tags in remote posts for east-asian languages (#30897 by @ThisIsMissEm) +- Add link to manage warning presets in admin navigation (#26199 by @vmstan) +- Add volume saving/reuse to video player (#27488 by @thehydrogen) +- Add Elasticsearch index size, ffmpeg and ImageMagick versions to the admin dashboard (#27301, #30710, #31130, and #30845 by @vmstan) +- Add `MASTODON_SIDEKIQ_READY_FILENAME` environment variable to use a file for Sidekiq to signal it is ready to process jobs (#30971 and #30988 by @renchap)\ + In the official Docker image, this is set to `sidekiq_process_has_started_and_will_begin_processing_jobs` so that Sidekiq will touch `tmp/sidekiq_process_has_started_and_will_begin_processing_jobs` to signal readiness. +- Add `S3_RETRY_LIMIT` environment variable to make S3 retries configurable (#23215 by @smiba) +- Add `S3_KEY_PREFIX` environment variable (#30181 by @S0yKaf) +- Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm) +- Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap) +- Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc) +- Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan) +- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, #30858 and #32104 by @ClearlyClaire, @Gargron, and @mjankowski)\ + Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\ + This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\ + This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future. +- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm) +- Add anchors to each authorized application in `/oauth/authorized_applications` (#31677 by @fowl2) +- Add active animation to header settings button (#30221, #30307, and #30388 by @daudix) +- Add OpenTelemetry instrumentation (#30130, #30322, #30353, #30350 and #31998 by @julianocosta89, @renchap, @robbkidd and @timetinytim)\ + See https://docs.joinmastodon.org/admin/config/#otel for documentation +- Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\ + This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index +- Add support for CORS to `POST /oauth/revoke` (#31743 by @ClearlyClaire) +- Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid) +- Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm) +- Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire) +- Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm) +- Add the role ID to the badge component (#29707 by @renchap) +- Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski) +- Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski) +- Add support for specifying custom CA cert for Elasticsearch through `ES_CA_FILE` (#29122 and #29147 by @ClearlyClaire) +- Add groundwork for annual reports for accounts (#28693 by @Gargron)\ + This lays the groundwork for a “year-in-review”/“wrapped” style report for local users, but is currently not in use. +- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire) +- Add date of account deletion in list of accounts in the admin interface (#25640 by @tribela) +- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem) +- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\ + This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3). +- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm) +- Add support for serving JRD `/.well-known/host-meta.json` in addition to XRD host-meta (#32206 by @c960657) +- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543) +- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus) +- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire) +- Add support for invite codes in the registration API (#27805 by @ClearlyClaire) +- Add HTML lang attribute to preview card descriptions (#27503 by @srapilly) +- Add display of relevant account warnings to report action logs (#27425 by @ClearlyClaire) +- Add validation of allowed schemes on preview card URLs (#27485 by @mjankowski) +- Add token introspection without read scope to `/api/v1/apps/verify_credentials` (#27142 by @ThisIsMissEm) +- Add support for cross-origin request to `/nodeinfo/2.0` (#27413 by @palant) +- Add variable delay before link verification of remote account links (#27351 by @ClearlyClaire) +- Add PWA shortcut to `/explore` page (#27235 by @jake-anto) ### Changed +- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, #31525, #32153, and #32201 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\ + This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\ + In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state. +- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, #31889 and #32033 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\ + The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\ + As part of this, the “Unlisted” privacy setting has been renamed to “Quiet public”. +- **Change design of modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, #31399, #31555, #31752, #31801, #31883, #31844, #31864, and #31943 by @ClearlyClaire, @Gargron, @tribela and @vmstan)\ + The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\ + They also have a more modern and consistent design, along with other confirmation modals in the application. +- **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, #31510 and #32128 by @ClearlyClaire, @Gargron, @mjankowski, @renchap, and @vmstan) +- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878, #29272, and #31912 by @Gargron) +- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, #29879, #32073 and #32132 by @c960657, @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\ + All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to read and keeping maximum compatibility across mail clients. +- **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\ + This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\ + In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\ + This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources). +- Change account search algorithm (#30803 by @Gargron) +- **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, #30795, #31612, and #31615 by @TheEssem, @ThisIsMissEm, @jippi, @renchap, @timetinytim, and @vmstan)\ + In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\ + The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\ + Administrators may need to update their setup accordingly. +- Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron) +- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros) +- Change embedded posts to use web UI (#31766, #32135 and #32271 by @Gargron) +- Change inner borders in media galleries in web UI (#31852 by @Gargron) +- Change design of media attachments and profile media tab in web UI (#31807, #32048, #31967, #32217, #32224 and #32257 by @ClearlyClaire and @Gargron) +- Change labels on thread indicators in web UI (#31806 by @Gargron) +- Change label of "Data export" menu item in settings interface (#32099 by @c960657) +- Change responsive break points on navigation panel in web UI (#32034 by @Gargron) +- Change cursor to `not-allowed` on disabled buttons (#32076 by @mjankowski) +- Change OAuth authorization prompt to not refer to apps as “third-party” (#32005 by @Gargron) - Change Mastodon to issue correct HTTP signatures by default (#31994 by @ClearlyClaire) ### Fixed @@ -27,9 +212,85 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fix broken notifications for mentions from local moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31484)) +- **Fix link preview cards not always preserving the original URL from the status** (#27312 by @Gargron) +- Fix log out from user menu not working on Safari (#31402 by @renchap) +- Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela) +- Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski) +- Fix error when accepting an appeal for sensitive posts deleted in the meantime (#32037 by @ClearlyClaire) +- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire) +- Fix Safari browser glitch related to horizontal scrolling (#31960 by @Gargron) +- Fix unresolvable mentions sometimes preventing processing incoming posts (#29215 by @tribela and @ClearlyClaire) +- Fix too many requests caused by relationship look-ups in web UI (#32042 by @Gargron) +- Fix links for reblogs in moderation interface (#31979 by @ClearlyClaire) +- Fix the appearance of avatars when they do not load (#31966 and #32270 by @Gargron and @renchap) +- Fix spurious error notifications for aborted requests in web UI (#31952 by @c960657) +- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil) +- Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire) +- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77) +- Fix wrapping in dashboard quick access buttons (#32043 by @renchap) +- Fix recently used tags hint being displayed in profile edition page when there is none (#32120 by @mjankowski) +- Fix checkbox lists on narrow screens in the settings interface (#32112 by @mjankowski) +- Fix the position of status action buttons being affected by interaction counters (#32084 by @renchap) +- Fix the summary of converted ActivityPub object types to be treated as HTML (#28629 by @Menrath) +- Fix cutoff of instance name in sign-up form (#30598 by @oneiros) +- Fix invalid date searches returning 503 errors (#31526 by @notchairmk) +- Fix invalid `visibility` values in `POST /api/v1/statuses` returning 500 errors (#31571 by @c960657) +- Fix some components re-rendering spuriously in web UI (#31879 and #31881 by @ClearlyClaire and @Gargron) +- Fix sort order of moderation notes on Reports and Accounts (#31528 by @ThisIsMissEm) +- Fix email language when recipient has no selected locale (#31747 by @ClearlyClaire) +- Fix frequently-used languages not correctly updating in the web UI (#31386 by @c960657) +- Fix `POST /api/v1/statuses` silently ignoring invalid `media_ids` parameter (#31681 by @c960657) +- Fix handling of the `BIND` environment variable in the streaming server (#31624 by @ThisIsMissEm) +- Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski) +- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm) +- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire) +- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski) +- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, #31445, #32091, #32147 and #32137 by @ClearlyClaire, @mjankowski, @valtlai and @vmstan) +- Fix editing description of media uploads with custom thumbnails (#32221 by @ClearlyClaire) +- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire) +- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire) +- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers) +- Fix unneeded requests to blocked domains when receiving relayed signed activities from them (#31161 by @ClearlyClaire) +- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire) +- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire) +- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski) +- Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524 by @ClearlyClaire) +- Fix being able to upload more than 4 media attachments in some cases (#29183 by @mashirozx) +- Fix preview card player getting embedded when clicking on the external link button (#29457 by @ClearlyClaire) +- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap) +- Fix filters title and keywords overflow (#29396 by @GeopJr) +- Fix incorrect date format in “Follows and followers” (#29390 by @JasonPunyon) +- Fix navigation item active highlight for some paths (#32159 by @mjankowski) +- Fix “Edit media” modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen) +- Fix modal container bounds (#29185 by @nico3333fr) +- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire) +- Fix moderation report updates through `PUT /api/v1/admin/reports/:id` not being logged in the audit log (#29044, #30342, and #31033 by @mjankowski, @tribela, and @vmstan) +- Fix moderation interface allowing to select rule violation when there are no server rules (#31458 by @ThisIsMissEm) +- Fix redirection from paths with url-encoded `@` to their decoded form (#31184 by @timothyjrogers) +- Fix Trending Tags pending review having an unstable sort order (#31473 by @ThisIsMissEm) +- Fix the emoji dropdown button always opening the dropdown instead of behaving like a toggle (#29012 by @jh97uk) +- Fix processing of incoming posts with bearcaps (#26527 by @kmycode) +- Fix support for IPv6 redis connections in streaming (#31229 by @ThisIsMissEm) +- Fix search form re-rendering spuriously in web UI (#28876 by @Gargron) +- Fix `RedownloadMediaWorker` not being called on transient S3 failure (#28714 by @ClearlyClaire) +- Fix ISO code for Canadian French from incorrect `fr-QC` to `fr-CA` (#26015 by @gunchleoc) +- Fix `.opus` file uploads being misidentified by Paperclip (#28580 by @vmstan) +- Fix loading local accounts with extraneous domain part in WebUI (#28559 by @ClearlyClaire) +- Fix destructive actions in dropdowns not using error color in light theme (#28484 by @logicalmoody) +- Fix call to inefficient `delete_matched` cache method in domain blocks (#28374 by @ClearlyClaire) +- Fix status edits not always being streamed to mentioned users (#28324 by @ClearlyClaire) +- Fix onboarding step descriptions being truncated on narrow screens (#28021 by @ClearlyClaire) +- Fix duplicate IDs in relationships and familiar_followers APIs (#27982 by @KevinBongart) +- Fix modal content not being selectable (#27813 by @pajowu) +- Fix Web UI not displaying appropriate explanation when a user hides their follows/followers (#27791 by @ClearlyClaire) +- Fix format-dependent redirects being cached regardless of requested format (#27632 by @ClearlyClaire) +- Fix confusing screen when visiting a confirmation link for an already-confirmed email (#27368 by @ClearlyClaire) +- Fix explore page reloading when you navigate back to it in web UI (#27489 by @Gargron) +- Fix missing redirection from `/home` to `/deck/home` in the advanced interface (#27378 by @Signez) +- 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 +## |4.2.11] - 2024-08-16 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index b85d97761..4c819ce9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -601,7 +601,7 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - propshaft (1.0.1) + propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -698,7 +698,7 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.7) + rexml (3.3.8) rotp (6.3.0) rouge (4.3.0) rpam2 (4.0.2) @@ -748,15 +748,15 @@ GEM parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) - rubocop-performance (1.21.1) + rubocop-performance (1.22.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.1) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) + rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.4) + rubocop-rspec (3.0.5) rubocop (~> 1.61) rubocop-rspec_rails (2.30.0) rubocop (~> 1.61) @@ -884,7 +884,7 @@ GEM webfinger (1.2.0) activesupport httpclient (>= 2.4) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/SECURITY.md b/SECURITY.md index 156954ce0..43ab4454c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | --------- | -| 4.2.x | Yes | -| 4.1.x | Yes | -| < 4.1 | No | +| Version | Supported | +| ------- | ---------------- | +| 4.3.x | Yes | +| 4.2.x | Yes | +| 4.1.x | Until 2025-04-08 | +| < 4.1 | No | diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index ebbdba59a..9485ecda4 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -13,7 +13,7 @@ module WebAppControllerConcern policy = ContentSecurityPolicy.new if policy.sso_host.present? - p.form_action policy.sso_host + p.form_action policy.sso_host, -> { "https://#{request.host}/auth/auth/" } else p.form_action :none end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index 0bff01ec2..ca8d46afe 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -15,7 +15,7 @@ module Settings end def create - session[:new_otp_secret] = User.generate_otp_secret(32) + session[:new_otp_secret] = User.generate_otp_secret redirect_to new_settings_two_factor_authentication_confirmation_path end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 201da9fbc..6dee587ba 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -7,7 +7,23 @@ module WellKnown def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true - render content_type: 'application/xrd+xml', formats: [:xml] + + respond_to do |format| + format.any do + render content_type: 'application/xrd+xml', formats: [:xml] + end + + format.json do + render json: { + links: [ + { + rel: 'lrdd', + template: @webfinger_template, + }, + ], + } + end + end end end end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e8d563412..51e28d8b4 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -35,4 +35,11 @@ module Admin::ActionLogsHelper end end end + + def sorted_action_log_types + Admin::ActionLogFilter::ACTION_TYPE_MAP + .keys + .map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] } + .sort_by(&:first) + end end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index 6096ff138..f87fdad70 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -18,6 +18,11 @@ module Admin::DashboardHelper end end + def date_range(range) + [l(range.first), l(range.last)] + .join(' - ') + end + def relevant_account_timestamp(account) timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago [account.user_current_sign_in_at, true] @@ -25,6 +30,8 @@ module Admin::DashboardHelper [account.user_current_sign_in_at, false] elsif account.user_pending? [account.user_created_at, true] + elsif account.suspended_at.present? && account.local? && account.user.nil? + [account.suspended_at, true] elsif account.last_status_at.present? [account.last_status_at, true] else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de00f76d3..8f9a433d8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true module ApplicationHelper - DANGEROUS_SCOPES = %w( - read - write - follow - ).freeze - RTL_LOCALES = %i( ar ckb @@ -95,8 +89,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end - def class_for_scope(scope) - 'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s) + def label_for_scope(scope) + safe_join [ + tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }), + tag.span(t("doorkeeper.scopes.#{scope}"), class: :hint), + ] end def can?(action, record) @@ -243,6 +240,10 @@ module ApplicationHelper full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg')) end + def copyable_input(options = {}) + tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options) + end + private def storage_host_var diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb deleted file mode 100644 index 482f4e19e..000000000 --- a/app/helpers/webfinger_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module WebfingerHelper - def webfinger!(uri) - Webfinger.new(uri).perform - end -end diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts index 0b3280c21..251546cb9 100644 --- a/app/javascript/mastodon/actions/markers.ts +++ b/app/javascript/mastodon/actions/markers.ts @@ -37,8 +37,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk( }); return; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if ('navigator' && 'sendBeacon' in navigator) { + } else if ('sendBeacon' in navigator) { // Failing that, we can use sendBeacon, but we have to encode the data as // FormData for DoorKeeper to recognize the token. const formData = new FormData(); diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index b40b04f8c..a359913e6 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -70,6 +70,10 @@ function dispatchAssociatedRecords( const supportedGroupedNotificationTypes = ['favourite', 'reblog']; +export function shouldGroupNotificationType(type: string) { + return supportedGroupedNotificationTypes.includes(type); +} + export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', async (_params, { getState }) => diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap index 2f0a2de32..124b50d8c 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap @@ -2,7 +2,7 @@ exports[` Autoplay renders a animated avatar 1`] = `
Autoplay renders a animated avatar 1`] = ` >
@@ -21,7 +23,7 @@ exports[` Autoplay renders a animated avatar 1`] = ` exports[` Still renders a still avatar 1`] = `
Still renders a still avatar 1`] = ` >
diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index 8b16296c2..f61d9676d 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -1,10 +1,11 @@ +import { useState, useCallback } from 'react'; + import classNames from 'classnames'; +import { useHovering } from 'mastodon/../hooks/useHovering'; +import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; -import { useHovering } from '../../hooks/useHovering'; -import { autoPlayGif } from '../initial_state'; - interface Props { account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there size: number; @@ -25,6 +26,8 @@ export const Avatar: React.FC = ({ counterBorderColor, }) => { const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const style = { ...styleFromParent, @@ -37,16 +40,28 @@ export const Avatar: React.FC = ({ ? account?.get('avatar') : account?.get('avatar_static'); + const handleLoad = useCallback(() => { + setLoading(false); + }, [setLoading]); + + const handleError = useCallback(() => { + setError(true); + }, [setError]); + return (
- {src && } + {src && !error && ( + + )} + {counter && (
{badges}
@@ -336,14 +336,14 @@ class MediaGallery extends PureComponent { return (
+ {children} + {(!visible || uncached) && (
{spoilerButton}
)} - {children} - {(visible && !uncached) && (
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 18830708d..b11382cf0 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -84,6 +84,7 @@ "alert.rate_limited.title": "معدل الطلبات محدود", "alert.unexpected.message": "لقد طرأ خطأ غير متوقّع.", "alert.unexpected.title": "المعذرة!", + "alt_text_badge.title": "نص بديل", "announcement.announcement": "إعلان", "attachments_list.unprocessed": "(غير معالَج)", "audio.hide": "إخفاء المقطع الصوتي", @@ -758,7 +759,7 @@ "status.history.edited": "عدله {name} {date}", "status.load_more": "حمّل المزيد", "status.media.open": "اضغط للفتح", - "status.media.show": "اضغط لإظهاره", + "status.media.show": "اضغط لإظهارها", "status.media_hidden": "وسائط مخفية", "status.mention": "أذكُر @{name}", "status.more": "المزيد", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 797d8cc22..d3a29eae4 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Ліміт перавышаны", "alert.unexpected.message": "Узнікла нечаканая памылка.", "alert.unexpected.title": "Вой!", + "alt_text_badge.title": "Альтернативный текст", "announcement.announcement": "Аб'ява", "attachments_list.unprocessed": "(неапрацаваны)", "audio.hide": "Схаваць аўдыя", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index b79ac156e..1b583b320 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Resposta en privat", "notification.label.reply": "Resposta", "notification.mention": "Menció", + "notification.mentioned_you": "{name} us ha mencionat", "notification.moderation-warning.learn_more": "Per a saber-ne més", "notification.moderation_warning": "Heu rebut un avís de moderació", "notification.moderation_warning.action_delete_statuses": "S'han eliminat algunes de les vostres publicacions.", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index f2d070888..52dc6a49e 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Cyfradd gyfyngedig", "alert.unexpected.message": "Digwyddodd gwall annisgwyl.", "alert.unexpected.title": "Wps!", + "alt_text_badge.title": "Testun Amgen", "announcement.announcement": "Cyhoeddiad", "attachments_list.unprocessed": "(heb eu prosesu)", "audio.hide": "Cuddio sain", @@ -221,6 +222,7 @@ "domain_block_modal.they_cant_follow": "Ni all neb o'r gweinydd hwn eich dilyn.", "domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.", "domain_block_modal.title": "Blocio parth?", + "domain_block_modal.you_will_lose_relationships": "Byddwch yn colli'r holl ddilynwyr a phobl rydych chi'n eu dilyn o'r gweinydd hwn.", "domain_block_modal.you_wont_see_posts": "Fyddwch chi ddim yn gweld postiadau na hysbysiadau gan ddefnyddwyr ar y gweinydd hwn.", "domain_pill.activitypub_lets_connect": "Mae'n caniatáu ichi gysylltu a rhyngweithio â phobl nid yn unig ar Mastodon, ond ar draws gwahanol apiau cymdeithasol hefyd.", "domain_pill.activitypub_like_language": "Mae ActivityPub fel yr iaith y mae Mastodon yn ei siarad â rhwydweithiau cymdeithasol eraill.", @@ -849,6 +851,11 @@ "upload_error.poll": "Nid oes modd llwytho ffeiliau â phleidleisiau.", "upload_form.audio_description": "Disgrifio ar gyfer pobl sydd â cholled clyw", "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg", + "upload_form.drag_and_drop.instructions": "I godi atodiad cyfryngau, pwyswch y space neu enter. Wrth lusgo, defnyddiwch y bysellau saeth i symud yr atodiad cyfryngau i unrhyw gyfeiriad penodol. Pwyswch space neu enter eto i ollwng yr atodiad cyfryngau yn ei safle newydd, neu pwyswch escape i ddiddymu.", + "upload_form.drag_and_drop.on_drag_cancel": "Cafodd llusgo ei ddiddymu. Cafodd atodiad cyfryngau {item} ei ollwng.", + "upload_form.drag_and_drop.on_drag_end": "Cafodd atodiad cyfryngau {item} ei ollwng.", + "upload_form.drag_and_drop.on_drag_over": "Symudwyd atodiad cyfryngau {item}.", + "upload_form.drag_and_drop.on_drag_start": "Atodiad cyfryngau godwyd {item}.", "upload_form.edit": "Golygu", "upload_form.thumbnail": "Newid llun bach", "upload_form.video_description": "Disgrifio ar gyfer pobl sydd â cholled clyw neu amhariad golwg", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 659f807d0..e225bb30a 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Privat svar", "notification.label.reply": "Besvar", "notification.mention": "Omtale", + "notification.mentioned_you": "{name} nævnte dig", "notification.moderation-warning.learn_more": "Læs mere", "notification.moderation_warning": "Du er tildelt en moderationsadvarsel", "notification.moderation_warning.action_delete_statuses": "Nogle af dine indlæg er blevet fjernet.", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 6f45d4fe5..b807d93ab 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -41,7 +41,7 @@ "account.go_to_profile": "Profil aufrufen", "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden", "account.in_memoriam": "Zum Andenken.", - "account.joined_short": "Beigetreten", + "account.joined_short": "Mitglied seit", "account.languages": "Ausgewählte Sprachen ändern", "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt", "account.locked_info": "Die Privatsphäre dieses Kontos wurde auf „geschützt“ gesetzt. Die Person bestimmt manuell, wer ihrem Profil folgen darf.", @@ -516,6 +516,7 @@ "notification.label.private_reply": "Private Antwort", "notification.label.reply": "Antwort", "notification.mention": "Erwähnung", + "notification.mentioned_you": "{name} erwähnte dich", "notification.moderation-warning.learn_more": "Mehr erfahren", "notification.moderation_warning": "Du wurdest von den Moderator*innen verwarnt", "notification.moderation_warning.action_delete_statuses": "Einige deiner Beiträge sind entfernt worden.", @@ -534,7 +535,7 @@ "notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert – darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.", "notification.relationships_severance_event.learn_more": "Mehr erfahren", "notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert – {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.", - "notification.status": "{name} hat gerade etwas gepostet", + "notification.status": "{name} veröffentlichte gerade", "notification.update": "{name} bearbeitete einen Beitrag", "notification_requests.accept": "Genehmigen", "notification_requests.accept_multiple": "{count, plural, one {# Anfrage genehmigen …} other {# Anfragen genehmigen …}}", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 85f893c63..2565f5da6 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Περιορισμός συχνότητας", "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.", "alert.unexpected.title": "Ουπς!", + "alt_text_badge.title": "Εναλλακτικό κείμενο", "announcement.announcement": "Ανακοίνωση", "attachments_list.unprocessed": "(μη επεξεργασμένο)", "audio.hide": "Απόκρυψη αρχείου ήχου", @@ -221,6 +222,8 @@ "domain_block_modal.they_cant_follow": "Κανείς από αυτόν τον διακομιστή δεν μπορεί να σε ακολουθήσει.", "domain_block_modal.they_wont_know": "Δεν θα ξέρουν ότι έχουν αποκλειστεί.", "domain_block_modal.title": "Αποκλεισμός τομέα;", + "domain_block_modal.you_will_lose_num_followers": "Θα χάσετε {followersCount, plural, one {{followersCountDisplay} ακόλουθο} other {{followersCountDisplay} ακόλουθους}} και {followingCount, plural, one {{followingCountDisplay} άτομο που ακολουθείτε} other {{followingCountDisplay} άτομα που ακολουθείτε}}.", + "domain_block_modal.you_will_lose_relationships": "Θα χάσετε όλους τους ακόλουθους και τα άτομα που ακολουθείτε από αυτόν τον διακομιστή.", "domain_block_modal.you_wont_see_posts": "Δεν θα βλέπεις αναρτήσεις ή ειδοποιήσεις από χρήστες σε αυτόν το διακομιστή.", "domain_pill.activitypub_lets_connect": "Σού επιτρέπει να συνδεθείς και να αλληλεπιδράσεις με τους ανθρώπους όχι μόνο στο Mastodon, αλλά και σε διαφορετικές κοινωνικές εφαρμογές.", "domain_pill.activitypub_like_language": "Το ActivityPub είναι σαν τη γλώσσα Mastodon μιλάει με άλλα κοινωνικά δίκτυα.", @@ -849,6 +852,11 @@ "upload_error.poll": "Στις δημοσκοπήσεις δεν επιτρέπεται η μεταφόρτωση αρχείου.", "upload_form.audio_description": "Περιγραφή για άτομα με προβλήματα ακοής", "upload_form.description": "Περιγραφή για άτομα με προβλήματα όρασης", + "upload_form.drag_and_drop.instructions": "Για να επιλέξετε ένα συνημμένο αρχείο πολυμέσων, πατήστε το Space ή το Enter. Ενώ το σέρνετε, χρησιμοποιήστε τα πλήκτρα βέλους για να μετακινήσετε το συνημμένο αρχείο πολυμέσων προς οποιαδήποτε κατεύθυνση. Πατήστε ξανά το Space ή το Enter για να αποθέσετε το συνημμένο αρχείο πολυμέσων στη νέα του θέση ή πατήστε το Escape για ακύρωση.", + "upload_form.drag_and_drop.on_drag_cancel": "Η μετακίνηση ακυρώθηκε. Έγινε απόθεση του συνημμένου αρχείου πολυμέσων «{item}».", + "upload_form.drag_and_drop.on_drag_end": "Έγινε απόθεση του συνημμένου αρχείου πολυμέσων «{item}».", + "upload_form.drag_and_drop.on_drag_over": "Έγινε μετακίνηση του συνημμένου αρχείου πολυμέσων «{item}».", + "upload_form.drag_and_drop.on_drag_start": "Έγινε επιλογή του συνημμένου αρχείου πολυμέσων «{item}».", "upload_form.edit": "Επεξεργασία", "upload_form.thumbnail": "Αλλαγή μικρογραφίας", "upload_form.video_description": "Περιγραφή για άτομα με προβλήματα ακοής ή όρασης", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index b1d61ddd3..da4c00520 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -852,6 +852,11 @@ "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people who are deaf or hard of hearing", "upload_form.description": "Describe for people who are blind or have low vision", + "upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.", + "upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.", + "upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.", + "upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.", + "upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.", "upload_form.edit": "Edit", "upload_form.thumbnail": "Change thumbnail", "upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index dde612ec0..7304b2f1f 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Private reply", "notification.label.reply": "Reply", "notification.mention": "Mention", + "notification.mentioned_you": "{name} mentioned you", "notification.moderation-warning.learn_more": "Learn more", "notification.moderation_warning": "You have received a moderation warning", "notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 4268c3853..5f6582fb6 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -22,10 +22,10 @@ "account.cancel_follow_request": "Nuligi peton por sekvado", "account.copy": "Kopii ligilon al profilo", "account.direct": "Private mencii @{name}", - "account.disable_notifications": "Ne plu sciigi min, kiam @{name} mesaĝas", + "account.disable_notifications": "Ĉesu sciigi min kiam @{name} afiŝas", "account.domain_blocked": "Domajno blokita", "account.edit_profile": "Redakti la profilon", - "account.enable_notifications": "Sciigi min, kiam @{name} mesaĝas", + "account.enable_notifications": "Sciigu min kiam @{name} afiŝos", "account.endorse": "Rekomendi ĉe via profilo", "account.featured_tags.last_status_at": "Lasta afîŝo je {date}", "account.featured_tags.last_status_never": "Neniu afiŝo", @@ -49,14 +49,14 @@ "account.mention": "Mencii @{name}", "account.moved_to": "{name} indikis, ke ria nova konto estas nun:", "account.mute": "Silentigi @{name}", - "account.mute_notifications_short": "Silentigu Sciigojn", + "account.mute_notifications_short": "Silentigu sciigojn", "account.mute_short": "Silentigu", "account.muted": "Silentigita", "account.mutual": "Reciproka", "account.no_bio": "Neniu priskribo estas provizita.", "account.open_original_page": "Malfermi la originalan paĝon", "account.posts": "Afiŝoj", - "account.posts_with_replies": "Mesaĝoj kaj respondoj", + "account.posts_with_replies": "Afiŝoj kaj respondoj", "account.report": "Raporti @{name}", "account.requested": "Atendo de aprobo. Klaku por nuligi la peton por sekvado", "account.requested_follow": "{name} petis sekvi vin", @@ -69,7 +69,7 @@ "account.unendorse": "Ne plu rekomendi ĉe la profilo", "account.unfollow": "Ĉesi sekvi", "account.unmute": "Ne plu silentigi @{name}", - "account.unmute_notifications_short": "Malsilentigu Sciigojn", + "account.unmute_notifications_short": "Malsilentigu sciigojn", "account.unmute_short": "Ne plu silentigi", "account_note.placeholder": "Alklaku por aldoni noton", "admin.dashboard.daily_retention": "Uzantoretenprocento lau tag post registro", @@ -81,7 +81,7 @@ "admin.impact_report.instance_followers": "Sekvantojn niaj uzantoj perdus", "admin.impact_report.instance_follows": "Sekvantojn ties uzantoj perdus", "admin.impact_report.title": "Influa reporto", - "alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.", + "alert.rate_limited.message": "Bonvolu reprovi poste {retry_time, time, medium}.", "alert.rate_limited.title": "Mesaĝkvante limigita", "alert.unexpected.message": "Neatendita eraro okazis.", "alert.unexpected.title": "Aj!", @@ -163,7 +163,7 @@ "compose_form.poll.switch_to_single": "Ŝanĝi la balotenketon por permesi unu solan elekton", "compose_form.poll.type": "Stilo", "compose_form.publish": "Afiŝo", - "compose_form.publish_form": "Afiŝi", + "compose_form.publish_form": "Nova afiŝo", "compose_form.reply": "Respondi", "compose_form.save_changes": "Ĝisdatigi", "compose_form.spoiler.marked": "Forigi la averton de enhavo", @@ -173,7 +173,7 @@ "confirmations.block.confirm": "Bloki", "confirmations.delete.confirm": "Forigi", "confirmations.delete.message": "Ĉu vi certas, ke vi volas forigi ĉi tiun afiŝon?", - "confirmations.delete.title": "Ĉu forigi Afiŝon?", + "confirmations.delete.title": "Ĉu forigi afiŝon?", "confirmations.delete_list.confirm": "Forigi", "confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?", "confirmations.delete_list.title": "Ĉu forigi liston?", @@ -213,9 +213,9 @@ "dismissable_banner.community_timeline": "Jen la plej novaj publikaj afiŝoj de uzantoj, kies kontojn gastigas {domain}.", "dismissable_banner.dismiss": "Eksigi", "dismissable_banner.explore_links": "Tiuj novaĵoj estas aktuale priparolataj de uzantoj en tiu ĉi kaj aliaj serviloj, sur la malcentrigita reto.", - "dismissable_banner.explore_statuses": "Ĉi tioj estas afiŝoj de socia reto kiu populariĝas hodiau.", + "dismissable_banner.explore_statuses": "Ĉi tiuj estas afiŝoj de la tuta socia reto, kiuj populariĝas hodiaŭ. Pli novaj afiŝoj kun pli da diskonigoj kaj plej ŝatataj estas rangigitaj pli alte.", "dismissable_banner.explore_tags": "Ĉi tiuj kradvostoj populariĝas en ĉi tiu kaj aliaj serviloj en la malcentraliza reto nun.", - "dismissable_banner.public_timeline": "Ĉi tioj estas plej lastaj publikaj afiŝoj de personoj ĉe socia reto kiu personoj ĉe {domain} sekvas.", + "dismissable_banner.public_timeline": "Ĉi tiuj estas la plej lastatempaj publikaj afiŝoj de homoj en la socia reto, kiujn homoj sur {domain} sekvas.", "domain_block_modal.block": "Bloki servilon", "domain_block_modal.block_account_instead": "Bloki @{name} anstataŭe", "domain_block_modal.they_can_interact_with_old_posts": "Homoj de ĉi tiu servilo povas interagi kun viaj malnovaj afiŝoj.", @@ -265,8 +265,8 @@ "empty_column.direct": "Vi ankoraŭ ne havas privatan mencion. Kiam vi sendos aŭ ricevos iun, tiu aperos ĉi tie.", "empty_column.domain_blocks": "Ankoraŭ neniu domajno estas blokita.", "empty_column.explore_statuses": "Nenio tendencas nun. Rekontrolu poste!", - "empty_column.favourited_statuses": "Vi ankoraŭ ne havas stelumitan afiŝon.", - "empty_column.favourites": "Ankoraŭ neniu stelumis tiun afiŝon.", + "empty_column.favourited_statuses": "Vi ankoraŭ ne havas plej ŝatatajn afiŝojn. Kiam vi ŝatatas unu, ĝi aperos ĉi tie.", + "empty_column.favourites": "Neniu ankoraŭ ŝatis ĉi tiun afiŝon. Kiam iu ŝatos ĝin, ili aperos ĉi tie.", "empty_column.follow_requests": "Vi ne ankoraŭ havas iun peton de sekvado. Kiam vi ricevos unu, ĝi aperos ĉi tie.", "empty_column.followed_tags": "Vi ankoraŭ ne sekvas iujn kradvortojn. Kiam vi faras, ili aperos ĉi tie.", "empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.", @@ -296,7 +296,7 @@ "filter_modal.added.review_and_configure": "Por kontroli kaj pli modifi ĉi tiu filtrilkategorio, iru al la {settings_link}.", "filter_modal.added.review_and_configure_title": "Filtrilopcioj", "filter_modal.added.settings_link": "opciopaĝo", - "filter_modal.added.short_explanation": "Ĉi tiu mesaĝo aldonitas al la filtrilkategorio: {title}.", + "filter_modal.added.short_explanation": "Ĉi tiu afiŝo aldonitas al la filtrilkategorio: {title}.", "filter_modal.added.title": "Filtrilo aldonita!", "filter_modal.select_filter.context_mismatch": "ne kongruas la kuntekston", "filter_modal.select_filter.expired": "eksvalidiĝinta", @@ -304,7 +304,7 @@ "filter_modal.select_filter.search": "Serĉi aŭ krei", "filter_modal.select_filter.subtitle": "Uzu ekzistantan kategorion aŭ kreu novan", "filter_modal.select_filter.title": "Filtri ĉi tiun afiŝon", - "filter_modal.title.status": "Filtri mesaĝon", + "filter_modal.title.status": "Filtri afiŝon", "filter_warning.matches_filter": "Filtrilo de kongruoj “{title}”", "filtered_notifications_banner.pending_requests": "El {count, plural, =0 {neniu} one {unu persono} other {# homoj}} vi eble konas", "filtered_notifications_banner.title": "Filtritaj sciigoj", @@ -351,7 +351,7 @@ "hashtag.column_settings.tag_toggle": "Aldoni pliajn etikedojn por ĉi tiu kolumno", "hashtag.counter_by_accounts": "{count, plural,one {{counter} partoprenanto} other {{counter} partoprenantoj}}", "hashtag.counter_by_uses": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}}", - "hashtag.counter_by_uses_today": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}} hodiau", + "hashtag.counter_by_uses_today": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}} hodiaŭ", "hashtag.follow": "Sekvi la kradvorton", "hashtag.unfollow": "Ne plu sekvi la kradvorton", "hashtags.and_other": "…kaj {count, plural,other {# pli}}", @@ -382,9 +382,9 @@ "ignore_notifications_modal.not_following_title": "Ĉu ignori sciigojn de homoj, kiujn vi ne sekvas?", "ignore_notifications_modal.private_mentions_title": "Ĉu ignori sciigojn de nepetitaj privataj mencioj?", "interaction_modal.description.favourite": "Per konto ĉe Mastodon, vi povas stelumiti ĉi tiun afiŝon por sciigi la afiŝanton ke vi aprezigas ŝin kaj konservas por la estonteco.", - "interaction_modal.description.follow": "Kun konto ĉe Mastodon, vi povos sekvi {name} por vidi ties mesaĝojn en via hejmo.", + "interaction_modal.description.follow": "Kun konto ĉe Mastodon, vi povas sekvi {name} por ricevi iliajn afiŝojn en via hejma fluo.", "interaction_modal.description.reblog": "Kun konto ĉe Mastodon, vi povas diskonigi ĉi tiun afiŝon, por ke viaj propraj sekvantoj vidu ĝin.", - "interaction_modal.description.reply": "Kun konto ĉe Mastodon, vi povos respondi al ĉi tiu mesaĝo.", + "interaction_modal.description.reply": "Kun konto ĉe Mastodon, vi povos respondi al ĉi tiu afiŝo.", "interaction_modal.login.action": "Prenu min hejmen", "interaction_modal.login.prompt": "Domajno de via hejma servilo, ekz. mastodon.social", "interaction_modal.no_account_yet": "Ĉu ne estas ĉe Mastodon?", @@ -402,12 +402,12 @@ "keyboard_shortcuts.back": "reveni", "keyboard_shortcuts.blocked": "Malfermi la liston de blokitaj uzantoj", "keyboard_shortcuts.boost": "Diskonigi la mesaĝon", - "keyboard_shortcuts.column": "fokusi mesaĝon en unu el la kolumnoj", + "keyboard_shortcuts.column": "Fokusi kolumnon", "keyboard_shortcuts.compose": "enfokusigi la tekstujon", "keyboard_shortcuts.description": "Priskribo", "keyboard_shortcuts.direct": "por malfermi la kolumnon pri privataj mencioj", "keyboard_shortcuts.down": "iri suben en la listo", - "keyboard_shortcuts.enter": "malfermi mesaĝon", + "keyboard_shortcuts.enter": "Malfermi afiŝon", "keyboard_shortcuts.favourite": "Stelumi afiŝon", "keyboard_shortcuts.favourites": "Malfermi la liston de la stelumoj", "keyboard_shortcuts.federated": "Malfermi la frataran templinion", @@ -421,16 +421,16 @@ "keyboard_shortcuts.my_profile": "malfermi vian profilon", "keyboard_shortcuts.notifications": "malfermi la kolumnon de sciigoj", "keyboard_shortcuts.open_media": "Malfermi plurmedion", - "keyboard_shortcuts.pinned": "malfermi la liston de alpinglitaj mesaĝoj", + "keyboard_shortcuts.pinned": "Malfermu alpinglitajn afiŝojn-liston", "keyboard_shortcuts.profile": "malfermi la profilon de la aŭtoro", - "keyboard_shortcuts.reply": "respondi", + "keyboard_shortcuts.reply": "Respondu al afiŝo", "keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado", "keyboard_shortcuts.search": "enfokusigi la serĉilon", "keyboard_shortcuts.spoilers": "Montri/kaŝi la kampon de averto de enhavo (\"CW\")", "keyboard_shortcuts.start": "malfermi la kolumnon «por komenci»", "keyboard_shortcuts.toggle_hidden": "Montri/kaŝi tekston malantaŭ la averto de enhavo (\"CW\")", "keyboard_shortcuts.toggle_sensitivity": "Montri/kaŝi plurmedion", - "keyboard_shortcuts.toot": "Krei novan mesaĝon", + "keyboard_shortcuts.toot": "Komencu novan afiŝon", "keyboard_shortcuts.unfocus": "malenfokusigi la tekstujon aŭ la serĉilon", "keyboard_shortcuts.up": "iri supren en la listo", "lightbox.close": "Fermi", @@ -476,9 +476,9 @@ "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.bookmarks": "Legosignoj", "navigation_bar.community_timeline": "Loka templinio", - "navigation_bar.compose": "Skribi novan mesaĝon", + "navigation_bar.compose": "Redakti novan afiŝon", "navigation_bar.direct": "Privataj mencioj", - "navigation_bar.discover": "Esplori", + "navigation_bar.discover": "Malkovri", "navigation_bar.domain_blocks": "Blokitaj domajnoj", "navigation_bar.explore": "Esplori", "navigation_bar.favourites": "Stelumoj", @@ -487,12 +487,12 @@ "navigation_bar.followed_tags": "Sekvataj kradvortoj", "navigation_bar.follows_and_followers": "Sekvatoj kaj sekvantoj", "navigation_bar.lists": "Listoj", - "navigation_bar.logout": "Adiaŭi", + "navigation_bar.logout": "Elsaluti", "navigation_bar.moderation": "Modereco", "navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.", "navigation_bar.personal": "Persone", - "navigation_bar.pins": "Alpinglitaj mesaĝoj", + "navigation_bar.pins": "Alpinglitaj afiŝoj", "navigation_bar.preferences": "Preferoj", "navigation_bar.public_timeline": "Fratara templinio", "navigation_bar.search": "Serĉi", @@ -516,6 +516,7 @@ "notification.label.private_reply": "Privata respondo", "notification.label.reply": "Respondi", "notification.mention": "Mencii", + "notification.mentioned_you": "{name} menciis vin", "notification.moderation-warning.learn_more": "Lerni pli", "notification.moderation_warning": "Vi ricevis moderigan averton", "notification.moderation_warning.action_delete_statuses": "Kelkaj el viaj afiŝoj estis forigitaj.", @@ -572,7 +573,7 @@ "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolumno", "notifications.column_settings.sound": "Eligi sonon", - "notifications.column_settings.status": "Novaj mesaĝoj:", + "notifications.column_settings.status": "Novaj afiŝoj:", "notifications.column_settings.unread_notifications.category": "Nelegitaj sciigoj", "notifications.column_settings.unread_notifications.highlight": "Marki nelegitajn sciigojn", "notifications.column_settings.update": "Redaktoj:", @@ -660,7 +661,7 @@ "poll.votes": "{votes, plural, one {# voĉdono} other {# voĉdonoj}}", "poll_button.add_poll": "Aldoni balotenketon", "poll_button.remove_poll": "Forigi balotenketon", - "privacy.change": "Agordi mesaĝan privatecon", + "privacy.change": "Ŝanĝu afiŝan privatecon", "privacy.direct.long": "Ĉiuj menciitaj en la afiŝo", "privacy.direct.short": "Specifaj homoj", "privacy.private.long": "Nur viaj sekvantoj", @@ -775,13 +776,13 @@ "sign_in_banner.sso_redirect": "Ensalutu aŭ Registriĝi", "status.admin_account": "Malfermi fasadon de moderigado por @{name}", "status.admin_domain": "Malfermu moderigan interfacon por {domain}", - "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco", + "status.admin_status": "Malfermi ĉi tiun afiŝon en la kontrola interfaco", "status.block": "Bloki @{name}", "status.bookmark": "Aldoni al la legosignoj", "status.cancel_reblog_private": "Ne plu diskonigi", "status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi", "status.continued_thread": "Daŭrigis fadenon", - "status.copy": "Kopii la ligilon al la mesaĝo", + "status.copy": "Kopii la ligilon al la afiŝo", "status.delete": "Forigi", "status.detailed_status": "Detala konversacia vido", "status.direct": "Private mencii @{name}", @@ -803,9 +804,9 @@ "status.more": "Pli", "status.mute": "Silentigi @{name}", "status.mute_conversation": "Silentigi konversacion", - "status.open": "Disvolvi la mesaĝon", + "status.open": "Pligrandigu ĉi tiun afiŝon", "status.pin": "Alpingli al la profilo", - "status.pinned": "Alpinglita mesaĝo", + "status.pinned": "Alpinglita afiŝo", "status.read_more": "Legi pli", "status.reblog": "Diskonigi", "status.reblog_private": "Diskonigi kun la sama videbleco", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 609175d94..7fec88a96 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Respuesta privada", "notification.label.reply": "Respuesta", "notification.mention": "Mención", + "notification.mentioned_you": "{name} te mencionó", "notification.moderation-warning.learn_more": "Aprendé más", "notification.moderation_warning": "Recibiste una advertencia de moderación", "notification.moderation_warning.action_delete_statuses": "Se eliminaron algunos de tus mensajes.", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 7c2e40033..ddfdf6960 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Respuesta privada", "notification.label.reply": "Respuesta", "notification.mention": "Mención", + "notification.mentioned_you": "{name} te ha mencionado", "notification.moderation-warning.learn_more": "Saber más", "notification.moderation_warning": "Has recibido una advertencia de moderación", "notification.moderation_warning.action_delete_statuses": "Se han eliminado algunas de tus publicaciones.", @@ -852,11 +853,11 @@ "upload_error.poll": "Subida de archivos no permitida con encuestas.", "upload_form.audio_description": "Describir para personas con problemas auditivos", "upload_form.description": "Describir para los usuarios con dificultad visual", - "upload_form.drag_and_drop.instructions": "Para recoger un archivo multimedia, pulsa la barra espaciadora o la tecla Enter. Mientras arrastras, utiliza las teclas de flecha para mover el archivo multimedia en cualquier dirección. Vuelve a pulsar la barra espaciadora o la tecla Enter para soltar el archivo multimedia en su nueva posición, o pulsa Escape para cancelar.", - "upload_form.drag_and_drop.on_drag_cancel": "Se canceló el arrastre. Se eliminó el archivo adjunto {item}.", - "upload_form.drag_and_drop.on_drag_end": "El archivo adjunto {item} ha sido eliminado.", - "upload_form.drag_and_drop.on_drag_over": "El archivo adjunto {item} se ha movido.", - "upload_form.drag_and_drop.on_drag_start": "Se ha recogido el archivo adjunto {item}.", + "upload_form.drag_and_drop.instructions": "Para recoger un archivo adjunto, pulsa la barra espaciadora o la tecla Intro. Mientras arrastras, usa las teclas de flecha para mover el archivo adjunto en cualquier dirección. Vuelve a pulsar la barra espaciadora o la tecla Intro para soltar el archivo adjunto en su nueva posición, o pulsa la tecla Escape para cancelar.", + "upload_form.drag_and_drop.on_drag_cancel": "Arrastre cancelado. El archivo adjunto {item} fue eliminado.", + "upload_form.drag_and_drop.on_drag_end": "El archivo adjunto {item} fue eliminado.", + "upload_form.drag_and_drop.on_drag_over": "El archivo adjunto {item} fue movido.", + "upload_form.drag_and_drop.on_drag_start": "Recogidos los archivos adjuntos {item}.", "upload_form.edit": "Editar", "upload_form.thumbnail": "Cambiar miniatura", "upload_form.video_description": "Describir para personas con problemas auditivos o visuales", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index b84148933..2aeb7d47e 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Respuesta privada", "notification.label.reply": "Respuesta", "notification.mention": "Mención", + "notification.mentioned_you": "{name} te ha mencionado", "notification.moderation-warning.learn_more": "Saber más", "notification.moderation_warning": "Has recibido una advertencia de moderación", "notification.moderation_warning.action_delete_statuses": "Se han eliminado algunas de tus publicaciones.", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 2e4cd661d..ac7ab097f 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Yksityinen vastaus", "notification.label.reply": "Vastaus", "notification.mention": "Maininta", + "notification.mentioned_you": "{name} mainitsi sinut", "notification.moderation-warning.learn_more": "Lue lisää", "notification.moderation_warning": "Olet saanut moderointivaroituksen", "notification.moderation_warning.action_delete_statuses": "Julkaisujasi on poistettu.", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index c7e752d37..5ad8ba557 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Privat svar", "notification.label.reply": "Svara", "notification.mention": "Umrøð", + "notification.mentioned_you": "{name} nevndi teg", "notification.moderation-warning.learn_more": "Lær meira", "notification.moderation_warning": "Tú hevur móttikið eina umsjónarávaring", "notification.moderation_warning.action_delete_statuses": "Onkrir av tínum postum eru strikaðir.", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index bdceb9bd3..d2b6f0e15 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Répondre en privé", "notification.label.reply": "Réponse", "notification.mention": "Mention", + "notification.mentioned_you": "{name} vous a mentionné·e", "notification.moderation-warning.learn_more": "En savoir plus", "notification.moderation_warning": "Vous avez reçu un avertissement de modération", "notification.moderation_warning.action_delete_statuses": "Certains de vos messages ont été supprimés.", @@ -852,6 +853,7 @@ "upload_error.poll": "L’envoi de fichiers n’est pas autorisé avec les sondages.", "upload_form.audio_description": "Décrire pour les personnes ayant des difficultés d’audition", "upload_form.description": "Décrire pour les malvoyants", + "upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Tout en faisant glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.", "upload_form.edit": "Modifier", "upload_form.thumbnail": "Changer la vignette", "upload_form.video_description": "Décrire pour les personnes ayant des problèmes de vue ou d'audition", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 2acad0209..941522017 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Répondre en privé", "notification.label.reply": "Réponse", "notification.mention": "Mention", + "notification.mentioned_you": "{name} vous a mentionné·e", "notification.moderation-warning.learn_more": "En savoir plus", "notification.moderation_warning": "Vous avez reçu un avertissement de modération", "notification.moderation_warning.action_delete_statuses": "Certains de vos messages ont été supprimés.", @@ -852,6 +853,7 @@ "upload_error.poll": "L’envoi de fichiers n’est pas autorisé avec les sondages.", "upload_form.audio_description": "Décrire pour les personnes ayant des difficultés d’audition", "upload_form.description": "Décrire pour les malvoyant·e·s", + "upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Tout en faisant glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.", "upload_form.edit": "Modifier", "upload_form.thumbnail": "Changer la vignette", "upload_form.video_description": "Décrire pour les personnes ayant des problèmes de vue ou d'audition", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 1d71ef36c..9fa2300fb 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Dataferkear beheind", "alert.unexpected.message": "Der is in ûnferwachte flater bard.", "alert.unexpected.title": "Oepsy!", + "alt_text_badge.title": "Alternative tekst", "announcement.announcement": "Oankundiging", "attachments_list.unprocessed": "(net ferwurke)", "audio.hide": "Audio ferstopje", @@ -221,6 +222,8 @@ "domain_block_modal.they_cant_follow": "Net ien op dizze server kin jo folgje.", "domain_block_modal.they_wont_know": "Se krije net te witten dat se blokkearre wurde.", "domain_block_modal.title": "Domein blokkearje?", + "domain_block_modal.you_will_lose_num_followers": "Jo ferlieze {followersCount, plural, one {{followersCountDisplay} folger} other {{followersCountDisplay} folgers}} en {followingCount, plural, one {{followingCountDisplay} persoan dy’t jo folgje} other {{followingCountDisplay} persoanen dy’t jo folgje}}.", + "domain_block_modal.you_will_lose_relationships": "Jo ferlieze alle folgers en minsken dy’t jo folgje fan dizze server.", "domain_block_modal.you_wont_see_posts": "Jo sjogge gjin berjochten of meldingen mear fan brûkers op dizze server.", "domain_pill.activitypub_lets_connect": "It soarget derfoar dat jo net allinnich mar ferbine en kommunisearje kinne mei minsken op Mastodon, mar ek mei oare sosjale apps.", "domain_pill.activitypub_like_language": "ActivityPub is de taal dy’t Mastodon mei oare sosjale netwurken sprekt.", @@ -433,6 +436,8 @@ "lightbox.close": "Slute", "lightbox.next": "Folgjende", "lightbox.previous": "Foarige", + "lightbox.zoom_in": "Oarspronklike grutte toane", + "lightbox.zoom_out": "Passend toane", "limited_account_hint.action": "Profyl dochs besjen", "limited_account_hint.title": "Dit profyl is troch de behearders fan {domain} ferstoppe.", "link_preview.author": "Troch {name}", @@ -454,6 +459,7 @@ "lists.subheading": "Jo listen", "load_pending": "{count, plural, one {# nij item} other {# nije items}}", "loading_indicator.label": "Lade…", + "media_gallery.hide": "Ferstopje", "moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.", "mute_modal.hide_from_notifications": "Meldingen ferstopje", "mute_modal.hide_options": "Opsjes ferstopje", @@ -510,6 +516,7 @@ "notification.label.private_reply": "Priveereaksje", "notification.label.reply": "Beäntwurdzje", "notification.mention": "Fermelding", + "notification.mentioned_you": "{name} hat dy fermeld", "notification.moderation-warning.learn_more": "Mear ynfo", "notification.moderation_warning": "Jo hawwe in moderaasje-warskôging ûntfongen", "notification.moderation_warning.action_delete_statuses": "Guon fan jo berjochten binne fuortsmiten.", @@ -774,6 +781,7 @@ "status.bookmark": "Blêdwizer tafoegje", "status.cancel_reblog_private": "Net langer booste", "status.cannot_reblog": "Dit berjocht kin net boost wurde", + "status.continued_thread": "Ferfolgje it petear", "status.copy": "Copy link to status", "status.delete": "Fuortsmite", "status.detailed_status": "Detaillearre petearoersjoch", @@ -782,6 +790,7 @@ "status.edit": "Bewurkje", "status.edited": "Lêst bywurke op {date}", "status.edited_x_times": "{count, plural, one {{count} kear} other {{count} kearen}} bewurke", + "status.embed": "Koade om op te nimmen", "status.favourite": "Favoryt", "status.favourites": "{count, plural, one {favoryt} other {favoriten}}", "status.filter": "Dit berjocht filterje", @@ -806,6 +815,7 @@ "status.reblogs.empty": "Net ien hat dit berjocht noch boost. Wannear’t ien dit docht, falt dat hjir te sjen.", "status.redraft": "Fuortsmite en opnij opstelle", "status.remove_bookmark": "Blêdwizer fuortsmite", + "status.replied_in_thread": "Antwurde yn petear", "status.replied_to": "Antwurde op {name}", "status.reply": "Beäntwurdzje", "status.replyAll": "Alle beäntwurdzje", @@ -843,6 +853,11 @@ "upload_error.poll": "It opladen fan bestannen is yn enkêten net tastien.", "upload_form.audio_description": "Describe for people with hearing loss", "upload_form.description": "Describe for the visually impaired", + "upload_form.drag_and_drop.instructions": "Druk op spaasje of Enter om in mediabylage op te pakken. Bruk de pylktoetsen om de bylage yn in bepaalde rjochting te ferpleatsen. Druk opnij op de spaasjebalke of Enter om de mediabylage op de nije posysje te pleatsen, of druk op Esc om te annulearjen.", + "upload_form.drag_and_drop.on_drag_cancel": "Slepen is annulearre. Mediabylage {item} is net ferpleatst.", + "upload_form.drag_and_drop.on_drag_end": "Mediabylage {item} is net ferpleatst.", + "upload_form.drag_and_drop.on_drag_over": "Mediabylage {item} is ferpleatst.", + "upload_form.drag_and_drop.on_drag_start": "Mediabylage {item} is oppakt.", "upload_form.edit": "Bewurkje", "upload_form.thumbnail": "Miniatuerôfbylding wizigje", "upload_form.video_description": "Describe for people with hearing loss or visual impairment", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index ecbc11f3a..f6d3f172c 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Cuingeachadh ùine", "alert.unexpected.message": "Thachair mearachd ris nach robh dùil.", "alert.unexpected.title": "Oich!", + "alt_text_badge.title": "Roghainn teacsa", "announcement.announcement": "Brath-fios", "attachments_list.unprocessed": "(gun phròiseasadh)", "audio.hide": "Falaich an fhuaim", @@ -221,6 +222,8 @@ "domain_block_modal.they_cant_follow": "Chan urrainn do neach sam bith a th’ air an fhrithealaiche seo do leantainn.", "domain_block_modal.they_wont_know": "Cha bhi fios aca gun deach am bacadh.", "domain_block_modal.title": "A bheil thu airson an àrainn a bhacadh?", + "domain_block_modal.you_will_lose_num_followers": "Caillidh tu {followersCount, plural, one {{followersCountDisplay} neach-leantainn} two {{followersCountDisplay} luchd-leantainn} few {{followersCountDisplay} luchd-leantainn} other {{followersCountDisplay} luchd-leantainn}} ’s {followingCount, plural, one {{followingCountDisplay} neach a tha thu a’ leantainn} two {{followingCountDisplay} daoine a tha thu a’ leantainn} few {{followingCountDisplay} daoine a tha thu a’ leantainn} other {{followingCountDisplay} daoine a tha thu a’ leantainn}}.", + "domain_block_modal.you_will_lose_relationships": "Caillidh tu a h-uile luchd-leantainn ’s neach a leanas tu air an fhrithealaiche seo.", "domain_block_modal.you_wont_see_posts": "Chan fhaic thu postaichean no brathan o chleachdaichean a th’ air an fhrithealaiche seo.", "domain_pill.activitypub_lets_connect": "Leigidh e leat ceangal a dhèanamh ri daoine chan ann air Mastodon a-mhàin ach air feadh aplacaidean sòisealta eile cuideachd agus conaltradh leotha.", "domain_pill.activitypub_like_language": "Tha ActivityPub coltach ri cànan a bhruidhneas Mastodon ri lìonraidhean sòisealta eile.", @@ -330,7 +333,7 @@ "footer.about": "Mu dhèidhinn", "footer.directory": "Eòlaire nam pròifil", "footer.get_app": "Faigh an aplacaid", - "footer.invite": "Thoir cuireadh do dhaoine", + "footer.invite": "Thoir cuireadh", "footer.keyboard_shortcuts": "Ath-ghoiridean a’ mheur-chlàir", "footer.privacy_policy": "Poileasaidh prìobhaideachd", "footer.source_code": "Seall am bun-tùs", @@ -849,6 +852,11 @@ "upload_error.poll": "Chan fhaod thu faidhle a luchdadh suas an cois cunntais-bheachd.", "upload_form.audio_description": "Mìnich e dhan fheadhainn le èisteachd bheag", "upload_form.description": "Mìnich e dhan fheadhainn le cion-lèirsinne", + "upload_form.drag_and_drop.instructions": "Airson ceanglachan meadhain a thogail, brùth air space no enter. Fhad ’ a bhios tu ’ga shlaodadh, cleachd na h-iuchraichean-saighde airson an ceanglachan meadhain a ghluasad gu comhair sam bith. Brùth air space no enter a-rithist airson an ceanglachen meadhain a leigeil às air an ionad ùr aige no brùth air escape airson sgur dheth.", + "upload_form.drag_and_drop.on_drag_cancel": "Chaidh sgur dhen t-slaodadh. Chaidh an ceanglachan meadhain {item} a leigeil às.", + "upload_form.drag_and_drop.on_drag_end": "Chaidh an ceanglachan meadhain {item} a leigeil às.", + "upload_form.drag_and_drop.on_drag_over": "Chaidh an ceanglachan meadhain {item} a ghluasad.", + "upload_form.drag_and_drop.on_drag_start": "Chaidh an ceanglachan meadhain {item} a thogail.", "upload_form.edit": "Deasaich", "upload_form.thumbnail": "Atharraich an dealbhag", "upload_form.video_description": "Mìnich e dhan fheadhainn le èisteachd bheag no cion-lèirsinne", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 3bc9e1ee5..27b4ad246 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -193,7 +193,7 @@ "confirmations.reply.message": "Ao responder sobrescribirás a mensaxe que estás a compor. Tes a certeza de que queres continuar?", "confirmations.reply.title": "Editar a publicación?", "confirmations.unfollow.confirm": "Deixar de seguir", - "confirmations.unfollow.message": "Desexas deixar de seguir a {name}?", + "confirmations.unfollow.message": "Tes certeza de querer deixar de seguir a {name}?", "confirmations.unfollow.title": "Deixar de seguir á usuaria?", "content_warning.hide": "Agochar publicación", "content_warning.show": "Mostrar igualmente", @@ -516,6 +516,7 @@ "notification.label.private_reply": "Resposta privada", "notification.label.reply": "Resposta", "notification.mention": "Mención", + "notification.mentioned_you": "{name} mencionoute", "notification.moderation-warning.learn_more": "Saber máis", "notification.moderation_warning": "Recibiches unha advertencia da moderación", "notification.moderation_warning.action_delete_statuses": "Algunha das túas publicacións foron eliminadas.", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 4ce0d4162..d9b0382f4 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -1,5 +1,5 @@ { - "about.blocks": "שרתים שנחסמו על ידי המנהלים", + "about.blocks": "שרתים מוגבלים", "about.contact": "יצירת קשר:", "about.disclaimer": "מסטודון היא תוכנת קוד פתוח חינמית וסימן מסחרי של Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "הסיבה אינה זמינה", @@ -34,9 +34,9 @@ "account.follow_back": "לעקוב בחזרה", "account.followers": "עוקבים", "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.", - "account.followers_counter": "{count, plural,one {עוקב אחד} other {{count} עוקבים}}", + "account.followers_counter": "{count, plural,one {עוקב אחד} other {{counter} עוקבים}}", "account.following": "נעקבים", - "account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {count}}}", + "account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {counter}}}", "account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.", "account.go_to_profile": "מעבר לפרופיל", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", @@ -62,7 +62,7 @@ "account.requested_follow": "{name} ביקשו לעקוב אחריך", "account.share": "שתף את הפרופיל של @{name}", "account.show_reblogs": "הצג הדהודים מאת @{name}", - "account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", + "account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}", "account.unblock": "להסיר חסימה ל- @{name}", "account.unblock_domain": "הסירי את החסימה של קהילת {domain}", "account.unblock_short": "הסר חסימה", @@ -85,6 +85,7 @@ "alert.rate_limited.title": "חלה הגבלה על קצב התעבורה", "alert.unexpected.message": "אירעה שגיאה בלתי צפויה.", "alert.unexpected.title": "אופס!", + "alt_text_badge.title": "כיתוב חלופי", "announcement.announcement": "הכרזה", "attachments_list.unprocessed": "(לא מעובד)", "audio.hide": "השתק", @@ -221,6 +222,8 @@ "domain_block_modal.they_cant_follow": "משתמש משרת זה לא יכול לעקוב אחריך.", "domain_block_modal.they_wont_know": "הם לא ידעו כי נחסמו.", "domain_block_modal.title": "לחסום שרת?", + "domain_block_modal.you_will_lose_num_followers": "{followersCount, plural,one {יאבד לך עוקב אחד}other {יאבדו לך {followersCountDisplay} עוקבים}} {followingCount, plural,one {ונעקב אחד}other {ו־{followingCountDisplay} נעקבים}}.", + "domain_block_modal.you_will_lose_relationships": "יאבדו לך כל העוקבים והנעקבים משרת זה.", "domain_block_modal.you_wont_see_posts": "לא תוכלו לראות הודעות ממשתמשים על שרת זה.", "domain_pill.activitypub_lets_connect": "מאפשר לך להתחבר ולהתרועע עם אחרים לא רק במסטודון, אלא גם ביישומים חברתיים שונים אחרים.", "domain_pill.activitypub_like_language": "אקטיביטיפאב היא למעשה השפה בה מסטודון מדבר עם רשתות חברתיות אחרות.", @@ -346,9 +349,9 @@ "hashtag.column_settings.tag_mode.any": "לפחות אחד מאלה", "hashtag.column_settings.tag_mode.none": "אף אחד מאלה", "hashtag.column_settings.tag_toggle": "כלול תגיות נוספות בטור זה", - "hashtag.counter_by_accounts": "{count, plural,one{{count} משתתף.ת}other{{count} משתתפיםות}}", - "hashtag.counter_by_uses": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", - "hashtag.counter_by_uses_today": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}} היום", + "hashtag.counter_by_accounts": "{count, plural,one{{count} משתתף.ת}other{{counter} משתתפיםות}}", + "hashtag.counter_by_uses": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}", + "hashtag.counter_by_uses_today": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}} היום", "hashtag.follow": "לעקוב אחרי תגית", "hashtag.unfollow": "להפסיק לעקוב אחרי תגית", "hashtags.and_other": "…{count, plural,other {ועוד #}}", @@ -439,7 +442,7 @@ "limited_account_hint.title": "פרופיל המשתמש הזה הוסתר על ידי המנחים של {domain}.", "link_preview.author": "מאת {name}", "link_preview.more_from_author": "עוד מאת {name}", - "link_preview.shares": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", + "link_preview.shares": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}", "lists.account.add": "הוסף לרשימה", "lists.account.remove": "הסר מרשימה", "lists.delete": "מחיקת רשימה", @@ -513,6 +516,7 @@ "notification.label.private_reply": "תשובה בפרטי", "notification.label.reply": "תשובה", "notification.mention": "אזכור", + "notification.mentioned_you": "אוזכרת על ידי {name}", "notification.moderation-warning.learn_more": "למידע נוסף", "notification.moderation_warning": "קיבלת אזהרה מצוות ניהול התוכן", "notification.moderation_warning.action_delete_statuses": "חלק מהודעותיך הוסרו.", @@ -837,7 +841,7 @@ "time_remaining.minutes": "נותרו {number, plural, one {# דקה} other {# דקות}}", "time_remaining.moments": "רגעים נותרו", "time_remaining.seconds": "נותרו {number, plural, one {# שניה} other {# שניות}}", - "trends.counter_by_accounts": "{count, plural, one {אדם אחד} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}", + "trends.counter_by_accounts": "{count, plural, one {אדם אחד} other {{counter} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}", "trends.trending_now": "נושאים חמים", "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "units.short.billion": "{count} מליארד", @@ -849,6 +853,11 @@ "upload_error.poll": "לא ניתן להעלות קובץ עם סקר.", "upload_form.audio_description": "תאר/י עבור לקויי שמיעה", "upload_form.description": "תיאור לכבדי ראיה", + "upload_form.drag_and_drop.instructions": "כדי לבחור קובץ מוצמד, יש ללחוץ על מקש רווח או אנטר. בעת הגרירה, השתמשו במקשי החיצים כדי להזיז את הקובץ המוצמד בכל כיוון. לחצו רווח או אנטר בשנית כדי לעזוב את הקובץ במקומו החדש, או לחצו אסקייפ לביטול.", + "upload_form.drag_and_drop.on_drag_cancel": "הגרירה בוטלה. קובץ המדיה {item} נעזב.", + "upload_form.drag_and_drop.on_drag_end": "קובץ המדיה {item} נעזב.", + "upload_form.drag_and_drop.on_drag_over": "קובץ המדיה {item} הוזז.", + "upload_form.drag_and_drop.on_drag_start": "קובץ המדיה {item} נבחר.", "upload_form.edit": "עריכה", "upload_form.thumbnail": "שנה/י תמונה ממוזערת", "upload_form.video_description": "תאר/י עבור לקויי שמיעה ולקויי ראייה", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 34a9949af..0fee79f8b 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Adatforgalom korlátozva", "alert.unexpected.message": "Váratlan hiba történt.", "alert.unexpected.title": "Hoppá!", + "alt_text_badge.title": "Helyettesítő szöveg", "announcement.announcement": "Közlemény", "attachments_list.unprocessed": "(feldolgozatlan)", "audio.hide": "Hang elrejtése", @@ -515,6 +516,7 @@ "notification.label.private_reply": "Privát válasz", "notification.label.reply": "Válasz", "notification.mention": "Említés", + "notification.mentioned_you": "{name} megemlített", "notification.moderation-warning.learn_more": "További információ", "notification.moderation_warning": "Moderációs figyelmeztetést kaptál", "notification.moderation_warning.action_delete_statuses": "Néhány bejegyzésedet eltávolították.", @@ -851,6 +853,11 @@ "upload_error.poll": "Szavazásnál nem lehet fájlt feltölteni.", "upload_form.audio_description": "Leírás siket vagy hallássérült emberek számára", "upload_form.description": "Leírás vak vagy gyengénlátó emberek számára", + "upload_form.drag_and_drop.instructions": "Egy médiamelléklet kiválasztásához nyomj Szóközt vagy Entert. Húzás közben használd a nyílgombokat a médiamelléklet adott irányba történő mozgatásához. A médiamelléklet ú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.", + "upload_form.drag_and_drop.on_drag_cancel": "Az áthúzás megszakítva. A(z) {item} médiamelléklet el lett dobva.", + "upload_form.drag_and_drop.on_drag_end": "A(z) {item} médiamelléklet el lett dobva.", + "upload_form.drag_and_drop.on_drag_over": "A(z) {item} médiamelléklet át lett helyezve.", + "upload_form.drag_and_drop.on_drag_start": "A(z) {item} médiamelléklet fel lett véve.", "upload_form.edit": "Szerkesztés", "upload_form.thumbnail": "Bélyegkép megváltoztatása", "upload_form.video_description": "Leírás siket, hallássérült, vak vagy gyengénlátó emberek számára", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index c78c5d784..bbd3a7a35 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Með takmörkum", "alert.unexpected.message": "Upp kom óvænt villa.", "alert.unexpected.title": "Úbbs!", + "alt_text_badge.title": "Hjálpartexti mynda", "announcement.announcement": "Auglýsing", "attachments_list.unprocessed": "(óunnið)", "audio.hide": "Fela hljóð", @@ -433,6 +434,8 @@ "lightbox.close": "Loka", "lightbox.next": "Næsta", "lightbox.previous": "Fyrra", + "lightbox.zoom_in": "Renna að raunstærð", + "lightbox.zoom_out": "Renna að svo passi", "limited_account_hint.action": "Birta notandasniðið samt", "limited_account_hint.title": "Þetta notandasnið hefur verið falið af umsjónarmönnum {domain}.", "link_preview.author": "Frá {name}", @@ -511,6 +514,7 @@ "notification.label.private_reply": "Einkasvar", "notification.label.reply": "Svara", "notification.mention": "Minnst á", + "notification.mentioned_you": "{name} minntist á þig", "notification.moderation-warning.learn_more": "Kanna nánar", "notification.moderation_warning": "Þú hefur fengið aðvörun frá umsjónarmanni", "notification.moderation_warning.action_delete_statuses": "Sumar færslurnar þínar hafa verið fjarlægðar.", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index ef709516c..885be73c6 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Rispondi in privato", "notification.label.reply": "Rispondi", "notification.mention": "Menziona", + "notification.mentioned_you": "{name} ti ha menzionato", "notification.moderation-warning.learn_more": "Scopri di più", "notification.moderation_warning": "Hai ricevuto un avviso di moderazione", "notification.moderation_warning.action_delete_statuses": "Alcuni dei tuoi post sono stati rimossi.", @@ -852,6 +853,11 @@ "upload_error.poll": "Caricamento del file non consentito con i sondaggi.", "upload_form.audio_description": "Descrizione per persone con deficit uditivi", "upload_form.description": "Descrizione per ipovedenti", + "upload_form.drag_and_drop.instructions": "Per selezionare un allegato multimediale, premi Spazio o Invio. Mentre trascini, usa i tasti con le frecce per spostare l'allegato multimediale in una qualsiasi direzione. Premi di nuovo Spazio o Invio per rilasciare l'allegato multimediale nella sua nuova posizione, oppure premi Esc per annullare.", + "upload_form.drag_and_drop.on_drag_cancel": "Il trascinamento è stato annullato. L'allegato multimediale {item} è stato eliminato.", + "upload_form.drag_and_drop.on_drag_end": "L'allegato multimediale {item} è stato eliminato.", + "upload_form.drag_and_drop.on_drag_over": "L'allegato multimediale {item} è stato spostato.", + "upload_form.drag_and_drop.on_drag_start": "L'allegato multimediale {item} è stato ricevuto.", "upload_form.edit": "Modifica", "upload_form.thumbnail": "Cambia la miniatura", "upload_form.video_description": "Descrizione per persone con deficit uditivi o ipovedenti", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index f5e7cc9c0..1810314c8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "制限に達しました", "alert.unexpected.message": "不明なエラーが発生しました。", "alert.unexpected.title": "エラー!", + "alt_text_badge.title": "代替テキスト", "announcement.announcement": "お知らせ", "attachments_list.unprocessed": "(未処理)", "audio.hide": "音声を閉じる", @@ -513,6 +514,7 @@ "notification.label.private_reply": "非公開の返信", "notification.label.reply": "返信", "notification.mention": "メンション", + "notification.mentioned_you": "{name} さんがあなたに返信しました", "notification.moderation-warning.learn_more": "さらに詳しく", "notification.moderation_warning": "管理者から警告が来ています", "notification.moderation_warning.action_delete_statuses": "あなたによるいくつかの投稿が削除されました。", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 9e2c9f3af..35e9be816 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -1,5 +1,5 @@ { - "about.blocks": "Ulac agbur", + "about.blocks": "Iqeddacen yettwaɛassen", "about.contact": "Anermis:", "about.disclaimer": "Mastodon d aseɣẓan ilelli, d aseɣẓan n uɣbalu yeldin, d tnezzut n Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Ulac taɣẓint", @@ -278,6 +278,8 @@ "hashtag.counter_by_uses_today": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}} assa", "hashtag.follow": "Ḍfeṛ ahacṭag", "hashtags.and_other": "…d {count, plural, one {}other {# nniḍen}}", + "hints.threads.replies_may_be_missing": "Tiririyin d-yusan deg iqeddacen nniḍen, yezmer ur d-ddant ara.", + "hints.threads.see_more": "Wali ugar n tririt deg {domain}", "home.column_settings.show_reblogs": "Ssken-d beṭṭu", "home.column_settings.show_replies": "Ssken-d tiririyin", "home.hide_announcements": "Ffer ulɣuyen", @@ -550,6 +552,7 @@ "report_notification.attached_statuses": "{count, plural, one {{count} n tsuffeɣt} other {{count} n tsuffiɣin}} ttwaqnent", "report_notification.categories.legal": "Azerfan", "report_notification.categories.other": "Ayen nniḍen", + "report_notification.categories.other_sentence": "ayen nniḍen", "report_notification.categories.spam": "Aspam", "report_notification.categories.spam_sentence": "aspam", "report_notification.open": "Ldi aneqqis", @@ -584,11 +587,13 @@ "status.bookmark": "Creḍ", "status.cancel_reblog_private": "Sefsex beṭṭu", "status.cannot_reblog": "Tasuffeɣt-a ur tezmir ara ad tettwabḍu tikelt-nniḍen", + "status.continued_thread": "Asqerdec yettkemmil", "status.copy": "Nɣel assaɣ ɣer tasuffeɣt", "status.delete": "Kkes", "status.direct": "Bder-d @{name} weḥd-s", "status.direct_indicator": "Abdar uslig", "status.edit": "Ẓreg", + "status.edited": "Taẓrigt taneggarut {date}", "status.edited_x_times": "Tettwaẓreg {count, plural, one {{count} n tikkelt} other {{count} n tikkal}}", "status.embed": "Awi-d tangalt n weslaɣ", "status.favourite": "Amenyaf", @@ -614,9 +619,10 @@ "status.reblogs.empty": "Ula yiwen ur yebḍi tajewwiqt-agi ar tura. Ticki yebḍa-tt yiwen, ad d-iban da.", "status.redraft": "Kkes tɛiwdeḍ tira", "status.remove_bookmark": "Kkes tacreḍt", + "status.replied_in_thread": "Y·t·erra-d deg usqerdec", "status.replied_to": "Y·terra-yas i {name}", "status.reply": "Err", - "status.replyAll": "Err i lxiḍ", + "status.replyAll": "Err i wesqerdec", "status.report": "Cetki ɣef @{name}", "status.sensitive_warning": "Agbur amḥulfu", "status.share": "Bḍu", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 2054dfc0f..edcbadd12 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -317,7 +317,7 @@ "follow_suggestions.curated_suggestion": "스태프의 추천", "follow_suggestions.dismiss": "다시 보지 않기", "follow_suggestions.featured_longer": "{domain} 팀이 손수 고름", - "follow_suggestions.friends_of_friends_longer": "내가 팔로우 하는 사람들 사이에서 인기", + "follow_suggestions.friends_of_friends_longer": "내가 팔로우한 사람들 사이에서 인기", "follow_suggestions.hints.featured": "이 프로필은 {domain} 팀이 손수 선택했습니다.", "follow_suggestions.hints.friends_of_friends": "이 프로필은 내가 팔로우 하는 사람들에게서 유명합니다.", "follow_suggestions.hints.most_followed": "이 프로필은 {domain}에서 가장 많이 팔로우 된 사람들 중 하나입니다.", @@ -516,6 +516,7 @@ "notification.label.private_reply": "개인 답글", "notification.label.reply": "답글", "notification.mention": "멘션", + "notification.mentioned_you": "{name} 님의 멘션", "notification.moderation-warning.learn_more": "더 알아보기", "notification.moderation_warning": "중재 경고를 받았습니다", "notification.moderation_warning.action_delete_statuses": "게시물 몇 개가 삭제되었습니다.", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 1c31574de..c0ca8f176 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -504,22 +504,23 @@ "notification.admin.report_statuses": "{name} rapporteerde {target} voor {category}", "notification.admin.report_statuses_other": "{name} rapporteerde {target}", "notification.admin.sign_up": "{name} heeft zich geregistreerd", - "notification.admin.sign_up.name_and_others": "{name} en {count, plural, one {# ander} other {# anderen}} hebben zich geregistreerd", + "notification.admin.sign_up.name_and_others": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben zich geregistreerd", "notification.favourite": "{name} markeerde jouw bericht als favoriet", - "notification.favourite.name_and_others_with_link": "{name} en {count, plural, one {# ander} other {# anderen}} hebben jouw bericht als favoriet gemarkeerd", + "notification.favourite.name_and_others_with_link": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben jouw bericht als favoriet gemarkeerd", "notification.follow": "{name} volgt jou nu", - "notification.follow.name_and_others": "{name} en {count, plural, one {# ander} other {# anderen}} hebben je gevolgd", + "notification.follow.name_and_others": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben je gevolgd", "notification.follow_request": "{name} wil jou graag volgen", - "notification.follow_request.name_and_others": "{name} en {count, plural, one {# ander} other {# anderen}} hebben gevraagd om je te volgen", + "notification.follow_request.name_and_others": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben gevraagd om je te volgen", "notification.label.mention": "Vermelding", "notification.label.private_mention": "Privébericht", "notification.label.private_reply": "Privéreactie", "notification.label.reply": "Reactie", "notification.mention": "Vermelding", + "notification.mentioned_you": "Je bent vermeld door {name}", "notification.moderation-warning.learn_more": "Meer informatie", "notification.moderation_warning": "Je hebt een moderatie-waarschuwing ontvangen", "notification.moderation_warning.action_delete_statuses": "Sommige van je berichten zijn verwijderd.", - "notification.moderation_warning.action_disable": "Je account is uitgeschakeld.", + "notification.moderation_warning.action_disable": "Jouw account is uitgeschakeld.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "Sommige van je berichten zijn gemarkeerd als gevoelig.", "notification.moderation_warning.action_none": "Jouw account heeft een moderatie-waarschuwing ontvangen.", "notification.moderation_warning.action_sensitive": "Je berichten worden vanaf nu als gevoelig gemarkeerd.", @@ -528,14 +529,14 @@ "notification.own_poll": "Jouw peiling is beëindigd", "notification.poll": "Een peiling waaraan jij hebt meegedaan is beëindigd", "notification.reblog": "{name} boostte jouw bericht", - "notification.reblog.name_and_others_with_link": "{name} en {count, plural, one {# ander} other {# anderen}} hebben jouw bericht geboost", + "notification.reblog.name_and_others_with_link": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben jouw bericht geboost", "notification.relationships_severance_event": "Verloren verbindingen met {name}", "notification.relationships_severance_event.account_suspension": "Een beheerder van {from} heeft {target} geschorst, wat betekent dat je geen updates meer van hen kunt ontvangen of met hen kunt communiceren.", - "notification.relationships_severance_event.domain_block": "Een beheerder van {from} heeft {target} geblokkeerd, inclusief {followersCount} van jouw volgers en {followingCount, plural, one {# account} other {# accounts}} die jij volgt.", + "notification.relationships_severance_event.domain_block": "Een beheerder van {from} heeft {target} geblokkeerd, inclusief {followersCount} van jouw volgers en {followingCount, plural, one {# account} other {# accounts}} die je volgt.", "notification.relationships_severance_event.learn_more": "Meer informatie", - "notification.relationships_severance_event.user_domain_block": "Je hebt {target} geblokkeerd, waarmee je {followersCount} van je volgers en {followingCount, plural, one {# account} other {# accounts}} die jij volgt, bent verloren.", + "notification.relationships_severance_event.user_domain_block": "Je hebt {target} geblokkeerd, waarmee je {followersCount} van je volgers en {followingCount, plural, one {# account} other {# accounts}} die je volgt, bent verloren.", "notification.status": "{name} heeft zojuist een bericht geplaatst", - "notification.update": "{name} heeft een bericht bewerkt", + "notification.update": "{name} bewerkte een bericht", "notification_requests.accept": "Accepteren", "notification_requests.accept_multiple": "{count, plural, one {# verzoek accepteren…} other {# verzoeken accepteren…}}", "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Verzoek accepteren} other {Verzoeken accepteren}}", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 69883a549..58ed018ba 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Privat svar", "notification.label.reply": "Svar", "notification.mention": "Omtale", + "notification.mentioned_you": "{name} nemnde deg", "notification.moderation-warning.learn_more": "Lær meir", "notification.moderation_warning": "Du har mottatt ei moderasjonsåtvaring", "notification.moderation_warning.action_delete_statuses": "Nokre av innlegga dine har blitt fjerna.", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 8cd3387ea..1dd5df32b 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -502,6 +502,8 @@ "notification.reblog": "{name} fremhevet ditt innlegg", "notification.status": "{name} la nettopp ut", "notification.update": "{name} redigerte et innlegg", + "notification_requests.minimize_banner": "Minimer banneret for filtrerte varsler", + "notification_requests.view": "Vis varsler", "notifications.clear": "Fjern varsler", "notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler permanent?", "notifications.column_settings.admin.report": "Nye rapporter:", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 4dc74db8c..c6555ebb7 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Odpowiedź prywatna", "notification.label.reply": "Odpowiedź", "notification.mention": "Wzmianka", + "notification.mentioned_you": "{name} wspomniał(a) o Tobie", "notification.moderation-warning.learn_more": "Dowiedz się więcej", "notification.moderation_warning": "Otrzymałeś/-łaś ostrzeżenie moderacyjne", "notification.moderation_warning.action_delete_statuses": "Niektóre twoje wpisy zostały usunięte.", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index d1a3ecdaa..b6d2fe112 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -437,6 +437,7 @@ "lightbox.next": "Próximo", "lightbox.previous": "Anterior", "lightbox.zoom_in": "Voltar para o tamanho real", + "lightbox.zoom_out": "Zoom para ajustar", "limited_account_hint.action": "Exibir perfil mesmo assim", "limited_account_hint.title": "Este perfil foi ocultado pelos moderadores do {domain}.", "link_preview.author": "Por {name}", @@ -515,6 +516,7 @@ "notification.label.private_reply": "Resposta privada", "notification.label.reply": "Resposta", "notification.mention": "Menção", + "notification.mentioned_you": "{name} te mencionou", "notification.moderation-warning.learn_more": "Aprender mais", "notification.moderation_warning": "Você recebeu um aviso de moderação", "notification.moderation_warning.action_delete_statuses": "Algumas das suas publicações foram removidas.", @@ -813,7 +815,7 @@ "status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.", "status.redraft": "Excluir e rascunhar", "status.remove_bookmark": "Remover do Salvos", - "status.replied_in_thread": "Respondido na discussão", + "status.replied_in_thread": "Respondido na conversa", "status.replied_to": "Em resposta a {name}", "status.reply": "Responder", "status.replyAll": "Responder a conversa", @@ -851,6 +853,11 @@ "upload_error.poll": "Mídias não podem ser anexadas em toots com enquetes.", "upload_form.audio_description": "Descrever para deficientes auditivos", "upload_form.description": "Descrever para deficientes visuais", + "upload_form.drag_and_drop.instructions": "Para pegar um anexo de mídia, pressione espaço ou enter. Enquanto arrastar, use as setas do teclado para mover o anexo de mídia em qualquer direção. Pressione espaço ou insira novamente para soltar o anexo de mídia em sua nova posição, ou pressione escape para cancelar.", + "upload_form.drag_and_drop.on_drag_cancel": "O arrastamento foi cancelado. O anexo da mídia {item} foi descartado.", + "upload_form.drag_and_drop.on_drag_end": "O anexo {item} foi removido.", + "upload_form.drag_and_drop.on_drag_over": "O anexo de mídia {item} foi movido.", + "upload_form.drag_and_drop.on_drag_start": "Foi coletado o anexo de mídia {item}.", "upload_form.edit": "Editar", "upload_form.thumbnail": "Alterar miniatura", "upload_form.video_description": "Descrever para deficientes auditivos ou visuais", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index bb7d062b9..227a7483a 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Màssimu de rechestas barigadu", "alert.unexpected.message": "Ddoe est istada una faddina.", "alert.unexpected.title": "Oh!", + "alt_text_badge.title": "Testu alternativu", "announcement.announcement": "Annùntziu", "attachments_list.unprocessed": "(non protzessadu)", "audio.hide": "Cua s'àudio", @@ -97,6 +98,8 @@ "block_modal.title": "Boles blocare s'utente?", "block_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent custa persone.", "boost_modal.combo": "Podes incarcare {combo} pro brincare custu sa borta chi benit", + "boost_modal.reblog": "Boles potentziare sa publicatzione?", + "boost_modal.undo_reblog": "Boles tzessare de potentziare sa publicatzione?", "bundle_column_error.copy_stacktrace": "Còpia s'informe de faddina", "bundle_column_error.error.body": "Sa pàgina pedida non faghiat a dda renderizare. Diat pòdere èssere pro neghe de una faddina in su còdighe nostru, o de unu problema de cumpatibilidade de su navigadore.", "bundle_column_error.error.title": "Oh, no!", @@ -104,11 +107,15 @@ "bundle_column_error.network.title": "Faddina de connessione", "bundle_column_error.retry": "Torra·bi a proare", "bundle_column_error.return": "Torra a sa pàgina printzipale", + "bundle_column_error.routing.body": "Impossìbile agatare sa pàgina rechesta. Seguru chi s'URL in sa barra de indiritzos est curretu?", "bundle_column_error.routing.title": "404", "bundle_modal_error.close": "Serra", "bundle_modal_error.message": "Faddina in su carrigamentu de custu cumponente.", "bundle_modal_error.retry": "Torra·bi a proare", + "closed_registrations.other_server_instructions": "Dae chi Mastodon est detzentralizadu, podes creare unu contu in un'àteru serbidore e interagire cun custu.", + "closed_registrations_modal.description": "Sa creatzione de contos in {domain} no est possìbile in custu momentu, però tene in cunsideru chi non tenes bisòngiu de unu contu ispetzìficu in {domain} pro impreare Mastodon.", "closed_registrations_modal.find_another_server": "Agata un'àteru serbidore", + "closed_registrations_modal.title": "Su registru a Mastodon", "column.about": "Informatziones", "column.blocks": "Persones blocadas", "column.bookmarks": "Sinnalibros", @@ -142,6 +149,7 @@ "compose.published.open": "Aberi", "compose.saved.body": "Publicatzione sarvada.", "compose_form.direct_message_warning_learn_more": "Àteras informatziones", + "compose_form.encryption_warning": "Is publicatziones a Mastodon no sunt critografados a nodu terminale. Non cumpartzas informatziones delicadas in Mastodon.", "compose_form.hashtag_warning": "Custa publicatzione no at a èssere ammustrada in peruna eticheta, dae chi no est pùblica. Isceti is publicatziones pùblicas podent èssere chircadas cun etichetas.", "compose_form.lock_disclaimer": "Su contu tuo no est {locked}. Cale si siat persone ti podet sighire pro bìdere is messàgios tuos chi imbies a sa gente chi ti sighit.", "compose_form.lock_disclaimer.lock": "blocadu", @@ -171,15 +179,23 @@ "confirmations.discard_edit_media.confirm": "Iscarta", "confirmations.discard_edit_media.message": "Tenes modìficas non sarvadas a is descritziones o a is anteprimas de is cuntenutos, ddas boles iscartare su matessi?", "confirmations.edit.confirm": "Modìfica", + "confirmations.edit.message": "Modifichende immoe as a subrascrìere su messàgiu chi ses iscriende. Seguru chi boles sighire?", + "confirmations.edit.title": "Boles subraiscrìere sa publicatzione?", "confirmations.logout.confirm": "Essi·nche", "confirmations.logout.message": "Seguru chi boles essire?", + "confirmations.logout.title": "Boles serrare sa sessione?", "confirmations.mute.confirm": "A sa muda", "confirmations.redraft.confirm": "Cantzella e torra a fàghere", "confirmations.redraft.message": "Seguru chi boles cantzellare e torrare a fàghere custa publicatzione? As a pèrdere is preferidos e is cumpartziduras, e is rispostas a su messàgiu originale ant a abarrare òrfanas.", + "confirmations.redraft.title": "Boles cantzellare e torrare a iscrìere sa publicatzione?", "confirmations.reply.confirm": "Risponde", "confirmations.reply.message": "Rispondende immoe as a subrascrìere su messàgiu chi ses iscriende. Seguru chi boles sighire?", + "confirmations.reply.title": "Boles subraiscrìere sa publicatzione?", "confirmations.unfollow.confirm": "Non sigas prus", "confirmations.unfollow.message": "Seguru chi non boles sighire prus a {name}?", + "confirmations.unfollow.title": "Boles tzessare de sighire s'utente?", + "content_warning.hide": "Cua sa publicatzione", + "content_warning.show": "Ammustra·dda su pròpiu", "conversation.delete": "Cantzella arresonada", "conversation.mark_as_read": "Signala comente lèghidu", "conversation.open": "Ammustra arresonada", @@ -193,7 +209,10 @@ "directory.recently_active": "Cun atividade dae pagu", "disabled_account_banner.account_settings": "Cunfiguratziones de su contu", "disabled_account_banner.text": "Su contu tuo {disabledAccount} no est ativu.", + "dismissable_banner.community_timeline": "Custas sunt is publicatziones pùblicas prus reghentes dae gente cun contu in {domain}.", "dismissable_banner.dismiss": "Iscarta", + "dismissable_banner.explore_links": "Custas sunt is istòrias de noas prus cumpartzidas in sa rete oe. Is istòrias prus noas publicadas dae gente prus diversa ant a èssere priorizadas.", + "dismissable_banner.explore_statuses": "Custas sunt publicatziones dae sa rete detzentralizada chi sunt retzende atentzione oe. Is publicatziones prus noas cun prus cumpartziduras e preferèntzias ant a èssere priorizadas.", "domain_block_modal.block": "Bloca su serbidore", "domain_block_modal.block_account_instead": "Bloca imbetzes a @{name}", "domain_block_modal.they_can_interact_with_old_posts": "Is persones de custu serbidore podent ancora interagire cun is publicatziones betzas tuas.", @@ -207,6 +226,7 @@ "domain_pill.their_handle": "S'identificadore suo:", "domain_pill.their_server": "Sa domo digitale sua, in ue istant totu is publicatziones suas.", "domain_pill.username": "Nòmine de utente", + "domain_pill.whats_in_a_handle": "Ite est un'identificadore?", "domain_pill.your_handle": "S'identificadore tuo:", "domain_pill.your_server": "Sa domo digitale tua, in ue istant totu is publicatziones tuas. Custa non t'agradat? Tràmuda serbidore in cale si siat momentu e bati·ti fintzas in fatu is sighidores tuos.", "domain_pill.your_username": "S'identificadore ùnicu tuo in custu serbidore. Si podent agatare utentes cun su matessi nòmine de utente in àteros serbidores.", @@ -254,6 +274,7 @@ "explore.trending_links": "Noas", "explore.trending_statuses": "Publicatziones", "explore.trending_tags": "Etichetas", + "filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.", "filter_modal.added.expired_title": "Filtru iscadidu.", "filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru", "filter_modal.added.settings_link": "pàgina de cunfiguratzione", @@ -277,7 +298,13 @@ "follow_suggestions.featured_longer": "Seberadu a manu dae s'iscuadra de {domain}", "follow_suggestions.friends_of_friends_longer": "Populare intre persones chi sighis", "follow_suggestions.hints.featured": "Custu profilu est istadu seberadu a manu dae s'iscuadra {domain}.", + "follow_suggestions.personalized_suggestion": "Cussìgiu personalizadu", + "follow_suggestions.popular_suggestion": "Cussìgiu populare", + "follow_suggestions.popular_suggestion_longer": "Populare a {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Profilos sìmiles a is chi as sighidu de reghente", "follow_suggestions.view_all": "Ammustra totu", + "follow_suggestions.who_to_follow": "Chie sighire", + "followed_tags": "Etichetas sighidas", "footer.about": "Informatziones", "footer.directory": "Diretòriu de profilos", "footer.get_app": "Otene s'aplicatzione", @@ -302,6 +329,11 @@ "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe", "hashtag.follow": "Sighi su hashtag", "hashtag.unfollow": "Non sigas prus s'eticheta", + "hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}", + "hints.profiles.posts_may_be_missing": "Podet èssere chi ammanchent tzertas publicatziones de custu profilu.", + "hints.profiles.see_more_posts": "Bide prus publicatziones a {domain}", + "hints.threads.replies_may_be_missing": "Podet èssere chi ammanchent rispostas dae àteros serbidores.", + "hints.threads.see_more": "Bide prus rispostas a {domain}", "home.column_settings.show_reblogs": "Ammustra is cumpartziduras", "home.column_settings.show_replies": "Ammustra rispostas", "home.hide_announcements": "Cua annùntzios", @@ -309,7 +341,14 @@ "home.pending_critical_update.link": "Ammustra is atualizatziones", "home.pending_critical_update.title": "Atualizatzione de seguresa crìtica a disponimentu.", "home.show_announcements": "Ammustra annùntzios", + "ignore_notifications_modal.disclaimer": "Mastodon non podet informare is utentes chi as innioradu is notìficas issoro. Inniorare notìficas no at a evitare chi s'imbient is messàgios.", + "ignore_notifications_modal.filter_instead": "Opuru filtra", + "ignore_notifications_modal.filter_to_act_users": "As a pòdere ancora atzetare, refudare o sinnalare a utentes", + "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrare agiudat a evitare possìbiles confusiones", "interaction_modal.description.reply": "Podes rispòndere a custa publicatzione cun unu contu de Mastodon.", + "interaction_modal.login.action": "Torra a sa pàgina printzipale", + "interaction_modal.login.prompt": "Su domìniu de su serbidore domèsticu tuo, pro esempru mastodon.social", + "interaction_modal.no_account_yet": "Non ses in Mastodon?", "interaction_modal.on_this_server": "In custu serbidore", "interaction_modal.title.follow": "Sighi a {name}", "interaction_modal.title.reply": "Risponde a sa publicatzione de {name}", @@ -353,11 +392,13 @@ "lightbox.next": "Imbeniente", "lightbox.previous": "Pretzedente", "limited_account_hint.title": "Custu profilu est istadu cuadu dae sa moderatzione de {domain}.", + "link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "lists.account.add": "Agiunghe a sa lista", "lists.account.remove": "Boga dae sa lista", "lists.delete": "Cantzella sa lista", "lists.edit": "Modìfica sa lista", "lists.edit.submit": "Muda su tìtulu", + "lists.exclusive": "Cua custas publicatziones dae sa pàgina printzipale", "lists.new.create": "Agiunghe lista", "lists.new.title_placeholder": "Tìtulu de sa lista noa", "lists.replies_policy.followed": "Cale si siat persone chi sighis", @@ -368,7 +409,20 @@ "lists.subheading": "Is listas tuas", "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}", "loading_indicator.label": "Carrighende…", + "media_gallery.hide": "Cua", + "moved_to_account_banner.text": "Su contu tuo {disabledAccount} est disativadu in custu momentu ca est istadu tramudadu a {movedToAccount}.", + "mute_modal.hide_from_notifications": "Cua dae is notìficas", + "mute_modal.hide_options": "Cua is optziones", + "mute_modal.indefinite": "Fintzas a cando no apo a torrare a ativare is notìficas", + "mute_modal.show_options": "Ammustra is optziones", + "mute_modal.they_can_mention_and_follow": "Ti podent mentovare e sighire, però no ddos as a bìdere.", + "mute_modal.they_wont_know": "No ant a ischire chi ddos as postu a sa muda.", + "mute_modal.title": "Boles pònnere a custu contu a sa muda?", + "mute_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent a custa persone.", + "mute_modal.you_wont_see_posts": "At a pòdere bìdere is publicatziones tuas, però tue no as a bìdere cussas suas.", "navigation_bar.about": "Informatziones", + "navigation_bar.administration": "Amministratzione", + "navigation_bar.advanced_interface": "Aberi s'interfache web avantzada", "navigation_bar.blocks": "Persones blocadas", "navigation_bar.bookmarks": "Sinnalibros", "navigation_bar.community_timeline": "Lìnia de tempus locale", @@ -380,10 +434,13 @@ "navigation_bar.favourites": "Preferidos", "navigation_bar.filters": "Faeddos a sa muda", "navigation_bar.follow_requests": "Rechestas de sighidura", + "navigation_bar.followed_tags": "Etichetas sighidas", "navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras", "navigation_bar.lists": "Listas", "navigation_bar.logout": "Essi", + "navigation_bar.moderation": "Moderatzione", "navigation_bar.mutes": "Persones a sa muda", + "navigation_bar.opened_in_classic_interface": "Publicatziones, contos e àteras pàginas ispetzìficas sunt abertas in manera predefinida in s'interfache web clàssica.", "navigation_bar.personal": "Informatziones personales", "navigation_bar.pins": "Publicatziones apicadas", "navigation_bar.preferences": "Preferèntzias", @@ -391,10 +448,24 @@ "navigation_bar.search": "Chirca", "navigation_bar.security": "Seguresa", "not_signed_in_indicator.not_signed_in": "Ti depes identificare pro atzèdere a custa resursa.", + "notification.admin.report": "{name} at sinnaladu a {target}", + "notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}", + "notification.admin.report_account_other": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target}", + "notification.admin.report_statuses": "{name} at sinnaladu a {target} pro {category}", + "notification.admin.report_statuses_other": "{name} at sinnaladu a {target}", "notification.admin.sign_up": "{name} at aderidu", + "notification.admin.sign_up.name_and_others": "{name} e {count, plural, one {un'àtera persone} other {àteras # persones}} si sunt registradas", "notification.favourite": "{name} at marcadu comente a preferidu s'istadu tuo", + "notification.favourite.name_and_others_with_link": "{name} e {count, plural, one {un'àtera persone} other {àteras # persones}} ant marcadu sa publicatzione tua comente preferida", "notification.follow": "{name} ti sighit", + "notification.follow.name_and_others": "{name} e {count, plural, one {un'àtera persone} other {àteras # persones}} ti sighint", "notification.follow_request": "{name} at dimandadu de ti sighire", + "notification.follow_request.name_and_others": "{name} e {count, plural, one {un'àtera persone} other {àteras # persones}} ant pedidu de ti sighire", + "notification.label.mention": "Mèntovu", + "notification.label.private_mention": "Mèntovu privadu", + "notification.label.private_reply": "Risposta privada", + "notification.label.reply": "Risposta", + "notification.mention": "Mèntovu", "notification.moderation-warning.learn_more": "Àteras informatziones", "notification.moderation_warning": "T'ant imbiadu un'avisu de moderatzione", "notification.moderation_warning.action_delete_statuses": "Unas cantas de is publicatziones tuas sunt istadas cantzelladas.", @@ -407,12 +478,30 @@ "notification.own_poll": "Sondàgiu acabbadu", "notification.poll": "Unu sondàgiu in su chi as votadu est acabbadu", "notification.reblog": "{name} at cumpartzidu sa publicatzione tua", + "notification.reblog.name_and_others_with_link": "{name} e {count, plural, one {un'àtera persone} other {àteras # persones}} ant potentziadu sa publicatzione tua", "notification.relationships_severance_event": "Connessiones pèrdidas cun {name}", + "notification.relationships_severance_event.account_suspension": "S'amministratzione de {from} at suspèndidu a {target}; custu bolet nàrrere chi non podes prus retzire atualizatziones dae in cue o interagire cun cussos contos.", + "notification.relationships_severance_event.domain_block": "S'amministratzione de {from} at blocadu a {target}, incluende {followersCount} sighiduras tuas e {followingCount, plural, one {un'àteru contu} other {àteros # contos}} chi sighis.", "notification.relationships_severance_event.learn_more": "Àteras informatziones", + "notification.relationships_severance_event.user_domain_block": "As blocadu a {target}, bogadu a {followersCount} contos chi ti sighint e {followingCount, plural, one {un'àteru contu} other {àteros # contos}} chi sighis.", "notification.status": "{name} at publicadu cosa", "notification.update": "{name} at modificadu una publicatzione", "notification_requests.accept": "Atzeta", + "notification_requests.accept_multiple": "{count, plural, one {Atzeta una rechesta…} other {Atzeta # rechestas…}}", + "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Atzeta sa rechesta} other {Atzeta is rechestas}}", + "notification_requests.confirm_accept_multiple.message": "Ses atzetende {count, plural, one {una rechesta de notìfica} other {# rechestas de notìfica}}. Seguru chi boles sighire?", + "notification_requests.confirm_accept_multiple.title": "Boles atzetare is rechestas de notìfica?", + "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Iscarta sa rechesta} other {Iscarta is rechestas}}", + "notification_requests.confirm_dismiss_multiple.message": "Ses acanta de iscartare {count, plural, one {una rechesta de notìfica} other {# rechestas de notìfica}}. No nche {count, plural, one {} other {}} as a pòdere prus atzèdere in manera sèmplitze. Seguru chi boles sighire?", + "notification_requests.confirm_dismiss_multiple.title": "Boles iscartare is rechestas de notìfica?", "notification_requests.dismiss": "Iscarta", + "notification_requests.dismiss_multiple": "{count, plural, one {Iscarta una rechesta…} other {Iscarta # rechestas…}}", + "notification_requests.edit_selection": "Modifica", + "notification_requests.exit_selection": "Fatu", + "notification_requests.explainer_for_limited_account": "Is notìficas de custu contu sunt istadas filtradas, ca su contu est istadu limitadu dae sa moderatzione.", + "notification_requests.explainer_for_limited_remote_account": "Is notìficas de custu contu sunt istadas filtradas, ca su contu o su serbidore suo est istadu limitadu dae sa moderatzione.", + "notification_requests.maximize": "Ismànnia", + "notification_requests.minimize_banner": "Mìnima su bànner de notìficas filtradas", "notification_requests.notifications_from": "Notìficas dae {name}", "notification_requests.title": "Notìficas filtradas", "notifications.clear": "Lìmpia notìficas", @@ -472,6 +561,11 @@ "poll_button.add_poll": "Agiunghe unu sondàgiu", "poll_button.remove_poll": "Cantzella su sondàgiu", "privacy.change": "Modìfica s'istadu de riservadesa", + "privacy.direct.long": "Totu is utentes mentovados in sa publicatzione", + "privacy.direct.short": "Persones ispetzìficas", + "privacy.private.long": "Isceti chie ti sighit", + "privacy.private.short": "Sighiduras", + "privacy.public.long": "Cale si siat persone a intro o a foras de Mastodon", "privacy.public.short": "Pùblicu", "privacy_policy.last_updated": "Ùrtima atualizatzione: {date}", "privacy_policy.title": "Polìtica de riservadesa", @@ -497,34 +591,64 @@ "report.categories.legal": "Giurìdicu", "report.categories.other": "Àteru", "report.categories.spam": "Àliga", + "report.category.subtitle": "Sèbera sa currispondèntzia prus arta", + "report.category.title": "Nara·nos ite sutzedet cun custu {type}", "report.category.title_account": "profilu", "report.category.title_status": "publicatzione", "report.close": "Fatu", + "report.comment.title": "Nch'at àteru chi depamus ischire?", "report.forward": "Torra a imbiare a {target}", "report.forward_hint": "Custu contu est de un'àteru serbidore. Ddi boles imbiare puru una còpia anònima de custu informe?", "report.mute": "A sa muda", + "report.mute_explanation": "No as a bìdere is publicatziones suas. Ti podet ancora sighire e bìdere is publicatziones, ma no at a ischire chi dd'as postu a sa muda.", "report.next": "Imbeniente", "report.placeholder": "Cummentos additzionales", "report.reasons.dislike": "Non mi praghet", "report.reasons.dislike_description": "Est una cosa chi non boles bìdere", "report.reasons.legal": "Illegale", "report.reasons.other": "Un'àtera cosa", + "report.reasons.other_description": "Su problema non currispondet a is àteras categorias", "report.reasons.spam": "Est àliga", + "report.rules.subtitle": "Seletziona totu is chi àplichent", + "report.statuses.subtitle": "Seletziona totu is chi àplichent", "report.submit": "Imbia", "report.target": "Informende de {target}", + "report.thanks.title": "Non boles bìdere custu?", + "report.thanks.title_actionable": "Gràtzias de sa sinnalatzione, dd'amus a averiguare.", "report.unfollow": "Non sigas prus a @{name}", + "report.unfollow_explanation": "Ses sighende custu contu. Si non boles bìdere is publicatziones suas in sa pàgina printzipale tua, no ddu sigas prus.", "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", "report_notification.categories.legal": "Giurìdicu", "report_notification.categories.legal_sentence": "cuntenutu illegale", "report_notification.categories.other": "Àteru", "report_notification.categories.other_sentence": "àteru", "report_notification.categories.spam": "Àliga", + "report_notification.categories.spam_sentence": "àliga", + "report_notification.open": "Aberi una sinnalatzione", + "search.no_recent_searches": "Nissuna chirca reghente", "search.placeholder": "Chirca", + "search.quick_action.account_search": "Profilos chi currispondent cun {x}", + "search.quick_action.go_to_account": "Bae a su profilu {x}", + "search.quick_action.go_to_hashtag": "Bae a s'eticheta {x}", + "search.quick_action.open_url": "Aberi s'URL in Mastodon", + "search.quick_action.status_search": "Publicatziones chi currispondent cun {x}", + "search.search_or_paste": "Chirca o incolla un'URL", + "search_popout.full_text_search_disabled_message": "No a disponimentu a {domain}.", + "search_popout.full_text_search_logged_out_message": "Isceti a disponimentu cun sa sessione aberta.", + "search_popout.language_code": "Còdighe de limba ISO", + "search_popout.options": "Optziones de chirca", + "search_popout.quick_actions": "Atziones lestras", + "search_popout.recent": "Chircas reghentes", + "search_popout.specific_date": "data ispetzìfica", "search_popout.user": "utente", "search_results.accounts": "Profilos", "search_results.all": "Totus", "search_results.hashtags": "Etichetas", + "search_results.nothing_found": "Impossìbile agatare currispondèntzias pro custos tèrmines de chirca", + "search_results.see_all": "Bide totu", "search_results.statuses": "Publicatziones", + "search_results.title": "Chirca {q}", + "server_banner.about_active_users": "Gente chi at impreadu custu serbidore is ùrtimas 30 dies (Utentes cun Atividade a su Mese)", "server_banner.active_users": "utentes ativos", "server_banner.administered_by": "Amministradu dae:", "server_banner.server_stats": "Istatìsticas de su serbidore:", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index ddf341584..da3b1eaef 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -33,7 +33,7 @@ "account.follow": "Sledovať", "account.follow_back": "Sledovať späť", "account.followers": "Sledovatelia", - "account.followers.empty": "Tento účet ešte nikto nesleduje.", + "account.followers.empty": "Ešte nikto nesleduje tohto užívateľa.", "account.followers_counter": "{count, plural, one {{counter} sledujúci} other {{counter} sledujúci}}", "account.following": "Sledovaný účet", "account.following_counter": "{count, plural, one {{counter} sledovaných} other {{counter} sledovaných}}", @@ -85,6 +85,7 @@ "alert.rate_limited.title": "Priveľa žiadostí", "alert.unexpected.message": "Vyskytla sa nečakaná chyba.", "alert.unexpected.title": "Ups!", + "alt_text_badge.title": "Alternatívny popis", "announcement.announcement": "Oznámenie", "attachments_list.unprocessed": "(nespracované)", "audio.hide": "Skryť zvuk", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 084ac9945..182f01887 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Përgjigje private", "notification.label.reply": "Përgjigje", "notification.mention": "Përmendje", + "notification.mentioned_you": "{name} ju ka përmendur", "notification.moderation-warning.learn_more": "Mësoni më tepër", "notification.moderation_warning": "Ju është dhënë një sinjalizim moderimi", "notification.moderation_warning.action_delete_statuses": "Disa nga postimet tuaja janë hequr.", @@ -852,6 +853,11 @@ "upload_error.poll": "Me pyetësorët s’lejohet ngarkim kartelash.", "upload_form.audio_description": "Përshkruajeni për persona me dëgjim të kufizuar", "upload_form.description": "Përshkruajeni për persona me probleme shikimi", + "upload_form.drag_and_drop.instructions": "Që të merrni një bashkëngjitje media, shtypni tastin Space ose Enter. Teksa bëhet tërheqje, përdorni tastet shigjetë për ta shpënë bashkëngjitjen media në cilëndo drejtori që doni. Shtypni sërish Space ose Enter që të lihet bashkëngjitja media në pozicionin e vet të ri, ose shtypni Esc, që të anulohet veprimi.", + "upload_form.drag_and_drop.on_drag_cancel": "Tërheqja u anulua. Bashkëngjitja media {item} u la.", + "upload_form.drag_and_drop.on_drag_end": "Bashkëngjitja media {item} u la.", + "upload_form.drag_and_drop.on_drag_over": "Bashkëngjitja media {item} u lëviz.", + "upload_form.drag_and_drop.on_drag_start": "U mor bashkëngjitja media {item}.", "upload_form.edit": "Përpunoni", "upload_form.thumbnail": "Ndryshoni miniaturën", "upload_form.video_description": "Përshkruajeni për persona me dëgjim të kufizuar ose probleme shikimi", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index b1e5a95ed..298852100 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -506,6 +506,7 @@ "notification.label.private_reply": "Privata svar", "notification.label.reply": "Svar", "notification.mention": "Nämn", + "notification.mentioned_you": "{name} nämnde dig", "notification.moderation-warning.learn_more": "Läs mer", "notification.moderation_warning": "Du har fått en moderationsvarning", "notification.moderation_warning.action_delete_statuses": "Några av dina inlägg har tagits bort.", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 160b6ec4e..88c48bfb4 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -512,6 +512,7 @@ "notification.label.private_reply": "การตอบกลับแบบส่วนตัว", "notification.label.reply": "การตอบกลับ", "notification.mention": "การกล่าวถึง", + "notification.mentioned_you": "{name} ได้กล่าวถึงคุณ", "notification.moderation-warning.learn_more": "เรียนรู้เพิ่มเติม", "notification.moderation_warning": "คุณได้รับคำเตือนการกลั่นกรอง", "notification.moderation_warning.action_delete_statuses": "เอาโพสต์บางส่วนของคุณออกแล้ว", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 6feae5d13..937b3e8e1 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -124,7 +124,7 @@ "column.direct": "Özel değinmeler", "column.directory": "Profillere göz at", "column.domain_blocks": "Engellenen alan adları", - "column.favourites": "Favorilerin", + "column.favourites": "Gözdelerin", "column.firehose": "Anlık Akışlar", "column.follow_requests": "Takip istekleri", "column.home": "Anasayfa", @@ -516,6 +516,7 @@ "notification.label.private_reply": "Özel yanıt", "notification.label.reply": "Yanıt", "notification.mention": "Bahsetme", + "notification.mentioned_you": "{name} sizden söz etti", "notification.moderation-warning.learn_more": "Daha fazlası", "notification.moderation_warning": "Hesabınız bir denetim uyarısı aldı", "notification.moderation_warning.action_delete_statuses": "Bazı gönderileriniz kaldırıldı.", @@ -812,7 +813,7 @@ "status.reblogged_by": "{name} yeniden paylaştı", "status.reblogs": "{count, plural, one {yeniden paylaşım} other {yeniden paylaşım}}", "status.reblogs.empty": "Henüz hiç kimse bu gönderiyi yeniden paylaşmadı. Herhangi bir kullanıcı yeniden paylaştığında burada görüntülenecek.", - "status.redraft": "Sil,Düzenle ve Yeniden paylaş", + "status.redraft": "Sil,Düzenle ve yeniden-paylaş", "status.remove_bookmark": "Yer işaretini kaldır", "status.replied_in_thread": "Akışta yanıtlandı", "status.replied_to": "{name} kullanıcısına yanıt verdi", @@ -852,6 +853,11 @@ "upload_error.poll": "Anketlerde dosya yüklemesine izin verilmez.", "upload_form.audio_description": "İşitme kaybı olan kişiler için yazı ekleyiniz", "upload_form.description": "Görme engelliler için açıklama", + "upload_form.drag_and_drop.instructions": "Bir medya eklentisini taşımak için, boşluk veya enter tuşuna basın. Sürükleme sırasında medya eklentisini herhangi bir yöne hareket ettirmek için ok tuşlarını kullanın. Medya eklentisini yeni konumuna bırakmak için tekrar boşluk veya enter tuşuna basın veya işlemi iptal etmek için escape tuşuna basın.", + "upload_form.drag_and_drop.on_drag_cancel": "Sürükleme iptal edildi. Medya eklentisi {item} bırakıldı.", + "upload_form.drag_and_drop.on_drag_end": "Medya eklentisi {item} bırakıldı.", + "upload_form.drag_and_drop.on_drag_over": "Medya eklentisi {item} hareket ettirildi.", + "upload_form.drag_and_drop.on_drag_start": "Medya eklentisi {item} tutuldu.", "upload_form.edit": "Düzenle", "upload_form.thumbnail": "Küçük resmi değiştir", "upload_form.video_description": "İşitme kaybı veya görme engeli olan kişiler için açıklama ekleyiniz", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 1753d0beb..5a9d388f0 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Приватна відповідь", "notification.label.reply": "Відповідь", "notification.mention": "Згадка", + "notification.mentioned_you": "{name} згадує вас", "notification.moderation-warning.learn_more": "Докладніше", "notification.moderation_warning": "Ви отримали попередження модерації", "notification.moderation_warning.action_delete_statuses": "Деякі з ваших дописів було вилучено.", @@ -852,6 +853,11 @@ "upload_error.poll": "Не можна завантажувати файли до опитувань.", "upload_form.audio_description": "Опишіть для людей із вадами слуху", "upload_form.description": "Опишіть для людей з вадами зору", + "upload_form.drag_and_drop.instructions": "Щоб вибрати медіавкладення, натисніть пробіл або Enter. Під час перетягування, використайте клавіші зі стрілками для переміщення вкладення в будь-якому напрямку. Натисніть пробіл або Enter знову, щоб залишити медіавкладення в новому положенні, або натисніть клавішу Escape, щоб скасувати.", + "upload_form.drag_and_drop.on_drag_cancel": "Перетягування скасовано. Медіавкладення {item} прибрано.", + "upload_form.drag_and_drop.on_drag_end": "Медіавкладення {item} прибрано.", + "upload_form.drag_and_drop.on_drag_over": "Медіавкладення {item} переміщено.", + "upload_form.drag_and_drop.on_drag_start": "Медіавкладення {item} вибрано.", "upload_form.edit": "Змінити", "upload_form.thumbnail": "Змінити мініатюру", "upload_form.video_description": "Опишіть для людей із вадами слуху або зору", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 770dcbaef..cf33a15d3 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "Trả lời riêng", "notification.label.reply": "Trả lời", "notification.mention": "Lượt nhắc", + "notification.mentioned_you": "{name} nhắc đến bạn", "notification.moderation-warning.learn_more": "Tìm hiểu", "notification.moderation_warning": "Bạn vừa nhận một cảnh báo kiểm duyệt", "notification.moderation_warning.action_delete_statuses": "Một vài tút của bạn bị gỡ.", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 0a4505d4e..74702e512 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "私人回复", "notification.label.reply": "回复", "notification.mention": "提及", + "notification.mentioned_you": "{name} 提到了你", "notification.moderation-warning.learn_more": "了解更多", "notification.moderation_warning": "你收到了一条管理警告", "notification.moderation_warning.action_delete_statuses": "你的一些嘟文已被移除。", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 2047a69f4..e93f7d518 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -516,6 +516,7 @@ "notification.label.private_reply": "私訊回嘟", "notification.label.reply": "回嘟", "notification.mention": "提及", + "notification.mentioned_you": "{name} 已提及您", "notification.moderation-warning.learn_more": "了解更多", "notification.moderation_warning": "您已收到管理員警告", "notification.moderation_warning.action_delete_statuses": "某些您的嘟文已被刪除。", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 9f66c0963..4e16c5b35 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -402,7 +402,7 @@ export default function compose(state = initialState, action) { .set('isUploadingThumbnail', false) .update('media_attachments', list => list.map(item => { if (item.get('id') === action.media.id) { - return fromJS(action.media); + return fromJS(action.media).set('unattached', item.get('unattached')); } return item; diff --git a/app/javascript/mastodon/reducers/notification_groups.ts b/app/javascript/mastodon/reducers/notification_groups.ts index f3c83ccd8..8b033f0fc 100644 --- a/app/javascript/mastodon/reducers/notification_groups.ts +++ b/app/javascript/mastodon/reducers/notification_groups.ts @@ -21,6 +21,7 @@ import { unmountNotifications, refreshStaleNotificationGroups, pollRecentNotifications, + shouldGroupNotificationType, } from 'mastodon/actions/notification_groups'; import { disconnectTimeline, @@ -205,6 +206,13 @@ function processNewNotification( groups: NotificationGroupsState['groups'], notification: ApiNotificationJSON, ) { + if (!shouldGroupNotificationType(notification.type)) { + notification = { + ...notification, + group_key: `ungrouped-${notification.id}`, + }; + } + const existingGroupIndex = groups.findIndex( (group) => group.type !== 'gap' && group.group_key === notification.group_key, @@ -242,7 +250,7 @@ function processNewNotification( groups.unshift(existingGroup); } } else { - // Create a new group + // We have not found an existing group, create a new one groups.unshift(createNotificationGroupFromNotificationJSON(notification)); } } diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js index 89ae20007..3e39c9a4e 100644 --- a/app/javascript/mastodon/service_worker/web_push_locales.js +++ b/app/javascript/mastodon/service_worker/web_push_locales.js @@ -6,6 +6,12 @@ const fs = require('fs'); const path = require('path'); +const { defineMessages } = require('react-intl'); + +const messages = defineMessages({ + mentioned_you: { id: 'notification.mentioned_you', defaultMessage: '{name} mentioned you' }, +}); + const filtered = {}; const filenames = fs.readdirSync(path.resolve(__dirname, '../locales')); @@ -20,7 +26,7 @@ filenames.forEach(filename => { 'notification.favourite': full['notification.favourite'] || '', 'notification.follow': full['notification.follow'] || '', 'notification.follow_request': full['notification.follow_request'] || '', - 'notification.mention': full['notification.mention'] || '', + 'notification.mention': full[messages.mentioned_you.id] || '', 'notification.reblog': full['notification.reblog'] || '', 'notification.poll': full['notification.poll'] || '', 'notification.status': full['notification.status'] || '', diff --git a/app/javascript/material-icons/400-24px/breaking_news-fill.svg b/app/javascript/material-icons/400-24px/breaking_news-fill.svg new file mode 100644 index 000000000..633ca48d5 --- /dev/null +++ b/app/javascript/material-icons/400-24px/breaking_news-fill.svg @@ -0,0 +1 @@ + \ 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 d7dd0c12f..c043f11a8 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/captive_portal-fill.svg b/app/javascript/material-icons/400-24px/captive_portal-fill.svg new file mode 100644 index 000000000..5c0b26fb6 --- /dev/null +++ b/app/javascript/material-icons/400-24px/captive_portal-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/captive_portal.svg b/app/javascript/material-icons/400-24px/captive_portal.svg index 1f0f09c77..5c0b26fb6 100644 --- a/app/javascript/material-icons/400-24px/captive_portal.svg +++ b/app/javascript/material-icons/400-24px/captive_portal.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/chat_bubble-fill.svg b/app/javascript/material-icons/400-24px/chat_bubble-fill.svg new file mode 100644 index 000000000..b47338a6c --- /dev/null +++ b/app/javascript/material-icons/400-24px/chat_bubble-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/chat_bubble.svg b/app/javascript/material-icons/400-24px/chat_bubble.svg index 7d210b460..05d976d24 100644 --- a/app/javascript/material-icons/400-24px/chat_bubble.svg +++ b/app/javascript/material-icons/400-24px/chat_bubble.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud-fill.svg b/app/javascript/material-icons/400-24px/cloud-fill.svg new file mode 100644 index 000000000..d049a74c0 --- /dev/null +++ b/app/javascript/material-icons/400-24px/cloud-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud.svg b/app/javascript/material-icons/400-24px/cloud.svg index 75b4e957f..a36bddda9 100644 --- a/app/javascript/material-icons/400-24px/cloud.svg +++ b/app/javascript/material-icons/400-24px/cloud.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_download-fill.svg b/app/javascript/material-icons/400-24px/cloud_download-fill.svg new file mode 100644 index 000000000..c55d49f7e --- /dev/null +++ b/app/javascript/material-icons/400-24px/cloud_download-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_download.svg b/app/javascript/material-icons/400-24px/cloud_download.svg index 2fc3717ff..8e9314800 100644 --- a/app/javascript/material-icons/400-24px/cloud_download.svg +++ b/app/javascript/material-icons/400-24px/cloud_download.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_sync-fill.svg b/app/javascript/material-icons/400-24px/cloud_sync-fill.svg new file mode 100644 index 000000000..0c648e19e --- /dev/null +++ b/app/javascript/material-icons/400-24px/cloud_sync-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_sync.svg b/app/javascript/material-icons/400-24px/cloud_sync.svg index dbf6adc00..461796e32 100644 --- a/app/javascript/material-icons/400-24px/cloud_sync.svg +++ b/app/javascript/material-icons/400-24px/cloud_sync.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_upload-fill.svg b/app/javascript/material-icons/400-24px/cloud_upload-fill.svg new file mode 100644 index 000000000..66a7bb22d --- /dev/null +++ b/app/javascript/material-icons/400-24px/cloud_upload-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/cloud_upload.svg b/app/javascript/material-icons/400-24px/cloud_upload.svg index 5e1a4b9ae..94968cb94 100644 --- a/app/javascript/material-icons/400-24px/cloud_upload.svg +++ b/app/javascript/material-icons/400-24px/cloud_upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/code-fill.svg b/app/javascript/material-icons/400-24px/code-fill.svg new file mode 100644 index 000000000..8ef5c55cd --- /dev/null +++ b/app/javascript/material-icons/400-24px/code-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/code.svg b/app/javascript/material-icons/400-24px/code.svg index 5bdc338f7..8ef5c55cd 100644 --- a/app/javascript/material-icons/400-24px/code.svg +++ b/app/javascript/material-icons/400-24px/code.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/computer-fill.svg b/app/javascript/material-icons/400-24px/computer-fill.svg new file mode 100644 index 000000000..91295d684 --- /dev/null +++ b/app/javascript/material-icons/400-24px/computer-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/computer.svg b/app/javascript/material-icons/400-24px/computer.svg index 8c5bd9110..b8af5d464 100644 --- a/app/javascript/material-icons/400-24px/computer.svg +++ b/app/javascript/material-icons/400-24px/computer.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 new file mode 100644 index 000000000..c42c79995 --- /dev/null +++ b/app/javascript/material-icons/400-24px/contact_mail-fill.svg @@ -0,0 +1 @@ + \ 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 1ae26cc4d..4547c48ec 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 new file mode 100644 index 000000000..3520f6961 --- /dev/null +++ b/app/javascript/material-icons/400-24px/database-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/database.svg b/app/javascript/material-icons/400-24px/database.svg index 54ca2f4e5..a3bc2bfbc 100644 --- a/app/javascript/material-icons/400-24px/database.svg +++ b/app/javascript/material-icons/400-24px/database.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/diamond-fill.svg b/app/javascript/material-icons/400-24px/diamond-fill.svg new file mode 100644 index 000000000..474968ad6 --- /dev/null +++ b/app/javascript/material-icons/400-24px/diamond-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/diamond.svg b/app/javascript/material-icons/400-24px/diamond.svg index 26f4814b4..b604492fa 100644 --- a/app/javascript/material-icons/400-24px/diamond.svg +++ b/app/javascript/material-icons/400-24px/diamond.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/filter_alt-fill.svg b/app/javascript/material-icons/400-24px/filter_alt-fill.svg new file mode 100644 index 000000000..ec1d90bba --- /dev/null +++ b/app/javascript/material-icons/400-24px/filter_alt-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/filter_alt.svg b/app/javascript/material-icons/400-24px/filter_alt.svg index 0294cf1da..e4af9efd5 100644 --- a/app/javascript/material-icons/400-24px/filter_alt.svg +++ b/app/javascript/material-icons/400-24px/filter_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/groups-fill.svg b/app/javascript/material-icons/400-24px/groups-fill.svg new file mode 100644 index 000000000..754eb0946 --- /dev/null +++ b/app/javascript/material-icons/400-24px/groups-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/groups.svg b/app/javascript/material-icons/400-24px/groups.svg index 0e795eb30..998ff0372 100644 --- a/app/javascript/material-icons/400-24px/groups.svg +++ b/app/javascript/material-icons/400-24px/groups.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/hide_source-fill.svg b/app/javascript/material-icons/400-24px/hide_source-fill.svg new file mode 100644 index 000000000..959631bc1 --- /dev/null +++ b/app/javascript/material-icons/400-24px/hide_source-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/hide_source.svg b/app/javascript/material-icons/400-24px/hide_source.svg index d103ed770..09633cef8 100644 --- a/app/javascript/material-icons/400-24px/hide_source.svg +++ b/app/javascript/material-icons/400-24px/hide_source.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 new file mode 100644 index 000000000..15ae2d8f3 --- /dev/null +++ b/app/javascript/material-icons/400-24px/inbox-fill.svg @@ -0,0 +1 @@ + \ 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 427817958..32c727e81 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/list-fill.svg b/app/javascript/material-icons/400-24px/list-fill.svg new file mode 100644 index 000000000..c9cbe35eb --- /dev/null +++ b/app/javascript/material-icons/400-24px/list-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/list.svg b/app/javascript/material-icons/400-24px/list.svg index 457a820ab..c9cbe35eb 100644 --- a/app/javascript/material-icons/400-24px/list.svg +++ b/app/javascript/material-icons/400-24px/list.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/mail-fill.svg b/app/javascript/material-icons/400-24px/mail-fill.svg new file mode 100644 index 000000000..5e7e4a2fb --- /dev/null +++ b/app/javascript/material-icons/400-24px/mail-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/mail.svg b/app/javascript/material-icons/400-24px/mail.svg index a92ea7b19..15e1d12d4 100644 --- a/app/javascript/material-icons/400-24px/mail.svg +++ b/app/javascript/material-icons/400-24px/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/mood-fill.svg b/app/javascript/material-icons/400-24px/mood-fill.svg new file mode 100644 index 000000000..9480d0fb9 --- /dev/null +++ b/app/javascript/material-icons/400-24px/mood-fill.svg @@ -0,0 +1 @@ + \ 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 27b353424..46cafa768 100644 --- a/app/javascript/material-icons/400-24px/mood.svg +++ b/app/javascript/material-icons/400-24px/mood.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/report-fill.svg b/app/javascript/material-icons/400-24px/report-fill.svg new file mode 100644 index 000000000..50c638869 --- /dev/null +++ b/app/javascript/material-icons/400-24px/report-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/report.svg b/app/javascript/material-icons/400-24px/report.svg index f281f0e1f..b08b5a1c9 100644 --- a/app/javascript/material-icons/400-24px/report.svg +++ b/app/javascript/material-icons/400-24px/report.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 new file mode 100644 index 000000000..b38091a8e --- /dev/null +++ b/app/javascript/material-icons/400-24px/safety_check-fill.svg @@ -0,0 +1 @@ + \ 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 f4eab46fb..87bdba21f 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/speed-fill.svg b/app/javascript/material-icons/400-24px/speed-fill.svg new file mode 100644 index 000000000..dca22ac52 --- /dev/null +++ b/app/javascript/material-icons/400-24px/speed-fill.svg @@ -0,0 +1 @@ + \ 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 ceb855c68..0837877f4 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/trending_up-fill.svg b/app/javascript/material-icons/400-24px/trending_up-fill.svg new file mode 100644 index 000000000..cd0e36896 --- /dev/null +++ b/app/javascript/material-icons/400-24px/trending_up-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/trending_up.svg b/app/javascript/material-icons/400-24px/trending_up.svg index 06f9ba206..cd0e36896 100644 --- a/app/javascript/material-icons/400-24px/trending_up.svg +++ b/app/javascript/material-icons/400-24px/trending_up.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 120bad27e..0712a0d3f 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -226,6 +226,10 @@ $content-width: 840px; gap: 5px; white-space: nowrap; + @media screen and (max-width: $mobile-breakpoint) { + flex: 1 0 50%; + } + &:hover, &:focus, &:active { @@ -1054,6 +1058,10 @@ a.name-tag, } } + .icon { + vertical-align: middle; + } + a.announcements-list__item__title { &:hover, &:focus, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 14de6e681..4ed05ead3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2077,7 +2077,6 @@ body > [data-popper-placement] { display: block; position: relative; border-radius: var(--avatar-border-radius); - background-color: var(--surface-background-color); img { width: 100%; @@ -2087,7 +2086,11 @@ body > [data-popper-placement] { display: inline-block; // to not show broken images } - &-inline { + &--loading { + background-color: var(--surface-background-color); + } + + &--inline { display: inline-block; vertical-align: middle; margin-inline-end: 5px; @@ -3611,6 +3614,7 @@ $ui-header-logo-wordmark-width: 99px; overflow-y: auto; width: 100%; height: 100%; + z-index: 0; } .drawer__inner__mastodon { @@ -10627,6 +10631,7 @@ noscript { gap: 8px; .logo { + width: 16px; height: 16px; color: $darker-text-color; } diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index af8ccf5b3..0cbf5c1d5 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -137,6 +137,7 @@ a.table-action-link { padding: 0 10px; color: $darker-text-color; font-weight: 500; + white-space: nowrap; &:hover { color: $highlight-text-color; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 09a8caf1f..928803cd6 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -42,6 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def process_status @tags = [] @mentions = [] + @unresolved_mentions = [] @silenced_account_ids = [] @params = {} @@ -55,6 +56,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end resolve_thread(@status) + resolve_unresolved_mentions(@status) fetch_replies(@status) distribute forward_for_reply @@ -197,6 +199,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if account.nil? @mentions << Mention.new(account: account, silent: false) + rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + @unresolved_mentions << tag['href'] end def process_emoji(tag) @@ -301,6 +305,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity ThreadResolveWorker.perform_async(status.id, in_reply_to_uri, { 'request_id' => @options[:request_id] }) end + def resolve_unresolved_mentions(status) + @unresolved_mentions.uniq.each do |uri| + MentionResolveWorker.perform_in(rand(30...600).seconds, status.id, uri, { 'request_id' => @options[:request_id] }) + end + end + def fetch_replies(status) collection = @object['replies'] return if collection.blank? diff --git a/app/lib/vacuum/imports_vacuum.rb b/app/lib/vacuum/imports_vacuum.rb index 700bd8184..b67865194 100644 --- a/app/lib/vacuum/imports_vacuum.rb +++ b/app/lib/vacuum/imports_vacuum.rb @@ -9,10 +9,10 @@ class Vacuum::ImportsVacuum private def clean_unconfirmed_imports! - BulkImport.state_unconfirmed.where(created_at: ..10.minutes.ago).reorder(nil).in_batches.delete_all + BulkImport.state_unconfirmed.where(created_at: ..10.minutes.ago).in_batches.delete_all end def clean_old_imports! - BulkImport.where(created_at: ..1.week.ago).reorder(nil).in_batches.delete_all + BulkImport.where(created_at: ..1.week.ago).in_batches.delete_all end end diff --git a/app/lib/web_push_request.rb b/app/lib/web_push_request.rb new file mode 100644 index 000000000..a43e22480 --- /dev/null +++ b/app/lib/web_push_request.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class WebPushRequest + SIGNATURE_ALGORITHM = 'p256ecdsa' + AUTH_HEADER = 'WebPush' + PAYLOAD_EXPIRATION = 24.hours + JWT_ALGORITHM = 'ES256' + JWT_TYPE = 'JWT' + + attr_reader :web_push_subscription + + delegate( + :endpoint, + :key_auth, + :key_p256dh, + to: :web_push_subscription + ) + + def initialize(web_push_subscription) + @web_push_subscription = web_push_subscription + end + + def audience + @audience ||= Addressable::URI.parse(endpoint).normalized_site + end + + def authorization_header + [AUTH_HEADER, encoded_json_web_token].join(' ') + end + + def crypto_key_header + [SIGNATURE_ALGORITHM, vapid_key.public_key_for_push_header].join('=') + end + + def encrypt(payload) + Webpush::Encryption.encrypt(payload, key_p256dh, key_auth) + end + + private + + def encoded_json_web_token + JWT.encode( + web_token_payload, + vapid_key.curve, + JWT_ALGORITHM, + typ: JWT_TYPE + ) + end + + def web_token_payload + { + aud: audience, + exp: PAYLOAD_EXPIRATION.from_now.to_i, + sub: payload_subject, + } + end + + def payload_subject + [:mailto, contact_email].join(':') + end + + def vapid_key + @vapid_key ||= Webpush::VapidKey.from_keys( + Rails.configuration.x.vapid_public_key, + Rails.configuration.x.vapid_private_key + ) + end + + def contact_email + @contact_email ||= ::Setting.site_contact_email + end +end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 4c374f5d5..a20992dcb 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -86,7 +86,7 @@ class NotificationMailer < ApplicationMailer end def thread_by_conversation! - return if @status.conversation.nil? + return if @status&.conversation.nil? conversation_message_id = "" diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 42b1c4953..e2f359a8c 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -21,7 +21,7 @@ class AccountFilter end def results - scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil) + scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor relevant_params.each do |key, value| next if key.to_s == 'page' diff --git a/app/models/admin/tag_filter.rb b/app/models/admin/tag_filter.rb index 6149c5217..5e75757b2 100644 --- a/app/models/admin/tag_filter.rb +++ b/app/models/admin/tag_filter.rb @@ -14,7 +14,7 @@ class Admin::TagFilter end def results - scope = Tag.reorder(nil) + scope = Tag.all params.each do |key, value| next if key == :page diff --git a/app/models/concerns/account/avatar.rb b/app/models/concerns/account/avatar.rb index 39f599db1..5ca8fa862 100644 --- a/app/models/concerns/account/avatar.rb +++ b/app/models/concerns/account/avatar.rb @@ -6,10 +6,13 @@ module Account::Avatar IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes + AVATAR_DIMENSIONS = [400, 400].freeze + AVATAR_GEOMETRY = [AVATAR_DIMENSIONS.first, AVATAR_DIMENSIONS.last].join('x') + class_methods do def avatar_styles(file) - styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } } - styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif' + styles = { original: { geometry: "#{AVATAR_GEOMETRY}#", file_geometry_parser: FastGeometryParser } } + styles[:static] = { geometry: "#{AVATAR_GEOMETRY}#", format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif' styles end diff --git a/app/models/concerns/account/header.rb b/app/models/concerns/account/header.rb index 44ae774e9..2a47097fc 100644 --- a/app/models/concerns/account/header.rb +++ b/app/models/concerns/account/header.rb @@ -5,7 +5,10 @@ module Account::Header IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes - MAX_PIXELS = 750_000 # 1500x500px + + HEADER_DIMENSIONS = [1500, 500].freeze + HEADER_GEOMETRY = [HEADER_DIMENSIONS.first, HEADER_DIMENSIONS.last].join('x') + MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last class_methods do def header_styles(file) diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb index 10715ac97..fa0586f57 100644 --- a/app/models/remote_follow.rb +++ b/app/models/remote_follow.rb @@ -3,7 +3,6 @@ class RemoteFollow include ActiveModel::Validations include RoutingHelper - include WebfingerHelper attr_accessor :acct, :addressable_template @@ -66,7 +65,7 @@ class RemoteFollow end def acct_resource - @acct_resource ||= webfinger!("acct:#{acct}") + @acct_resource ||= Webfinger.new("acct:#{acct}").perform rescue Webfinger::Error, HTTP::ConnectionError nil end diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index fd0e23cb8..9d2b0fb37 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -18,13 +18,25 @@ class ReportFilter def results scope = Report.unresolved - params.each do |key, value| + relevant_params.each do |key, value| scope = scope.merge scope_for(key, value) end scope end + private + + def relevant_params + params.tap do |args| + args.delete(:target_origin) if origin_is_remote_and_domain_present? + end + end + + def origin_is_remote_and_domain_present? + params[:target_origin] == 'remote' && params[:by_target_domain].present? + end + def scope_for(key, value) case key.to_sym when :by_target_domain diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index d0a77daf0..8b8e533d3 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -28,6 +28,8 @@ class SessionActivation < ApplicationRecord before_create :assign_access_token + DEFAULT_SCOPES = %w(read write follow).freeze + class << self def active?(id) id && exists?(session_id: id) @@ -64,7 +66,7 @@ class SessionActivation < ApplicationRecord { application_id: Doorkeeper::Application.find_by(superapp: true)&.id, resource_owner_id: user_id, - scopes: 'read write follow', + scopes: DEFAULT_SCOPES.join(' '), expires_in: Doorkeeper.configuration.access_token_expires_in, use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?, } diff --git a/app/models/user.rb b/app/models/user.rb index 67415b59e..e72a6a2c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -71,7 +71,8 @@ class User < ApplicationRecord ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze devise :two_factor_authenticatable, - otp_secret_encryption_key: Rails.configuration.x.otp_secret + otp_secret_encryption_key: Rails.configuration.x.otp_secret, + otp_secret_length: 32 include LegacyOtpSecret # Must be after the above `devise` line in order to override the legacy method diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index ddfd08146..9d30881bf 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -29,26 +29,6 @@ class Web::PushSubscription < ApplicationRecord delegate :locale, to: :associated_user - def encrypt(payload) - Webpush::Encryption.encrypt(payload, key_p256dh, key_auth) - end - - def audience - @audience ||= Addressable::URI.parse(endpoint).normalized_site - end - - def crypto_key_header - p256ecdsa = vapid_key.public_key_for_push_header - - "p256ecdsa=#{p256ecdsa}" - end - - def authorization_header - jwt = JWT.encode({ aud: audience, exp: 24.hours.from_now.to_i, sub: "mailto:#{contact_email}" }, vapid_key.curve, 'ES256', typ: 'JWT') - - "WebPush #{jwt}" - end - def pushable?(notification) policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification) end @@ -92,14 +72,6 @@ class Web::PushSubscription < ApplicationRecord ) end - def vapid_key - @vapid_key ||= Webpush::VapidKey.from_keys(Rails.configuration.x.vapid_public_key, Rails.configuration.x.vapid_private_key) - end - - def contact_email - @contact_email ||= ::Setting.site_contact_email - end - def alert_enabled_for_notification_type?(notification) truthy?(data&.dig('alerts', notification.type.to_s)) end diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb index 2c372c2ec..560cf424e 100644 --- a/app/services/activitypub/fetch_remote_actor_service.rb +++ b/app/services/activitypub/fetch_remote_actor_service.rb @@ -3,7 +3,6 @@ class ActivityPub::FetchRemoteActorService < BaseService include JsonLdHelper include DomainControlHelper - include WebfingerHelper class Error < StandardError; end @@ -45,7 +44,7 @@ class ActivityPub::FetchRemoteActorService < BaseService private def check_webfinger! - webfinger = webfinger!("acct:#{@username}@#{@domain}") + webfinger = Webfinger.new("acct:#{@username}@#{@domain}").perform confirmed_username, confirmed_domain = split_acct(webfinger.subject) if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero? @@ -54,7 +53,7 @@ class ActivityPub::FetchRemoteActorService < BaseService return end - webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}") + webfinger = Webfinger.new("acct:#{confirmed_username}@#{confirmed_domain}").perform @username, @domain = split_acct(webfinger.subject) raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{@uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index 4f049a5ae..cadc7d2d1 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -2,6 +2,7 @@ class ActivityPub::ProcessCollectionService < BaseService include JsonLdHelper + include DomainControlHelper def call(body, actor, **options) @account = actor @@ -69,6 +70,9 @@ class ActivityPub::ProcessCollectionService < BaseService end def verify_account! + return unless @json['signature'].is_a?(Hash) + return if domain_not_allowed?(@json['signature']['creator']) + @options[:relayed_through_actor] = @account @account = ActivityPub::LinkedDataSignature.new(@json).verify_actor! @account = nil unless @account.is_a?(Account) diff --git a/app/services/purge_domain_service.rb b/app/services/purge_domain_service.rb index ca0f0d441..feab8aa1d 100644 --- a/app/services/purge_domain_service.rb +++ b/app/services/purge_domain_service.rb @@ -16,12 +16,12 @@ class PurgeDomainService < BaseService end def purge_accounts! - Account.remote.where(domain: @domain).reorder(nil).find_each do |account| + Account.remote.where(domain: @domain).find_each do |account| DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) end end def purge_emojis! - CustomEmoji.remote.where(domain: @domain).reorder(nil).find_each(&:destroy) + CustomEmoji.remote.where(domain: @domain).find_each(&:destroy) end end diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 8a5863bab..cd96b55c7 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -2,7 +2,6 @@ class ResolveAccountService < BaseService include DomainControlHelper - include WebfingerHelper include Redisable include Lockable @@ -81,7 +80,7 @@ class ResolveAccountService < BaseService end def process_webfinger!(uri) - @webfinger = webfinger!("acct:#{uri}") + @webfinger = Webfinger.new("acct:#{uri}").perform confirmed_username, confirmed_domain = split_acct(@webfinger.subject) if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? @@ -91,7 +90,7 @@ class ResolveAccountService < BaseService end # Account doesn't match, so it may have been redirected - @webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}") + @webfinger = Webfinger.new("acct:#{confirmed_username}@#{confirmed_domain}").perform @username, @domain = split_acct(@webfinger.subject) raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml index c02c8f0ad..a5d418829 100644 --- a/app/views/admin/action_logs/index.html.haml +++ b/app/views/admin/action_logs/index.html.haml @@ -16,7 +16,7 @@ %strong= t('admin.action_logs.filter_by_action') .input.select.optional = form.select :action_type, - options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }, params[:action_type]), + options_for_select(sorted_action_log_types, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all') - if @action_logs.empty? diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 27d8f4790..2b4d02fa6 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -2,9 +2,7 @@ = t('admin.dashboard.title') - content_for :heading_actions do - = l(@time_period.first) - = ' - ' - = l(@time_period.last) + = date_range(@time_period) - unless @system_checks.empty? .flash-message-stack diff --git a/app/views/admin/email_domain_blocks/new.html.haml b/app/views/admin/email_domain_blocks/new.html.haml index dd4b83ee3..2dfdca937 100644 --- a/app/views/admin/email_domain_blocks/new.html.haml +++ b/app/views/admin/email_domain_blocks/new.html.haml @@ -16,7 +16,7 @@ label: I18n.t('admin.email_domain_blocks.allow_registrations_with_approval'), wrapper: :with_label - - if defined?(@resolved_records) + - if defined?(@resolved_records) && @resolved_records.any? %p.hint= t('admin.email_domain_blocks.resolved_dns_records_hint_html') .batch-table diff --git a/app/views/admin/instances/_dashboard.html.haml b/app/views/admin/instances/_dashboard.html.haml new file mode 100644 index 000000000..ef8500103 --- /dev/null +++ b/app/views/admin/instances/_dashboard.html.haml @@ -0,0 +1,66 @@ +-# locals: (instance_domain:, period_end_at:, period_start_at:) +%p + = material_symbol 'info' + = t('admin.instances.totals_time_period_hint_html') + +.dashboard + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + href: admin_accounts_path(origin: 'remote', by_domain: instance_domain), + label: t('admin.instances.dashboard.instance_accounts_measure'), + measure: 'instance_accounts', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_statuses_measure'), + measure: 'instance_statuses', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_media_attachments_measure'), + measure: 'instance_media_attachments', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_follows_measure'), + measure: 'instance_follows', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_followers_measure'), + measure: 'instance_followers', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :counter, + end_at: period_end_at, + href: admin_reports_path(by_target_domain: instance_domain), + label: t('admin.instances.dashboard.instance_reports_measure'), + measure: 'instance_reports', + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :dimension, + dimension: 'instance_accounts', + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_accounts_dimension'), + limit: 8, + params: { domain: instance_domain }, + start_at: period_start_at + .dashboard__item + = react_admin_component :dimension, + dimension: 'instance_languages', + end_at: period_end_at, + label: t('admin.instances.dashboard.instance_languages_dimension'), + limit: 8, + params: { domain: instance_domain }, + start_at: period_start_at diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index c55eb89dc..812a9c887 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -3,77 +3,10 @@ - if current_user.can?(:view_dashboard) - content_for :heading_actions do - = l(@time_period.first) - = ' - ' - = l(@time_period.last) + = date_range(@time_period) - if @instance.persisted? - %p - = material_symbol 'info' - = t('admin.instances.totals_time_period_hint_html') - - .dashboard - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain), - label: t('admin.instances.dashboard.instance_accounts_measure'), - measure: 'instance_accounts', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_statuses_measure'), - measure: 'instance_statuses', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_media_attachments_measure'), - measure: 'instance_media_attachments', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_follows_measure'), - measure: 'instance_follows', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_followers_measure'), - measure: 'instance_followers', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :counter, - end_at: @time_period.last, - href: admin_reports_path(by_target_domain: @instance.domain), - label: t('admin.instances.dashboard.instance_reports_measure'), - measure: 'instance_reports', - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :dimension, - dimension: 'instance_accounts', - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_accounts_dimension'), - limit: 8, - params: { domain: @instance.domain }, - start_at: @time_period.first - .dashboard__item - = react_admin_component :dimension, - dimension: 'instance_languages', - end_at: @time_period.last, - label: t('admin.instances.dashboard.instance_languages_dimension'), - limit: 8, - params: { domain: @instance.domain }, - start_at: @time_period.first - + = render 'dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first - else %p = t('admin.instances.unknown_instance') diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml index 8bd5f10fe..53eac1d0c 100644 --- a/app/views/admin/invites/_invite.html.haml +++ b/app/views/admin/invites/_invite.html.haml @@ -2,7 +2,7 @@ %td .input-copy .input-copy__wrapper - %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) } + = copyable_input value: public_invite_url(invite_code: invite.code) %button{ type: :button }= t('generic.copy') %td diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml index 93387843b..462ca312a 100644 --- a/app/views/admin/tags/show.html.haml +++ b/app/views/admin/tags/show.html.haml @@ -4,9 +4,7 @@ - content_for :heading_actions do - if current_user.can?(:view_dashboard) .time-period - = l(@time_period.first) - = ' - ' - = l(@time_period.last) + = date_range(@time_period) = link_to t('admin.tags.open'), tag_url(@tag), class: 'button', target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml index 892fdc5a0..7c94062de 100644 --- a/app/views/invites/_invite.html.haml +++ b/app/views/invites/_invite.html.haml @@ -2,7 +2,7 @@ %td .input-copy .input-copy__wrapper - %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) } + = copyable_input value: public_invite_url(invite_code: invite.code) %button{ type: :button }= t('generic.copy') - if invite.valid_for_use? diff --git a/app/views/oauth/authorizations/show.html.haml b/app/views/oauth/authorizations/show.html.haml index a5122a87f..bdff33636 100644 --- a/app/views/oauth/authorizations/show.html.haml +++ b/app/views/oauth/authorizations/show.html.haml @@ -3,5 +3,5 @@ %p= t('doorkeeper.authorizations.show.title') .input-copy .input-copy__wrapper - %input.oauth-code{ type: 'text', spellcheck: 'false', readonly: true, value: params[:code] } + = copyable_input value: params[:code], class: 'oauth-code' %button{ type: :button }= t('generic.copy') diff --git a/app/views/settings/applications/_form.html.haml b/app/views/settings/applications/_form.html.haml index 66ea8bc12..85fdefa0f 100644 --- a/app/views/settings/applications/_form.html.haml +++ b/app/views/settings/applications/_form.html.haml @@ -18,7 +18,7 @@ .field-group .input.with_block_label - %label= t('activerecord.attributes.doorkeeper/application.scopes') + = form.label t('activerecord.attributes.doorkeeper/application.scopes'), required: true %span.hint= t('simple_form.hints.defaults.scopes') - Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each_value do |value| @@ -29,7 +29,7 @@ hint: false, include_blank: false, item_wrapper_tag: 'li', - label_method: ->(scope) { safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, + label_method: ->(scope) { label_for_scope(scope) }, label: false, required: false, selected: form.object.scopes.all, diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml index 80eaed5c3..e3011947a 100644 --- a/app/views/settings/applications/index.html.haml +++ b/app/views/settings/applications/index.html.haml @@ -18,7 +18,9 @@ - @applications.each do |application| %tr %td= link_to application.name, settings_application_path(application) - %th= application.scopes + %th + - application.scopes.to_s.split.each do |scope| + = tag.samp(scope, class: 'information-badge', title: t("doorkeeper.scopes.#{scope}")) %td = table_link_to 'close', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } diff --git a/app/views/settings/applications/show.html.haml b/app/views/settings/applications/show.html.haml index 19630cf49..bfde27daa 100644 --- a/app/views/settings/applications/show.html.haml +++ b/app/views/settings/applications/show.html.haml @@ -15,15 +15,16 @@ %td %code= @application.secret %tr - %th{ rowspan: 2 }= t('applications.your_token') + %th= t('applications.your_token') %td %code= current_user.token_for_app(@application).token %tr + %th %td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post %hr/ -= simple_form_for @application, url: settings_application_path(@application), method: :put do |form| += simple_form_for @application, url: settings_application_path(@application) do |form| = render form .actions diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 320bb0c7c..273c5a4ba 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -61,7 +61,8 @@ %tbody - @backups.each do |backup| %tr - %td= l backup.created_at + %td + %time.formatted{ datetime: backup.created_at.iso8601, title: l(backup.created_at) }= l backup.created_at - if backup.processed? %td= number_to_human_size backup.dump_file_size %td= table_link_to 'download', t('exports.archive_takeout.download'), download_backup_url(backup) diff --git a/app/views/settings/imports/index.html.haml b/app/views/settings/imports/index.html.haml index 634631b5a..55421991e 100644 --- a/app/views/settings/imports/index.html.haml +++ b/app/views/settings/imports/index.html.haml @@ -55,7 +55,10 @@ = t("imports.states.#{import.state}") %td #{import.imported_items} / #{import.total_items} - %td= l(import.created_at) + %td + %time.formatted{ datetime: import.created_at.iso8601, title: l(import.created_at) } + = l(import.created_at) + %td - num_failed = import.processed_items - import.imported_items - if num_failed.positive? diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index f34ce4a6a..22f88c7cd 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = button_tag t('generic.save_changes'), class: 'button', form: 'edit_user' -= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f| += simple_form_for current_user, url: settings_preferences_appearance_path, html: { id: :edit_user } do |f| .fields-row .fields-group.fields-row__column.fields-row__column-6 = f.input :locale, diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index de318dda5..a8e179d01 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = button_tag t('generic.save_changes'), class: 'button', form: 'edit_notification' -= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put, id: 'edit_notification' } do |f| += 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' diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index e2888f721..92df8e521 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences' -= simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put, id: 'edit_preferences' } do |f| += simple_form_for current_user, url: settings_preferences_other_path, html: { id: :edit_preferences } do |f| = render 'shared/error_messages', object: current_user = f.simple_fields_for :settings, current_user.settings do |ff| diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml index 3c1438258..f7241cfb2 100644 --- a/app/views/settings/privacy/show.html.haml +++ b/app/views/settings/privacy/show.html.haml @@ -5,7 +5,7 @@ %h2= t('settings.profile') = render partial: 'settings/shared/profile_navigation' -= simple_form_for @account, url: settings_privacy_path, html: { method: :put } do |f| += simple_form_for @account, url: settings_privacy_path do |f| = render 'shared/error_messages', object: @account %p.lead= t('privacy.hint_html') diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 8fb213251..427a4fa95 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -5,7 +5,7 @@ %h2= t('settings.profile') = render partial: 'settings/shared/profile_navigation' -= simple_form_for @account, url: settings_profile_path, html: { method: :put, id: 'edit_profile' } do |f| += simple_form_for @account, url: settings_profile_path, html: { id: :edit_profile } do |f| = render 'shared/error_messages', object: @account %p.lead= t('edit_profile.hint_html') @@ -34,7 +34,7 @@ .fields-row__column.fields-row__column-6 .fields-group = f.input :avatar, - hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(Account::Avatar::LIMIT)), + hint: t('simple_form.hints.defaults.avatar', dimensions: Account::Avatar::AVATAR_GEOMETRY, size: number_to_human_size(Account::Avatar::LIMIT)), input_html: { accept: Account::Avatar::IMAGE_MIME_TYPES.join(',') }, wrapper: :with_block_label @@ -50,7 +50,7 @@ .fields-row__column.fields-row__column-6 .fields-group = f.input :header, - hint: t('simple_form.hints.defaults.header', dimensions: '1500x500', size: number_to_human_size(Account::Header::LIMIT)), + hint: t('simple_form.hints.defaults.header', dimensions: Account::Header::HEADER_GEOMETRY, size: number_to_human_size(Account::Header::LIMIT)), input_html: { accept: Account::Header::IMAGE_MIME_TYPES.join(',') }, wrapper: :with_block_label diff --git a/app/views/settings/shared/_profile_navigation.html.haml b/app/views/settings/shared/_profile_navigation.html.haml index d490bd756..2f81cb5cf 100644 --- a/app/views/settings/shared/_profile_navigation.html.haml +++ b/app/views/settings/shared/_profile_navigation.html.haml @@ -1,7 +1,7 @@ .content__heading__tabs = render_navigation renderer: :links do |primary| :ruby - primary.item :profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path - primary.item :privacy, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path + primary.item :edit_profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path + primary.item :privacy_reach, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path primary.item :verification, safe_join([material_symbol('check'), t('verification.verification')]), settings_verification_path primary.item :featured_tags, safe_join([material_symbol('tag'), t('settings.featured_tags')]), settings_featured_tags_path diff --git a/app/views/settings/two_factor_authentication/confirmations/new.html.haml b/app/views/settings/two_factor_authentication/confirmations/new.html.haml index 0b8278a10..a35479b84 100644 --- a/app/views/settings/two_factor_authentication/confirmations/new.html.haml +++ b/app/views/settings/two_factor_authentication/confirmations/new.html.haml @@ -5,7 +5,7 @@ %p.hint= t('otp_authentication.instructions_html') .qr-wrapper - .qr-code!= @qrcode.as_svg(padding: 0, module_size: 4) + .qr-code!= @qrcode.as_svg(padding: 0, module_size: 4, use_path: true) .qr-alternative %p.hint= t('otp_authentication.manual_instructions') diff --git a/app/views/settings/verifications/show.html.haml b/app/views/settings/verifications/show.html.haml index 5318b0767..560807f27 100644 --- a/app/views/settings/verifications/show.html.haml +++ b/app/views/settings/verifications/show.html.haml @@ -16,7 +16,7 @@ .input-copy.lead .input-copy__wrapper - %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: link_to('Mastodon', ActivityPub::TagManager.instance.url_for(@account), rel: 'me').to_str } + = copyable_input value: link_to('Mastodon', ActivityPub::TagManager.instance.url_for(@account), rel: :me) %button{ type: :button }= t('generic.copy') %p.lead= t('verification.extra_instructions_html') @@ -31,7 +31,7 @@ = material_symbol 'check', class: 'verified-badge__mark' %span= field.value -= simple_form_for @account, url: settings_verification_path, html: { method: :put, class: 'form-section' } do |f| += 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') @@ -50,13 +50,13 @@ = image_tag frontend_asset_url('images/preview.png'), alt: '', class: 'status-card__image-image' .status-card__content %span.status-card__host - %span= t('author_attribution.s_blog', name: @account.username) + %span= t('author_attribution.s_blog', name: display_name(@account)) · %time.time-ago{ datetime: 1.year.ago.to_date.iso8601 } %strong.status-card__title= t('author_attribution.example_title') .more-from-author = logo_as_symbol(:icon) - = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + content_tag(:bdi, display_name(@account)) }) + = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + tag.bdi(display_name(@account)) }) .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/severed_relationships/index.html.haml b/app/views/severed_relationships/index.html.haml index 7c599e9c0..cc9439b46 100644 --- a/app/views/severed_relationships/index.html.haml +++ b/app/views/severed_relationships/index.html.haml @@ -15,7 +15,9 @@ %tbody - @events.each do |event| %tr - %td= l event.created_at + %td + %time.formatted{ datetime: event.created_at.iso8601, title: l(event.created_at) } + = l(event.created_at) %td= t("severed_relationships.event_type.#{event.type}", target_name: event.target_name) - if event.purged? %td{ rowspan: 2 }= t('severed_relationships.purged') diff --git a/app/workers/filtered_notification_cleanup_worker.rb b/app/workers/filtered_notification_cleanup_worker.rb index 2b955da3c..87ff6a9eb 100644 --- a/app/workers/filtered_notification_cleanup_worker.rb +++ b/app/workers/filtered_notification_cleanup_worker.rb @@ -4,6 +4,6 @@ class FilteredNotificationCleanupWorker include Sidekiq::Worker def perform(account_id, from_account_id) - Notification.where(account_id: account_id, from_account_id: from_account_id, filtered: true).reorder(nil).in_batches(order: :desc).delete_all + Notification.where(account_id: account_id, from_account_id: from_account_id, filtered: true).in_batches(order: :desc).delete_all end end diff --git a/app/workers/mention_resolve_worker.rb b/app/workers/mention_resolve_worker.rb new file mode 100644 index 000000000..72dcd9633 --- /dev/null +++ b/app/workers/mention_resolve_worker.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class MentionResolveWorker + include Sidekiq::Worker + include ExponentialBackoff + include JsonLdHelper + + sidekiq_options queue: 'pull', retry: 7 + + def perform(status_id, uri, options = {}) + status = Status.find_by(id: status_id) + return if status.nil? + + account = account_from_uri(uri) + account = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: options[:request_id]) if account.nil? + + return if account.nil? + + status.mentions.create!(account: account, silent: false) + rescue ActiveRecord::RecordNotFound + # Do nothing + rescue Mastodon::UnexpectedResponseError => e + response = e.response + + if response_error_unsalvageable?(response) + # Give up + else + raise e + end + end + + private + + def account_from_uri(uri) + ActivityPub::TagManager.instance.uri_to_resource(uri, Account) + end +end diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb index 9f58d9225..f75512833 100644 --- a/app/workers/scheduler/user_cleanup_scheduler.rb +++ b/app/workers/scheduler/user_cleanup_scheduler.rb @@ -16,7 +16,7 @@ class Scheduler::UserCleanupScheduler private def clean_unconfirmed_accounts! - User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).reorder(nil).find_in_batches do |batch| + User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch| # We have to do it separately because of missing database constraints AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all Account.where(id: batch.map(&:account_id)).delete_all diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index 7e9691aab..104503f13 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -16,10 +16,10 @@ class Web::PushNotificationWorker # in the meantime, so we have to double-check before proceeding return unless @notification.activity.present? && @subscription.pushable?(@notification) - payload = @subscription.encrypt(push_notification_json) + payload = web_push_request.encrypt(push_notification_json) - request_pool.with(@subscription.audience) do |http_client| - request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client) + request_pool.with(web_push_request.audience) do |http_client| + request = Request.new(:post, web_push_request.endpoint, body: payload.fetch(:ciphertext), http_client: http_client) request.add_headers( 'Content-Type' => 'application/octet-stream', @@ -27,8 +27,8 @@ class Web::PushNotificationWorker 'Urgency' => URGENCY, 'Content-Encoding' => 'aesgcm', 'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}", - 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{@subscription.crypto_key_header}", - 'Authorization' => @subscription.authorization_header + 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}", + 'Authorization' => web_push_request.authorization_header ) request.perform do |response| @@ -50,17 +50,27 @@ class Web::PushNotificationWorker private - def push_notification_json - json = I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do - ActiveModelSerializers::SerializableResource.new( - @notification, - serializer: Web::NotificationSerializer, - scope: @subscription, - scope_name: :current_push_subscription - ).as_json - end + def web_push_request + @web_push_request || WebPushRequest.new(@subscription) + end - Oj.dump(json) + def push_notification_json + Oj.dump(serialized_notification_in_subscription_locale.as_json) + end + + def serialized_notification_in_subscription_locale + I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do + serialized_notification + end + end + + def serialized_notification + ActiveModelSerializers::SerializableResource.new( + @notification, + serializer: Web::NotificationSerializer, + scope: @subscription, + scope_name: :current_push_subscription + ) end def request_pool diff --git a/config/locales/activerecord.fy.yml b/config/locales/activerecord.fy.yml index b571ab60d..69139eba1 100644 --- a/config/locales/activerecord.fy.yml +++ b/config/locales/activerecord.fy.yml @@ -15,6 +15,12 @@ fy: user/invite_request: text: Reden errors: + attributes: + domain: + invalid: is in ûnjildige domeinnamme + messages: + invalid_domain_on_line: "%{value} is in ûnjildige domeinnamme" + too_many_lines: giet oer de limyt fan %{limit} rigels models: account: attributes: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 965f354a3..ced8de4ef 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -446,6 +446,7 @@ ca: title: Blocar el nou domini de correu-e no_email_domain_block_selected: No s'han canviat els bloqueigs de domini perquè no se n'ha seleccionat cap not_permitted: No permés + resolved_dns_records_hint_html: El nom del domini resol als següents dominis MX, que són els responsables finals per a acceptar els correus. Blocar un domini MX blocarà els registres des de qualsevol adreça de correu que utilitzi el mateix domini MX, encara que el nom visible sigui diferent. Vigileu de no blocar els grans proveïdors de correu. resolved_through_html: Resolt mitjançant %{domain} title: Dominis de correu-e blocats export_domain_allows: @@ -800,6 +801,7 @@ ca: destroyed_msg: La càrrega al lloc s'ha suprimit correctament! software_updates: critical_update: Crítica - si us plau, actualitza ràpidament + description: Es recomana de mantenir actualizada la instal·lació de Mastodon per a beneficiar-se de les darreres correccions i característiques. A més, de vegades és fonamental actualitzar-la per a evitar problemes de seguretat. Per això Mastodon comprova si hi ha actualitzacions cada 30 minuts i us notificarà d'acord amb les preferències de notificacions de correu electrònic. documentation_link: Més informació release_notes: Notes de llançament title: Actualitzacions disponibles @@ -1155,6 +1157,7 @@ ca: account_status: Estat del compte confirming: Esperant que es completi la confirmació del correu-e. functional: El teu compte està completament operatiu. + pending: La vostra sol·licitud està pendent de revisió pel nostre personal. Això pot trigar una mica. Rebreu un correu electrònic quan s'aprovi. redirecting_to: El teu compte és inactiu perquè actualment està redirigint a %{acct}. self_destruct: Com que %{domain} tanca, només tindreu accés limitat al vostre compte. view_strikes: Veure accions del passat contra el teu compte @@ -1202,6 +1205,9 @@ ca: before: 'Abans de procedir, llegiu amb cura aquestes notes:' caches: El contingut que ha estat memoritzat en la memòria cau per altres servidors pot persistir data_removal: Els teus tuts i altres dades seran permanentment eliminades + email_change_html: Podeu canviar l'adreça de correu sense eliminar el vostre compte + email_contact_html: Si encara no arriba, podeu enviar un correu-e a %{email} per a demanar ajuda + email_reconfirmation_html: Si no rebeu el correu electrònic de confirmació , podeu tornar-lo a demanar irreversible: No seràs capaç de restaurar o reactivar el teu compte more_details_html: Per a més detalls, llegeix la política de privadesa. username_available: El teu nom d'usuari esdevindrà altre cop disponible diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 1ae6aa08f..a70d08ed8 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -931,6 +931,9 @@ cy: message_html: Nid ydych wedi diffinio unrhyw reolau gweinydd. sidekiq_process_check: message_html: Does dim proses Sidekiq yn rhedeg ar gyfer y ciw(iau) %{value}. Adolygwch eich ffurfweddiad Sidekiq + software_version_check: + action: Gweld y diweddariadau sydd ar gael + message_html: Mae diweddariad Mastodon ar gael. software_version_critical_check: action: Gweld y diweddariadau sydd ar gael message_html: Mae diweddariad hanfodol Mastodon ar gael, diweddarwch cyn gynted â phosibl. @@ -1796,6 +1799,7 @@ cy: delete: Dileu cyfrif development: Datblygu edit_profile: Golygu proffil + export: Allforio featured_tags: Prif hashnodau import: Mewnforio import_and_export: Mewnforio ac allforio diff --git a/config/locales/de.yml b/config/locales/de.yml index c82e65282..7a8469df6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -79,7 +79,7 @@ de: invite_request_text: Begründung für das Beitreten invited_by: Eingeladen von ip: IP-Adresse - joined: Beigetreten + joined: Mitglied seit location: all: Alle local: Lokal diff --git a/config/locales/devise.eo.yml b/config/locales/devise.eo.yml index 9d946967e..43aef271f 100644 --- a/config/locales/devise.eo.yml +++ b/config/locales/devise.eo.yml @@ -73,9 +73,11 @@ eo: title: Unu el viaj sekurecaj ŝlosiloj estis forigita webauthn_disabled: explanation: Aŭtentigo per sekurecaj ŝlosiloj estas malŝaltita por via konto. + extra: Ensaluto nun eblas uzante nur la ĵetonon generitan de la parigita tempbazita unufoja pasvorta aplikaĵo. subject: 'Mastodon: sekureca-ŝlosila aŭtentigo malebligita' title: Sekurecaj ŝlosiloj malaktivigitaj webauthn_enabled: + explanation: Sekurecŝlosila aŭtentigo estas ebligita por via konto. extra: Via sekureca ŝlosilo nun povas esti uzata por ensaluto. subject: 'Mastodon: sekureca-ŝlosila aŭtentigo ebligita' title: Sekurecaj ŝlosiloj aktivigitaj diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml index 59877b6bd..984eff887 100644 --- a/config/locales/doorkeeper.el.yml +++ b/config/locales/doorkeeper.el.yml @@ -60,6 +60,7 @@ el: error: title: Εμφανίστηκε σφάλμα new: + prompt_html: Το %{client_name} επιθυμεί το δικαίωμα πρόσβασης στον λογαριασμό σας. Εγκρίνετε αυτό το αίτημα μόνο αν αναγνωρίζετε και εμπιστεύεστε αυτήν την πηγή. review_permissions: Ανασκόπηση δικαιωμάτων title: Απαιτείται έγκριση show: diff --git a/config/locales/doorkeeper.fy.yml b/config/locales/doorkeeper.fy.yml index 94e67d17a..180c7e733 100644 --- a/config/locales/doorkeeper.fy.yml +++ b/config/locales/doorkeeper.fy.yml @@ -60,6 +60,7 @@ fy: error: title: Der is in flater bard new: + prompt_html: "%{client_name} freget om tagong ta jo account. Keur dit fersyk allinnich goed as jo dizze boarne werkenne en fertrouwe." review_permissions: Tastimmingen beoardiele title: Autorisaasje fereaske show: diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml index adee6bd3d..5d7148b84 100644 --- a/config/locales/doorkeeper.gl.yml +++ b/config/locales/doorkeeper.gl.yml @@ -69,7 +69,7 @@ gl: buttons: revoke: Retirar autorización confirmations: - revoke: Estás segura? + revoke: Tes certeza? index: authorized_at: Autorizada o %{date} description_html: Estas aplicacións poden acceder á túa conta usando a API. Se ves aplicacións que non recoñeces, ou hai comportamentos non consentidos dalgunha delas, podes revogar o acceso. diff --git a/config/locales/el.yml b/config/locales/el.yml index 610ae4026..4496ec51a 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -953,6 +953,7 @@ el: used_by_over_week: one: Χρησιμοποιήθηκε από ένα άτομο την τελευταία εβδομάδα other: Χρησιμοποιήθηκε από %{count} άτομα την τελευταία εβδομάδα + title: Προτάσεις και τάσεις trending: Τάσεις warning_presets: add_new: Πρόσθεση νέου @@ -1037,7 +1038,9 @@ el: guide_link_text: Μπορεί να συνεισφέρει ο οποιοσδήποτε. sensitive_content: Ευαίσθητο περιεχόμενο application_mailer: + notification_preferences: Αλλαγή προτιμήσεων email salutation: "%{name}," + settings: 'Αλλαγή προτιμήσεων email: %{link}' unsubscribe: Κατάργηση εγγραφής view: 'Προβολή:' view_profile: Προβολή προφίλ @@ -1084,6 +1087,7 @@ el: or_log_in_with: Ή συνδέσου με privacy_policy_agreement_html: Έχω διαβάσει και συμφωνώ με την πολιτική απορρήτου progress: + confirm: Επιβεβαίωση email details: Τα στοιχεία σας review: Η αξιολόγησή μας rules: Αποδοχή κανόνων @@ -1123,6 +1127,8 @@ el: view_strikes: Προβολή προηγούμενων ποινών εναντίον του λογαριασμού σας too_fast: Η φόρμα υποβλήθηκε πολύ γρήγορα, προσπαθήστε ξανά. use_security_key: Χρήση κλειδιού ασφαλείας + author_attribution: + example_title: Δείγμα κειμένου challenge: confirm: Συνέχεια hint_html: "Συμβουλή: Δεν θα σου ζητήσουμε τον κωδικό ασφαλείας σου ξανά για την επόμενη ώρα." @@ -1342,7 +1348,10 @@ el: time_started: Ξεκίνησε στις titles: blocking: Εισαγωγή αποκλεισμένων λογαριασμών + bookmarks: Εισαγωγή σελιδοδεικτών + domain_blocking: Εισαγωγή αποκλεισμένων τομέων following: Εισαγωγή λογαριασμών που ακολουθείτε + lists: Εισαγωγή λιστών type: Τύπος εισαγωγής type_groups: destructive: Μπλοκ & σίγαση @@ -1351,6 +1360,7 @@ el: bookmarks: Σελιδοδείκτες domain_blocking: Λίστα αποκλεισμένων τομέων following: Λίστα ατόμων που ακολουθείτε + lists: Λίστες muting: Λίστα αποσιωπήσεων upload: Μεταμόρφωση invites: @@ -1365,6 +1375,7 @@ el: '86400': 1 μέρα expires_in_prompt: Ποτέ generate: Δημιουργία συνδέσμου πρόσκλησης + invalid: Αυτή η πρόσκληση δεν είναι έγκυρη invited_by: 'Σε προσκάλεσε ο/η:' max_uses: one: 1 χρήσης @@ -1388,6 +1399,11 @@ el: failed_sign_in_html: Αποτυχημένη προσπάθεια σύνδεσης με %{method} από %{ip} (%{browser}) successful_sign_in_html: Επιτυχής σύνδεση με %{method} από %{ip} (%{browser}) title: Ιστορικό ελέγχου ταυτότητας + mail_subscriptions: + unsubscribe: + action: Ναι, κατάργηση συνδρομής + complete: Η συνδρομή καταργήθηκε + title: Κατάργηση συνδρομής media_attachments: validations: images_and_video: Δεν γίνεται να προσθέσεις βίντεο σε ανάρτηση που ήδη περιέχει εικόνες @@ -1467,6 +1483,7 @@ el: update: subject: "%{name} επεξεργάστηκε μια ανάρτηση" notifications: + email_events: Συμβάντα για ειδοποιήσεις μέσω email email_events_hint: 'Επέλεξε συμβάντα για τα οποία θέλεις να λαμβάνεις ειδοποιήσεις μέσω email:' number: human: @@ -1507,12 +1524,18 @@ el: other: Άλλες posting_defaults: Προεπιλογές ανάρτησης public_timelines: Δημόσιες ροές + privacy: + privacy: Απόρρητο + search: Αναζήτηση privacy_policy: title: Πολιτική Απορρήτου reactions: errors: limit_reached: Το όριο διαφορετικών αντιδράσεων ξεπεράστηκε unrecognized_emoji: δεν είναι ένα αναγνωρισμένο emoji + redirects: + prompt: Αν εμπιστεύεστε αυτόν τον σύνδεσμο, κάντε κλικ σε αυτόν για να συνεχίσετε. + title: Αποχωρείτε από το %{instance}. relationships: activity: Δραστηριότητα λογαριασμού confirm_follow_selected_followers: Είσαι βέβαιος ότι θες να ακολουθήσεις τους επιλεγμένους ακόλουθους; @@ -1548,6 +1571,9 @@ el: over_daily_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων αναρτήσεων για εκείνη τη μέρα over_total_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων αναρτήσεων too_soon: Η προγραμματισμένη ημερομηνία πρέπει να είναι στο μέλλον + self_destruct: + lead_html: Δυστυχώς, το %{domain} κλείνει οριστικά. Αν είχατε λογαριασμό εκεί, δεν θα μπορείτε να συνεχίσετε τη χρήση του, αλλά μπορείτε ακόμα να ζητήσετε ένα αντίγραφο ασφαλείας των δεδομένων σας. + title: Αυτός ο διακομιστής κλείνει οριστικά sessions: activity: Τελευταία δραστηριότητα browser: Φυλλομετρητής @@ -1604,10 +1630,12 @@ el: delete: Διαγραφή λογαριασμού development: Ανάπτυξη edit_profile: Επεξεργασία προφίλ + export: Εξαγωγή featured_tags: Παρεχόμενες ετικέτες import: Εισαγωγή import_and_export: Εισαγωγή και εξαγωγή migrate: Μετακόμιση λογαριασμού + notifications: Ειδοποιήσεις μέσω email preferences: Προτιμήσεις profile: Προφίλ relationships: Ακολουθείς και σε ακολουθούν @@ -1615,6 +1643,12 @@ el: strikes: Παραπτώματα από ομάδα συντονισμού two_factor_authentication: Πιστοποίηση 2 παραγόντων webauthn_authentication: Κλειδιά ασφαλείας + severed_relationships: + download: Λήψη (%{count}) + event_type: + account_suspension: Αναστολή λογαριασμού (%{target_name}) + domain_block: Αναστολή διακομιστή (%{target_name}) + type: Συμβάν statuses: attached: audio: @@ -1697,11 +1731,13 @@ el: contrast: Mastodon (Υψηλή αντίθεση) default: Mastodon (Σκοτεινό) mastodon-light: Mastodon (Ανοιχτόχρωμο) + system: Αυτόματο (θέμα συστήματος) time: formats: default: "%b %d, %Y, %H:%M" month: "%b %Y" time: "%H:%M" + with_time_zone: "%d %b %Y, %H:%M %Z" two_factor_authentication: add: Προσθήκη disable: Απενεργοποίηση 2FA @@ -1789,6 +1825,10 @@ el: feature_action: Μάθε περισσότερα feature_audience: Το Mastodon σού παρέχει μια μοναδική δυνατότητα διαχείρισης του κοινού σου χωρίς μεσάζοντες. Το Mastodon όταν αναπτύσσεται στη δική σου υποδομή σού επιτρέπει να ακολουθείς και να ακολουθείσαι από οποιονδήποτε άλλο συνδεδεμένο διακομιστή Mastodon και κανείς δεν τον ελέγχει, εκτός από σένα. feature_audience_title: Χτίσε το κοινό σου με σιγουριά + post_action: Σύνθεση + share_action: Κοινοποίηση + share_title: Μοιραστείτε το προφίλ σας στο Mastodon + sign_in_action: Σύνδεση subject: Καλώς ήρθες στο Mastodon title: Καλώς όρισες, %{name}! users: @@ -1798,7 +1838,10 @@ el: otp_lost_help_html: Αν χάσεις πρόσβαση και στα δύο, μπορείς να επικοινωνήσεις με %{email} signed_in_as: 'Έχεις συνδεθεί ως:' verification: + here_is_how: Δείτε πώς verification: Πιστοποίηση + verified_links: Οι επαληθευμένοι σύνδεσμοι σας + website_verification: Επαλήθευση ιστοτόπου webauthn_credentials: add: Προσθήκη νέου κλειδιού ασφαλείας create: diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index fbd48d1c3..cc9e5b741 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -875,6 +875,9 @@ en-GB: message_html: You haven't defined any server rules. sidekiq_process_check: message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration + software_version_check: + action: See available updates + message_html: A Mastodon update is available. software_version_critical_check: action: See available updates message_html: A critical Mastodon update is available, please update as quickly as possible. diff --git a/config/locales/eo.yml b/config/locales/eo.yml index a3f968d13..13a234326 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -218,18 +218,22 @@ eo: update_custom_emoji: Ĝisdatigi proprajn emoĝiojn update_domain_block: Ĝigdatigi domajnan blokadon update_ip_block: Krei IP-regulon + update_report: Ĝisdatigo de Raporto update_status: Ĝisdatigi afiŝon update_user_role: Ĝisdatigi rolon actions: approve_appeal_html: "%{name} aprobis apelacion kontraŭ moderiga decido de %{target}" approve_user_html: "%{name} aprobis registriĝon de %{target}" assigned_to_self_report_html: "%{name} asignis signalon %{target} al si mem" + change_email_user_html: "%{name} ŝanĝis retadreson de uzanto %{target}" change_role_user_html: "%{name} ŝanĝis rolon de %{target}" + confirm_user_html: "%{name} konfirmis retadreson de uzanto %{target}" create_account_warning_html: "%{name} sendis averton al %{target}" create_announcement_html: "%{name} kreis novan anoncon %{target}" create_custom_emoji_html: "%{name} alŝutis novan emoĝion %{target}" create_domain_allow_html: "%{name} aldonis domajnon %{target} al la blanka listo" create_domain_block_html: "%{name} blokis domajnon %{target}" + create_email_domain_block_html: "%{name} blokis retpoŝtan domajnon %{target}" create_ip_block_html: "%{name} kreis regulon por IP %{target}" create_unavailable_domain_html: "%{name} ĉesis sendon al domajno %{target}" create_user_role_html: "%{name} kreis rolon de %{target}" @@ -238,6 +242,7 @@ eo: destroy_custom_emoji_html: "%{name} forigis emoĝion %{target}" destroy_domain_allow_html: "%{name} forigis domajnon %{target} el la blanka listo" destroy_domain_block_html: "%{name} malblokis domajnon %{target}" + destroy_email_domain_block_html: "%{name} malblokis retpoŝtan domajnon %{target}" destroy_instance_html: "%{name} forigis domajnon %{target}" destroy_ip_block_html: "%{name} forigis regulon por IP %{target}" destroy_status_html: "%{name} forigis mesaĝojn de %{target}" @@ -1270,6 +1275,8 @@ eo: merge_long: Konservi ekzistajn registrojn kaj aldoni novajn overwrite: Anstataŭigi overwrite_long: Anstataŭigi la nunajn registrojn per la novaj + preambles: + muting_html: Vi estas silentonta ĝis %{total_items} kontoj de %{filename}. preface: Vi povas importi datumojn, kiujn vi eksportis el alia servilo, kiel liston de homoj, kiujn vi sekvas aŭ blokas. recent_imports: Lastatempaj importoj states: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 0f4c8452c..2a4740b87 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -180,21 +180,21 @@ es-MX: confirm_user: Confirmar Usuario create_account_warning: Crear Advertencia create_announcement: Crear Anuncio - create_canonical_email_block: Crear Bloqueo de Correo Electrónico + create_canonical_email_block: Crear bloqueo de correo electrónico create_custom_emoji: Crear Emoji Personalizado create_domain_allow: Crear Permiso de Dominio create_domain_block: Crear Bloqueo de Dominio - create_email_domain_block: Crear Bloqueo de Dominio de Correo Electrónico + create_email_domain_block: Crear bloqueo de dominio de correo electrónico create_ip_block: Crear regla IP create_unavailable_domain: Crear Dominio No Disponible create_user_role: Crear rol demote_user: Degradar Usuario destroy_announcement: Eliminar Anuncio - destroy_canonical_email_block: Eliminar Bloqueo de Correo Electrónico + destroy_canonical_email_block: Eliminar bloqueo de correo electrónico destroy_custom_emoji: Eliminar Emoji Personalizado destroy_domain_allow: Eliminar Permiso de Dominio destroy_domain_block: Eliminar Bloqueo de Dominio - destroy_email_domain_block: Eliminar Bloqueo de Dominio de Correo Electrónico + destroy_email_domain_block: Eliminar bloqueo de dominio de correo electrónico destroy_instance: Purgar dominio destroy_ip_block: Eliminar regla IP destroy_status: Eliminar Estado @@ -876,8 +876,8 @@ es-MX: sidekiq_process_check: message_html: No hay ningún proceso Sidekiq en ejecución para la(s) cola(s) %{value}. Por favor, revise su configuración de Sidekiq software_version_check: - action: Ver actualizaciones disponibles - message_html: Hay disponible una actualización de Mastodon. + action: Ver las actualizaciones disponibles + message_html: Ya está disponible una actualización de Mastodon. software_version_critical_check: action: Ver actualizaciones disponibles message_html: Una actualización crítica de Mastodon está disponible, por favor actualice lo antes posible. @@ -1453,8 +1453,8 @@ es-MX: emails: notification_emails: favourite: correos de notificación de favoritos - follow: correos de notificación de nuevos seguidores - follow_request: correos de notificación de solicitud de seguidor + follow: correos electrónicos de notificación de seguimiento + follow_request: correos electrónicos de solicitud de seguimiento mention: correos de notificación de menciones reblog: correos de notificación de impulsos resubscribe_html: Si te has dado de baja por error, puedes volver a darte de alta desde tus ajustes de notificaciones por correo. diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 0379af34e..ee1f13cc1 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -24,6 +24,8 @@ fy: admin: account_actions: action: Aksje útfiere + already_silenced: Dizze account is al beheind. + already_suspended: Dizze account is al opskort. title: Moderaasjemaatregelen tsjin %{acct} nimme account_moderation_notes: create: Lit in opmerking efter @@ -45,6 +47,7 @@ fy: title: E-mailadres foar %{username} wizigje change_role: changed_msg: Rol mei sukses wizige! + edit_roles: Brûkersrollen beheare label: Rol wizigje no_role: Gjin rol title: Rol fan %{username} wizigje @@ -601,6 +604,7 @@ fy: suspend_description_html: De account en alle ynhâld sil net tagonklik wêze en úteinlik fuortsmiten wurde, en ynteraksje hjirmei sil net mooglik wêze. Binnen 30 dagen werom te draaien. Dit slút alle rapportaazjes oer dizze account. actions_description_html: Beslis hokker maatregel nommen wurde moat om dizze rapportaazje op te lossen. Wannear’t jo in (straf)maatregel tsjin it rapportearre account nimme, kriget de account in e-mailmelding, behalve wannear’t de spam-kategory keazen is. actions_description_remote_html: Beslút hokker aksje nommen wurde moat om dizze rapportaazje ôf te hanneljen. Dit hat allinnich ynfloed op hoe’t jo server kommunisearret mei dizze eksterne account en omgiet mei de ynhâld. + actions_no_posts: Dit rapport hat gjin byhearrende berjochten om fuort te smiten add_to_report: Mear oan de rapportaazje tafoegje already_suspended_badges: local: Al opskoarte op dizze server @@ -871,6 +875,9 @@ fy: message_html: Jo hawwe foar dizze server gjin regels opsteld. sidekiq_process_check: message_html: Der draait gjin Sidekiq-proses foar de wachtrige(n) %{value}. Kontrolearje jo Sidekiq-konfiguraasje + software_version_check: + action: Beskikbere fernijingen besjen + message_html: Der is in Mastodon-fernijing beskikber. software_version_critical_check: action: Beskikbere fernijingen besjen message_html: Der is in kritike fernijing foar Mastodon beskikber. Wurkje sa gau as mooglik by. @@ -1156,6 +1163,12 @@ fy: view_strikes: Besjoch de earder troch moderatoaren fêststelde skeiningen dy’t jo makke hawwe too_fast: Formulier is te fluch yntsjinne. Probearje it nochris. use_security_key: Befeiligingskaai brûke + author_attribution: + example_title: Faorbyldtekst + hint_html: Bepaal hoe’t wy jo fermelde, wannear’t jo keppelingen op Mastodon dield wurde. + more_from_html: Mear fan %{name} + s_blog: Weblog fan %{name} + title: Auteur-attribúsje challenge: confirm: Trochgean hint_html: "Tip: Wy freegje jo it kommende oere net mear nei jo wachtwurd." @@ -1682,6 +1695,7 @@ fy: delete: Account fuortsmite development: Untwikkelers edit_profile: Profyl bewurkje + export: Eksportearje featured_tags: Utljochte hashtags import: Ymportearje import_and_export: Ymportearje en eksportearje @@ -1931,6 +1945,7 @@ fy: instructions_html: Kopiearje en plak de ûndersteande koade yn de HTML fan jo website. Foegje dernei it adres fan jo website ta oan ien fan de ekstra fjilden op jo profyl op it ljepblêd ‘Profyl bewurkje’ en bewarje de wizigingen. verification: Ferifikaasje verified_links: Jo ferifiearre keppelingen + website_verification: Website-ferifikaasje webauthn_credentials: add: Nije befeiligingskaai tafoegje create: diff --git a/config/locales/gd.yml b/config/locales/gd.yml index 90d03c74e..a030b0d18 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -903,6 +903,9 @@ gd: message_html: Cha do mhìnich thu riaghailtean an fhrithealaiche fhathast. sidekiq_process_check: message_html: Chan eil pròiseas Sidekiq sam bith a ruith dhan chiutha/dha na ciuthan %{value}. Thoir sùil air an rèiteachadh Sidekiq agad + software_version_check: + action: Faic na h-ùrachaidhean a tha ri fhaighinn + message_html: Tha ùrachadh Mastodon ri fhaighinn. software_version_critical_check: action: Faic na h-ùrachaidhean a tha ri fhaighinn message_html: Tha ùrachadh èiginneach air Mastodon ri fhaighinn, ùraich cho luath ’s a ghabhas. @@ -1021,7 +1024,7 @@ gd: delete: Sguab às edit_preset: Deasaich rabhadh ro-shuidhichte empty: Cha do mhìnich thu ro-sheataichean rabhaidhean fhathast. - title: Ro-sheataichean rabhaidhean + title: Rabhaidhean ro-shocraichte webhooks: add_new: Cuir puing-dheiridh ris delete: Sguab às @@ -1744,6 +1747,7 @@ gd: delete: Sguabadh às cunntais development: Leasachadh edit_profile: Deasaich a’ phròifil + export: Às-phortadh featured_tags: Tagaichean hais brosnaichte import: Ion-phortadh import_and_export: Ion-phortadh ⁊ às-phortadh diff --git a/config/locales/he.yml b/config/locales/he.yml index c7af22660..846b0d14a 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -903,6 +903,9 @@ he: message_html: לא הוגדרו שום כללי שרת. sidekiq_process_check: message_html: שום הליכי Sidekiq לא רצים עבור %{value} תור(ות). בחנו בבקשה את הגדרות Sidekiq + software_version_check: + action: ראו עדכונים זמינים + message_html: עדכון מסטודון זמין כעת. software_version_critical_check: action: ראו עדכונים זמינים message_html: יצא עדכון קריטי למסטודון, נא לעדכן את תוכנת מסטודון בהקדם האפשרי. @@ -1744,6 +1747,7 @@ he: delete: מחיקת חשבון development: פיתוח edit_profile: עריכת פרופיל + export: ייצוא featured_tags: תגיות נבחרות import: יבוא import_and_export: יבוא ויצוא diff --git a/config/locales/is.yml b/config/locales/is.yml index 4af26eea0..c4d7978de 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -877,6 +877,9 @@ is: message_html: Þú hefur ekki skilgreint neinar reglur fyrir netþjón. sidekiq_process_check: message_html: Ekkert Sidekiq-ferli er í gangi fyrir %{value} biðröð/biðraðir. Endilega athugaðu Sidekiq-uppsetninguna þína + software_version_check: + action: Skoða tiltækar uppfærslur + message_html: Uppfærsla er tiltæk fyrir Mastodon. software_version_critical_check: action: Skoða tiltækar uppfærslur message_html: Áríðandi uppfærsla Mastodon er tiltæk, uppfærðu eins fljótt og auðið er. @@ -1696,6 +1699,7 @@ is: delete: Eyðing notandaaðgangs development: Þróun edit_profile: Breyta notandasniði + export: Flytja út featured_tags: Myllumerki með aukið vægi import: Flytja inn import_and_export: Inn- og útflutningur diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 41f93397d..fc9290715 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1141,6 +1141,9 @@ ja: view_strikes: 過去のストライクを表示 too_fast: フォームの送信が速すぎます。もう一度やり直してください。 use_security_key: セキュリティキーを使用 + author_attribution: + example_title: サンプルテキスト + s_blog: "%{name} のブログ" challenge: confirm: 続ける hint_html: 以後1時間はパスワードの再入力を求めません @@ -1659,6 +1662,7 @@ ja: delete: アカウントの削除 development: 開発 edit_profile: プロフィールを編集 + export: エクスポート featured_tags: 注目のハッシュタグ import: データのインポート import_and_export: インポート・エクスポート @@ -1903,6 +1907,7 @@ ja: instructions_html: 以下のコードをコピーしてwebサイトのHTMLに貼り付けます。次に「プロフィールを編集」タブから、「プロフィール補足情報」のいずれかの欄にwebサイトのURLを記入し、「変更を保存」をクリックしてください。 verification: 認証 verified_links: 確認済みリンク + website_verification: ウェブサイトの認証 webauthn_credentials: add: セキュリティキーを追加 create: diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 12bda84d4..993488f1f 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -264,6 +264,8 @@ kab: domain: Taɣult new: create: Rnu taɣult + export_domain_allows: + no_file: Ula d yiwen ufaylu ma yettwafran export_domain_blocks: no_file: Ulac afaylu yettwafernen follow_recommendations: @@ -529,6 +531,10 @@ kab: account_status: Addad n umiḍan functional: Amiḍan-inek·inem yetteddu s lekmal-is. use_security_key: Seqdec tasarut n teɣlist + author_attribution: + example_title: Amedya n weḍris + more_from_html: Ugar s ɣur %{name} + s_blog: Ablug n %{name} challenge: confirm: Kemmel invalid_password: Yir awal uffir @@ -685,6 +691,9 @@ kab: subject: Yuder-ik·ikem-id %{name} reblog: subject: "%{name} yesselha addad-ik·im" + title: Azuzer amaynut + status: + subject: "%{name} akken i d-y·t·essufeɣ" number: human: decimal_units: diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 16844a95c..2cc8ff6a4 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -34,6 +34,7 @@ lv: created_msg: Moderācijas piezīme ir veiksmīgi izveidota! destroyed_msg: Moderācijas piezīme ir veiksmīgi iznīcināta! accounts: + add_email_domain_block: Liegt e-pasta domēnu approve: Apstiprināt approved_msg: Veiksmīgi apstiprināts %{username} reģistrēšanās pieteikums are_you_sure: Vai esi pārliecināts? @@ -61,6 +62,7 @@ lv: demote: Pazemināt destroyed_msg: Lietotāja %{username} dati tagad ievietoti rindā, lai tos nekavējoties izdzēstu disable: Iesaldēt + disable_sign_in_token_auth: Atspējot autentificēšanos ar e-pasta pilnvaru disable_two_factor_authentication: Atspējot 2FA disabled: Iesaldēts display_name: Parādāmais vārds @@ -69,6 +71,7 @@ lv: email: E-pasts email_status: E-pasta statuss enable: Atsaldēt + enable_sign_in_token_auth: Iespējot autentificēšanos ar e-pasta pilnvaru enabled: Iespējots enabled_msg: Veiksmīgi atsaldēts %{username} konts followers: Sekotāji @@ -180,17 +183,21 @@ lv: confirm_user: Apstiprināt lietotāju create_account_warning: Izveidot Brīdinājumu create_announcement: Izveidot Paziņojumu + create_canonical_email_block: Izveidot e-pasta liegumu create_custom_emoji: Izveidot pielāgotu emocijzīmi create_domain_allow: Izveidot Domēna Atļauju create_domain_block: Izveidot Domēna Bloku + create_email_domain_block: Izveidot e-pasta domēna liegumu create_ip_block: Izveidot IP noteikumu create_unavailable_domain: Izveidot Nepieejamu Domēnu create_user_role: Izveidot lomu demote_user: Pazemināt Lietotāju destroy_announcement: Dzēst Paziņojumu + destroy_canonical_email_block: Izdzēst e-pasta liegumu destroy_custom_emoji: Dzēst pielāgoto emocijzīmi destroy_domain_allow: Dzēst Domēna Atļauju destroy_domain_block: Dzēst Domēna Bloku + destroy_email_domain_block: Izdzēst e-pasta domēna liegumu destroy_instance: Attīrīt domēnu destroy_ip_block: Dzēst IP noteikumu destroy_status: Izdzēst Rakstu diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 7681a04ea..b37417efc 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1526,8 +1526,8 @@ nl: title: Nieuw volgverzoek mention: action: Reageren - body: 'Jij bent door %{name} vermeld in:' - subject: Jij bent vermeld door %{name} + body: 'Je bent door %{name} vermeld in:' + subject: Je bent vermeld door %{name} title: Nieuwe vermelding poll: subject: Een peiling van %{name} is beëindigd diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 8f6afc242..ca5e34ee5 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1,7 +1,7 @@ --- nn: about: - about_mastodon_html: 'Framtidas sosiale nettverk: Ingen annonsar, ingen verksemder som overvaker deg, etisk design og desentralisering! Eig idéane dine med Mastodon!' + about_mastodon_html: 'Framtidas sosiale nettverk: Ingen annonsar, ingen verksemder som overvaker deg, etisk design og desentralisering! Eig dataene dine med Mastodon!' contact_missing: Ikkje sett contact_unavailable: I/T hosted_on: "%{domain} er vert for Mastodon" @@ -39,7 +39,7 @@ nn: avatar: Bilete by_domain: Domene change_email: - changed_msg: Konto-e-posten er endra! + changed_msg: E-post for konto er endra! current_email: Noverande e-post label: Byt e-post new_email: Ny e-post @@ -62,7 +62,7 @@ nn: disable: Slå av disable_sign_in_token_auth: Slå av e-post-token-autentisering disable_two_factor_authentication: Slå av 2FA - disabled: Slege av + disabled: Inaktiv display_name: Synleg namn domain: Domene edit: Rediger diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 5e7317c5f..6451e7c60 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1888,7 +1888,7 @@ pt-BR: none: Aviso sensitive: Conta marcada como sensível silence: Conta silenciada - suspend: Conta banida + suspend: Conta suspensa welcome: apps_android_action: Disponível no Google Play apps_ios_action: Disponível na App Store diff --git a/config/locales/sc.yml b/config/locales/sc.yml index 306e670b7..9ab62cea7 100644 --- a/config/locales/sc.yml +++ b/config/locales/sc.yml @@ -30,17 +30,25 @@ sc: created_msg: As creadu una nota de moderatzione. destroyed_msg: As cantzelladu una nota de moderatzione. accounts: + add_email_domain_block: Bloca domìniu de posta eletrònica approve: Aprova approved_msg: Sa dimanda de registru de %{username} est istada aprovada are_you_sure: Seguru? avatar: Immàgine de profilu by_domain: Domìniu change_email: + changed_msg: Indiritzu eletrònicu de su contu modificadu! current_email: Indiritzu eletrònicu atuale label: Muda s'indiritzu eletrònicu new_email: Indiritzu eletrònicu nou submit: Muda s'indiritzu eletrònicu title: Muda s'indiritzu eletrònicu de %{username} + change_role: + changed_msg: Ruolu modificadu. + edit_roles: Gesti is ruolos de utente + label: Modifica su ruolu + no_role: Nissunu ruolu + title: Modifica su ruolu de %{username} confirm: Cunfirma confirmed: Cunfirmadu confirming: Cunfirmende @@ -91,6 +99,7 @@ sc: most_recent_ip: IP prus reghente no_account_selected: Perunu contu est istadu mudadu, dae chi non nd'as seletzionadu no_limits_imposed: Sena lìmites + no_role_assigned: Nissunu ruolu assignadu not_subscribed: Sena sutiscritzione pending: De revisionare perform_full_suspension: Suspèndidu @@ -108,12 +117,19 @@ sc: removed_header_msg: S'immàgine de intestatzione de %{username} est istada bogada resend_confirmation: already_confirmed: Custa persone est giai cunfirmada + send: Torra a imbiare su ligòngiu de cunfirmatzione + success: Ligòngiu de cunfirmatzione imbiadu. reset: Reseta reset_password: Reseta sa crae resubscribe: Torra a sutascrìere + role: Ruolu search: Chirca + search_same_email_domain: Àteras persones cun su pròpiu domìniu de posta search_same_ip: Àteras persones cun sa pròpiu IP security: Seguresa + security_measures: + only_password: Crae isceti + password_and_2fa: Crae e 2FA sensitive: Sensìbile sensitized: Marcadu comente sensìbile shared_inbox_url: URL de intrada cumpartzida @@ -129,6 +145,8 @@ sc: suspension_irreversible: Is datos de custu contu sunt istados cantzellados in manera irreversìbile. Podes bogare sa suspensione a su contu pro chi si potzat impreare, ma no at a recuperare datu perunu de is chi teniat in antis. suspension_reversible_hint_html: Su contu est istadu suspèndidu, e is datos ant a èssere cantzelladu de su totu su %{date}. Finas a tando, su contu si podet ripristinare sena efetu malu perunu. Si boles cantzellare totu is datos de su contu immediatamente ddu podes fàghere inoghe in bassu. title: Contos + unblock_email: Isbloca s'indiritzu de posta eletrònica + unblocked_email_msg: Posta eletrònica de %{username} isblocada unconfirmed_email: Posta eletrònica sena cunfirmare undo_sensitized: Boga sa marcadura comente sensìbile undo_silenced: Non pòngias a sa muda @@ -143,21 +161,31 @@ sc: whitelisted: Federatzione permìtida action_logs: action_types: + approve_user: Aprova s'utente assigned_to_self_report: Assigna s'informe + change_email_user: Muda s'indiritzu eletrònicu pro s'utente + change_role_user: Muda su ruolu de s'utente confirm_user: Cunfirma s'utente create_account_warning: Crea un'avisu create_announcement: Crea un'annùntziu + create_canonical_email_block: Crea unu blocu de posta eletrònica create_custom_emoji: Crea un'emoji personalizadu create_domain_allow: Crea unu domìniu permìtidu create_domain_block: Crea unu blocu de domìniu + create_email_domain_block: Crea unu blocu de domìniu de posta eletrònica create_ip_block: Crea una règula IP + create_unavailable_domain: Crea unu domìniu no a disponimentu + create_user_role: Crea unu ruolu demote_user: Degrada s'utente destroy_announcement: Cantzella s'annùntziu + destroy_canonical_email_block: Cantzella su blocu de posta eletrònica destroy_custom_emoji: Cantzella s'emoji personalizadu destroy_domain_allow: Cantzella su domìniu permìtidu destroy_domain_block: Cantzella su blocu de domìniu + destroy_email_domain_block: Cantzella su blocu de domìniu de posta eletrònica destroy_ip_block: Cantzella sa règula IP destroy_status: Cantzella s'istadu + destroy_unavailable_domain: Cantzella su domìniu no a disponimentu disable_2fa_user: Disativa 2FA disable_custom_emoji: Disativa s'emoji personalizadu disable_user: Disativa utente @@ -165,23 +193,33 @@ sc: enable_user: Ativa utente memorialize_account: Torra in unu contu de regordu promote_user: Promove utente + reject_user: Refuda s'utente remove_avatar_user: Cantzella immàgine de profilu reopen_report: Torra a abèrrere s'informe + resend_user: Torra a imbiare messàgiu eletrònicu de cunfirmatzione reset_password_user: Reseta sa crae resolve_report: Isorve s'informe sensitive_account: Marca is cuntenutos multimediales in su contu tuo comente sensìbiles silence_account: Pone custu contu a sa muda suspend_account: Suspende custu contu unassigned_report: Boga s'assignatzione de custu informe + unblock_email_account: Isbloca s'indiritzu de posta eletrònica unsensitive_account: Boga sa marcadura "sensìbiles" a is elementos multimediales in su contu tuo unsilence_account: Boga custu contu de is contos a sa muda unsuspend_account: Boga custu contu de is contos suspèndidos update_announcement: Atualiza s'annùntziu update_custom_emoji: Atualiza s'emoji personalizadu update_domain_block: Atualiza blocu de domìniu + update_ip_block: Atualiza sa règula IP + update_report: Atualiza s'informe update_status: Atualiza s'istadu + update_user_role: Atualiza su ruolu actions: + approve_user_html: "%{name} at aprovadu su registru de %{target}" assigned_to_self_report_html: "%{name} s'est auto-assignadu s'informe %{target}" + change_email_user_html: "%{name} at mudadu s'indiritzu de posta eletrònica de s'utente %{target}" + change_role_user_html: "%{name} at mudadu su ruolu de %{target}" + confirm_user_html: "%{name} at cunfirmadu s'indiritzu de posta eletrònica de s'utente %{target}" create_account_warning_html: "%{name} at imbiadu un'avisu a %{target}" create_announcement_html: "%{name} at creadu un'annùntziu nou %{target}" create_custom_emoji_html: "%{name} at carrigadu un'emoji nou %{target}" @@ -291,6 +329,8 @@ sc: confirm_suspension: cancel: Annulla confirm: Suspende + remove_all_data: Custu at a cantzellare totu su cuntenutu, elementos multimediales e datos de profilu pro is contos de custu domìniu dae su serbidore tuo. + stop_communication: Su serbidore tuo at a tzessare sa comunicatzione cun custos serbidores. title: Cunfirma su blocu de domìniu de %{domain} created_msg: Protzessende su blocu de domìniu destroyed_msg: Su blocu de domìniu est istadu iscontzadu @@ -361,9 +401,12 @@ sc: dashboard: instance_accounts_dimension: Contos prus sighidos instance_accounts_measure: contos sarvados + instance_languages_dimension: Limbas printzipales instance_reports_measure: informes a subra de àtere + instance_statuses_measure: publicatziones sarvadas delivery: all: Totus + unavailable: No a disponimentu delivery_available: Sa cunsigna est a disponimentu empty: Perunu domìniu agatadu. known_accounts: diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml index e7deca946..11b56bd50 100644 --- a/config/locales/simple_form.fy.yml +++ b/config/locales/simple_form.fy.yml @@ -3,6 +3,7 @@ fy: simple_form: hints: account: + attribution_domains_as_text: Beskermet tsjin net korrekte attribúsjes. discoverable: Jo iepenbiere berjochten kinne útljochte wurde op ferskate plakken binnen Mastodon en jo account kin oanrekommandearre wurde oan oare brûkers. display_name: Jo folsleine namme of in aardige bynamme. fields: Jo website, persoanlike foarnammewurden, leeftiid, alles wat jo mar kwyt wolle. @@ -130,6 +131,7 @@ fy: name: Jo kinne elk wurd mei in haadletter begjinne, om sa bygelyks de tekst mear lêsber te meitsjen user: chosen_languages: Allinnich berjochten yn de selektearre talen wurde op de iepenbiere tiidline toand + role: De rol bepaalt hokker rjochten in brûker hat. user_role: color: Kleur dy’t brûkt wurdt foar de rol yn de UI, as RGB yn heksadesimaal formaat highlighted: Dit makket de rol iepenbier sichtber @@ -142,6 +144,7 @@ fy: url: Wêr’t eveneminten nei ta stjoerd wurde labels: account: + attribution_domains_as_text: Allinnich bepaalde websites tastean discoverable: Profyl en bydragen yn sykalgoritmen opnimme litte fields: name: Label diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index 9cc32457f..523e9a5fc 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -86,6 +86,7 @@ lv: custom_css: Vari lietot pielāgotus stilus Mastodon tīmekļa versijā. favicon: WEBP, PNG, GIF vai JPG. Aizstāj noklusējuma Mastodon favikonu ar pielāgotu. mascot: Ignorē ilustrāciju uzlabotajā tīmekļa saskarnē. + media_cache_retention_period: Informācijas nesēju datnes no ierakstiem, kurus ir veikuši attālie lietotāji, tiek kešoti šajā serverī. Kad ir iestatīta apstiprinoša vērtība, informācijas nesēji tiks izdzēsti pēc norādītā dienu skaita. Ja informācijas nesēju dati tiks pieprasīti pēc tam, kad tie tika izdzēsti, tie tiks atkārtoti lejupielādēti, ja avota saturs joprojām būs pieejams. Saišu priekšskatījuma karšu vaicājumu biežuma ierobežojumu dēļ ir ieteicams iestatīt šo vērtību vismaz 14 dienas vai saišu priekšskatījuma kartes netiks atjauninātas pēc pieprasījuma pirms tā laika. peers_api_enabled: Domēna vārdu saraksts, ar kuriem šis serveris ir saskāries fediversā. Šeit nav iekļauti dati par to, vai tu veic federāciju ar noteiktu serveri, tikai tavs serveris par to zina. To izmanto dienesti, kas apkopo statistiku par federāciju vispārīgā nozīmē. profile_directory: Profilu direktorijā ir uzskaitīti visi lietotāji, kuri ir izvēlējušies būt atklājami. require_invite_text: 'Ja pierakstīšanai nepieciešama manuāla apstiprināšana, izdari tā, lai teksta: “Kāpēc vēlaties pievienoties?” ievade ir obligāta, nevis opcionāla' diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 7f8aaa01d..bf30cdb1b 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -295,7 +295,7 @@ nl: favourite: Wanneer iemand jouw bericht als favoriet markeert follow: Wanneer iemand jou is gaan volgen follow_request: Wanneer iemand jou wil volgen - mention: Wanneer iemand jou heeft vermeld + mention: Je bent door iemand vermeld pending_account: Wanneer een nieuw account moet worden beoordeeld reblog: Wanneer iemand jouw bericht heeft geboost report: Nieuwe rapportage is ingediend diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 8076682ed..399ecc061 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -8,10 +8,10 @@ sk: title: Ohľadom accounts: followers: - few: Sledovateľov + few: Sledujúcich many: Sledovateľov one: Sledujúci - other: Sledovatelia + other: Sledujúci following: Nasledujem instance_actor_flash: Toto konto je virtuálny aktér, ktorý predstavuje samotný server, a nie konkrétneho používateľa. Používa sa na účely federácie a nemal by byť pozastavený. last_active: naposledy aktívny @@ -1167,7 +1167,7 @@ sk: dormant: Spiace follow_failure: Nemožno nasledovať niektoré z vybraných účtov. follow_selected_followers: Následuj označených sledovatelov - followers: Následovatelia + followers: Sledovatelia following: Nasledovaní invited: Pozvaný/á last_active: Naposledy aktívny diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 738263191..f1e098370 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1,7 +1,7 @@ --- tr: about: - about_mastodon_html: Mastodon ücretsiz ve açık kaynaklı bir sosyal ağdır. Merkezileştirilmemiş yapısı sayesinde diğer ticari sosyal platformların aksine iletişimininizin tek bir firmada tutulmasının/yönetilmesinin önüne geçer. Güvendiğiniz bir sunucuyu seçerek oradaki kişilerle etkileşimde bulunabilirsiniz. Herkes kendi Mastodon sunucusunu kurabilir ve sorunsuz bir şekilde Mastodon sosyal ağına dahil edebilir. + about_mastodon_html: Mastodon ücretsiz ve açık kaynaklı bir sosyal ağdır. Merkezi olmayan yapısı sayesinde diğer ticari sosyal platformların aksine iletişimininizin tek bir firmada tutulmasının/yönetilmesinin önüne geçer. Güvendiğiniz bir sunucuyu seçerek oradaki kişilerle etkileşimde bulunabilirsiniz. Herkes kendi Mastodon sunucusunu kurabilir ve sorunsuz bir şekilde Mastodon sosyal ağına dahil edebilir! contact_missing: Ayarlanmadı contact_unavailable: Bulunamadı hosted_on: Mastodon %{domain} üzerinde barındırılıyor diff --git a/config/navigation.rb b/config/navigation.rb index 33efb78b1..7ec7ecb7e 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -24,18 +24,18 @@ SimpleNavigation::Configuration.run do |navigation| n.item :filters, safe_join([material_symbol('filter_alt'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? && !self_destruct } n.item :statuses_cleanup, safe_join([material_symbol('history'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? && !self_destruct } - n.item :security, safe_join([material_symbol('lock'), t('settings.account')]), edit_user_registration_path do |s| - s.item :password, safe_join([material_symbol('lock'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes} + n.item :security, safe_join([material_symbol('account_circle'), t('settings.account')]), edit_user_registration_path do |s| + s.item :password, safe_join([material_symbol('lock'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{^/auth|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes} s.item :two_factor_authentication, safe_join([material_symbol('safety_check'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_path, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys} s.item :authorized_apps, safe_join([material_symbol('list_alt'), t('settings.authorized_apps')]), oauth_authorized_applications_path, if: -> { !self_destruct } end n.item :data, safe_join([material_symbol('cloud_sync'), t('settings.import_and_export')]), settings_export_path do |s| - s.item :import, safe_join([material_symbol('cloud_upload'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? && !self_destruct } + s.item :import, safe_join([material_symbol('cloud_upload'), t('settings.import')]), settings_imports_path, highlights_on: %r{/settings/imports}, if: -> { current_user.functional? && !self_destruct } s.item :export, safe_join([material_symbol('cloud_download'), t('settings.export')]), settings_export_path end - n.item :invites, safe_join([material_symbol('person_add'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? && !self_destruct } + n.item :user_invites, safe_join([material_symbol('person_add'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? && !self_destruct } n.item :development, safe_join([material_symbol('code'), t('settings.development')]), settings_applications_path, highlights_on: %r{/settings/applications}, if: -> { current_user.functional? && !self_destruct } n.item :trends, safe_join([material_symbol('trending_up'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) && !self_destruct } do |s| @@ -51,7 +51,9 @@ SimpleNavigation::Configuration.run do |navigation| s.item :accounts, safe_join([material_symbol('groups'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|admin/account_moderation_notes|/admin/pending_accounts|/admin/users}, if: -> { current_user.can?(:manage_users) } s.item :tags, safe_join([material_symbol('tag'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags}, if: -> { current_user.can?(:manage_taxonomies) } s.item :invites, safe_join([material_symbol('person_add'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) } - s.item :instances, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) } + s.item :instances, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows|/admin/export_domain_blocks}, if: lambda { + current_user.can?(:manage_federation) + } s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) } s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) } s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) } diff --git a/config/routes.rb b/config/routes.rb index 890102955..83170fba0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,7 +67,7 @@ Rails.application.routes.draw do scope path: '.well-known' do scope module: :well_known do get 'oauth-authorization-server', to: 'oauth_metadata#show', as: :oauth_metadata, defaults: { format: 'json' } - get 'host-meta', to: 'host_meta#show', as: :host_meta, defaults: { format: 'xml' } + get 'host-meta', to: 'host_meta#show', as: :host_meta get 'nodeinfo', to: 'node_info#index', as: :nodeinfo, defaults: { format: 'json' } get 'webfinger', to: 'webfinger#show', as: :webfinger end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 50c4c1059..3dba6fa5b 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -144,8 +144,10 @@ namespace :admin do end resources :users, only: [] do - resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications' - resource :role, only: [:show, :update], controller: 'users/roles' + scope module: :users do + resource :two_factor_authentication, only: [:destroy] + resource :role, only: [:show, :update] + end end resources :custom_emojis, only: [:index, :new, :create] do diff --git a/db/migrate/.rubocop.yml b/db/migrate/.rubocop.yml index 4e23800dd..6f8b6cc60 100644 --- a/db/migrate/.rubocop.yml +++ b/db/migrate/.rubocop.yml @@ -2,3 +2,8 @@ inherit_from: ../../.rubocop.yml Naming/VariableNumber: CheckSymbols: false + +# Enabled here as workaround for https://docs.rubocop.org/rubocop/configuration.html#path-relativity +Rails/CreateTableWithTimestamps: + Include: + - '*.rb' diff --git a/db/migrate/20170508230434_create_conversation_mutes.rb b/db/migrate/20170508230434_create_conversation_mutes.rb index 01122c451..4beba9dfa 100644 --- a/db/migrate/20170508230434_create_conversation_mutes.rb +++ b/db/migrate/20170508230434_create_conversation_mutes.rb @@ -2,7 +2,7 @@ class CreateConversationMutes < ActiveRecord::Migration[5.0] def change - create_table :conversation_mutes do |t| + create_table :conversation_mutes do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.integer :account_id, null: false t.bigint :conversation_id, null: false end diff --git a/db/migrate/20170823162448_create_status_pins.rb b/db/migrate/20170823162448_create_status_pins.rb index c8d3fab3a..2cf3a85ca 100644 --- a/db/migrate/20170823162448_create_status_pins.rb +++ b/db/migrate/20170823162448_create_status_pins.rb @@ -2,7 +2,7 @@ class CreateStatusPins < ActiveRecord::Migration[5.1] def change - create_table :status_pins do |t| + create_table :status_pins do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false end diff --git a/db/migrate/20171116161857_create_list_accounts.rb b/db/migrate/20171116161857_create_list_accounts.rb index ff9ab3faa..b0371e4c8 100644 --- a/db/migrate/20171116161857_create_list_accounts.rb +++ b/db/migrate/20171116161857_create_list_accounts.rb @@ -2,7 +2,7 @@ class CreateListAccounts < ActiveRecord::Migration[5.2] def change - create_table :list_accounts do |t| + create_table :list_accounts do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false t.belongs_to :follow, foreign_key: { on_delete: :cascade }, null: false diff --git a/db/migrate/20180929222014_create_account_conversations.rb b/db/migrate/20180929222014_create_account_conversations.rb index 9386b86e7..4e85e68d4 100644 --- a/db/migrate/20180929222014_create_account_conversations.rb +++ b/db/migrate/20180929222014_create_account_conversations.rb @@ -2,7 +2,7 @@ class CreateAccountConversations < ActiveRecord::Migration[5.2] def change - create_table :account_conversations do |t| + create_table :account_conversations do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.belongs_to :account, foreign_key: { on_delete: :cascade } t.belongs_to :conversation, foreign_key: { on_delete: :cascade } t.bigint :participant_account_ids, array: true, null: false, default: [] diff --git a/db/migrate/20181007025445_create_pghero_space_stats.rb b/db/migrate/20181007025445_create_pghero_space_stats.rb index ddaf4aef3..696b53d8d 100644 --- a/db/migrate/20181007025445_create_pghero_space_stats.rb +++ b/db/migrate/20181007025445_create_pghero_space_stats.rb @@ -2,7 +2,7 @@ class CreatePgheroSpaceStats < ActiveRecord::Migration[5.2] def change - create_table :pghero_space_stats do |t| + create_table :pghero_space_stats do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.text :database t.text :schema t.text :relation diff --git a/db/migrate/20190103124649_create_scheduled_statuses.rb b/db/migrate/20190103124649_create_scheduled_statuses.rb index a66546187..02b4916be 100644 --- a/db/migrate/20190103124649_create_scheduled_statuses.rb +++ b/db/migrate/20190103124649_create_scheduled_statuses.rb @@ -2,7 +2,7 @@ class CreateScheduledStatuses < ActiveRecord::Migration[5.2] def change - create_table :scheduled_statuses do |t| + create_table :scheduled_statuses do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.belongs_to :account, foreign_key: { on_delete: :cascade } t.datetime :scheduled_at, index: true t.jsonb :params diff --git a/db/migrate/20220824233535_create_status_trends.rb b/db/migrate/20220824233535_create_status_trends.rb index 52dcbf8f3..e68e5b7c1 100644 --- a/db/migrate/20220824233535_create_status_trends.rb +++ b/db/migrate/20220824233535_create_status_trends.rb @@ -2,7 +2,7 @@ class CreateStatusTrends < ActiveRecord::Migration[6.1] def change - create_table :status_trends do |t| + create_table :status_trends do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } t.references :account, null: false, foreign_key: { on_delete: :cascade } t.float :score, null: false, default: 0 diff --git a/db/migrate/20221006061337_create_preview_card_trends.rb b/db/migrate/20221006061337_create_preview_card_trends.rb index 934a06e24..266f64402 100644 --- a/db/migrate/20221006061337_create_preview_card_trends.rb +++ b/db/migrate/20221006061337_create_preview_card_trends.rb @@ -2,7 +2,7 @@ class CreatePreviewCardTrends < ActiveRecord::Migration[6.1] def change - create_table :preview_card_trends do |t| + create_table :preview_card_trends do |t| # rubocop:disable Rails/CreateTableWithTimestamps t.references :preview_card, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } t.float :score, null: false, default: 0 t.integer :rank, null: false, default: 0 diff --git a/db/migrate/20241007071624_fix_notification_permission_foreign_keys.rb b/db/migrate/20241007071624_fix_notification_permission_foreign_keys.rb new file mode 100644 index 000000000..11b82de8a --- /dev/null +++ b/db/migrate/20241007071624_fix_notification_permission_foreign_keys.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class FixNotificationPermissionForeignKeys < ActiveRecord::Migration[7.1] + def up + safety_assured do + execute <<~SQL.squish + ALTER TABLE notification_permissions + DROP CONSTRAINT fk_rails_7c0bed08df, + ADD CONSTRAINT fk_rails_7c0bed08df + FOREIGN KEY (account_id) + REFERENCES accounts(id) + ON DELETE CASCADE, + + DROP CONSTRAINT fk_rails_e3e0aaad70, + ADD CONSTRAINT fk_rails_e3e0aaad70 + FOREIGN KEY (from_account_id) + REFERENCES accounts(id) + ON DELETE CASCADE + SQL + end + end + + def down + safety_assured do + execute <<~SQL.squish + ALTER TABLE notification_permissions + DROP CONSTRAINT fk_rails_7c0bed08df, + ADD CONSTRAINT fk_rails_7c0bed08df + FOREIGN KEY (account_id) + REFERENCES accounts(id), + + DROP CONSTRAINT fk_rails_e3e0aaad70, + ADD CONSTRAINT fk_rails_e3e0aaad70 + FOREIGN KEY (from_account_id) + REFERENCES accounts(id) + SQL + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f9f2d9739..68067841e 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[7.1].define(version: 2024_09_16_190140) do +ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1291,8 +1291,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_09_16_190140) do add_foreign_key "mentions", "statuses", on_delete: :cascade add_foreign_key "mutes", "accounts", column: "target_account_id", name: "fk_eecff219ea", on_delete: :cascade add_foreign_key "mutes", "accounts", name: "fk_b8d8daf315", on_delete: :cascade - add_foreign_key "notification_permissions", "accounts" - add_foreign_key "notification_permissions", "accounts", column: "from_account_id" + add_foreign_key "notification_permissions", "accounts", column: "from_account_id", on_delete: :cascade + add_foreign_key "notification_permissions", "accounts", on_delete: :cascade add_foreign_key "notification_policies", "accounts", on_delete: :cascade add_foreign_key "notification_requests", "accounts", column: "from_account_id", on_delete: :cascade add_foreign_key "notification_requests", "accounts", on_delete: :cascade diff --git a/docker-compose.yml b/docker-compose.yml index 41876d26f..37cb16497 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.3.0-rc.1 + image: ghcr.io/mastodon/mastodon:v4.3.0 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.3.0-rc.1 + image: ghcr.io/mastodon/mastodon-streaming:v4.3.0 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.3.0-rc.1 + image: ghcr.io/mastodon/mastodon:v4.3.0 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 9d6348f89..60555fc0f 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'rc.1' + '' end def prerelease diff --git a/lib/tasks/icons.rake b/lib/tasks/icons.rake index 96e0a1431..9a05cf349 100644 --- a/lib/tasks/icons.rake +++ b/lib/tasks/icons.rake @@ -38,6 +38,20 @@ def find_used_icons end end + Rails.root.join('config', 'navigation.rb').open('r') do |file| + pattern = /material_symbol\('(?[^']*)'\)/ + file.each_line do |line| + match = pattern.match(line) + next if match.blank? + + # navigation.rb only uses 400x24 icons, per material_symbol() in + # app/helpers/application_helper.rb + icons_by_weight_and_size[400] ||= {} + icons_by_weight_and_size[400][24] ||= Set.new + icons_by_weight_and_size[400][24] << match['icon'] + end + end + icons_by_weight_and_size end diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 713ea3ff1..4a6956cb0 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -208,7 +208,7 @@ RSpec.describe Auth::SessionsController do context 'when using two-factor authentication' do context 'with OTP enabled as second factor' do let!(:user) do - Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret) end let!(:recovery_codes) do @@ -230,7 +230,7 @@ RSpec.describe Auth::SessionsController do context 'when using email and password after an unfinished log-in attempt to a 2FA-protected account' do let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret) end before do @@ -342,7 +342,7 @@ RSpec.describe Auth::SessionsController do context 'with WebAuthn and OTP enabled as second factor' do let!(:user) do - Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret) end let!(:webauthn_credential) do diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb index 34eaacdf4..224310b7e 100644 --- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do def qr_code_markup RQRCode::QRCode.new( 'otpauth://totp/cb6e6126.ngrok.io:local-part%40domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io' - ).as_svg(padding: 0, module_size: 4) + ).as_svg(padding: 0, module_size: 4, use_path: true) end end diff --git a/spec/fabricators/account_domain_block_fabricator.rb b/spec/fabricators/account_domain_block_fabricator.rb index 83df509da..a211b7c66 100644 --- a/spec/fabricators/account_domain_block_fabricator.rb +++ b/spec/fabricators/account_domain_block_fabricator.rb @@ -2,5 +2,5 @@ Fabricator(:account_domain_block) do account { Fabricate.build(:account) } - domain 'example.com' + domain { sequence { |n| "host-#{n}.example" } } end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 0986ae171..bdc8fd9d5 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -63,6 +63,24 @@ RSpec.describe ActivityPub::Activity::Create do } end + let(:invalid_mention_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), 'post2'].join('/'), + type: 'Note', + to: [ + 'https://www.w3.org/ns/activitystreams#Public', + ActivityPub::TagManager.instance.uri_for(follower), + ], + content: '@bob lorem ipsum', + published: 1.hour.ago.utc.iso8601, + updated: 1.hour.ago.utc.iso8601, + tag: { + type: 'Mention', + href: 'http://notexisting.dontexistingtld/actor', + }, + } + end + def activity_for_object(json) { '@context': 'https://www.w3.org/ns/activitystreams', @@ -117,6 +135,25 @@ RSpec.describe ActivityPub::Activity::Create do # Creates two notifications expect(Notification.count).to eq 2 end + + it 'ignores unprocessable mention', :aggregate_failures do + stub_request(:get, invalid_mention_json[:tag][:href]).to_raise(HTTP::ConnectionError) + # When receiving the post that contains an invalid mention… + described_class.new(activity_for_object(invalid_mention_json), sender, delivery: true).perform + + # NOTE: Refering explicitly to the workers is a bit awkward + DistributionWorker.drain + FeedInsertWorker.drain + + # …it creates a status + status = Status.find_by(uri: invalid_mention_json[:id]) + + # Check the process did not crash + expect(status.nil?).to be false + + # It has queued a mention resolve job + expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything) + end end describe '#perform' do diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 4c6107d9f..d97c01858 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -14,6 +14,17 @@ RSpec.describe NotificationMailer do end end + shared_examples 'delivery without status' do + context 'when notification target_status is missing' do + before { allow(notification).to receive(:target_status).and_return(nil) } + + it 'does not deliver mail' do + emails = capture_emails { mail.deliver_now } + expect(emails).to be_empty + end + end + end + let(:receiver) { Fabricate(:user, account_attributes: { username: 'alice' }) } let(:sender) { Fabricate(:account, username: 'bob') } let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') } @@ -37,6 +48,7 @@ RSpec.describe NotificationMailer do end include_examples 'delivery to non functional user' + include_examples 'delivery without status' end describe 'follow' do @@ -75,6 +87,7 @@ RSpec.describe NotificationMailer do end include_examples 'delivery to non functional user' + include_examples 'delivery without status' end describe 'reblog' do @@ -95,6 +108,7 @@ RSpec.describe NotificationMailer do end include_examples 'delivery to non functional user' + include_examples 'delivery without status' end describe 'follow_request' do diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 06bf07ed7..48e78830d 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -3,66 +3,175 @@ require 'rails_helper' RSpec.describe Export do + subject { described_class.new(account) } + let(:account) { Fabricate(:account) } let(:target_accounts) do - [{}, { username: 'one', domain: 'local.host' }].map(&method(:Fabricate).curry(2).call(:account)) + [ + Fabricate(:account), + Fabricate(:account, username: 'one', domain: 'local.host'), + ] end - describe 'to_csv' do - it 'returns a csv of the blocked accounts' do - target_accounts.each { |target_account| account.block!(target_account) } + describe '#to_bookmarks_csv' do + before { Fabricate.times(2, :bookmark, account: account) } - export = described_class.new(account).to_blocked_accounts_csv - results = export.strip.split + let(:export) { CSV.parse(subject.to_bookmarks_csv) } - expect(results.size).to eq 2 - expect(results.first).to eq 'one@local.host' + it 'returns a csv of bookmarks' do + expect(export) + .to contain_exactly( + include(/statuses/), + include(/statuses/) + ) end + end + + describe '#to_blocked_accounts_csv' do + before { target_accounts.each { |target_account| account.block!(target_account) } } + + let(:export) { CSV.parse(subject.to_blocked_accounts_csv) } + + it 'returns a csv of the blocked accounts' do + expect(export) + .to contain_exactly( + include('one@local.host'), + include(be_present) + ) + end + end + + describe '#to_muted_accounts_csv' do + before { target_accounts.each { |target_account| account.mute!(target_account) } } + + let(:export) { CSV.parse(subject.to_muted_accounts_csv) } it 'returns a csv of the muted accounts' do - target_accounts.each { |target_account| account.mute!(target_account) } - - export = described_class.new(account).to_muted_accounts_csv - results = export.strip.split("\n") - - expect(results.size).to eq 3 - expect(results.first).to eq 'Account address,Hide notifications' - expect(results.second).to eq 'one@local.host,true' + expect(export) + .to contain_exactly( + contain_exactly('Account address', 'Hide notifications'), + include('one@local.host', 'true'), + include(be_present) + ) end + end + + describe '#to_following_accounts_csv' do + before { target_accounts.each { |target_account| account.follow!(target_account) } } + + let(:export) { CSV.parse(subject.to_following_accounts_csv) } it 'returns a csv of the following accounts' do - target_accounts.each { |target_account| account.follow!(target_account) } - - export = described_class.new(account).to_following_accounts_csv - results = export.strip.split("\n") - - expect(results.size).to eq 3 - expect(results.first).to eq 'Account address,Show boosts,Notify on new posts,Languages' - expect(results.second).to eq 'one@local.host,true,false,' + expect(export) + .to contain_exactly( + contain_exactly('Account address', 'Show boosts', 'Notify on new posts', 'Languages'), + include('one@local.host', 'true', 'false', be_blank), + include(be_present) + ) end end - describe 'total_storage' do + describe '#to_lists_csv' do + before do + target_accounts.each do |target_account| + account.follow!(target_account) + Fabricate(:list, account: account).accounts << target_account + end + end + + let(:export) { CSV.parse(subject.to_lists_csv) } + + it 'returns a csv of the lists' do + expect(export) + .to contain_exactly( + include('one@local.host'), + include(be_present) + ) + end + end + + describe '#to_blocked_domains_csv' do + before { Fabricate.times(2, :account_domain_block, account: account) } + + let(:export) { CSV.parse(subject.to_blocked_domains_csv) } + + it 'returns a csv of the blocked domains' do + expect(export) + .to contain_exactly( + include(/example/), + include(/example/) + ) + end + end + + describe '#total_storage' do it 'returns the total size of the media attachments' do media_attachment = Fabricate(:media_attachment, account: account) - expect(described_class.new(account).total_storage).to eq media_attachment.file_file_size || 0 + expect(subject.total_storage).to eq media_attachment.file_file_size || 0 end end - describe 'total_follows' do - it 'returns the total number of the followed accounts' do - target_accounts.each { |target_account| account.follow!(target_account) } - expect(described_class.new(account.reload).total_follows).to eq 2 + describe '#total_statuses' do + before { Fabricate.times(2, :status, account: account) } + + it 'returns the total number of statuses' do + expect(subject.total_statuses).to eq(2) end + end + + describe '#total_bookmarks' do + before { Fabricate.times(2, :bookmark, account: account) } + + it 'returns the total number of bookmarks' do + expect(subject.total_bookmarks).to eq(2) + end + end + + describe '#total_follows' do + before { target_accounts.each { |target_account| account.follow!(target_account) } } + + it 'returns the total number of the followed accounts' do + expect(subject.total_follows).to eq(2) + end + end + + describe '#total_lists' do + before { Fabricate.times(2, :list, account: account) } + + it 'returns the total number of lists' do + expect(subject.total_lists).to eq(2) + end + end + + describe '#total_followers' do + before { target_accounts.each { |target_account| target_account.follow!(account) } } + + it 'returns the total number of the follower accounts' do + expect(subject.total_followers).to eq(2) + end + end + + describe '#total_blocks' do + before { target_accounts.each { |target_account| account.block!(target_account) } } it 'returns the total number of the blocked accounts' do - target_accounts.each { |target_account| account.block!(target_account) } - expect(described_class.new(account.reload).total_blocks).to eq 2 + expect(subject.total_blocks).to eq(2) end + end + + describe '#total_mutes' do + before { target_accounts.each { |target_account| account.mute!(target_account) } } it 'returns the total number of the muted accounts' do - target_accounts.each { |target_account| account.mute!(target_account) } - expect(described_class.new(account.reload).total_mutes).to eq 2 + expect(subject.total_mutes).to eq(2) + end + end + + describe '#total_domain_blocks' do + before { Fabricate.times(2, :account_domain_block, account: account) } + + it 'returns the total number of account domain blocks' do + expect(subject.total_domain_blocks).to eq(2) end end end diff --git a/spec/models/report_filter_spec.rb b/spec/models/report_filter_spec.rb index 8668eb3d1..51933e475 100644 --- a/spec/models/report_filter_spec.rb +++ b/spec/models/report_filter_spec.rb @@ -30,4 +30,17 @@ RSpec.describe ReportFilter do expect(Report).to have_received(:resolved) end end + + context 'when given remote target_origin and also by_target_domain' do + let!(:matching_report) { Fabricate :report, target_account: Fabricate(:account, domain: 'match.example') } + let!(:non_matching_report) { Fabricate :report, target_account: Fabricate(:account, domain: 'other.example') } + + it 'preserves the domain value' do + filter = described_class.new(by_target_domain: 'match.example', target_origin: 'remote') + + expect(filter.results) + .to include(matching_report) + .and not_include(non_matching_report) + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ee03b49bc..84cee0974 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -161,6 +161,11 @@ RSpec.configure do |config| host! Rails.configuration.x.local_domain end + config.before :each, type: :system do + # Align with capybara config so that rails helpers called from rspec use matching host + host! 'localhost:3000' + end + config.after do Rails.cache.clear redis.del(redis.keys) diff --git a/spec/requests/auth/sessions/security_key_options_spec.rb b/spec/requests/auth/sessions/security_key_options_spec.rb index 6246e4beb..e53b9802b 100644 --- a/spec/requests/auth/sessions/security_key_options_spec.rb +++ b/spec/requests/auth/sessions/security_key_options_spec.rb @@ -6,7 +6,7 @@ require 'webauthn/fake_client' RSpec.describe 'Security Key Options' do describe 'GET /auth/sessions/security_key_options' do let!(:user) do - Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret) end context 'with WebAuthn and OTP enabled as second factor' do diff --git a/spec/requests/well_known/host_meta_spec.rb b/spec/requests/well_known/host_meta_spec.rb index 09f17baa8..726911dda 100644 --- a/spec/requests/well_known/host_meta_spec.rb +++ b/spec/requests/well_known/host_meta_spec.rb @@ -9,19 +9,39 @@ RSpec.describe 'The /.well-known/host-meta request' do expect(response) .to have_http_status(200) .and have_attributes( - media_type: 'application/xrd+xml', - body: host_meta_xml_template + media_type: 'application/xrd+xml' + ) + + doc = Nokogiri::XML(response.parsed_body) + expect(doc.at_xpath('/xrd:XRD/xrd:Link[@rel="lrdd"]/@template', 'xrd' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0').value) + .to eq 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}' + end + + it 'returns http success with valid JSON response with .json extension' do + get '/.well-known/host-meta.json' + + expect(response) + .to have_http_status(200) + .and have_attributes( + media_type: 'application/json' + ) + + expect(response.parsed_body) + .to include( + links: [ + 'rel' => 'lrdd', + 'template' => 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}', + ] ) end - private + it 'returns http success with valid JSON response with Accept header' do + get '/.well-known/host-meta', headers: { 'Accept' => 'application/json' } - def host_meta_xml_template - <<~XML - - - - - XML + expect(response) + .to have_http_status(200) + .and have_attributes( + media_type: 'application/json' + ) end end diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb index 6578e939a..84081059b 100644 --- a/spec/routing/well_known_routes_spec.rb +++ b/spec/routing/well_known_routes_spec.rb @@ -4,9 +4,14 @@ require 'rails_helper' RSpec.describe 'Well Known routes' do describe 'the host-meta route' do - it 'routes to correct place with xml format' do + it 'routes to correct place' do expect(get('/.well-known/host-meta')) - .to route_to('well_known/host_meta#show', format: 'xml') + .to route_to('well_known/host_meta#show') + end + + it 'routes to correct place with json format' do + expect(get('/.well-known/host-meta.json')) + .to route_to('well_known/host_meta#show', format: 'json') end end diff --git a/spec/services/account_statuses_cleanup_service_spec.rb b/spec/services/account_statuses_cleanup_service_spec.rb index 857bd4fda..553d20029 100644 --- a/spec/services/account_statuses_cleanup_service_spec.rb +++ b/spec/services/account_statuses_cleanup_service_spec.rb @@ -27,39 +27,35 @@ RSpec.describe AccountStatusesCleanupService do end context 'when given a normal budget of 10' do - it 'reports 3 deleted statuses' do - expect(subject.call(account_policy, 10)).to eq 3 - end + it 'reports 3 deleted statuses and records last deleted id, deletes statuses, preserves recent unrelated statuses' do + expect(subject.call(account_policy, 10)) + .to eq(3) - it 'records the last deleted id' do - subject.call(account_policy, 10) - expect(account_policy.last_inspected).to eq [old_status.id, another_old_status.id].max - end + expect(account_policy.last_inspected) + .to eq [old_status.id, another_old_status.id].max - it 'actually deletes the statuses' do - subject.call(account_policy, 10) - expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil - expect { recent_status.reload }.to_not raise_error - end - - it 'preserves recent and unrelated statuses' do - subject.call(account_policy, 10) - expect { unrelated_status.reload }.to_not raise_error - expect { recent_status.reload }.to_not raise_error + expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])) + .to be_nil + expect { recent_status.reload } + .to_not raise_error + expect { unrelated_status.reload } + .to_not raise_error + expect { recent_status.reload } + .to_not raise_error end end context 'when called repeatedly with a budget of 2' do - it 'reports 2 then 1 deleted statuses' do - expect(subject.call(account_policy, 2)).to eq 2 - expect(subject.call(account_policy, 2)).to eq 1 - end + it 'reports 2 then 1 deleted statuses and deletes in expected order' do + expect(subject.call(account_policy, 2)) + .to eq(2) + expect(Status.find_by(id: very_old_status.id)) + .to be_nil - it 'actually deletes the statuses in the expected order' do - subject.call(account_policy, 2) - expect(Status.find_by(id: very_old_status.id)).to be_nil - subject.call(account_policy, 2) - expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil + expect(subject.call(account_policy, 2)) + .to eq(1) + expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])) + .to be_nil end end @@ -90,19 +86,24 @@ RSpec.describe AccountStatusesCleanupService do end end - it 'reports 0 deleted statuses then 0 then 3 then 0 again' do - expect(subject.call(account_policy, 10)).to eq 0 - expect(subject.call(account_policy, 10)).to eq 0 - expect(subject.call(account_policy, 10)).to eq 3 - expect(subject.call(account_policy, 10)).to eq 0 + it 'reports 0 deleted statuses then 0 then 3 then 0 again, and keeps id under oldest deletable record' do + expect(subject.call(account_policy, 10)) + .to eq(0) + expect(subject.call(account_policy, 10)) + .to eq(0) + expect(subject.call(account_policy, 10)) + .to eq(3) + expect(subject.call(account_policy, 10)) + .to eq(0) + expect(account_policy.last_inspected) + .to be < oldest_deletable_record_id end - it 'never causes the recorded id to get higher than oldest deletable toot' do - subject.call(account_policy, 10) - subject.call(account_policy, 10) - subject.call(account_policy, 10) - subject.call(account_policy, 10) - expect(account_policy.last_inspected).to be < Mastodon::Snowflake.id_at(account_policy.min_status_age.seconds.ago, with_random: false) + def oldest_deletable_record_id + Mastodon::Snowflake.id_at( + account_policy.min_status_age.seconds.ago, + with_random: false + ) 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 a97e84080..b6ceba374 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -2,10 +2,6 @@ require 'rails_helper' -def poll_option_json(name, votes) - { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } -end - RSpec.describe ActivityPub::ProcessStatusUpdateService do subject { described_class.new } @@ -294,7 +290,6 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do context 'when originally without media attachments' do before do stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png')) - subject.call(status, json, json) end let(:payload) do @@ -310,19 +305,18 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do } end - it 'updates media attachments' do - media_attachment = status.reload.ordered_media_attachments.first + it 'updates media attachments, fetches attachment, records media change in edit' do + subject.call(status, json, json) - expect(media_attachment).to_not be_nil - expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' - end + expect(status.reload.ordered_media_attachments.first) + .to be_present + .and(have_attributes(remote_url: 'https://example.com/foo.png')) - it 'fetches the attachment' do - expect(a_request(:get, 'https://example.com/foo.png')).to have_been_made - end + expect(a_request(:get, 'https://example.com/foo.png')) + .to have_been_made - it 'records media change in edit' do - expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty + expect(status.edits.reload.last.ordered_media_attachment_ids) + .to_not be_empty end end @@ -344,27 +338,26 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do before do allow(RedownloadMediaWorker).to receive(:perform_async) + end + + it 'updates the existing media attachment in-place, does not queue redownload, updates media, records media change' do subject.call(status, json, json) - end - it 'updates the existing media attachment in-place' do - media_attachment = status.media_attachments.ordered.reload.first + expect(status.media_attachments.ordered.reload.first) + .to be_present + .and have_attributes( + remote_url: 'https://example.com/foo.png', + description: 'A picture' + ) - expect(media_attachment).to_not be_nil - expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' - expect(media_attachment.description).to eq 'A picture' - end + expect(RedownloadMediaWorker) + .to_not have_received(:perform_async) - it 'does not queue redownload for the existing media attachment' do - expect(RedownloadMediaWorker).to_not have_received(:perform_async) - end + expect(status.ordered_media_attachments.map(&:remote_url)) + .to eq %w(https://example.com/foo.png) - it 'updates media attachments' do - expect(status.ordered_media_attachments.map(&:remote_url)).to eq %w(https://example.com/foo.png) - end - - it 'records media change in edit' do - expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty + expect(status.edits.reload.last.ordered_media_attachment_ids) + .to_not be_empty end end @@ -372,10 +365,11 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do before do poll = Fabricate(:poll, status: status) status.update(preloadable_poll: poll) - subject.call(status, json, json) end it 'removes poll and records media change in edit' do + subject.call(status, json, json) + expect(status.reload.poll).to be_nil expect(status.edits.reload.last.poll_options).to be_nil end @@ -398,15 +392,13 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do } end - before do - subject.call(status, json, json) - end - it 'creates a poll and records media change in edit' do - poll = status.reload.poll + subject.call(status, json, json) + + expect(status.reload.poll) + .to be_present + .and have_attributes(options: %w(Foo Bar Baz)) - expect(poll).to_not be_nil - expect(poll.options).to eq %w(Foo Bar Baz) expect(status.edits.reload.last.poll_options).to eq %w(Foo Bar Baz) end end @@ -419,4 +411,8 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do .to eq '2021-09-08 22:39:25 UTC' end end + + def poll_option_json(name, votes) + { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } + end end diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 533b791fb..de2857280 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -12,15 +12,15 @@ RSpec.describe AuthorizeFollowService do before do FollowRequest.create(account: bob, target_account: sender) + end + + it 'removes follow request and creates follow relation' do subject.call(bob, sender) - end - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'creates follow relation' do - expect(bob.following?(sender)).to be true + expect(bob) + .to_not be_requested(sender) + expect(bob) + .to be_following(sender) end end @@ -30,19 +30,17 @@ RSpec.describe AuthorizeFollowService do before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, bob.inbox_url).to_return(status: 200) + end + + it 'removes follow request, creates follow relation, send accept activity', :inline_jobs do subject.call(bob, sender) - end - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'creates follow relation' do - expect(bob.following?(sender)).to be true - end - - it 'sends an accept activity', :inline_jobs do - expect(a_request(:post, bob.inbox_url)).to have_been_made.once + expect(bob) + .to_not be_requested(sender) + expect(bob) + .to be_following(sender) + expect(a_request(:post, bob.inbox_url)) + .to have_been_made.once end end end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index 628bb198e..1ff73a633 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -24,32 +24,38 @@ RSpec.describe BatchedRemoveStatusService, :inline_jobs do status_alice_hello status_alice_other + end + it 'removes status records, removes from author and local follower feeds, notifies stream, sends delete' do subject.call([status_alice_hello, status_alice_other]) + + expect { Status.find(status_alice_hello.id) } + .to raise_error ActiveRecord::RecordNotFound + expect { Status.find(status_alice_other.id) } + .to raise_error ActiveRecord::RecordNotFound + + expect(feed_ids_for(alice)) + .to_not include(status_alice_hello.id, status_alice_other.id) + + expect(feed_ids_for(jeff)) + .to_not include(status_alice_hello.id, status_alice_other.id) + + expect(redis) + .to have_received(:publish) + .with("timeline:#{jeff.id}", any_args).at_least(:once) + + expect(redis) + .to have_received(:publish) + .with('timeline:public', any_args).at_least(:once) + + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.at_least_once end - it 'removes statuses' do - expect { Status.find(status_alice_hello.id) }.to raise_error ActiveRecord::RecordNotFound - expect { Status.find(status_alice_other.id) }.to raise_error ActiveRecord::RecordNotFound - end - - it 'removes statuses from author\'s home feed' do - expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id) - end - - it 'removes statuses from local follower\'s home feed' do - expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id) - end - - it 'notifies streaming API of followers' do - expect(redis).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once) - end - - it 'notifies streaming API of public timeline' do - expect(redis).to have_received(:publish).with('timeline:public', any_args).at_least(:once) - end - - it 'sends delete activity to followers' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once + def feed_ids_for(account) + HomeFeed + .new(account) + .get(10) + .pluck(:id) end end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 46dd69198..d9687a540 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -26,15 +26,16 @@ RSpec.describe BlockService do before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + end + + it 'creates a blocking relation and send block activity', :inline_jobs do subject.call(sender, bob) - end - it 'creates a blocking relation' do - expect(sender.blocking?(bob)).to be true - end + expect(sender) + .to be_blocking(bob) - it 'sends a block activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end end diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb index 0197f81a4..f52fc4d7d 100644 --- a/spec/services/bulk_import_service_spec.rb +++ b/spec/services/bulk_import_service_spec.rb @@ -24,30 +24,19 @@ RSpec.describe BulkImportService do ].map { |data| import.rows.create!(data: data) } end - before do - account.follow!(Fabricate(:account)) - end + before { account.follow!(Fabricate(:account)) } - it 'does not immediately change who the account follows' do - expect { subject.call(import) }.to_not(change { account.reload.active_relationships.to_a }) - end + it 'does not immediately change who the account follows, enqueues workers, sends follow requests after worker run' do + expect { subject.call(import) } + .to_not(change { account.reload.active_relationships.to_a }) - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows.map(&:id)) - it 'requests to follow all the listed users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') + expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }) + .to contain_exactly('user@foo.bar', 'unknown@unknown.bar') end end @@ -71,31 +60,20 @@ RSpec.describe BulkImportService do account.follow!(to_be_unfollowed) end - it 'unfollows user not present on list' do - subject.call(import) - expect(account.following?(to_be_unfollowed)).to be false - end + it 'updates the existing follow relationship as expected and unfollows user not on list, enqueues workers, sends follow reqs after worker run' do + expect { subject.call(import) } + .to change { Follow.where(account: account, target_account: followed).pick(:show_reblogs, :notify, :languages) }.from([true, false, nil]).to([false, true, ['en']]) - it 'updates the existing follow relationship as expected' do - expect { subject.call(import) }.to change { Follow.where(account: account, target_account: followed).pick(:show_reblogs, :notify, :languages) }.from([true, false, nil]).to([false, true, ['en']]) - end + expect(account) + .to_not be_following(to_be_unfollowed) - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows[1..].map(&:id)) - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') + expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }) + .to contain_exactly('user@foo.bar', 'unknown@unknown.bar') end end @@ -110,30 +88,19 @@ RSpec.describe BulkImportService do ].map { |data| import.rows.create!(data: data) } end - before do - account.block!(Fabricate(:account, username: 'already_blocked', domain: 'remote.org')) - end + before { account.block!(Fabricate(:account, username: 'already_blocked', domain: 'remote.org')) } - it 'does not immediately change who the account blocks' do - expect { subject.call(import) }.to_not(change { account.reload.blocking.to_a }) - end + it 'does not immediately change who the account blocks, enqueues worker, blocks after run' do + expect { subject.call(import) } + .to_not(change { account.reload.blocking.to_a }) - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows.map(&:id)) - it 'blocks all the listed users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.blocking.map(&:acct)).to contain_exactly('already_blocked@remote.org', 'user@foo.bar', 'unknown@unknown.bar') + expect(account.reload.blocking.map(&:acct)) + .to contain_exactly('already_blocked@remote.org', 'user@foo.bar', 'unknown@unknown.bar') end end @@ -157,27 +124,18 @@ RSpec.describe BulkImportService do account.block!(to_be_unblocked) end - it 'unblocks user not present on list' do + it 'unblocks user not present on list, enqueues worker, requests follow after run' do subject.call(import) + expect(account.blocking?(to_be_unblocked)).to be false - end - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows[1..].map(&:id)) - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.blocking.map(&:acct)).to contain_exactly('blocked@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') + expect(account.blocking.map(&:acct)) + .to contain_exactly('blocked@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') end end @@ -192,30 +150,19 @@ RSpec.describe BulkImportService do ].map { |data| import.rows.create!(data: data) } end - before do - account.mute!(Fabricate(:account, username: 'already_muted', domain: 'remote.org')) - end + before { account.mute!(Fabricate(:account, username: 'already_muted', domain: 'remote.org')) } - it 'does not immediately change who the account blocks' do - expect { subject.call(import) }.to_not(change { account.reload.muting.to_a }) - end + it 'does not immediately change who the account blocks, enqueures worker, mutes users after worker run' do + expect { subject.call(import) } + .to_not(change { account.reload.muting.to_a }) - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows.map(&:id)) - it 'mutes all the listed users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.muting.map(&:acct)).to contain_exactly('already_muted@remote.org', 'user@foo.bar', 'unknown@unknown.bar') + expect(account.reload.muting.map(&:acct)) + .to contain_exactly('already_muted@remote.org', 'user@foo.bar', 'unknown@unknown.bar') end end @@ -239,31 +186,19 @@ RSpec.describe BulkImportService do account.mute!(to_be_unmuted) end - it 'updates the existing mute as expected' do - expect { subject.call(import) }.to change { Mute.where(account: account, target_account: muted).pick(:hide_notifications) }.from(false).to(true) - end + it 'updates the existing mute as expected and unblocks user not on list, and enqueues worker, and requests follow after worker run' do + expect { subject.call(import) } + .to change { Mute.where(account: account, target_account: muted).pick(:hide_notifications) }.from(false).to(true) - it 'unblocks user not present on list' do - subject.call(import) expect(account.muting?(to_be_unmuted)).to be false - end - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end + expect(row_worker_job_args) + .to match_array(rows[1..].map(&:id)) - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) + stub_resolve_account_and_drain_workers - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.muting.map(&:acct)).to contain_exactly('muted@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') + expect(account.muting.map(&:acct)) + .to contain_exactly('muted@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') end end @@ -284,13 +219,11 @@ RSpec.describe BulkImportService do account.block_domain!('blocked.com') end - it 'blocks all the new domains' do + it 'blocks all the new domains and marks import finished' do subject.call(import) - expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to-block.com') - end - it 'marks the import as finished' do - subject.call(import) + expect(account.domain_blocks.pluck(:domain)) + .to contain_exactly('alreadyblocked.com', 'blocked.com', 'to-block.com') expect(import.reload.state_finished?).to be true end end @@ -312,14 +245,13 @@ RSpec.describe BulkImportService do account.block_domain!('blocked.com') end - it 'blocks all the new domains' do + it 'blocks all the new domains and marks import finished' do subject.call(import) - expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to-block.com') - end - it 'marks the import as finished' do - subject.call(import) - expect(import.reload.state_finished?).to be true + expect(account.domain_blocks.pluck(:domain)) + .to contain_exactly('blocked.com', 'to-block.com') + expect(import.reload.state_finished?) + .to be true end end @@ -347,22 +279,16 @@ RSpec.describe BulkImportService do account.bookmarks.create!(status: bookmarked) end - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'updates the bookmarks as expected once the workers have run' do + it 'enqueues workers for the expected rows and updates bookmarks after worker run' do subject.call(import) - service_double = instance_double(ActivityPub::FetchRemoteStatusService) - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } - allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } + expect(row_worker_job_args) + .to match_array(rows.map(&:id)) - Import::RowWorker.drain + stub_fetch_remote_and_drain_workers - expect(account.bookmarks.map { |bookmark| bookmark.status.uri }).to contain_exactly(already_bookmarked.uri, status.uri, bookmarked.uri, 'https://domain.unknown/foo') + expect(account.bookmarks.map { |bookmark| bookmark.status.uri }) + .to contain_exactly(already_bookmarked.uri, status.uri, bookmarked.uri, 'https://domain.unknown/foo') end end @@ -390,23 +316,48 @@ RSpec.describe BulkImportService do account.bookmarks.create!(status: bookmarked) end - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'updates the bookmarks as expected once the workers have run' do + it 'enqueues workers for the expected rows and updates bookmarks' do subject.call(import) - service_double = instance_double(ActivityPub::FetchRemoteStatusService) - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } - allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } + expect(row_worker_job_args) + .to match_array(rows.map(&:id)) - Import::RowWorker.drain + stub_fetch_remote_and_drain_workers - expect(account.bookmarks.map { |bookmark| bookmark.status.uri }).to contain_exactly(status.uri, bookmarked.uri, 'https://domain.unknown/foo') + expect(account.bookmarks.map { |bookmark| bookmark.status.uri }) + .to contain_exactly(status.uri, bookmarked.uri, 'https://domain.unknown/foo') end end + + def row_worker_job_args + Import::RowWorker + .jobs + .pluck('args') + .flatten + end + + def stub_resolve_account_and_drain_workers + resolve_account_service_double = instance_double(ResolveAccountService) + allow(ResolveAccountService) + .to receive(:new) + .and_return(resolve_account_service_double) + allow(resolve_account_service_double) + .to receive(:call) + .with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double) + .to receive(:call) + .with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + end + + def stub_fetch_remote_and_drain_workers + service_double = instance_double(ActivityPub::FetchRemoteStatusService) + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) + allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } + allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } + + Import::RowWorker.drain + end end end diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index c39362def..123e8699c 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -11,11 +11,9 @@ RSpec.describe FavouriteService do let(:bob) { Fabricate(:account) } let(:status) { Fabricate(:status, account: bob) } - before do - subject.call(sender, status) - end - it 'creates a favourite' do + subject.call(sender, status) + expect(status.favourites.first).to_not be_nil end end @@ -26,15 +24,16 @@ RSpec.describe FavouriteService do before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200, body: '', headers: {}) + end + + it 'creates a favourite and sends like activity', :inline_jobs do subject.call(sender, status) - end - it 'creates a favourite' do - expect(status.favourites.first).to_not be_nil - end + expect(status.favourites.first) + .to_not be_nil - it 'sends a like activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 0c4cd6004..bbd8a6f99 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -143,15 +143,16 @@ RSpec.describe FollowService do before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200, body: '', headers: {}) + end + + it 'creates follow request and sends an activity to inbox', :inline_jobs do subject.call(sender, bob) - end - it 'creates follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil - end + expect(FollowRequest.find_by(account: sender, target_account: bob)) + .to_not be_nil - it 'sends a follow activity to the inbox', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 2c202d3e5..3cc83d82f 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -16,20 +16,25 @@ RSpec.describe ProcessMentionsService do before do account.block!(individually_blocked_account) account.domain_blocks.create!(domain: domain_blocked_account.domain) - - subject.call(status) end - it 'creates a mention to the non-blocked account' do - expect(non_blocked_account.mentions.where(status: status).count).to eq 1 + it 'creates a mention to the non-blocked account but not the individually or domain blocked accounts' do + expect { subject.call(status) } + .to create_mention_for_non_blocked + .and skip_mention_for_individual + .and skip_mention_for_domain_blocked end - it 'does not create a mention to the individually blocked account' do - expect(individually_blocked_account.mentions.where(status: status).count).to eq 0 + def create_mention_for_non_blocked + change { non_blocked_account.mentions.where(status: status).count }.to(1) end - it 'does not create a mention to the domain-blocked account' do - expect(domain_blocked_account.mentions.where(status: status).count).to eq 0 + def skip_mention_for_individual + not_change { individually_blocked_account.mentions.where(status: status).count }.from(0) + end + + def skip_mention_for_domain_blocked + not_change { domain_blocked_account.mentions.where(status: status).count }.from(0) end end @@ -40,11 +45,9 @@ RSpec.describe ProcessMentionsService do context 'with a valid remote user' do let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - before do - subject.call(status) - end - it 'creates a mention' do + subject.call(status) + expect(remote_user.mentions.where(status: status).count).to eq 1 end end @@ -53,11 +56,9 @@ RSpec.describe ProcessMentionsService 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) } - before do - subject.call(status, save_records: false) - end - it 'creates exactly one mention' do + subject.call(status, save_records: false) + expect(status.mentions.size).to eq 1 end end @@ -66,11 +67,9 @@ RSpec.describe ProcessMentionsService do let!(:remote_user) { Fabricate(:account, username: 'sneak', protocol: :activitypub, domain: 'xn--hresiar-mxa.ch', inbox_url: 'http://example.com/inbox') } let!(:status) { Fabricate(:status, account: account, text: 'Hello @sneak@hæresiar.ch') } - before do - subject.call(status) - end - it 'creates a mention' do + subject.call(status) + expect(remote_user.mentions.where(status: status).count).to eq 1 end end @@ -79,11 +78,9 @@ RSpec.describe ProcessMentionsService do let!(:remote_user) { Fabricate(:account, username: 'foo', protocol: :activitypub, domain: 'xn--y9a3aq.xn--y9a3aq', inbox_url: 'http://example.com/inbox') } let!(:status) { Fabricate(:status, account: account, text: 'Hello @foo@հայ.հայ') } - before do - subject.call(status) - end - it 'creates a mention' do + subject.call(status) + expect(remote_user.mentions.where(status: status).count).to eq 1 end end @@ -95,10 +92,11 @@ RSpec.describe ProcessMentionsService do before do stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 404) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com').to_return(status: 500) - subject.call(status) end it 'creates a mention' do + subject.call(status) + expect(remote_user.mentions.where(status: status).count).to eq 1 end end diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index d2c7a0020..eec0d6c1e 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -10,17 +10,15 @@ RSpec.describe RejectFollowService do describe 'local' do let(:bob) { Fabricate(:account) } - before do - FollowRequest.create(account: bob, target_account: sender) + before { FollowRequest.create(account: bob, target_account: sender) } + + it 'removes follow request and does not create relation' do subject.call(bob, sender) - end - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'does not create follow relation' do - expect(bob.following?(sender)).to be false + expect(bob) + .to_not be_requested(sender) + expect(bob) + .to_not be_following(sender) end end @@ -30,19 +28,17 @@ RSpec.describe RejectFollowService do before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, bob.inbox_url).to_return(status: 200) + end + + it 'removes follow request, does not create relation, sends reject activity', :inline_jobs do subject.call(bob, sender) - end - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'does not create follow relation' do - expect(bob.following?(sender)).to be false - end - - it 'sends a reject activity', :inline_jobs do - expect(a_request(:post, bob.inbox_url)).to have_been_made.once + expect(bob) + .to_not be_requested(sender) + expect(bob) + .to_not be_following(sender) + expect(a_request(:post, bob.inbox_url)) + .to have_been_made.once end end end diff --git a/spec/services/remove_from_followers_service_spec.rb b/spec/services/remove_from_followers_service_spec.rb index 515600096..381daf1a5 100644 --- a/spec/services/remove_from_followers_service_spec.rb +++ b/spec/services/remove_from_followers_service_spec.rb @@ -10,13 +10,13 @@ RSpec.describe RemoveFromFollowersService do describe 'local' do let(:sender) { Fabricate(:account, username: 'alice') } - before do - Follow.create(account: sender, target_account: bob) - subject.call(bob, sender) - end + before { Follow.create(account: sender, target_account: bob) } it 'does not create follow relation' do - expect(bob.followed_by?(sender)).to be false + subject.call(bob, sender) + + expect(bob) + .to_not be_followed_by(sender) end end @@ -26,15 +26,16 @@ RSpec.describe RemoveFromFollowersService do before do Follow.create(account: sender, target_account: bob) stub_request(:post, sender.inbox_url).to_return(status: 200) + end + + it 'does not create follow relation and sends reject activity', :inline_jobs do subject.call(bob, sender) - end - it 'does not create follow relation' do - expect(bob.followed_by?(sender)).to be false - end + expect(bob) + .to_not be_followed_by(sender) - it 'sends a reject activity', :inline_jobs do - expect(a_request(:post, sender.inbox_url)).to have_been_made.once + expect(a_request(:post, sender.inbox_url)) + .to have_been_made.once end end end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 08f519b53..f2b46f05b 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -28,42 +28,38 @@ RSpec.describe RemoveStatusService, :inline_jobs do Fabricate(:status, account: bill, reblog: status, uri: 'hoge') end - it 'removes status from author\'s home feed' do - subject.call(status) - expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status.id) - end - - it 'removes status from local follower\'s home feed' do - subject.call(status) - expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status.id) - end - - it 'publishes to public media timeline' do + it 'removes status from notifications and from author and local follower home feeds, publishes to media timeline, sends delete activities' do allow(redis).to receive(:publish).with(any_args) - subject.call(status) + expect { subject.call(status) } + .to remove_status_from_notifications - expect(redis).to have_received(:publish).with('timeline:public:media', Oj.dump(event: :delete, payload: status.id.to_s)) - end + expect(home_feed_ids(alice)) + .to_not include(status.id) + expect(home_feed_ids(jeff)) + .to_not include(status.id) - it 'sends Delete activity to followers' do - subject.call(status) + expect(redis) + .to have_received(:publish).with('timeline:public:media', Oj.dump(event: :delete, payload: status.id.to_s)) expect(delete_delivery(hank, status)) .to have_been_made.once - end - - it 'sends Delete activity to rebloggers' do - subject.call(status) expect(delete_delivery(bill, status)) .to have_been_made.once end - it 'remove status from notifications' do - expect { subject.call(status) }.to change { - Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count - }.from(1).to(0) + def home_feed_ids(personage) + HomeFeed + .new(personage) + .get(10) + .pluck(:id) + end + + def remove_status_from_notifications + change { Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count } + .from(1) + .to(0) end def delete_delivery(target, status) diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index 6518c5c27..4659e1c7a 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -31,14 +31,13 @@ RSpec.describe ReportService do context 'when forward is true', :inline_jobs do let(:forward) { true } - it 'sends ActivityPub payload when forward is true' do - subject.call(source_account, remote_account, forward: forward) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made - end - - it 'has an uri' do + it 'has a URI and sends ActivityPub payload' do report = subject.call(source_account, remote_account, forward: forward) - expect(report.uri).to_not be_nil + + expect(report.uri) + .to_not be_nil + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made end context 'when reporting a reply on a different remote server' do @@ -122,13 +121,12 @@ RSpec.describe ReportService do status.mentions.create(account: source_account) end - it 'creates a report' do - expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1) - end + it 'creates a report and attaches the DM to the report' do + expect { subject.call } + .to change { target_account.targeted_reports.count }.from(0).to(1) - it 'attaches the DM to the report' do - subject.call - expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]] + expect(target_account.targeted_reports.pluck(:status_ids)) + .to eq [[status.id]] end end @@ -146,13 +144,12 @@ RSpec.describe ReportService do status.mentions.create(account: source_account) end - it 'creates a report' do - expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1) - end + it 'creates a report and attaches DM to report' do + expect { subject.call } + .to change { target_account.targeted_reports.count }.from(0).to(1) - it 'attaches the DM to the report' do - subject.call - expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]] + expect(target_account.targeted_reports.pluck(:status_ids)) + .to eq [[status.id]] end end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index a856e019a..1bd4e9a8e 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -22,37 +22,38 @@ RSpec.describe ResolveAccountService do context 'when domain is banned' do before { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) } - it 'does not return an account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil - end - - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made + it 'does not return an account or make a webfinger query' do + expect(subject.call('foo@ap.example.com', skip_webfinger: true)) + .to be_nil + expect(webfinger_discovery_request) + .to_not have_been_made end end context 'when domain is not banned' do - it 'returns the expected account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to eq remote_account - end - - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made + it 'returns the expected account and does not make a webfinger query' do + expect(subject.call('foo@ap.example.com', skip_webfinger: true)) + .to eq remote_account + expect(webfinger_discovery_request) + .to_not have_been_made end end end context 'when account is not known' do - it 'does not return an account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil + it 'does not return an account and does not make webfinger query' do + expect(subject.call('foo@ap.example.com', skip_webfinger: true)) + .to be_nil + expect(webfinger_discovery_request) + .to_not have_been_made end + end - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made - end + def webfinger_discovery_request + a_request( + :get, + 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com' + ) end end @@ -84,13 +85,11 @@ RSpec.describe ResolveAccountService do allow(AccountDeletionWorker).to receive(:perform_async) end - it 'returns nil' do - expect(subject.call('hoge@example.com')).to be_nil - end - - it 'queues account deletion worker' do - subject.call('hoge@example.com') - expect(AccountDeletionWorker).to have_received(:perform_async) + it 'returns nil and queues deletion worker' do + expect(subject.call('hoge@example.com')) + .to be_nil + expect(AccountDeletionWorker) + .to have_received(:perform_async) end end @@ -110,9 +109,12 @@ RSpec.describe ResolveAccountService do it 'returns new remote account' do account = subject.call('Foo@redirected.example.com') - expect(account.activitypub?).to be true - expect(account.acct).to eq 'foo@ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account) + .to have_attributes( + activitypub?: true, + acct: 'foo@ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox' + ) end end @@ -125,9 +127,12 @@ RSpec.describe ResolveAccountService do it 'returns new remote account' do account = subject.call('Foo@redirected.example.com') - expect(account.activitypub?).to be true - expect(account.acct).to eq 'foo@ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account) + .to have_attributes( + activitypub?: true, + acct: 'foo@ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox' + ) end end @@ -161,9 +166,12 @@ RSpec.describe ResolveAccountService do it 'returns new remote account' do account = subject.call('foo@ap.example.com') - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account) + .to have_attributes( + activitypub?: true, + domain: 'ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox' + ) end context 'with multiple types' do @@ -174,10 +182,13 @@ RSpec.describe ResolveAccountService do it 'returns new remote account' do account = subject.call('foo@ap.example.com') - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.actor_type).to eq 'Person' + expect(account) + .to have_attributes( + activitypub?: true, + domain: 'ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox', + actor_type: 'Person' + ) end end end @@ -186,20 +197,21 @@ RSpec.describe ResolveAccountService do let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') } let!(:status) { Fabricate(:status, account: duplicate, text: 'foo') } - it 'returns new remote account' do + it 'returns new remote account and merges accounts', :inline_jobs do account = subject.call('foo@ap.example.com') - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.uri).to eq 'https://ap.example.com/users/foo' - end + expect(account) + .to have_attributes( + activitypub?: true, + domain: 'ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox', + uri: 'https://ap.example.com/users/foo' + ) - it 'merges accounts', :inline_jobs do - account = subject.call('foo@ap.example.com') - - expect(status.reload.account_id).to eq account.id - expect(Account.where(uri: account.uri).count).to eq 1 + expect(status.reload.account_id) + .to eq account.id + expect(Account.where(uri: account.uri).count) + .to eq 1 end end @@ -210,11 +222,15 @@ RSpec.describe ResolveAccountService do it 'returns new remote account' do account = subject.call('foo@ap.example.com') - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.uri).to eq 'https://ap.example.com/users/foo' - expect(status.reload.account).to eq(account) + expect(account) + .to have_attributes( + activitypub?: true, + domain: 'ap.example.com', + inbox_url: 'https://ap.example.com/users/foo/inbox', + uri: 'https://ap.example.com/users/foo' + ) + expect(status.reload.account) + .to eq(account) end end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 80f2a5a4b..eaf00c1ed 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -51,12 +51,11 @@ RSpec.describe ResolveURLService do let(:url) { 'https://example.com/@foo/42' } let(:uri) { 'https://example.com/users/foo/statuses/42' } - it 'returns status by url' do - expect(subject.call(url, on_behalf_of: account)).to eq(status) - end - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to eq(status) + it 'returns status by URL or URI' do + expect(subject.call(url, on_behalf_of: account)) + .to eq(status) + expect(subject.call(uri, on_behalf_of: account)) + .to eq(status) end end @@ -75,12 +74,11 @@ RSpec.describe ResolveURLService do let(:url) { 'https://example.com/@foo/42' } let(:uri) { 'https://example.com/users/foo/statuses/42' } - it 'does not return the status by url' do - expect(subject.call(url, on_behalf_of: account)).to be_nil - end - - it 'does not return the status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to be_nil + it 'does not return the status by URL or URI' do + expect(subject.call(url, on_behalf_of: account)) + .to be_nil + expect(subject.call(uri, on_behalf_of: account)) + .to be_nil end end @@ -107,22 +105,20 @@ RSpec.describe ResolveURLService do account.follow!(poster) end - it 'returns status by url' do - expect(subject.call(url, on_behalf_of: account)).to eq(status) - end - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to eq(status) + it 'returns status by URL or URI' do + expect(subject.call(url, on_behalf_of: account)) + .to eq(status) + expect(subject.call(uri, on_behalf_of: account)) + .to eq(status) end end context 'when the account does not follow the poster' do - it 'does not return the status by url' do - expect(subject.call(url, on_behalf_of: account)).to be_nil - end - - it 'does not return the status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to be_nil + it 'does not return the status by URL or URI' do + expect(subject.call(url, on_behalf_of: account)) + .to be_nil + expect(subject.call(uri, on_behalf_of: account)) + .to be_nil end end end diff --git a/spec/services/translate_status_service_spec.rb b/spec/services/translate_status_service_spec.rb index 0779fbbe6..cd92fb8d1 100644 --- a/spec/services/translate_status_service_spec.rb +++ b/spec/services/translate_status_service_spec.rb @@ -32,20 +32,14 @@ RSpec.describe TranslateStatusService do allow(TranslationService).to receive_messages(configured?: true, configured: translation_service) end - it 'returns translated status content' do - expect(service.call(status, 'es').content).to eq '

Hola

' - end - - it 'returns source language' do - expect(service.call(status, 'es').detected_source_language).to eq 'en' - end - - it 'returns translation provider' do - expect(service.call(status, 'es').provider).to eq 'Dummy' - end - - it 'returns original status' do - expect(service.call(status, 'es').status).to eq status + it 'returns translated status content and source language and provider and original status' do + expect(service.call(status, 'es')) + .to have_attributes( + content: '

Hola

', + detected_source_language: 'en', + provider: 'Dummy', + status: status + ) end describe 'status has content with custom emoji' do @@ -155,26 +149,16 @@ RSpec.describe TranslateStatusService do let!(:source_texts) { service.send(:source_texts) } it 'returns formatted poll options' do - expect(source_texts.size).to eq 3 - expect(source_texts.values).to eq %w(

Hello

Blue Green) - end - - it 'has a first key with content' do - expect(source_texts.keys.first).to eq :content - end - - it 'has the first option in the second key with correct options' do - option1 = source_texts.keys.second - expect(option1).to be_a Poll::Option - expect(option1.id).to eq '0' - expect(option1.title).to eq 'Blue' - end - - it 'has the second option in the third key with correct options' do - option2 = source_texts.keys.third - expect(option2).to be_a Poll::Option - expect(option2.id).to eq '1' - expect(option2.title).to eq 'Green' + expect(source_texts) + .to have_attributes( + size: 3, + values: %w(

Hello

Blue Green), + keys: contain_exactly( + eq(:content), + be_a(Poll::Option).and(have_attributes(id: '0', title: 'Blue')), + be_a(Poll::Option).and(have_attributes(id: '1', title: 'Green')) + ) + ) end end end diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb index 405fe1cfd..daa1d480a 100644 --- a/spec/services/unblock_domain_service_spec.rb +++ b/spec/services/unblock_domain_service_spec.rb @@ -12,26 +12,32 @@ RSpec.describe UnblockDomainService do let!(:silenced) { Fabricate(:account, domain: 'example.com', silenced_at: domain_block.created_at) } let!(:suspended) { Fabricate(:account, domain: 'example.com', suspended_at: domain_block.created_at) } - it 'unsilences accounts and removes block' do - domain_block.update(severity: :silence) + context 'with severity of silence' do + before { domain_block.update(severity: :silence) } - subject.call(domain_block) - expect_deleted_domain_block - expect(silenced.reload.silenced?).to be false - expect(suspended.reload.suspended?).to be true - expect(independently_suspended.reload.suspended?).to be true - expect(independently_silenced.reload.silenced?).to be true + it 'unsilences accounts and removes block' do + subject.call(domain_block) + + expect_deleted_domain_block + expect(silenced.reload.silenced?).to be false + expect(suspended.reload.suspended?).to be true + expect(independently_suspended.reload.suspended?).to be true + expect(independently_silenced.reload.silenced?).to be true + end end - it 'unsuspends accounts and removes block' do - domain_block.update(severity: :suspend) + context 'with severity of suspend' do + before { domain_block.update(severity: :suspend) } - subject.call(domain_block) - expect_deleted_domain_block - expect(suspended.reload.suspended?).to be false - expect(silenced.reload.silenced?).to be false - expect(independently_suspended.reload.suspended?).to be true - expect(independently_silenced.reload.silenced?).to be true + it 'unsuspends accounts and removes block' do + subject.call(domain_block) + + expect_deleted_domain_block + expect(suspended.reload.suspended?).to be false + expect(silenced.reload.silenced?).to be false + expect(independently_suspended.reload.suspended?).to be true + expect(independently_silenced.reload.silenced?).to be true + end end end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 6132e7441..a2c5188f0 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -10,13 +10,13 @@ RSpec.describe UnblockService do describe 'local' do let(:bob) { Fabricate(:account) } - before do - sender.block!(bob) - subject.call(sender, bob) - end + before { sender.block!(bob) } it 'destroys the blocking relation' do - expect(sender.blocking?(bob)).to be false + subject.call(sender, bob) + + expect(sender) + .to_not be_blocking(bob) end end @@ -26,15 +26,15 @@ RSpec.describe UnblockService do before do sender.block!(bob) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + end + + it 'destroys the blocking relation and sends unblock activity', :inline_jobs do subject.call(sender, bob) - end - it 'destroys the blocking relation' do - expect(sender.blocking?(bob)).to be false - end - - it 'sends an unblock activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(sender) + .to_not be_blocking(bob) + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end end diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 0c206c4b9..6cf24ca5e 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -10,13 +10,13 @@ RSpec.describe UnfollowService do describe 'local' do let(:bob) { Fabricate(:account, username: 'bob') } - before do - sender.follow!(bob) - subject.call(sender, bob) - end + before { sender.follow!(bob) } it 'destroys the following relation' do - expect(sender.following?(bob)).to be false + subject.call(sender, bob) + + expect(sender) + .to_not be_following(bob) end end @@ -26,15 +26,15 @@ RSpec.describe UnfollowService do before do sender.follow!(bob) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + end + + it 'destroys the following relation and sends unfollow activity' do subject.call(sender, bob) - end - it 'destroys the following relation' do - expect(sender.following?(bob)).to be false - end - - it 'sends an unfollow activity' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(sender) + .to_not be_following(bob) + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end @@ -44,15 +44,15 @@ RSpec.describe UnfollowService do before do bob.follow!(sender) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + end + + it 'destroys the following relation and sends a reject activity' do subject.call(bob, sender) - end - it 'destroys the following relation' do - expect(bob.following?(sender)).to be false - end - - it 'sends a reject activity' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + expect(sender) + .to_not be_following(bob) + expect(a_request(:post, 'http://example.com/inbox')) + .to have_been_made.once end end end diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb index d066db481..f9059af07 100644 --- a/spec/services/update_account_service_spec.rb +++ b/spec/services/update_account_service_spec.rb @@ -18,23 +18,19 @@ RSpec.describe UpdateAccountService do FollowService.new.call(alice, account) FollowService.new.call(bob, account) FollowService.new.call(eve, account) + end + it 'auto accepts pending follow requests from appropriate accounts' do subject.call(account, { locked: false }) - end - it 'auto-accepts pending follow requests' do - expect(alice.following?(account)).to be true - expect(alice.requested?(account)).to be false - end + expect(alice).to be_following(account) + expect(alice).to_not be_requested(account) - it 'does not auto-accept pending follow requests from silenced users' do - expect(bob.following?(account)).to be false - expect(bob.requested?(account)).to be true - end + expect(bob).to_not be_following(account) + expect(bob).to be_requested(account) - it 'auto-accepts pending follow requests from muted users so as to not leak mute' do - expect(eve.following?(account)).to be true - expect(eve.requested?(account)).to be false + expect(eve).to be_following(account) + expect(eve).to_not be_requested(account) end end end diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb index de06fb13c..7c92adeff 100644 --- a/spec/services/update_status_service_spec.rb +++ b/spec/services/update_status_service_spec.rb @@ -10,15 +10,15 @@ RSpec.describe UpdateStatusService do before do allow(ActivityPub::DistributionWorker).to receive(:perform_async) + end + + it 'does not create an edit or notify anyone' do subject.call(status, status.account_id, text: 'Foo') - end - it 'does not create an edit' do - expect(status.reload.edits).to be_empty - end - - it 'does not notify anyone' do - expect(ActivityPub::DistributionWorker).to_not have_received(:perform_async) + expect(status.reload.edits) + .to be_empty + expect(ActivityPub::DistributionWorker) + .to_not have_received(:perform_async) end end @@ -28,18 +28,16 @@ RSpec.describe UpdateStatusService do before do PreviewCardsStatus.create(status: status, preview_card: preview_card) + end + + it 'updates text, resets card, saves edit history' do subject.call(status, status.account_id, text: 'Bar') - end - it 'updates text' do - expect(status.reload.text).to eq 'Bar' - end - - it 'resets preview card' do - expect(status.reload.preview_card).to be_nil - end - - it 'saves edit history' do + expect(status.reload) + .to have_attributes( + text: 'Bar', + preview_card: be_nil + ) expect(status.edits.ordered.pluck(:text)).to eq %w(Foo Bar) end end @@ -50,15 +48,15 @@ RSpec.describe UpdateStatusService do before do PreviewCardsStatus.create(status: status, preview_card: preview_card) + end + + it 'updates content warning and saves history' do subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar') - end - it 'updates content warning' do - expect(status.reload.spoiler_text).to eq 'Bar' - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:text, :spoiler_text)).to eq [['Foo', ''], ['Foo', 'Bar']] + expect(status.reload.spoiler_text) + .to eq 'Bar' + expect(status.edits.ordered.pluck(:text, :spoiler_text)) + .to eq [['Foo', ''], ['Foo', 'Bar']] end end @@ -69,23 +67,19 @@ RSpec.describe UpdateStatusService do before do status.media_attachments << detached_media_attachment + end + + it 'updates media attachments, handles attachments, saves history' do subject.call(status, status.account_id, text: 'Foo', media_ids: [attached_media_attachment.id.to_s]) - end - it 'updates media attachments' do - expect(status.ordered_media_attachments).to eq [attached_media_attachment] - end - - it 'does not detach detached media attachments' do - expect(detached_media_attachment.reload.status_id).to eq status.id - end - - it 'attaches attached media attachments' do - expect(attached_media_attachment.reload.status_id).to eq status.id - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:ordered_media_attachment_ids)).to eq [[detached_media_attachment.id], [attached_media_attachment.id]] + expect(status.ordered_media_attachments) + .to eq [attached_media_attachment] + expect(detached_media_attachment.reload.status_id) + .to eq status.id + expect(attached_media_attachment.reload.status_id) + .to eq status.id + expect(status.edits.ordered.pluck(:ordered_media_attachment_ids)) + .to eq [[detached_media_attachment.id], [attached_media_attachment.id]] end end @@ -95,19 +89,18 @@ RSpec.describe UpdateStatusService do before do status.media_attachments << media_attachment + end + + it 'does not detach media attachment, updates description, and saves history' do subject.call(status, status.account_id, text: 'Foo', media_ids: [media_attachment.id.to_s], media_attributes: [{ id: media_attachment.id, description: 'New description' }]) - end - it 'does not detach media attachment' do - expect(media_attachment.reload.status_id).to eq status.id - end - - it 'updates the media attachment description' do - expect(media_attachment.reload.description).to eq 'New description' - end - - it 'saves edit history' do - expect(status.edits.ordered.map { |edit| edit.ordered_media_attachments.map(&:description) }).to eq [['Old description'], ['New description']] + expect(media_attachment.reload) + .to have_attributes( + status_id: status.id, + description: 'New description' + ) + expect(status.edits.ordered.map { |edit| edit.ordered_media_attachments.map(&:description) }) + .to eq [['Old description'], ['New description']] end end @@ -120,28 +113,27 @@ RSpec.describe UpdateStatusService do before do status.update(poll: poll) VoteService.new.call(voter, poll, [0]) + end + + it 'updates poll, resets votes, saves history, requeues notifications' do subject.call(status, status.account_id, text: 'Foo', poll: { options: %w(Bar Baz Foo), expires_in: 5.days.to_i }) - end - it 'updates poll' do poll = status.poll.reload - expect(poll.options).to eq %w(Bar Baz Foo) - end - it 'resets votes' do - poll = status.poll.reload - expect(poll.votes_count).to eq 0 - expect(poll.votes.count).to eq 0 - expect(poll.cached_tallies).to eq [0, 0, 0] - end + expect(poll) + .to have_attributes( + options: %w(Bar Baz Foo), + votes_count: 0, + cached_tallies: [0, 0, 0] + ) + expect(poll.votes.count) + .to eq(0) - it 'saves edit history' do - expect(status.edits.ordered.pluck(:poll_options)).to eq [%w(Foo Bar), %w(Bar Baz Foo)] - end + expect(status.edits.ordered.pluck(:poll_options)) + .to eq [%w(Foo Bar), %w(Bar Baz Foo)] - it 'requeues expiration notification' do - poll = status.poll.reload - expect(PollExpirationNotifyWorker).to have_enqueued_sidekiq_job(poll.id).at(poll.expires_at + 5.minutes) + expect(PollExpirationNotifyWorker) + .to have_enqueued_sidekiq_job(poll.id).at(poll.expires_at + 5.minutes) end end @@ -151,16 +143,13 @@ RSpec.describe UpdateStatusService do let!(:bob) { Fabricate(:account, username: 'bob') } let!(:status) { PostStatusService.new.call(account, text: 'Hello @alice') } - before do + it 'changes mentions and keeps old as silent' do subject.call(status, status.account_id, text: 'Hello @bob') - end - it 'changes mentions' do - expect(status.active_mentions.pluck(:account_id)).to eq [bob.id] - end - - it 'keeps old mentions as silent mentions' do - expect(status.mentions.pluck(:account_id)).to contain_exactly(alice.id, bob.id) + expect(status.active_mentions.pluck(:account_id)) + .to eq [bob.id] + expect(status.mentions.pluck(:account_id)) + .to contain_exactly(alice.id, bob.id) end end @@ -168,11 +157,9 @@ RSpec.describe UpdateStatusService do let!(:account) { Fabricate(:account) } let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') } - before do - subject.call(status, status.account_id, text: 'Hello #bar') - end - it 'changes tags' do + subject.call(status, status.account_id, text: 'Hello #bar') + expect(status.tags.pluck(:name)).to eq %w(bar) end end diff --git a/spec/system/invites_spec.rb b/spec/system/invites_spec.rb index c57de871c..fc60ce591 100644 --- a/spec/system/invites_spec.rb +++ b/spec/system/invites_spec.rb @@ -7,10 +7,7 @@ RSpec.describe 'Invites' do let(:user) { Fabricate :user } - before do - host! 'localhost:3000' # TODO: Move into before for all system specs? - sign_in user - end + before { sign_in user } describe 'Viewing invites' do it 'Lists existing user invites' do diff --git a/spec/system/oauth_spec.rb b/spec/system/oauth_spec.rb index 0f96a5967..64ac75879 100644 --- a/spec/system/oauth_spec.rb +++ b/spec/system/oauth_spec.rb @@ -179,7 +179,7 @@ RSpec.describe 'Using OAuth from an external app' do end context 'when the user has set up TOTP' do - let(:user) { Fabricate(:user, email: email, password: password, otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) } + let(:user) { Fabricate(:user, email: email, password: password, otp_required_for_login: true, otp_secret: User.generate_otp_secret) } it 'when accepting the authorization request' do params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } diff --git a/spec/workers/mention_resolve_worker_spec.rb b/spec/workers/mention_resolve_worker_spec.rb new file mode 100644 index 000000000..5e23876b4 --- /dev/null +++ b/spec/workers/mention_resolve_worker_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe MentionResolveWorker do + let(:status_id) { -42 } + let(:uri) { 'https://example.com/users/unknown' } + + describe '#perform' do + subject { described_class.new.perform(status_id, uri, {}) } + + context 'with a non-existent status' do + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'with a valid user' do + let(:status) { Fabricate(:status) } + let(:status_id) { status.id } + + let(:service_double) { instance_double(ActivityPub::FetchRemoteAccountService) } + + before do + allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_double) + + allow(service_double).to receive(:call).with(uri, anything) { Fabricate(:account, domain: 'example.com', uri: uri) } + end + + it 'resolves the account and adds a new mention', :aggregate_failures do + expect { subject } + .to change { status.reload.mentions }.from([]).to(a_collection_including(having_attributes(account: having_attributes(uri: uri), silent: false))) + + expect(service_double).to have_received(:call).once + end + end + end +end diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb index ced21d5bf..7f836d99e 100644 --- a/spec/workers/web/push_notification_worker_spec.rb +++ b/spec/workers/web/push_notification_worker_spec.rb @@ -22,27 +22,48 @@ RSpec.describe Web::PushNotificationWorker do let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } } describe 'perform' do + around do |example| + original_private = Rails.configuration.x.vapid_private_key + original_public = Rails.configuration.x.vapid_public_key + Rails.configuration.x.vapid_private_key = vapid_private_key + Rails.configuration.x.vapid_public_key = vapid_public_key + example.run + Rails.configuration.x.vapid_private_key = original_private + Rails.configuration.x.vapid_public_key = original_public + end + before do - allow(subscription).to receive_messages(contact_email: contact_email, vapid_key: vapid_key) - allow(Web::PushSubscription).to receive(:find).with(subscription.id).and_return(subscription) + Setting.site_contact_email = contact_email + allow(Webpush::Encryption).to receive(:encrypt).and_return(payload) allow(JWT).to receive(:encode).and_return('jwt.encoded.payload') stub_request(:post, endpoint).to_return(status: 201, body: '') - - subject.perform(subscription.id, notification.id) end it 'calls the relevant service with the correct headers' do - expect(a_request(:post, endpoint).with(headers: { - 'Content-Encoding' => 'aesgcm', - 'Content-Type' => 'application/octet-stream', - 'Crypto-Key' => "dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=#{vapid_public_key.delete('=')}", - 'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g', - 'Ttl' => '172800', - 'Urgency' => 'normal', - 'Authorization' => 'WebPush jwt.encoded.payload', - }, body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr")).to have_been_made + subject.perform(subscription.id, notification.id) + + expect(web_push_endpoint_request) + .to have_been_made + end + + def web_push_endpoint_request + a_request( + :post, + endpoint + ).with( + headers: { + 'Content-Encoding' => 'aesgcm', + 'Content-Type' => 'application/octet-stream', + 'Crypto-Key' => "dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=#{vapid_public_key.delete('=')}", + 'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g', + 'Ttl' => '172800', + 'Urgency' => 'normal', + 'Authorization' => 'WebPush jwt.encoded.payload', + }, + body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr" + ) end end end diff --git a/yarn.lock b/yarn.lock index 38402229b..8970981d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1763,9 +1763,9 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-light-dark-function@npm:^2.0.2": - version: 2.0.2 - resolution: "@csstools/postcss-light-dark-function@npm:2.0.2" +"@csstools/postcss-light-dark-function@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/postcss-light-dark-function@npm:2.0.4" dependencies: "@csstools/css-parser-algorithms": "npm:^3.0.1" "@csstools/css-tokenizer": "npm:^3.0.1" @@ -1773,7 +1773,7 @@ __metadata: "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f8973c435868998e5d6af1fc0c35b27bbf65fa9d0c35f5055c689b8ee2807a16802044e296f7def39a7253ae544fb49559e8273ee22eb4e21845aa980a1bc82b + checksum: 10c0/0176422ad9747953964b1ceff002df1ecb1952ebc481db6192070d68777135b582ea6fd32ae819b9c64c96cb9170bd6907c647c85b48daa4984b7ed3d7f9bccb languageName: node linkType: hard @@ -13921,8 +13921,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.0.0": - version: 10.0.3 - resolution: "postcss-preset-env@npm:10.0.3" + version: 10.0.5 + resolution: "postcss-preset-env@npm:10.0.5" dependencies: "@csstools/postcss-cascade-layers": "npm:^5.0.0" "@csstools/postcss-color-function": "npm:^4.0.2" @@ -13936,7 +13936,7 @@ __metadata: "@csstools/postcss-ic-unit": "npm:^4.0.0" "@csstools/postcss-initial": "npm:^2.0.0" "@csstools/postcss-is-pseudo-class": "npm:^5.0.0" - "@csstools/postcss-light-dark-function": "npm:^2.0.2" + "@csstools/postcss-light-dark-function": "npm:^2.0.4" "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" "@csstools/postcss-logical-overflow": "npm:^2.0.0" "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0" @@ -13987,7 +13987,7 @@ __metadata: postcss-selector-not: "npm:^8.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/da42caa2aab4d825fddfde00ebe2416d338c7b9a6f79a68840297888a8384f85991991c3fa10cf2d359fb230c885375f5cebd7bd63972725cd2a596d218f8b6a + checksum: 10c0/db5eb1175cb26bed3f1a4c47acc67935ffc784520321470520e59de366ac6f91be1e609fe36056af707ed20f7910721287cff0fae416c437dd3e944de13ffd05 languageName: node linkType: hard